mirror of
https://github.com/opensupports/opensupports.git
synced 2025-04-08 18:35:06 +02:00
Compare commits
651 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
20720ca4f9 | ||
|
40016635a8 | ||
|
68b2d2bf63 | ||
|
36c5f3264b | ||
|
82fd54ffd9 | ||
|
8c732f5dda | ||
|
b620baf5ed | ||
|
d3b47eaa73 | ||
|
90e7923aec | ||
|
0ecf88237f | ||
|
713a5b5ee1 | ||
|
b578a26225 | ||
|
93fa9e12a3 | ||
|
3720baf370 | ||
|
b1ef69c60c | ||
|
615f42a91b | ||
|
39f8a601db | ||
|
c82aaa001e | ||
|
04923b0e9d | ||
|
5e7f39df05 | ||
|
e190710252 | ||
|
a74d6ab4d7 | ||
|
a5da776d27 | ||
|
41d7aa5406 | ||
|
f2adb160be | ||
|
62bd70cc3b | ||
|
0f6c64674e | ||
|
861f7dc254 | ||
|
83b8e8094b | ||
|
81cc579d57 | ||
|
b3c8819d83 | ||
|
86cad910ec | ||
|
f890fdc2d3 | ||
|
c64eb9930b | ||
|
74870c632a | ||
|
e0738f1c88 | ||
|
545f88236d | ||
|
2e5bfa611e | ||
|
784d470ba5 | ||
|
54a5a9803d | ||
|
d5552c0f73 | ||
|
daf1db847c | ||
|
2df12aa5e3 | ||
|
c80c026617 | ||
|
9cf71dcf66 | ||
|
a264d384a1 | ||
|
d90b7d48c3 | ||
|
527843f00f | ||
|
3e3a95f518 | ||
|
639d40ddb0 | ||
|
fbee7275d5 | ||
|
7df190ea01 | ||
|
1228d593d0 | ||
|
c1a7befbed | ||
|
6e195a9109 | ||
|
bf7c1ba8f9 | ||
|
8b4b73402e | ||
|
4cf446483d | ||
|
ea6bc7f436 | ||
|
37209ef3fc | ||
|
83e75cc572 | ||
|
9291aa66a4 | ||
|
b9f5f7fcf1 | ||
|
fe1dd1bd48 | ||
|
5dd6b7acdc | ||
|
4bd8df1d5e | ||
|
8d7b178fa1 | ||
|
237801e9ed | ||
|
950439bf47 | ||
|
6cb538616d | ||
|
156b285344 | ||
|
b39e4c2a5f | ||
|
5a1b558a6d | ||
|
4b9a55b334 | ||
|
e7daf76274 | ||
|
402af565a9 | ||
|
f5e9b2602c | ||
|
f1d746f9f9 | ||
|
c0fc7933ed | ||
|
f31586874e | ||
|
d2c126aad9 | ||
|
ffd09b841f | ||
|
e23468d3be | ||
|
84f36e89dc | ||
|
9715cdc9a1 | ||
|
b39dca642b | ||
|
6f6acc925d | ||
|
d2e6dc2fbe | ||
|
0df57af11e | ||
|
8400a1caf0 | ||
|
f045c08b2e | ||
|
645e64532b | ||
|
b9f935df5f | ||
|
edddc8d2c5 | ||
|
eaeaaa647e | ||
|
d7ccff1a5a | ||
|
c5f1aa2b92 | ||
|
9ed4caf202 | ||
|
018863ab3e | ||
|
7015e21966 | ||
|
7cdb6d3603 | ||
|
1836849fa5 | ||
|
9f9e1dbd91 | ||
|
60b1b5eec5 | ||
|
7b4427d3e3 | ||
|
09150d6940 | ||
|
02cf8f0da3 | ||
|
e15bd15f07 | ||
|
c657d8291f | ||
|
b2e43430b1 | ||
|
8c17d22ab3 | ||
|
143776febe | ||
|
6536050fdd | ||
|
9a4374d371 | ||
|
b8be664809 | ||
|
e9a1a2e5be | ||
|
0f976ebde9 | ||
|
c64f1f1ea6 | ||
|
5d4fe0250b | ||
|
af15d0116d | ||
|
6ccb389492 | ||
|
ae076de88f | ||
|
59fb9eaef3 | ||
|
ffe7ef8e0b | ||
|
27e86c934c | ||
|
064f00388a | ||
|
fc93ad4c00 | ||
|
c22a1cdbb3 | ||
|
89cb4c18fd | ||
|
167d7927db | ||
|
55c89d58cc | ||
|
0bcc775944 | ||
|
e6441179c9 | ||
|
45a59e0b20 | ||
|
9f7b11413c | ||
|
e554bb64d1 | ||
|
0088332562 | ||
|
42c5dd7210 | ||
|
1a9e1a852c | ||
|
49dc1ab56c | ||
|
1ea4509e4f | ||
|
c5d6068e97 | ||
|
51eee4ed7b | ||
|
1e0e0134a3 | ||
|
bc9023b8a6 | ||
|
534bf3624a | ||
|
a7cb7f376c | ||
|
c3088406da | ||
|
ea8d0719eb | ||
|
e4a7fe8783 | ||
|
ca54d19bd9 | ||
|
5416ef4009 | ||
|
af3d95cf4d | ||
|
3dd76f214d | ||
|
16435925b6 | ||
|
ea273970d1 | ||
|
b9b21ef950 | ||
|
bb1f5d0ade | ||
|
15f765cf85 | ||
|
9feb7d6cd4 | ||
|
94926f90e6 | ||
|
b02acfdf7b | ||
|
560f231e51 | ||
|
7fb7be3860 | ||
|
b8944a3f04 | ||
|
a64c9f2255 | ||
|
994a39ad6d | ||
|
b73d6d534d | ||
|
d4cdbab203 | ||
|
80a9a958a8 | ||
|
9125944bc3 | ||
|
c00720d6a2 | ||
|
5184c31907 | ||
|
817240e0b4 | ||
|
52eae4d242 | ||
|
d3638787e6 | ||
|
68c3975ea4 | ||
|
6e6d2d83e7 | ||
|
371f111706 | ||
|
bb89956e9a | ||
|
a89a465ac9 | ||
|
9f915a3291 | ||
|
0fd23ac6b8 | ||
|
5021781d25 | ||
|
47f92569ef | ||
|
c92ecf25dc | ||
|
38136ade4d | ||
|
da534e3018 | ||
|
0518b0ac17 | ||
|
5d2e4fcb1f | ||
|
7a803dd7ff | ||
|
4077dac8c7 | ||
|
c4e211518c | ||
|
56ca30a7c3 | ||
|
fad3dce646 | ||
|
deb590ceee | ||
|
e36b984b23 | ||
|
e397d45c53 | ||
|
e4a9366b07 | ||
|
bbbc845fe7 | ||
|
a46140381b | ||
|
d72aec3976 | ||
|
1c5d156723 | ||
|
f72f2ac074 | ||
|
01a494ac23 | ||
|
4fd9db8651 | ||
|
01718cf92b | ||
|
96990c8c04 | ||
|
bb9873be4f | ||
|
4970b18c2d | ||
|
92e222f10d | ||
|
76b7e2c6e7 | ||
|
38c90e2c07 | ||
|
938e25b2fa | ||
|
1ecf619892 | ||
|
57b5ea8820 | ||
|
7866880152 | ||
|
6d938bead4 | ||
|
5b16393659 | ||
|
e3b57bd106 | ||
|
2bf6b2c23f | ||
|
384f7c93d7 | ||
|
785e2d8ac5 | ||
|
47ea47f971 | ||
|
c4a2c48eae | ||
|
791e0969e9 | ||
|
302a29db41 | ||
|
2e37c35b41 | ||
|
03df5725f7 | ||
|
9634fdfeb0 | ||
|
5a62ce10b7 | ||
|
cd73606852 | ||
|
507be7bff2 | ||
|
f8342ffa16 | ||
|
79527c690d | ||
|
14d81cff24 | ||
|
62497d1263 | ||
|
57434e2ef7 | ||
|
f5c96f814a | ||
|
d3f95bde05 | ||
|
82d6f47eea | ||
|
3f68febdd7 | ||
|
6d476d5d5d | ||
|
7daa53e47d | ||
|
b0b3ed7238 | ||
|
c7af383ef2 | ||
|
17e9dcf53b | ||
|
91d2c203d3 | ||
|
6a34c24d7d | ||
|
ec51024f93 | ||
|
978a988035 | ||
|
1e7a3e2f4a | ||
|
21290c600f | ||
|
13019ab446 | ||
|
09b7a049ed | ||
|
6fd392bc15 | ||
|
1d19d5578b | ||
|
eca89ea9b4 | ||
|
1f8a95b4da | ||
|
92a96c276b | ||
|
923b9c47c0 | ||
|
3d06020d5d | ||
|
482167f765 | ||
|
a9a0d61a69 | ||
|
50f5dd24b9 | ||
|
212bf0b1d0 | ||
|
64cfd5a8f1 | ||
|
2aded07b56 | ||
|
f31d0aa377 | ||
|
10adc62d90 | ||
|
33bf2c42dd | ||
|
5b75e5d732 | ||
|
c6c1a57c57 | ||
|
c2ce516b4d | ||
|
bd5db08ba5 | ||
|
55e88f2ac6 | ||
|
22b378e0ba | ||
|
31b58308e8 | ||
|
954fc27c54 | ||
|
57098c661e | ||
|
e947e72e09 | ||
|
0a4484133b | ||
|
5b1d3d8b50 | ||
|
aedae876d6 | ||
|
364aca247e | ||
|
a2502bc1c6 | ||
|
dc278db845 | ||
|
d520b96932 | ||
|
af0071dec2 | ||
|
ca63c3d08b | ||
|
74de20641f | ||
|
e44559618f | ||
|
72a9b1ef0e | ||
|
c941a4792d | ||
|
2d75f87def | ||
|
a06681085b | ||
|
f5ef43b020 | ||
|
ae34631bca | ||
|
f4b3ea2a65 | ||
|
46ca19c2cc | ||
|
7ccadadb81 | ||
|
9cff8cd789 | ||
|
b28e744ff7 | ||
|
c55d8ea964 | ||
|
b5639051f4 | ||
|
03d9127a8b | ||
|
23c750e6b9 | ||
|
fd8b431f55 | ||
|
c862c0a6fa | ||
|
8a6099174f | ||
|
a5078ddbc3 | ||
|
e24ec402e4 | ||
|
cd4e86c7bd | ||
|
e6466b76ac | ||
|
a6c3cfeba9 | ||
|
ae4aa4ead9 | ||
|
a77432ab12 | ||
|
e7aed6979f | ||
|
71a1d434ef | ||
|
fe31163a8a | ||
|
e63809400c | ||
|
42f8be1870 | ||
|
118b7e97cf | ||
|
0dd8e75241 | ||
|
780aa64d19 | ||
|
546fdb8ab5 | ||
|
179252303c | ||
|
b267a29d06 | ||
|
059a6325e9 | ||
|
880caa0388 | ||
|
bbe66b4dea | ||
|
8b32d5e86b | ||
|
8248ec4d03 | ||
|
f3b70ce497 | ||
|
f87f809b15 | ||
|
6ff46163b3 | ||
|
7c1315c244 | ||
|
0174233a24 | ||
|
5705115f73 | ||
|
943c910181 | ||
|
d56c46dba6 | ||
|
56b50596cb | ||
|
4141b31992 | ||
|
7b608a6d06 | ||
|
eb9a970353 | ||
|
9ca6632ee9 | ||
|
19df5857b3 | ||
|
ec98767e25 | ||
|
51e498f8c9 | ||
|
4d18bc9aa6 | ||
|
b52a65497e | ||
|
76cfbc8753 | ||
|
3435a8f2df | ||
|
5f1d002339 | ||
|
63313c4675 | ||
|
97d6bd6a36 | ||
|
af92a6bbf2 | ||
|
9f4a293107 | ||
|
cc9c51b856 | ||
|
8be43bb7ea | ||
|
702b786202 | ||
|
d5f5d988fb | ||
|
e64e8e65f1 | ||
|
9f84239c3e | ||
|
d25da2c5ff | ||
|
38c6295a7b | ||
|
f9e8a0abec | ||
|
843bd13281 | ||
|
b9e4a55b91 | ||
|
0bd099d05a | ||
|
c8634d457c | ||
|
7de248e5b1 | ||
|
61ce46d5c7 | ||
|
870f5fea46 | ||
|
2099af5f3a | ||
|
58be6127fb | ||
|
bac138757e | ||
|
2678947f85 | ||
|
682eedba0a | ||
|
1bbf4c49d7 | ||
|
6c04e50ea8 | ||
|
33af34ab12 | ||
|
5219b91388 | ||
|
ec88e02ad4 | ||
|
e8dab6922b | ||
|
01a0435f57 | ||
|
7e1749dbd1 | ||
|
44cab04157 | ||
|
8ea9c6ee9b | ||
|
57d3766c9d | ||
|
bc38411275 | ||
|
8aaacc5b2a | ||
|
995d302a49 | ||
|
c56be37340 | ||
|
545c910d5e | ||
|
b81c628faa | ||
|
1d84aa488c | ||
|
1c4bd7df17 | ||
|
5ab155f8ce | ||
|
fa5300a731 | ||
|
a883f4d430 | ||
|
bedf55d1ad | ||
|
f5f78a549c | ||
|
431ef44c8d | ||
|
e12857392b | ||
|
364f10f03a | ||
|
87e2984fa4 | ||
|
afa76ce059 | ||
|
643f3c81a9 | ||
|
93ade9cf0f | ||
|
18be18ebf8 | ||
|
7eae717b0c | ||
|
f3a0fbf7da | ||
|
d7e1edea01 | ||
|
5331c3363e | ||
|
aa7cefc959 | ||
|
0bfb36afd6 | ||
|
f558e64afc | ||
|
fb80336583 | ||
|
7e42cd97ea | ||
|
a477941b18 | ||
|
81a7300b14 | ||
|
d9becc4e45 | ||
|
ec3b1ec32a | ||
|
22efd7ea93 | ||
|
d0a6dd06d8 | ||
|
9041c21b8b | ||
|
4b2d27e757 | ||
|
620cd6b876 | ||
|
b2483f58c7 | ||
|
efbe553076 | ||
|
f2421b19f0 | ||
|
b9104c4c8a | ||
|
bbf20bb1de | ||
|
a992cb3f31 | ||
|
89fa93bc56 | ||
|
2ffb88c979 | ||
|
c475d0e35c | ||
|
68281e0985 | ||
|
eb055b1f79 | ||
|
896802f793 | ||
|
201d6367de | ||
|
8c35449ce4 | ||
|
ec0b50fd73 | ||
|
60de566446 | ||
|
d307b01832 | ||
|
63ef66198a | ||
|
875a45451a | ||
|
fdfe3a0ed2 | ||
|
97aa52e0db | ||
|
5ff7d114e5 | ||
|
309440caf7 | ||
|
844de1e10f | ||
|
c0f1f932c6 | ||
|
2e4817b144 | ||
|
5fe5ae7f32 | ||
|
514a278574 | ||
|
051f2f567c | ||
|
46db3bf27e | ||
|
fbfc32c4f8 | ||
|
a8859fa6a1 | ||
|
3c82e87d08 | ||
|
73f62d5463 | ||
|
45f846c2d9 | ||
|
b7b4161cd9 | ||
|
0278da8942 | ||
|
596aaf6a7f | ||
|
b81a28114f | ||
|
59413434b0 | ||
|
4c3049a4fa | ||
|
c70e9a444d | ||
|
5073188d71 | ||
|
36cfb1bcbe | ||
|
1106cd89b8 | ||
|
53e88a78f7 | ||
|
b495b83a93 | ||
|
74f26fb3f5 | ||
|
69d0c58172 | ||
|
94a8cd4431 | ||
|
37ab1c5817 | ||
|
6e0b7b7c80 | ||
|
5e356da79a | ||
|
863a43c501 | ||
|
28d50e7d24 | ||
|
0a5928f0bb | ||
|
f40e2228d5 | ||
|
3e94369f6b | ||
|
600ea95b16 | ||
|
a89973d366 | ||
|
f99767e884 | ||
|
223a39ace9 | ||
|
19a083996c | ||
|
a355fc30da | ||
|
3918bdcd61 | ||
|
5d3a805285 | ||
|
3b84142004 | ||
|
9cce0ccc84 | ||
|
30dcca3ea3 | ||
|
133d59e4a7 | ||
|
56f6aabd01 | ||
|
2b0f71d158 | ||
|
1f97a63983 | ||
|
d96e8b44b1 | ||
|
36d09ec919 | ||
|
1d3d18b345 | ||
|
5bf8ff94bc | ||
|
fad0e8eafb | ||
|
7092d19c27 | ||
|
c970c9923f | ||
|
4627405242 | ||
|
4b80a9b397 | ||
|
eed0fdce03 | ||
|
a43829c288 | ||
|
956dac1600 | ||
|
e35e843a29 | ||
|
ba5750a20d | ||
|
9a116c5c29 | ||
|
28f26d0956 | ||
|
ef023d139b | ||
|
f6f8262880 | ||
|
673a92c196 | ||
|
fcf6b887db | ||
|
a4c44fb9ab | ||
|
623a81b51d | ||
|
8a195f829b | ||
|
58c6f2e63f | ||
|
57feafd453 | ||
|
0339ef366f | ||
|
08a6f7b8d0 | ||
|
2ceb73edd7 | ||
|
466c37cfeb | ||
|
17e2dabeae | ||
|
c7f489d988 | ||
|
5c03a29f3a | ||
|
429796aee8 | ||
|
a85386a162 | ||
|
234de7ed2c | ||
|
31dd690cf2 | ||
|
7df893f18a | ||
|
004be8a55a | ||
|
0780863257 | ||
|
c6435dbe6b | ||
|
a309314e16 | ||
|
7b7dc334d8 | ||
|
e8365fc24e | ||
|
287c5afb2a | ||
|
e47df8e6dc | ||
|
761c3e4792 | ||
|
60036873b0 | ||
|
7439afff63 | ||
|
cc2fc2c295 | ||
|
e0d4a46929 | ||
|
aa795c3099 | ||
|
80934ff3f8 | ||
|
50818d162d | ||
|
d2b6f1cc30 | ||
|
1bc30240bd | ||
|
cbdda1e1dc | ||
|
de47dea0c7 | ||
|
7fcfe6a283 | ||
|
f9e74d8758 | ||
|
ba1d72d004 | ||
|
6596e29d9c | ||
|
80b6bcea8a | ||
|
79569fcfde | ||
|
b4dc92b8f1 | ||
|
540a91ccd5 | ||
|
ca80e3ed1f | ||
|
fb3f1a9c16 | ||
|
e28e1bbeb1 | ||
|
ace895a4a2 | ||
|
b2f9663001 | ||
|
1a5f38f6de | ||
|
5c88c8ac2a | ||
|
245254fa57 | ||
|
4cfa152164 | ||
|
77a388e225 | ||
|
54cc704dc6 | ||
|
7dd88a8f82 | ||
|
e1dffaef16 | ||
|
b56ff24a7d | ||
|
4251e3b5e7 | ||
|
59d59b6a7e | ||
|
048d18e3cb | ||
|
85eced56ff | ||
|
a187e06ce3 | ||
|
1533331e1d | ||
|
a756ac7210 | ||
|
7d949c0719 | ||
|
e855966561 | ||
|
e9dfbfb97b | ||
|
e8848d898e | ||
|
5f0996f243 | ||
|
221fee715d | ||
|
4d2d0eac31 | ||
|
0657857981 | ||
|
886d372f38 | ||
|
c436365c07 | ||
|
aa86fd9763 | ||
|
a2e505c33d | ||
|
9d67a19c35 | ||
|
e8c1ffeea7 | ||
|
8654433702 | ||
|
3cc622876c | ||
|
614b5a1a67 | ||
|
e6b23c7f29 | ||
|
13bd53b326 | ||
|
f5d77bdd54 | ||
|
5d83143664 | ||
|
9808ff5923 | ||
|
b09ccdec02 | ||
|
6e8c3279da | ||
|
917520601e | ||
|
c70868d3fa | ||
|
71689e021e | ||
|
c850fb30a9 | ||
|
099dd5a5a0 | ||
|
11c4401bfc | ||
|
b8bac44d43 | ||
|
5680660e5e | ||
|
b2fb45262b | ||
|
1edf282942 | ||
|
debe851546 | ||
|
d3fc148920 | ||
|
ac11db5505 | ||
|
8605001c44 | ||
|
9a40c63cf4 | ||
|
2adc708045 | ||
|
c3d1637615 | ||
|
5405f2b9ec | ||
|
b283748c42 | ||
|
ceb2717bd2 | ||
|
bd5fa9d520 | ||
|
c9ef8f1766 | ||
|
73b92bba86 | ||
|
a2d3908c4d | ||
|
4c52f6c6f5 | ||
|
4111401518 | ||
|
64b3820bd4 | ||
|
8882b53b3b | ||
|
60066bb9ca | ||
|
e33c97116a | ||
|
36b9c99243 | ||
|
80589c9ac5 | ||
|
8181c51da9 | ||
|
becb41d3ce | ||
|
28839575a2 | ||
|
b4232e5fd6 | ||
|
3f7dcc8d2c | ||
|
7740952956 | ||
|
621c44e28c |
190
.circleci/config.yml
Normal file
190
.circleci/config.yml
Normal file
@ -0,0 +1,190 @@
|
||||
version: 2.1
|
||||
orbs:
|
||||
php: circleci/php@1.0.2
|
||||
node: circleci/node@1.1.6
|
||||
aws-cli: circleci/aws-cli@1.0.0
|
||||
jobs:
|
||||
install_composer_packages:
|
||||
docker:
|
||||
- image: 'cimg/base:edge'
|
||||
steps:
|
||||
- checkout
|
||||
- php/install-php:
|
||||
version: '7.0'
|
||||
- php/install-composer
|
||||
|
||||
- run:
|
||||
name: Install php extensions
|
||||
command: |
|
||||
sudo add-apt-repository ppa:ondrej/php
|
||||
sudo apt update
|
||||
sudo apt install php7.0-imap -y
|
||||
sudo apt install php7.0-xml -y
|
||||
sudo apt install zip unzip php7.0-zip php7.0-mbstring -y
|
||||
|
||||
- php/install-packages:
|
||||
app-dir: server/
|
||||
|
||||
- persist_to_workspace:
|
||||
root: .
|
||||
paths:
|
||||
- .
|
||||
|
||||
install_node_packages:
|
||||
docker:
|
||||
- image: circleci/node:11.15.0-stretch
|
||||
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: .
|
||||
|
||||
- restore_cache:
|
||||
keys:
|
||||
- node-cache-{{ checksum "client/package.json" }}
|
||||
|
||||
- run:
|
||||
name: Install dependencies
|
||||
command: |
|
||||
sudo npm install -g npm@6.7.0
|
||||
sudo npm install -g mocha@6.2.0
|
||||
cd client && npm install
|
||||
|
||||
- save_cache:
|
||||
paths:
|
||||
- client/node_modules
|
||||
key: node-cache-{{ checksum "client/package.json" }}
|
||||
|
||||
- persist_to_workspace:
|
||||
root: .
|
||||
paths:
|
||||
- .
|
||||
|
||||
deploy_staging_files:
|
||||
docker:
|
||||
- image: circleci/node:11.15.0-stretch
|
||||
|
||||
environment:
|
||||
- GIT_COMMIT_DESC: git log --format=oneline -n 1 $CIRCLE_SHA1
|
||||
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: .
|
||||
|
||||
- deploy:
|
||||
name: Deploy staging files
|
||||
command: |
|
||||
if [ ! "$CIRCLE_BRANCH" = "master" ]; then exit 0; fi
|
||||
if [[ "$GIT_COMMIT_DESC" = Release* ]]; then exit 0; fi
|
||||
sudo apt update
|
||||
sudo apt install -y lftp
|
||||
make deploy-staging-files
|
||||
make deploy-staging-population
|
||||
|
||||
add_release_commit:
|
||||
docker:
|
||||
- image: circleci/node:11.15.0-stretch
|
||||
|
||||
parameters:
|
||||
version:
|
||||
type: string
|
||||
default: ""
|
||||
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: .
|
||||
|
||||
- add_ssh_keys:
|
||||
fingerprints:
|
||||
- "45:1e:cf:38:3f:9f:97:87:5b:b8:fd:e1:6c:71:11:41"
|
||||
|
||||
- run:
|
||||
name: Commit new version
|
||||
command: |
|
||||
export VERSION=<< parameters.version >>
|
||||
cd version_upgrades/release_script
|
||||
npm i
|
||||
npm run modify-files
|
||||
cd ../..
|
||||
ssh-keyscan -H github.com >> ~/.ssh/known_hosts
|
||||
git config --global user.email "ivan@opensupports.com"
|
||||
git config --global user.name "CircleCI-BOT"
|
||||
git add .
|
||||
git commit -m "Release $VERSION"
|
||||
git checkout -b release-${VERSION}
|
||||
git push origin release-${VERSION}
|
||||
|
||||
- persist_to_workspace:
|
||||
root: .
|
||||
paths:
|
||||
- .
|
||||
|
||||
add_release_tag:
|
||||
docker:
|
||||
- image: circleci/node:11.15.0-stretch
|
||||
|
||||
parameters:
|
||||
version:
|
||||
type: string
|
||||
default: ""
|
||||
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: .
|
||||
|
||||
- add_ssh_keys:
|
||||
fingerprints:
|
||||
- "45:1e:cf:38:3f:9f:97:87:5b:b8:fd:e1:6c:71:11:41"
|
||||
|
||||
- run:
|
||||
name: Add Release tag
|
||||
command: |
|
||||
export VERSION=<< parameters.version >>
|
||||
sudo apt-get update
|
||||
sudo apt-get install lftp
|
||||
make build-release-bundles
|
||||
make upload-bundles
|
||||
# make push-prerelease-tag
|
||||
make populate-staging-release
|
||||
|
||||
- persist_to_workspace:
|
||||
root: .
|
||||
paths:
|
||||
- .
|
||||
|
||||
parameters:
|
||||
version:
|
||||
type: string
|
||||
default: ""
|
||||
run_build:
|
||||
type: boolean
|
||||
default: true
|
||||
|
||||
workflows:
|
||||
build:
|
||||
when:
|
||||
and:
|
||||
- equal: [ master, << pipeline.git.branch >> ]
|
||||
- << pipeline.parameters.run_build >>
|
||||
jobs:
|
||||
- install_composer_packages
|
||||
- install_node_packages:
|
||||
requires:
|
||||
- install_composer_packages
|
||||
- deploy_staging_files:
|
||||
requires:
|
||||
- install_node_packages
|
||||
release:
|
||||
when: << pipeline.parameters.version >>
|
||||
jobs:
|
||||
- install_composer_packages
|
||||
- install_node_packages:
|
||||
requires:
|
||||
- install_composer_packages
|
||||
- add_release_commit:
|
||||
version: << pipeline.parameters.version >>
|
||||
requires:
|
||||
- install_node_packages
|
||||
- add_release_tag:
|
||||
version: << pipeline.parameters.version >>
|
||||
requires:
|
||||
- add_release_commit
|
14
.github/workflows/run-tests.yml
vendored
Normal file
14
.github/workflows/run-tests.yml
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
name: run-tests
|
||||
on: [push]
|
||||
jobs:
|
||||
run-tests:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- run: cd server && make build
|
||||
- run: cd server && make run
|
||||
- run: cd server && make install-not-interactive
|
||||
- run: cd server && make setup-vendor-permissions
|
||||
- run: cd server && make test-not-interactive
|
||||
- run: cd tests && make build
|
||||
- run: cd tests && make run-not-interactive
|
5
.gitignore
vendored
5
.gitignore
vendored
@ -1,11 +1,12 @@
|
||||
.idea
|
||||
.jshintrc
|
||||
tests/Gemfile.lock
|
||||
server/composer.lock
|
||||
.DS_Store
|
||||
server/vendor
|
||||
server/files/
|
||||
!server/files/.gitkeep
|
||||
!server/files/.htaccess
|
||||
server/.dbdata
|
||||
server/.fakemail
|
||||
server/apidoc
|
||||
dist/
|
||||
.env
|
||||
|
@ -4,20 +4,22 @@ php:
|
||||
- '5.6'
|
||||
- '7.0'
|
||||
- '7.1'
|
||||
- '7.2'
|
||||
|
||||
services:
|
||||
- mysql
|
||||
|
||||
before_install:
|
||||
- rvm use 2.2 --install --binary --fuzzy
|
||||
- rvm use 2.3 --install --binary --fuzzy
|
||||
- ruby --version
|
||||
- mysql -e 'CREATE DATABASE development;'
|
||||
- nvm install 6.14.4
|
||||
- npm install -g npm@6.1.0
|
||||
- npm install -g mocha
|
||||
- npm install -g mocha@6.2.0
|
||||
- cd client
|
||||
- npm install
|
||||
- cd ../tests
|
||||
- gem install bundler
|
||||
- bundle install
|
||||
- gem install bacon
|
||||
- cd ../server
|
||||
|
80
DEVELOPMENT.md
Normal file
80
DEVELOPMENT.md
Normal file
@ -0,0 +1,80 @@
|
||||
# Development
|
||||
|
||||
Here is a guide of how to set up the development environment in OpenSupports.
|
||||
|
||||
## Requirements
|
||||
* PHP 5.6+
|
||||
* MySQL 4.1+
|
||||
|
||||
### Getting up and running FRONT-END (client folder)
|
||||
1. Update: `sudo apt update`
|
||||
2. Clone this repo: `git clone https://github.com/opensupports/opensupports.git`
|
||||
3. Install `nvm`: https://github.com/nvm-sh/nvm
|
||||
4. Use node version 11.15.0: `nvm install 11` followed by `nvm use 11`
|
||||
5. Go to client: `cd opensupports/client`
|
||||
6. Install dependencies: `npm install`
|
||||
7. Rebuild node-sass: `npm rebuild node-sass`
|
||||
8. Run: `npm start` (PHP server api it must be running at :8080)
|
||||
10. Go to the main app: `http://localhost:3000/app`
|
||||
11. Your browser will automatically be opened and directed to the browser-sync proxy address.
|
||||
12. Use `npm start-fixtures` to enable fixtures and not require php server to be running.
|
||||
|
||||
OpenSupport uses by default the port 3000, but this port could already be used. If this is the case, you can modify this in the file: `client/webpack.config.js`.
|
||||
|
||||
##### Production Task
|
||||
|
||||
Just as there is a task for development, there is also a `npm build` task for putting the project into a production-ready state. This will run each of the tasks, while also adding the image minification task discussed above and the result store in `dist/` folder.
|
||||
|
||||
**Reminder:** Notice there is `index.html` and `index.php`. The first one searches the backend server where `config.js` says it, the second one uses `/api` to find the server. If you want to run OpenSupports in a single server, then use `index.php`.
|
||||
|
||||
#### Frontend Unit Testing
|
||||
1. Do the steps described before.
|
||||
2. Install mocha: `npm install -g mocha@6.2.0`
|
||||
3. Run `npm test` to run the tests.
|
||||
|
||||
### Getting up and running BACK-END (server folder)
|
||||
1. Install [Docker CE](https://docs.docker.com/install/)
|
||||
2. Go to the server folder: `cd opensupports/server`
|
||||
3. Run `make build` to build the images
|
||||
4. Run `make install` to install composer dependencies
|
||||
|
||||
- `make run` runs the backend and database
|
||||
- `make stop` stop backend and database server
|
||||
- `make log` show live server logs
|
||||
- `make db` access to mysql database console
|
||||
- `make sh` access to backend docker container bash
|
||||
- `make test` run phpunit tests
|
||||
- `make doc` to build the documentation (requires `apidoc`)
|
||||
|
||||
Server api runs on `http://localhost:8080/`
|
||||
Also, there's a *phpmyadmin* instance running on `http://localhost:6060/`,
|
||||
you can access with the username `root` and empty password
|
||||
|
||||
##### Building
|
||||
Once you've installed dependencies for frontend and backend, you can run `./build.sh` and it will generate a zip file inside `dist/` ready for distribution. You can use this file to install OpenSupports on a serving following the [installation instructions](https://github.com/opensupports/opensupports/wiki/Installation)
|
||||
|
||||
##### BACKEND API RUBY TESTING
|
||||
|
||||
1. Go to tests folder: `cd opensupports/tests`
|
||||
2. Run `make build` to install ruby container and its required dependencies
|
||||
|
||||
- `make run` for running tests (database will be cleared)
|
||||
- `make clear` for clearing database
|
||||
|
||||
##### BACKEND FAKE SMTP SERVER
|
||||
If you're doing development, you can use a FakeSMTP server to see the mails that are being sent.
|
||||
|
||||
1. Install Java if you don't have it yet:
|
||||
|
||||
`sudo apt-get install default-jre`
|
||||
`sudo apt-get install default-jdk`
|
||||
|
||||
2. [Download FakeSMTP](https://nilhcem.github.io/FakeSMTP/download.html)
|
||||
|
||||
3. Extract the file from the zip and run it:
|
||||
|
||||
`java -jar fakeSMTP-2.0.jar`
|
||||
|
||||
4. Set the port to 7070 and start the SMTP server.
|
||||
|
||||
5. Every time the application sends an email, it will be reflected there.
|
71
Makefile
Normal file
71
Makefile
Normal file
@ -0,0 +1,71 @@
|
||||
#!make
|
||||
-include .env
|
||||
|
||||
deploy-staging-files:
|
||||
./build.sh
|
||||
mv dist/opensupports_dev.zip .
|
||||
make upload-bundles
|
||||
|
||||
deploy-staging-population:
|
||||
curl --request POST \
|
||||
--url https://circleci.com/api/v2/project/github/opensupports/staging-population/pipeline \
|
||||
--header 'Circle-Token: ${CIRCLE_API_USER_TOKEN}' \
|
||||
--header 'content-type: application/json' \
|
||||
--data '{"branch":"master","parameters":{"server_to_deploy": "dev1"}}'
|
||||
curl --request POST \
|
||||
--url https://circleci.com/api/v2/project/github/opensupports/staging-population/pipeline \
|
||||
--header 'Circle-Token: ${CIRCLE_API_USER_TOKEN}' \
|
||||
--header 'content-type: application/json' \
|
||||
--data '{"branch":"master","parameters":{"server_to_deploy": "dev2"}}'
|
||||
curl --request POST \
|
||||
--url https://circleci.com/api/v2/project/github/opensupports/staging-population/pipeline \
|
||||
--header 'Circle-Token: ${CIRCLE_API_USER_TOKEN}' \
|
||||
--header 'content-type: application/json' \
|
||||
--data '{"branch":"master","parameters":{"server_to_deploy": "dev3"}}'
|
||||
curl --request POST \
|
||||
--url https://circleci.com/api/v2/project/github/opensupports/staging-population/pipeline \
|
||||
--header 'Circle-Token: ${CIRCLE_API_USER_TOKEN}' \
|
||||
--header 'content-type: application/json' \
|
||||
--data '{"branch":"master","parameters":{"server_to_deploy": "dev4"}}'
|
||||
build-release-bundles:
|
||||
$(eval UPGRADE_ZIP="opensupports_v$(VERSION)_update.zip")
|
||||
./build.sh
|
||||
mv dist/opensupports_dev.zip .
|
||||
cp opensupports_dev.zip ${UPGRADE_ZIP} && \
|
||||
mv opensupports_dev.zip opensupports_v${VERSION}.zip && \
|
||||
zip -d ${UPGRADE_ZIP} "api/config.php" && \
|
||||
(( \
|
||||
zip -r ${UPGRADE_ZIP} "version_upgrades/${VERSION}" && \
|
||||
zip -r ${UPGRADE_ZIP} "version_upgrades/mysql_connect.php" \
|
||||
) || true)
|
||||
|
||||
upload-bundles:
|
||||
for file in *.zip ; do \
|
||||
lftp -c "open -u $(FTP_USER),$(FTP_PASSWORD) $(FTP_HOST); set ssl:verify-certificate no; put -O /files/ $${file}"; \
|
||||
done
|
||||
|
||||
push-prerelease-tag:
|
||||
echo -e "Release v${VERSION}\n====\n" > log.txt && \
|
||||
git log $(git describe --tags --abbrev=0 @^)..@ --pretty=format:'%s' >> log.txt
|
||||
# ./version_upgrades/release_script/node_modules/.bin/github-release upload \
|
||||
# --owner opensupports \
|
||||
# --repo opensupports \
|
||||
# --draft true\
|
||||
# --tag "v$(VERSION)" \
|
||||
# --release-name "Release v$(VERSION)" \
|
||||
# --body "$(<log.txt)" \
|
||||
# opensupports_v${VERSION}.zip opensupports_v${VERSION}_update.zip
|
||||
|
||||
populate-staging-release:
|
||||
curl --request POST \
|
||||
--url https://circleci.com/api/v2/project/github/opensupports/staging-population/pipeline \
|
||||
--header 'Circle-Token: ${CIRCLE_API_USER_TOKEN}' \
|
||||
--header 'content-type: application/json' \
|
||||
--data '{"branch":"master","parameters":{"server_to_deploy": "westeros", "version_to_deploy": "${VERSION}"}}'
|
||||
curl --request POST \
|
||||
--url https://circleci.com/api/v2/project/github/opensupports/staging-population/pipeline \
|
||||
--header 'Circle-Token: ${CIRCLE_API_USER_TOKEN}' \
|
||||
--header 'content-type: application/json' \
|
||||
--data '{"branch":"master","parameters":{"server_to_deploy": "senate", "version_to_deploy": "${VERSION}_update"}}'
|
||||
|
||||
deploy-staging-release: build-release-bundles upload-bundles populate-staging-release
|
101
README.md
101
README.md
@ -1,92 +1,61 @@
|
||||

|
||||
<div align="center">
|
||||
|
||||
[](https://travis-ci.org/opensupports/opensupports) v4.2.0
|
||||

|
||||
|
||||
OpenSupports is an open source ticket system built primarily with PHP and ReactJS.
|
||||
Please, visit our website for more information: [http://www.opensupports.com/](http://www.opensupports.com/)
|
||||
OpenSupports is a simple and beautiful open source ticket system. <br />
|
||||
<a href="https://www.opensupports.com/"><strong>Learn more »</strong></a>
|
||||
<br />
|
||||
<p align="center">
|
||||
<a href="https://www.opensupports.com/">Website</a> •
|
||||
<a href="https://docs.opensupports.com/">Docs</a> •
|
||||
<a href="https://opensupports.com/demo/">Demo</a> •
|
||||
<a href="https://www.opensupports.com/pricing/">Official Subscription</a>
|
||||
</p>
|
||||
|
||||
## Requirements
|
||||
* PHP 5.6+
|
||||
* MySQL 4.1+
|
||||
</div>
|
||||
|
||||
## Development
|
||||
Here is a guide of how to set up the development environment in OpenSupports.
|
||||
## 🌱 About the Project
|
||||
|
||||
### Getting up and running FRONT-END (client folder)
|
||||
1. Update: `sudo apt-get update`
|
||||
2. Clone this repo: `git clone https://github.com/opensupports/opensupports.git`
|
||||
3. Install node 4.x version:
|
||||
- `sudo apt-get install curl`
|
||||
- `curl -sL https://deb.nodesource.com/setup_4.x | sudo -E bash -`
|
||||
- `sudo apt-get install -y nodejs`
|
||||
4. Install npm: `sudo apt-get install npm`
|
||||
5. Install gulp: `sudo npm install -g gulp`
|
||||
6. Go to client: `cd opensupports/client`
|
||||
7. Install dependencies: `npm install`
|
||||
8. Rebuild node-sass: `npm rebuild node-sass`
|
||||
9. Run: `gulp dev`
|
||||
10. Go to the main app: `http://localhost:3000/app` or to the component demo `http://localhost:3000/demo`
|
||||
11. Your browser will automatically be opened and directed to the browser-sync proxy address.
|
||||
12. Use `gulp dev --api` to disable fixtures and use the real PHP server api (it must be running at :8080).
|
||||
### What Customers See
|
||||
|
||||
Now that `gulp dev` is running, the server is up as well and serving files from the `/build` directory. Any changes in the `/src` directory will be automatically processed by Gulp and the changes will be injected to any open browsers pointed at the proxy address.
|
||||

|
||||
|
||||
OpenSupport uses by default the port 3000, but this port could already be used. If this is the case, you can modify this in the file: `client/gulp/config.js`.
|
||||
### What Staff Members See
|
||||
|
||||
##### Production Task
|
||||

|
||||
|
||||
Just as there is a `gulp dev` task for development, there is also a `gulp prod` task for putting the project into a production-ready state. This will run each of the tasks, while also adding the image minification task discussed above.
|
||||
## 🙌🏼 Ticket System for Absolutely Everyone
|
||||
|
||||
**Reminder:** Notice there is `index.html` and `index.php`. The first one searches the backend server where `config.js` says it, the second one uses `/api` to find the server. If you want to run OpenSupports in a single server, then use `index.php`.
|
||||
OpenSupports is a simple and beautiful open source ticket system.
|
||||
|
||||
#### Frontend Unit Testing
|
||||
1. Do the steps described before.
|
||||
2. Install mocha: `sudo npm install -g mocha`
|
||||
3. Run `npm test` to run the tests.
|
||||
It is a web application that provides you with a better management of your users’ queries. They send you tickets through OpenSupports and you can handle them appropriately.
|
||||
|
||||
### Getting up and running BACK-END (server folder)
|
||||
1. Install [Docker CE](https://docs.docker.com/install/)
|
||||
2. Go to the server folder: `cd opensupports/server`
|
||||
3. Run `make build` to build the images
|
||||
4. Run `make install` to install composer dependencies
|
||||
Self-hosted, or [hosted by us](https://www.opensupports.com/pricing/), API-driven, and ready to be deployed on your own domain.
|
||||
|
||||
- `make run` runs the backend and database
|
||||
- `make stop` stop backend and database server
|
||||
- `make log` show live server logs
|
||||
- `make db` access to mysql database console
|
||||
- `make sh` access to backend docker container bash
|
||||
- `make test` run phpunit tests
|
||||
- `make doc` to build the documentation (requires `apidoc`)
|
||||
## 🧐 Stay Up-to-Date
|
||||
|
||||
Server api runs on `http://localhost:8080/`
|
||||
Also, there's a *phpmyadmin* instance running on `http://localhost:6060/`,
|
||||
you can access with the username `root` and empty password
|
||||
OpenSupports is growing and steadily incorporating new features. You might want to **add a star to the project** (or watch updates) to be notified about new releases.
|
||||
|
||||
##### Building
|
||||
Once you've installed dependencies for frontend and backend, you can run `./build.sh` and it will generate a zip file inside `dist/` ready for distribution. You can use this file to install OpenSupports on a serving following the [installation instructions](https://github.com/opensupports/opensupports/wiki/Installation)
|
||||
## 💪🏼 Features
|
||||
|
||||
##### BACKEND API RUBY TESTING
|
||||
Check out our [most important features](https://opensupports.com/features) at our website.
|
||||
|
||||
1. Go to tests folder: `cd opensupports/tests`
|
||||
2. Run `make install` to install ruby and its the required dependencies
|
||||
Are we missing something? [Suggest an improvement](https://github.com/opensupports/opensupports/issues/new)!
|
||||
|
||||
- `make run` for running tests (database will be cleared)
|
||||
- `make clear` for clearing database
|
||||
## 🛠 Install
|
||||
|
||||
##### BACKEND FAKE SMTP SERVER
|
||||
If you're doing development, you can use a FakeSMTP server to see the mails that are being sent.
|
||||
OpenSupports can be hosted on your own servers, or [hosted by us](https://www.opensupports.com/pricing/).
|
||||
|
||||
1. Install Java if you don't have it yet:
|
||||
There are multiple benefits to having the system hosted by its creators, including official support into any problem you might encounter.
|
||||
|
||||
`sudo apt-get install default-jre`
|
||||
`sudo apt-get install default-jdk`
|
||||
But in the case you prefer your development team to deal with the installation, maintenance (upgrades, backups, etc.), and integrations, we charge you nothing for it, OpenSupports is **free and open-source**!
|
||||
|
||||
2. [Download FakeSMTP](https://nilhcem.github.io/FakeSMTP/download.html)
|
||||
Check out our [installation guide](https://docs.opensupports.com/guides/installation/).
|
||||
|
||||
3. Extract the file from the zip and run it:
|
||||
## 👨🏼💻 Development
|
||||
|
||||
`java -jar fakeSMTP-2.0.jar`
|
||||
Are you a programmer? You can help us to fix bugs and build OpenSupports' features!
|
||||
|
||||
4. Set the port to 7070 and start the SMTP server.
|
||||
Check out our [development guide](./DEVELOPMENT.md) to get your development environment up and running.
|
||||
|
||||
5. Every time the application sends an email, it will be reflected there.
|
||||
And even if you are not a programmer, you can help us by [reporting problems or suggesting improvements](https://github.com/opensupports/opensupports/issues/new), we love feedback and learn a lot from it!
|
||||
|
19
SECURITY.md
Normal file
19
SECURITY.md
Normal file
@ -0,0 +1,19 @@
|
||||
# Security Policy
|
||||
This document is intended to provide a guide to properly disclosure security issues found in our open source software.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
If you find a vulnerability or potential security issue in OpenSupports. Please contact us at contact@opensupports.com
|
||||
|
||||
We will acknowledge your email within 48 hours, and will send a more detailed response within 48 hours indicating the next steps in handling your report. After the initial reply to your report, we will endeavor to keep you informed of the progress towards a fix and full announcement, and may ask for additional information or guidance.
|
||||
|
||||
## Disclosure Policy
|
||||
|
||||
When we receive a security bug report, we will assign it to a
|
||||
primary handler. This person will coordinate the fix and release process,
|
||||
involving the following steps:
|
||||
|
||||
* Confirm the problem and determine the affected versions.
|
||||
* Audit code to find any potential similar problems.
|
||||
* Prepare fixes for all releases still under maintenance. These fixes will be
|
||||
released as fast as possible in a new OpenSupports version.
|
39
build.sh
39
build.sh
@ -1,38 +1,37 @@
|
||||
echo "1/3 Building frontend..."
|
||||
cd client
|
||||
gulp prod --api
|
||||
npm run build
|
||||
rm build/index.html
|
||||
echo "2/3 Creating api folder..."
|
||||
cd ../server
|
||||
rm -rf files
|
||||
mkdir files
|
||||
echo -n > config.php
|
||||
mkdir files2
|
||||
mv files/.htaccess files2
|
||||
rm -rf files/
|
||||
mv files2 files
|
||||
cd ..
|
||||
mkdir api
|
||||
cp server/index.php api
|
||||
cp server/.htaccess api
|
||||
cp server/composer.json api
|
||||
cp server/composer.lock api
|
||||
cp -R server/controllers api
|
||||
cp -R server/data api
|
||||
cp -R server/libs api
|
||||
cp -R server/models api
|
||||
cp -R server/vendor api
|
||||
mkdir api/files
|
||||
touch api/files/.keep
|
||||
echo -n > api/config.php
|
||||
mv server/index.php api
|
||||
mv server/.htaccess api
|
||||
mv server/composer.json api
|
||||
mv server/composer.lock api
|
||||
mv server/controllers api
|
||||
mv server/data api
|
||||
mv server/libs api
|
||||
mv server/models api
|
||||
mv server/vendor api
|
||||
mv server/files api
|
||||
cp server/config.php api
|
||||
chmod -R 755 .
|
||||
cp client/src/index.php client/build
|
||||
echo "3/3 Generating zip..."
|
||||
cd client/build
|
||||
zip opensupports_dev.zip index.php
|
||||
zip -u opensupports_dev.zip .htaccess
|
||||
zip -u opensupports_dev.zip css/main.css
|
||||
zip -u opensupports_dev.zip js/main.js
|
||||
zip -ur opensupports_dev.zip fonts
|
||||
zip -u opensupports_dev.zip bundle.js
|
||||
zip -ur opensupports_dev.zip images
|
||||
mv opensupports_dev.zip ../..
|
||||
cd ../..
|
||||
zip -ur opensupports_dev.zip api
|
||||
rm -rf dist
|
||||
mkdir dist
|
||||
mv opensupports_dev.zip dist
|
||||
rm -rf api
|
||||
|
@ -1,3 +1,4 @@
|
||||
{
|
||||
"optional": ["es7.classProperties"]
|
||||
}
|
||||
"presets": ["@babel/preset-env", "@babel/preset-react"],
|
||||
"plugins": ["@babel/plugin-proposal-class-properties", "add-module-exports"]
|
||||
}
|
||||
|
11
client/Makefile
Normal file
11
client/Makefile
Normal file
@ -0,0 +1,11 @@
|
||||
build:
|
||||
@docker pull node:11.15.0
|
||||
|
||||
run: stop
|
||||
@docker run --platform=linux/amd64 --network os-net --name opensupports-client -v $(PWD):/client:delegated -p 3000:3000 node:11.15.0 sh -c "cd client && npm install && npm start"
|
||||
|
||||
sh: stop
|
||||
@docker run -it --platform=linux/amd64 --network os-net --name opensupports-client -v $(PWD):/client:delegated -p 3000:3000 node:11.15.0 sh -c "bash"
|
||||
|
||||
stop:
|
||||
@docker rm -f opensupports-client || true
|
@ -1,31 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
|
||||
'serverport': 3006,
|
||||
|
||||
'scripts': {
|
||||
'src': './src/*.js',
|
||||
'dest': './build/js/'
|
||||
},
|
||||
|
||||
'images': {
|
||||
'src': './src/assets/images/**/*.{jpeg,jpg,png}',
|
||||
'dest': './build/images/'
|
||||
},
|
||||
|
||||
'styles': {
|
||||
'src': './src/**/*.scss',
|
||||
'dest': './build/css/'
|
||||
},
|
||||
|
||||
'fonts': {
|
||||
'src': './src/scss/font_awesome/fonts/*',
|
||||
'dest': './build/fonts/'
|
||||
},
|
||||
|
||||
'sourceDir': './src/',
|
||||
|
||||
'buildDir': './build/'
|
||||
|
||||
};
|
@ -1,11 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var fs = require('fs');
|
||||
var onlyScripts = require('./util/script-filter');
|
||||
var tasks = fs.readdirSync('./gulp/tasks/').filter(onlyScripts);
|
||||
|
||||
process.env.NODE_PATH = './src';
|
||||
|
||||
tasks.forEach(function(task) {
|
||||
require('./tasks/' + task);
|
||||
});
|
@ -1,14 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var config = require('../config');
|
||||
var browserSync = require('browser-sync');
|
||||
var gulp = require('gulp');
|
||||
|
||||
gulp.task('browserSync', function() {
|
||||
|
||||
browserSync({
|
||||
proxy: 'localhost:' + config.serverport,
|
||||
startPath: '/'
|
||||
});
|
||||
|
||||
});
|
@ -1,78 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var gulp = require('gulp');
|
||||
var gulpif = require('gulp-if');
|
||||
var gutil = require('gulp-util');
|
||||
var source = require('vinyl-source-stream');
|
||||
var streamify = require('gulp-streamify');
|
||||
var sourcemaps = require('gulp-sourcemaps');
|
||||
var rename = require('gulp-rename');
|
||||
var watchify = require('watchify');
|
||||
var browserify = require('browserify');
|
||||
var babelify = require('babelify');
|
||||
var uglify = require('gulp-uglify');
|
||||
var browserSync = require('browser-sync');
|
||||
var debowerify = require('debowerify');
|
||||
var handleErrors = require('../util/handle-errors');
|
||||
var config = require('../config');
|
||||
var util = require('gulp-util');
|
||||
|
||||
// Based on: http://blog.avisi.nl/2014/04/25/how-to-keep-a-fast-build-with-browserify-and-reactjs/
|
||||
function buildScript(file, watch) {
|
||||
|
||||
var bundler = browserify({
|
||||
entries: [config.sourceDir + file],
|
||||
debug: !global.isProd,
|
||||
insertGlobalVars: {
|
||||
isProd: function () {
|
||||
return (global.isProd) ? "'enabled'" : "'disabled'";
|
||||
},
|
||||
noFixtures: function() {
|
||||
return (util.env['api']) ? "'enabled'" : "'disabled'";
|
||||
}
|
||||
},
|
||||
cache: {},
|
||||
packageCache: {},
|
||||
fullPaths: false
|
||||
});
|
||||
|
||||
if ( watch ) {
|
||||
bundler = watchify(bundler);
|
||||
bundler.on('update', rebundle);
|
||||
}
|
||||
|
||||
bundler.transform(babelify, {'optional': ['es7.classProperties']});
|
||||
bundler.transform(debowerify);
|
||||
|
||||
function rebundle() {
|
||||
var stream = bundler.bundle();
|
||||
|
||||
gutil.log('Rebundle...');
|
||||
|
||||
return stream.on('error', handleErrors)
|
||||
.pipe(source(file))
|
||||
.pipe(gulpif(global.isProd, streamify(uglify())))
|
||||
.pipe(streamify(rename({
|
||||
basename: 'main'
|
||||
})))
|
||||
.pipe(gulpif(!global.isProd, sourcemaps.write('./')))
|
||||
.pipe(gulp.dest(config.scripts.dest))
|
||||
.pipe(gulpif(browserSync.active, browserSync.reload({ stream: true, once: true })));
|
||||
}
|
||||
|
||||
return rebundle();
|
||||
|
||||
}
|
||||
|
||||
gulp.task('browserify', function() {
|
||||
|
||||
// Only run watchify if NOT production
|
||||
return buildScript('index.js', !global.isProd);
|
||||
|
||||
});
|
||||
|
||||
gulp.task('config', function() {
|
||||
|
||||
return gulp.src(config.sourceDir + 'config.js')
|
||||
.pipe(gulp.dest(config.scripts.dest))
|
||||
});
|
@ -1,11 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var config = require('../config');
|
||||
var gulp = require('gulp');
|
||||
var del = require('del');
|
||||
|
||||
gulp.task('clean', function(cb) {
|
||||
|
||||
del([config.buildDir], cb);
|
||||
|
||||
});
|
@ -1,10 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var gulp = require('gulp');
|
||||
var config = require('../config');
|
||||
|
||||
gulp.task('copyFonts', function() {
|
||||
|
||||
return gulp.src(config.fonts.src)
|
||||
.pipe(gulp.dest(config.fonts.dest))
|
||||
});
|
@ -1,11 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var gulp = require('gulp');
|
||||
|
||||
gulp.task('copyIcons', function() {
|
||||
|
||||
// Copy icons from root directory to build/
|
||||
return gulp.src(['./*.png', './favicon.ico'])
|
||||
.pipe(gulp.dest('build/'));
|
||||
|
||||
});
|
@ -1,12 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var gulp = require('gulp');
|
||||
var config = require('../config');
|
||||
|
||||
gulp.task('copyIndex', function() {
|
||||
|
||||
gulp.src(config.sourceDir + 'index.html').pipe(gulp.dest(config.buildDir));
|
||||
gulp.src(config.sourceDir + 'index.php').pipe(gulp.dest(config.buildDir));
|
||||
gulp.src(config.sourceDir + '.htaccess').pipe(gulp.dest(config.buildDir));
|
||||
|
||||
});
|
@ -1,10 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var gulp = require('gulp');
|
||||
//var config = require('../config');
|
||||
|
||||
gulp.task('deploy', ['prod'], function() {
|
||||
|
||||
// Deploy to hosting environment
|
||||
|
||||
});
|
@ -1,15 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var gulp = require('gulp');
|
||||
var runSequence = require('run-sequence');
|
||||
|
||||
gulp.task('dev', ['clean'], function(callback) {
|
||||
|
||||
callback = callback || function() {};
|
||||
|
||||
global.isProd = false;
|
||||
|
||||
// Run all tasks once
|
||||
return runSequence(['sass', 'imagemin', 'browserify', 'copyFonts', 'copyIndex', 'copyIcons', 'config'], 'watch', callback);
|
||||
|
||||
});
|
@ -1,17 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var gulp = require('gulp');
|
||||
var gulpif = require('gulp-if');
|
||||
var imagemin = require('gulp-imagemin');
|
||||
var browserSync = require('browser-sync');
|
||||
var config = require('../config');
|
||||
|
||||
gulp.task('imagemin', function() {
|
||||
|
||||
// Run imagemin task on all images
|
||||
return gulp.src(config.images.src)
|
||||
.pipe(gulpif(global.isProd, imagemin()))
|
||||
.pipe(gulp.dest(config.images.dest))
|
||||
.pipe(gulpif(browserSync.active, browserSync.reload({ stream: true, once: true })));
|
||||
|
||||
});
|
@ -1,15 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var gulp = require('gulp');
|
||||
var runSequence = require('run-sequence');
|
||||
|
||||
gulp.task('prod', ['clean'], function(callback) {
|
||||
process.env.NODE_ENV = 'production';
|
||||
|
||||
callback = callback || function() {};
|
||||
|
||||
global.isProd = true;
|
||||
|
||||
runSequence(['sass', 'imagemin', 'browserify', 'copyFonts', 'copyIndex', 'copyIcons'], callback);
|
||||
|
||||
});
|
@ -1,29 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var gulp = require('gulp');
|
||||
var sass = require('gulp-sass');
|
||||
var gulpif = require('gulp-if');
|
||||
var browserSync = require('browser-sync');
|
||||
var autoprefixer = require('gulp-autoprefixer');
|
||||
var bulkSass = require('gulp-sass-bulk-import');
|
||||
var plumber = require('gulp-plumber');
|
||||
var handleErrors = require('../util/handle-errors');
|
||||
var config = require('../config');
|
||||
|
||||
gulp.task('sass', function () {
|
||||
|
||||
return gulp.src(config.styles.src)
|
||||
.pipe(bulkSass())
|
||||
.pipe(plumber())
|
||||
.pipe(sass({
|
||||
sourceComments: global.isProd ? 'none' : 'map',
|
||||
sourceMap: 'sass',
|
||||
outputStyle: global.isProd ? 'compressed' : 'nested'
|
||||
}))
|
||||
.on('error', handleErrors)
|
||||
.pipe(autoprefixer("last 2 versions", "> 1%", "ie 8"))
|
||||
.on('error', handleErrors)
|
||||
.pipe(gulp.dest(config.styles.dest))
|
||||
.pipe(gulpif(browserSync.active, browserSync.reload({ stream: true })));
|
||||
|
||||
});
|
@ -1,44 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var config = require('../config');
|
||||
var http = require('http');
|
||||
var express = require('express');
|
||||
var gulp = require('gulp');
|
||||
var gutil = require('gulp-util');
|
||||
var morgan = require('morgan');
|
||||
var proxy = require('express-http-proxy');
|
||||
|
||||
gulp.task('server', function() {
|
||||
|
||||
var server = express();
|
||||
|
||||
// log all requests to the console
|
||||
server.use(morgan('dev'));
|
||||
server.use(express.static(config.buildDir));
|
||||
|
||||
// Proxy php server api
|
||||
server.use('/api', proxy('http://localhost:8080', {
|
||||
forwardPath: function(req, res) {
|
||||
return require('url').parse(req.url).path;
|
||||
}
|
||||
}));
|
||||
|
||||
// Serve index.html for all routes to leave routing up to react-router
|
||||
server.all('/*', function(req, res) {
|
||||
res.sendFile('index.html', { root: 'build' });
|
||||
});
|
||||
|
||||
// Start webserver if not already running
|
||||
var s = http.createServer(server);
|
||||
s.on('error', function(err){
|
||||
if(err.code === 'EADDRINUSE'){
|
||||
gutil.log('Development server is already started at port ' + config.serverport);
|
||||
}
|
||||
else {
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
|
||||
s.listen(config.serverport);
|
||||
|
||||
});
|
@ -1,10 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var gulp = require('gulp');
|
||||
//var config = require('../config');
|
||||
|
||||
gulp.task('test', function() {
|
||||
|
||||
// Run all tests
|
||||
|
||||
});
|
@ -1,13 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var gulp = require('gulp');
|
||||
var config = require('../config');
|
||||
|
||||
gulp.task('watch', ['browserSync', 'server'], function() {
|
||||
|
||||
// Scripts are automatically watched by Watchify inside Browserify task
|
||||
gulp.watch(config.styles.src, ['sass']);
|
||||
gulp.watch(config.images.src, ['imagemin']);
|
||||
gulp.watch(config.sourceDir + 'index.html', ['copyIndex']);
|
||||
|
||||
});
|
@ -1,27 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var notify = require('gulp-notify');
|
||||
|
||||
module.exports = function(error) {
|
||||
|
||||
if( !global.isProd ) {
|
||||
|
||||
var args = Array.prototype.slice.call(arguments);
|
||||
|
||||
// Send error to notification center with gulp-notify
|
||||
notify.onError({
|
||||
title: 'Compile Error',
|
||||
message: '<%= error.message %>'
|
||||
}).apply(this, args);
|
||||
|
||||
// Keep gulp from hanging on this task
|
||||
this.emit('end');
|
||||
|
||||
} else {
|
||||
// Log the error and stop the process
|
||||
// to prevent broken code from building
|
||||
console.log(error);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
};
|
@ -1,9 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var path = require('path');
|
||||
|
||||
// Filters out non .coffee and .js files. Prevents
|
||||
// accidental inclusion of possible hidden files
|
||||
module.exports = function(name) {
|
||||
return /(\.(js|coffee)$)/i.test(path.extname(name));
|
||||
};
|
@ -1,5 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
global.isProd = false;
|
||||
|
||||
require('./gulp');
|
21698
client/package-lock.json
generated
Normal file
21698
client/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "OpenSupports",
|
||||
"version": "4.2.0",
|
||||
"version": "4.11.0",
|
||||
"author": "Ivan Diaz <contact@opensupports.com>",
|
||||
"description": "Open source ticket system made with PHP and ReactJS",
|
||||
"repository": {
|
||||
@ -9,69 +9,83 @@
|
||||
},
|
||||
"private": false,
|
||||
"engines": {
|
||||
"node": "^0.12.x",
|
||||
"npm": "^2.1.x"
|
||||
"node": "^11.15.x",
|
||||
"npm": "^6.7.x"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "export NODE_PATH=src && mocha src/lib-test/preprocessor.js --compilers js:babel-core/register --recursive src/**/**/__tests__/*-test.js"
|
||||
"start": "webpack-dev-server --display-reasons --display-error-details --history-api-fallback --progress --colors",
|
||||
"start-fixtures": "webpack-dev-server --env.FIXTURES=1",
|
||||
"build": "./node_modules/.bin/rimraf build && NODE_ENV=production ./node_modules/.bin/webpack -p --devtool none",
|
||||
"test": "export NODE_PATH=src && mocha src/lib-test/preprocessor.js --require @babel/register --recursive src/**/**/__tests__/*-test.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.5.5",
|
||||
"@babel/core": "^7.5.5",
|
||||
"@babel/node": "^7.5.5",
|
||||
"@babel/plugin-proposal-class-properties": "^7.4.4",
|
||||
"@babel/plugin-transform-modules-commonjs": "^7.5.0",
|
||||
"@babel/preset-env": "^7.5.5",
|
||||
"@babel/preset-react": "^7.0.0",
|
||||
"@babel/register": "^7.5.5",
|
||||
"axios-mock-adapter": "^1.15.0",
|
||||
"babel-core": "^5.8.22",
|
||||
"babel-plugin-transform-class-properties": "^6.11.5",
|
||||
"babel-register": "^6.7.2",
|
||||
"babelify": "^6.1.x",
|
||||
"browser-sync": "^2.7.13",
|
||||
"browserify": "^10.2.6",
|
||||
"babel-loader": "^8.0.6",
|
||||
"babel-plugin-add-module-exports": "^1.0.2",
|
||||
"browser-sync": "^2.27.5",
|
||||
"chai": "^3.5.0",
|
||||
"copy-webpack-plugin": "^5.0.3",
|
||||
"css-loader": "^3.0.0",
|
||||
"debowerify": "^1.3.1",
|
||||
"del": "^1.2.0",
|
||||
"eslint": "^5.16.0",
|
||||
"eslint-loader": "^2.1.2",
|
||||
"express": "^4.13.1",
|
||||
"express-http-proxy": "^0.6.0",
|
||||
"gulp": "^3.9.0",
|
||||
"gulp-autoprefixer": "^2.3.1",
|
||||
"gulp-connect-php": "0.0.5",
|
||||
"gulp-if": "^1.2.5",
|
||||
"gulp-imagemin": "^2.3.0",
|
||||
"gulp-notify": "^2.2.0",
|
||||
"gulp-plumber": "^1.2.0",
|
||||
"gulp-rename": "^1.2.2",
|
||||
"gulp-sass": "^4.0.1",
|
||||
"gulp-sass-bulk-import": "^1.0.1",
|
||||
"gulp-sourcemaps": "^1.5.2",
|
||||
"gulp-streamify": "0.0.5",
|
||||
"gulp-uglify": "^2.1.2",
|
||||
"gulp-util": "^3.0.6",
|
||||
"file-loader": "^4.0.0",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"humps": "^0.6.0",
|
||||
"jsdom": "^8.4.1",
|
||||
"mocha": "^6.2.0",
|
||||
"morgan": "^1.6.1",
|
||||
"node-sass": "^4.12.0",
|
||||
"nodemon": "^1.19.1",
|
||||
"path": "^0.12.7",
|
||||
"proxyquire": "^1.7.4",
|
||||
"react-addons-test-utils": "^15.0.1",
|
||||
"rimraf": "^2.6.3",
|
||||
"run-sequence": "^1.1.1",
|
||||
"sass-loader": "^7.1.0",
|
||||
"sinon": "^1.17.3",
|
||||
"sinon-chai": "^2.8.0",
|
||||
"style-loader": "^0.23.1",
|
||||
"stylelint-webpack-plugin": "^0.10.5",
|
||||
"vinyl-source-stream": "^1.1.0",
|
||||
"watchify": "^3.2.x"
|
||||
"watchify": "^3.2.x",
|
||||
"webpack": "^4.34.0",
|
||||
"webpack-bundle-analyzer": "^3.3.2",
|
||||
"webpack-cli": "^3.3.4",
|
||||
"webpack-dev-server": "^3.7.1",
|
||||
"webpack-import-glob": "^2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"app-module-path": "^1.0.3",
|
||||
"axios": "^0.18.0",
|
||||
"chart.js": "^2.4.0",
|
||||
"axios": "^0.21.1",
|
||||
"chart.js": "^2.9.3",
|
||||
"classnames": "^2.2.5",
|
||||
"history": "^3.0.0",
|
||||
"html-to-text": "^4.0.0",
|
||||
"keycode": "^2.1.4",
|
||||
"localStorage": "^1.0.3",
|
||||
"lodash": "^3.10.0",
|
||||
"lodash": "^4.17.21",
|
||||
"messageformat": "^0.2.2",
|
||||
"moment": "^2.27.0",
|
||||
"qs": "^6.5.2",
|
||||
"query-string": "^6.12.1",
|
||||
"quill-image-resize-module-react": "^3.0.0",
|
||||
"quill-magic-url": "^4.1.3",
|
||||
"random-string": "^0.2.0",
|
||||
"react": "^15.4.2",
|
||||
"react-chartjs-2": "^2.0.0",
|
||||
"react-chartjs-2": "^2.10.0",
|
||||
"react-document-title": "^1.0.2",
|
||||
"react-dom": "^15.4.2",
|
||||
"react-draft-wysiwyg": "^1.12.13",
|
||||
"react-google-recaptcha": "^0.5.2",
|
||||
"react-motion": "^0.4.7",
|
||||
"react-quill": "^1.3.1",
|
||||
|
@ -1,5 +1,5 @@
|
||||
export default {
|
||||
login: stub(),
|
||||
logout: stub(),
|
||||
initSession: stub()
|
||||
checkSession: stub()
|
||||
};
|
@ -1,163 +1,160 @@
|
||||
const sessionStoreMock = require('lib-app/__mocks__/session-store-mock');
|
||||
const APICallMock = require('lib-app/__mocks__/api-call-mock');
|
||||
const storeMock = {
|
||||
dispatch: stub().returns({
|
||||
then: stub()
|
||||
})
|
||||
};
|
||||
// const sessionStoreMock = require('lib-app/__mocks__/session-store-mock');
|
||||
// const APICallMock = require('lib-app/__mocks__/api-call-mock');
|
||||
// const storeMock = require('app/__mocks__/store-mock');
|
||||
|
||||
const SessionActions = requireUnit('actions/session-actions', {
|
||||
'lib-app/api-call': APICallMock,
|
||||
'lib-app/session-store': sessionStoreMock,
|
||||
'app/store': storeMock
|
||||
});
|
||||
// const SessionActions = requireUnit('actions/session-actions', {
|
||||
// 'lib-app/api-call': APICallMock,
|
||||
// 'lib-app/session-store': sessionStoreMock,
|
||||
// 'app/store': storeMock
|
||||
// });
|
||||
|
||||
describe('Session Actions,', function () {
|
||||
// describe('Session Actions,', function () {
|
||||
|
||||
describe('login action', function () {
|
||||
it('should return LOGIN with with a result promise', function () {
|
||||
APICallMock.call.returns({
|
||||
then: function (resolve) {
|
||||
resolve({
|
||||
data: {
|
||||
userId: 14,
|
||||
token: 'SOME_TOKEN'
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
// describe('login action', function () {
|
||||
// it('should return LOGIN with with a result promise', function () {
|
||||
// APICallMock.call.returns({
|
||||
// then: function (resolve) {
|
||||
// resolve({
|
||||
// data: {
|
||||
// userId: 14,
|
||||
// token: 'SOME_TOKEN'
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
// });
|
||||
|
||||
let loginData = {
|
||||
email: 'SOME_EMAIL',
|
||||
password: 'SOME_PASSWORD',
|
||||
remember: false
|
||||
};
|
||||
// let loginData = {
|
||||
// email: 'SOME_EMAIL',
|
||||
// password: 'SOME_PASSWORD',
|
||||
// remember: false
|
||||
// };
|
||||
|
||||
expect(SessionActions.login(loginData).type).to.equal('LOGIN');
|
||||
expect(storeMock.dispatch).to.have.been.calledWithMatch({type: 'USER_DATA'});
|
||||
expect(APICallMock.call).to.have.been.calledWith({
|
||||
path: '/user/get',
|
||||
data: {
|
||||
csrf_userid: 14,
|
||||
csrf_token: 'SOME_TOKEN'
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
// expect(SessionActions.login(loginData).type).to.equal('LOGIN');
|
||||
// expect(storeMock.dispatch).to.have.been.calledWithMatch({type: 'USER_DATA'});
|
||||
// expect(APICallMock.call).to.have.been.calledWith({
|
||||
// path: '/user/get',
|
||||
// data: {
|
||||
// csrf_userid: 14,
|
||||
// csrf_token: 'SOME_TOKEN'
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
|
||||
describe('autoLogin action', function () {
|
||||
it('should return LOGIN_AUTO with remember data from sessionStore', function () {
|
||||
APICallMock.call.returns({
|
||||
then: function (resolve) {
|
||||
resolve({
|
||||
data: {
|
||||
userId: 14
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
sessionStoreMock.getRememberData.returns({
|
||||
token: 'SOME_TOKEN',
|
||||
userId: 'SOME_ID',
|
||||
expiration: 'SOME_EXPIRATION'
|
||||
});
|
||||
// describe('autoLogin action', function () {
|
||||
// it('should return LOGIN_AUTO with remember data from sessionStore', function () {
|
||||
// APICallMock.call.returns({
|
||||
// then: function (resolve) {
|
||||
// resolve({
|
||||
// data: {
|
||||
// userId: 14
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
// });
|
||||
// sessionStoreMock.getRememberData.returns({
|
||||
// token: 'SOME_TOKEN',
|
||||
// userId: 'SOME_ID',
|
||||
// expiration: 'SOME_EXPIRATION'
|
||||
// });
|
||||
|
||||
expect(SessionActions.autoLogin().type).to.equal('LOGIN_AUTO');
|
||||
expect(storeMock.dispatch).to.have.been.calledWithMatch({type: 'USER_DATA'});
|
||||
expect(APICallMock.call).to.have.been.calledWith({
|
||||
path: '/user/login',
|
||||
data: {
|
||||
rememberToken: 'SOME_TOKEN',
|
||||
userId: 'SOME_ID',
|
||||
isAutomatic: true
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
// expect(SessionActions.autoLogin().type).to.equal('LOGIN_AUTO');
|
||||
// expect(storeMock.dispatch).to.have.been.calledWithMatch({type: 'USER_DATA'});
|
||||
// expect(APICallMock.call).to.have.been.calledWith({
|
||||
// path: '/user/login',
|
||||
// data: {
|
||||
// rememberToken: 'SOME_TOKEN',
|
||||
// userId: 'SOME_ID',
|
||||
// remember: 1,
|
||||
// isAutomatic: 1
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
|
||||
describe('logout action', function () {
|
||||
it('should return LOGOUT and call /user/logout', function () {
|
||||
APICallMock.call.returns('API_RESULT');
|
||||
APICallMock.call.reset();
|
||||
// describe('logout action', function () {
|
||||
// it('should return LOGOUT and call /user/logout', function () {
|
||||
// APICallMock.call.returns('API_RESULT');
|
||||
// APICallMock.call.reset();
|
||||
|
||||
expect(SessionActions.logout()).to.deep.equal({
|
||||
type: 'LOGOUT',
|
||||
payload: 'API_RESULT'
|
||||
});
|
||||
expect(APICallMock.call).to.have.been.calledWith({
|
||||
path: '/user/logout',
|
||||
data: {}
|
||||
});
|
||||
});
|
||||
});
|
||||
// expect(SessionActions.logout()).to.deep.equal({
|
||||
// type: 'LOGOUT',
|
||||
// payload: 'API_RESULT'
|
||||
// });
|
||||
// expect(APICallMock.call).to.have.been.calledWith({
|
||||
// path: '/user/logout',
|
||||
// data: {}
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
|
||||
describe('initSession action', function () {
|
||||
beforeEach(function () {
|
||||
APICallMock.call.returns({
|
||||
then: function (resolve) {
|
||||
resolve({
|
||||
data: {
|
||||
sessionActive: false
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
APICallMock.call.reset();
|
||||
storeMock.dispatch.reset();
|
||||
sessionStoreMock.isLoggedIn.returns(true)
|
||||
});
|
||||
// describe('checkSession action', function () {
|
||||
// beforeEach(function () {
|
||||
// APICallMock.call.returns({
|
||||
// then: function (resolve) {
|
||||
// resolve({
|
||||
// data: {
|
||||
// sessionActive: false
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
// });
|
||||
// APICallMock.call.reset();
|
||||
// storeMock.dispatch.reset();
|
||||
// sessionStoreMock.isLoggedIn.returns(true)
|
||||
// });
|
||||
|
||||
after(function () {
|
||||
APICallMock.call.returns(new Promise(function (resolve) {
|
||||
resolve({
|
||||
data: {
|
||||
sessionActive: true
|
||||
}
|
||||
});
|
||||
}));
|
||||
});
|
||||
// after(function () {
|
||||
// APICallMock.call.returns(new Promise(function (resolve) {
|
||||
// resolve({
|
||||
// data: {
|
||||
// sessionActive: true
|
||||
// }
|
||||
// });
|
||||
// }));
|
||||
// });
|
||||
|
||||
it('should return CHECK_SESSION and dispatch SESSION_CHECKED if session is active', function () {
|
||||
APICallMock.call.returns({
|
||||
then: function (resolve) {
|
||||
resolve({
|
||||
data: {
|
||||
sessionActive: true
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
// it('should return CHECK_SESSION and dispatch SESSION_CHECKED if session is active', function () {
|
||||
// APICallMock.call.returns({
|
||||
// then: function (resolve) {
|
||||
// resolve({
|
||||
// data: {
|
||||
// sessionActive: true
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
// });
|
||||
|
||||
expect(SessionActions.initSession().type).to.equal('CHECK_SESSION');
|
||||
expect(storeMock.dispatch).to.have.been.calledWith({type: 'SESSION_CHECKED'});
|
||||
expect(APICallMock.call).to.have.been.calledWith({
|
||||
path: '/user/check-session',
|
||||
data: {}
|
||||
});
|
||||
});
|
||||
// expect(SessionActions.checkSession().type).to.equal('CHECK_SESSION');
|
||||
// expect(storeMock.dispatch).to.have.been.calledWith({type: 'SESSION_CHECKED'});
|
||||
// expect(APICallMock.call).to.have.been.calledWith({
|
||||
// path: '/user/check-session',
|
||||
// data: {}
|
||||
// });
|
||||
// });
|
||||
|
||||
it('should return CHECK_SESSION and dispatch LOGOUT_FULFILLED if session is not active and no remember data', function () {
|
||||
sessionStoreMock.isRememberDataExpired.returns(true);
|
||||
// it('should return CHECK_SESSION and dispatch LOGOUT_FULFILLED if session is not active and no remember data', function () {
|
||||
// sessionStoreMock.isRememberDataExpired.returns(true);
|
||||
|
||||
expect(SessionActions.initSession().type).to.equal('CHECK_SESSION');
|
||||
expect(storeMock.dispatch).to.have.been.calledWith({type: 'LOGOUT_FULFILLED'});
|
||||
expect(APICallMock.call).to.have.been.calledWith({
|
||||
path: '/user/check-session',
|
||||
data: {}
|
||||
});
|
||||
});
|
||||
// expect(SessionActions.checkSession().type).to.equal('CHECK_SESSION');
|
||||
// expect(storeMock.dispatch).to.have.been.calledWith({type: 'LOGOUT_FULFILLED'});
|
||||
// expect(APICallMock.call).to.have.been.calledWith({
|
||||
// path: '/user/check-session',
|
||||
// data: {}
|
||||
// });
|
||||
// });
|
||||
|
||||
it('should return CHECK_SESSION and dispatch LOGIN_AUTO if session is not active but remember data exists', function () {
|
||||
sessionStoreMock.isRememberDataExpired.returns(false);
|
||||
// it('should return CHECK_SESSION and dispatch LOGIN_AUTO if session is not active but remember data exists', function () {
|
||||
// sessionStoreMock.isRememberDataExpired.returns(false);
|
||||
|
||||
expect(SessionActions.initSession().type).to.equal('CHECK_SESSION');
|
||||
expect(storeMock.dispatch).to.not.have.been.calledWith({type: 'LOGOUT_FULFILLED'});
|
||||
expect(APICallMock.call).to.have.been.calledWith({
|
||||
path: '/user/check-session',
|
||||
data: {}
|
||||
});
|
||||
// expect(SessionActions.checkSession().type).to.equal('CHECK_SESSION');
|
||||
// expect(storeMock.dispatch).to.not.have.been.calledWith({type: 'LOGOUT_FULFILLED'});
|
||||
// expect(APICallMock.call).to.have.been.calledWith({
|
||||
// path: '/user/check-session',
|
||||
// data: {}
|
||||
// });
|
||||
|
||||
expect(storeMock.dispatch).to.have.been.calledWith(SessionActions.autoLogin());
|
||||
});
|
||||
});
|
||||
});
|
||||
// expect(storeMock.dispatch).to.have.been.calledWith(SessionActions.autoLogin());
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
|
@ -12,32 +12,32 @@ export default {
|
||||
};
|
||||
},
|
||||
|
||||
retrieveMyTickets() {
|
||||
retrieveMyTickets({page, closed = 0, departmentId = 0, pageSize = 10}) {
|
||||
return {
|
||||
type: 'MY_TICKETS',
|
||||
payload: API.call({
|
||||
path: '/staff/get-tickets',
|
||||
data: {}
|
||||
data: {page, closed, departmentId, pageSize}
|
||||
})
|
||||
};
|
||||
},
|
||||
|
||||
retrieveNewTickets() {
|
||||
retrieveNewTickets({page, departmentId = 0, pageSize = 10}) {
|
||||
return {
|
||||
type: 'NEW_TICKETS',
|
||||
payload: API.call({
|
||||
path: '/staff/get-new-tickets',
|
||||
data: {}
|
||||
data: {page, departmentId, pageSize}
|
||||
})
|
||||
};
|
||||
},
|
||||
|
||||
retrieveAllTickets(page = 1) {
|
||||
retrieveAllTickets(page = 1, query = '', closed = 0) {
|
||||
return {
|
||||
type: 'ALL_TICKETS',
|
||||
payload: API.call({
|
||||
path: '/staff/get-all-tickets',
|
||||
data: {page}
|
||||
data: {page, query, closed}
|
||||
})
|
||||
};
|
||||
},
|
||||
|
15
client/src/actions/login-form-actions.js
Normal file
15
client/src/actions/login-form-actions.js
Normal file
@ -0,0 +1,15 @@
|
||||
|
||||
export default {
|
||||
showLoginForm() {
|
||||
return {
|
||||
type: 'SHOW_LOGIN_FORM',
|
||||
payload: true
|
||||
};
|
||||
},
|
||||
hideLoginForm() {
|
||||
return {
|
||||
type: 'HIDE_LOGIN_FORM',
|
||||
payload: false
|
||||
};
|
||||
}
|
||||
};
|
81
client/src/actions/search-filters-actions.js
Normal file
81
client/src/actions/search-filters-actions.js
Normal file
@ -0,0 +1,81 @@
|
||||
import API from 'lib-app/api-call';
|
||||
import searchTicketsUtils from 'lib-app/search-tickets-utils';
|
||||
import history from 'lib-app/history';
|
||||
|
||||
export default {
|
||||
setLoadingInTrue() {
|
||||
return {
|
||||
type: 'SEARCH_FILTERS_SET_LOADING_IN_TRUE',
|
||||
payload: {}
|
||||
}
|
||||
},
|
||||
retrieveSearchTickets(ticketQueryListState, filters = {}, pageSize = 10) {
|
||||
return {
|
||||
type: 'SEARCH_TICKETS',
|
||||
payload: API.call({
|
||||
path: '/ticket/search',
|
||||
data: {
|
||||
...filters,
|
||||
page: ticketQueryListState.page,
|
||||
pageSize
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
changeForm(form) {
|
||||
return {
|
||||
type: 'SEARCH_FILTERS_CHANGE_FORM',
|
||||
payload: form
|
||||
}
|
||||
},
|
||||
changeFilters(listConfig) {
|
||||
const filtersForAPI = searchTicketsUtils.getFiltersForAPI(listConfig.filters);
|
||||
|
||||
return {
|
||||
type: 'SEARCH_FILTERS_CHANGE_FILTERS',
|
||||
payload: {...listConfig, filtersForAPI}
|
||||
}
|
||||
},
|
||||
setDefaultFormValues() {
|
||||
return {
|
||||
type: 'SEARCH_FILTERS_SET_DEFAULT_FORM_VALUES',
|
||||
payload: {}
|
||||
}
|
||||
},
|
||||
changeShowFilters(showFilters) {
|
||||
return {
|
||||
type: 'SEARCH_FILTERS_CHANGE_SHOW_FILTERS',
|
||||
payload: showFilters
|
||||
}
|
||||
},
|
||||
changePage(filtersWithPage) {
|
||||
const filtersForAPI = searchTicketsUtils.getFiltersForAPI(filtersWithPage);
|
||||
const currentPath = window.location.pathname;
|
||||
const urlQuery = searchTicketsUtils.getFiltersForURL({
|
||||
filters: filtersForAPI,
|
||||
shouldRemoveCustomParam: false,
|
||||
shouldRemoveUseInitialValuesParam: true
|
||||
});
|
||||
urlQuery && history.push(`${currentPath}${urlQuery}`);
|
||||
|
||||
return {
|
||||
type: 'SEARCH_FILTERS_CHANGE_PAGE',
|
||||
payload: {...filtersWithPage, ...filtersForAPI}
|
||||
}
|
||||
},
|
||||
changeOrderBy(filtersWithOrderBy) {
|
||||
const filtersForAPI = searchTicketsUtils.getFiltersForAPI(filtersWithOrderBy);
|
||||
const currentPath = window.location.pathname;
|
||||
const urlQuery = searchTicketsUtils.getFiltersForURL({
|
||||
filters: filtersForAPI,
|
||||
shouldRemoveCustomParam: false,
|
||||
shouldRemoveUseInitialValuesParam: true
|
||||
});
|
||||
urlQuery && history.push(`${currentPath}${urlQuery}`);
|
||||
|
||||
return {
|
||||
type: 'SEARCH_FILTERS_CHANGE_ORDER_BY',
|
||||
payload: {...filtersWithOrderBy, ...filtersForAPI}
|
||||
}
|
||||
},
|
||||
};
|
@ -1,5 +1,8 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
import API from 'lib-app/api-call';
|
||||
import AdminDataActions from 'actions/admin-data-actions';
|
||||
import ConfigActions from 'actions/config-actions';
|
||||
import sessionStore from 'lib-app/session-store';
|
||||
import store from 'app/store';
|
||||
|
||||
@ -12,13 +15,16 @@ export default {
|
||||
let loginCall = () => {
|
||||
API.call({
|
||||
path: '/user/login',
|
||||
data: loginData
|
||||
data: _.extend(loginData, {remember: loginData.remember * 1})
|
||||
}).then((result) => {
|
||||
store.dispatch(this.getUserData(result.data.userId, result.data.token, result.data.staff)).then(() => {
|
||||
if(result.data.staff) {
|
||||
store.dispatch(AdminDataActions.retrieveCustomResponses());
|
||||
}
|
||||
});
|
||||
store.dispatch(this.getUserData(result.data.userId, result.data.token, result.data.staff))
|
||||
.then(() => store.dispatch(ConfigActions.updateData()))
|
||||
.then(() => {
|
||||
if(result.data.staff) {
|
||||
store.dispatch(AdminDataActions.retrieveCustomResponses());
|
||||
store.dispatch(AdminDataActions.retrieveStaffMembers());
|
||||
}
|
||||
});
|
||||
|
||||
resolve(result);
|
||||
}).catch((result) => {
|
||||
@ -48,11 +54,12 @@ export default {
|
||||
data: {
|
||||
userId: rememberData.userId,
|
||||
rememberToken: rememberData.token,
|
||||
isAutomatic: true
|
||||
staff: rememberData.isStaff,
|
||||
remember: 1,
|
||||
}
|
||||
}).then((result) => {
|
||||
store.dispatch(this.getUserData(result.data.userId, result.data.token));
|
||||
|
||||
store.dispatch(this.getUserData(result.data.userId, result.data.token, result.data.staff));
|
||||
|
||||
return result;
|
||||
})
|
||||
};
|
||||
@ -94,7 +101,7 @@ export default {
|
||||
};
|
||||
},
|
||||
|
||||
initSession() {
|
||||
checkSession() {
|
||||
return {
|
||||
type: 'CHECK_SESSION',
|
||||
payload: new Promise((resolve, reject) => {
|
||||
|
@ -3,7 +3,7 @@ const TicketInfo = ReactMock();
|
||||
const Table = ReactMock();
|
||||
const Button = ReactMock();
|
||||
const Tooltip = ReactMock();
|
||||
const Dropdown = ReactMock();
|
||||
const DepartmentDropdown = ReactMock();
|
||||
const i18n = stub().returnsArg(0);
|
||||
|
||||
const TicketList = requireUnit('app-components/ticket-list', {
|
||||
@ -11,8 +11,15 @@ const TicketList = requireUnit('app-components/ticket-list', {
|
||||
'core-components/table': Table,
|
||||
'core-components/button': Button,
|
||||
'core-components/tooltip': Tooltip,
|
||||
'core-components/drop-down': Dropdown,
|
||||
'lib-app/i18n': i18n
|
||||
'app-components/department-dropdown': DepartmentDropdown,
|
||||
'lib-app/i18n': i18n,
|
||||
'react-redux': {
|
||||
connect: function() {
|
||||
return function(param) {
|
||||
return param;
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
describe('TicketList component', function () {
|
||||
@ -28,11 +35,11 @@ describe('TicketList component', function () {
|
||||
id: 1,
|
||||
name: 'Sales Support'
|
||||
},
|
||||
priority: 'low',
|
||||
author: {
|
||||
id: 3,
|
||||
name: 'Francisco Villegas'
|
||||
}
|
||||
},
|
||||
tags: []
|
||||
};
|
||||
let list = _.range(5).map(() => ticket);
|
||||
|
||||
@ -50,71 +57,46 @@ describe('TicketList component', function () {
|
||||
|
||||
function renderTicketList(props = {}) {
|
||||
ticketList = TestUtils.renderIntoDocument(
|
||||
<TicketList tickets={tickets} {...props}/>
|
||||
<TicketList tickets={tickets} {...props}></TicketList>
|
||||
);
|
||||
|
||||
table = TestUtils.scryRenderedComponentsWithType(ticketList, Table);
|
||||
dropdown = TestUtils.scryRenderedComponentsWithType(ticketList, Dropdown);
|
||||
dropdown = TestUtils.scryRenderedComponentsWithType(ticketList, DepartmentDropdown);
|
||||
}
|
||||
|
||||
it('should pass correct props to Table', function () {
|
||||
renderTicketList();
|
||||
expect(table[0].props.loading).to.equal(false);
|
||||
expect(table[0].props.pageSize).to.equal(10);
|
||||
expect(table[0].props.headers).to.deep.equal([
|
||||
expect(table[0].props.headers[0]).to.deep.equal(
|
||||
{
|
||||
key: 'number',
|
||||
value: i18n('NUMBER'),
|
||||
className: 'ticket-list__number col-md-1'
|
||||
},
|
||||
});
|
||||
expect(table[0].props.headers[1]).to.deep.equal(
|
||||
{
|
||||
key: 'title',
|
||||
value: i18n('TITLE'),
|
||||
className: 'ticket-list__title col-md-6'
|
||||
},
|
||||
});
|
||||
expect(table[0].props.headers[2]).to.deep.equal(
|
||||
{
|
||||
key: 'department',
|
||||
value: i18n('DEPARTMENT'),
|
||||
className: 'ticket-list__department col-md-3'
|
||||
},
|
||||
{
|
||||
key: 'date',
|
||||
value: i18n('DATE'),
|
||||
className: 'ticket-list__date col-md-2'
|
||||
}
|
||||
]);
|
||||
});
|
||||
expect(table[0].props.headers[3].key).to.equal('date');
|
||||
expect(table[0].props.headers[3].value.props.children[0]).to.equal(i18n('DATE'));
|
||||
expect(table[0].props.headers[3].value.props.children[1]).to.equal(null);
|
||||
expect(table[0].props.headers[3].className).to.equal('ticket-list__date col-md-2');
|
||||
});
|
||||
|
||||
|
||||
it('should pass loading to Table', function () {
|
||||
renderTicketList({loading: true});
|
||||
expect(table[0].props.loading).to.equal(true);
|
||||
});
|
||||
|
||||
it('should pass correct compare function to Table', function () {
|
||||
let minCompare = table[0].props.comp;
|
||||
|
||||
let row1 = {
|
||||
closed: false,
|
||||
unread: false,
|
||||
date: '20160405'
|
||||
};
|
||||
let row2 = {
|
||||
closed: false,
|
||||
unread: false,
|
||||
date: '20160406'
|
||||
};
|
||||
expect(minCompare(row1, row2)).to.equal(1);
|
||||
|
||||
row1.unread = true;
|
||||
expect(minCompare(row1, row2)).to.equal(-1);
|
||||
|
||||
row2.unread = true;
|
||||
expect(minCompare(row1, row2)).to.equal(1);
|
||||
|
||||
row2.date = '20160401';
|
||||
expect(minCompare(row1, row2)).to.equal(-1);
|
||||
});
|
||||
|
||||
describe('when using secondary type', function () {
|
||||
beforeEach(function () {
|
||||
renderTicketList({
|
||||
@ -127,45 +109,41 @@ describe('TicketList component', function () {
|
||||
});
|
||||
|
||||
it('should pass correct props to Table', function () {
|
||||
expect(table[0].props.headers).to.deep.equal([
|
||||
expect(table[0].props.headers[0]).to.deep.equal(
|
||||
{
|
||||
key: 'number',
|
||||
value: i18n('NUMBER'),
|
||||
className: 'ticket-list__number col-md-1'
|
||||
},
|
||||
});
|
||||
expect(table[0].props.headers[1]).to.deep.equal(
|
||||
{
|
||||
key: 'title',
|
||||
value: i18n('TITLE'),
|
||||
className: 'ticket-list__title col-md-4'
|
||||
},
|
||||
{
|
||||
key: 'priority',
|
||||
value: i18n('PRIORITY'),
|
||||
className: 'ticket-list__priority col-md-1'
|
||||
},
|
||||
});
|
||||
expect(table[0].props.headers[2]).to.deep.equal(
|
||||
{
|
||||
key: 'department',
|
||||
value: i18n('DEPARTMENT'),
|
||||
className: 'ticket-list__department col-md-2'
|
||||
},
|
||||
});
|
||||
expect(table[0].props.headers[3]).to.deep.equal(
|
||||
{
|
||||
key: 'author',
|
||||
value: i18n('AUTHOR'),
|
||||
className: 'ticket-list__author col-md-2'
|
||||
},
|
||||
{
|
||||
key: 'date',
|
||||
value: i18n('DATE'),
|
||||
className: 'ticket-list__date col-md-2'
|
||||
}
|
||||
]);
|
||||
});
|
||||
expect(table[0].props.headers[4].key).to.equal('date');
|
||||
expect(table[0].props.headers[4].value.props.children[0]).to.equal(i18n('DATE'));
|
||||
expect(table[0].props.headers[4].value.props.children[1]).to.equal(null);
|
||||
expect(table[0].props.headers[4].className).to.equal('ticket-list__date col-md-2');
|
||||
});
|
||||
|
||||
it('should pass correct props to dropdown', function () {
|
||||
expect(dropdown[0].props.items).to.deep.equal([
|
||||
{content: i18n('ALL_DEPARTMENTS')},
|
||||
{content: 'Sales Support'},
|
||||
{content: 'Tech Help'}
|
||||
expect(dropdown[0].props.departments).to.deep.equal([
|
||||
{name: i18n('ALL_DEPARTMENTS')},
|
||||
{name: 'Sales Support', id: 1},
|
||||
{name: 'Tech Help', id: 2}
|
||||
]);
|
||||
expect(dropdown[0].props.size).to.equal('medium');
|
||||
});
|
||||
@ -185,4 +163,4 @@ describe('TicketList component', function () {
|
||||
expect(table[0].props.rows.length).to.equal(10);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -18,10 +18,12 @@ class ActivityRow extends React.Component {
|
||||
'CREATE_TICKET',
|
||||
'RE_OPEN',
|
||||
'DEPARTMENT_CHANGED',
|
||||
'PRIORITY_CHANGED',
|
||||
'EDIT_TITLE',
|
||||
'EDIT_COMMENT',
|
||||
|
||||
'EDIT_SETTINGS',
|
||||
'SIGNUP',
|
||||
'INVITE',
|
||||
'ADD_TOPIC',
|
||||
'ADD_ARTICLE',
|
||||
'DELETE_TOPIC',
|
||||
@ -56,16 +58,16 @@ class ActivityRow extends React.Component {
|
||||
'CREATE_TICKET',
|
||||
'RE_OPEN',
|
||||
'DEPARTMENT_CHANGED',
|
||||
'PRIORITY_CHANGED'
|
||||
'COMMENT_EDITED',
|
||||
'EDIT_TITLE',
|
||||
'EDIT_COMMENT',
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="activity-row">
|
||||
<Icon {...this.getIconProps()} className="activity-row__icon"/>
|
||||
<span>
|
||||
<Link className="activity-row__name-link" to={this.getNameLinkDestination()}>
|
||||
{this.props.author.name}
|
||||
</Link>
|
||||
{this.renderAuthorName()}
|
||||
</span>
|
||||
<span className="activity-row__message"> {i18n('ACTIVITY_' + this.props.type)} </span>
|
||||
{_.includes(ticketRelatedTypes, this.props.type) ? this.renderTicketNumber() : this.props.to}
|
||||
@ -74,6 +76,18 @@ class ActivityRow extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
renderAuthorName() {
|
||||
let name = this.props.author.name;
|
||||
|
||||
if (this.props.author.id) {
|
||||
name = <Link className="activity-row__name-link" to={this.getNameLinkDestination()}>
|
||||
{this.props.author.name}
|
||||
</Link>;
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
renderTicketNumber() {
|
||||
let ticketNumber = (this.props.mode === 'staff') ? this.props.ticketNumber : this.props.to;
|
||||
|
||||
@ -99,10 +113,12 @@ class ActivityRow extends React.Component {
|
||||
'CREATE_TICKET': 'ticket',
|
||||
'RE_OPEN': 'unlock-alt',
|
||||
'DEPARTMENT_CHANGED': 'exchange',
|
||||
'PRIORITY_CHANGED': 'exclamation',
|
||||
'EDIT_TITLE': 'edit',
|
||||
'EDIT_COMMENT': 'edit',
|
||||
|
||||
'EDIT_SETTINGS': 'wrench',
|
||||
'SIGNUP': 'user-plus',
|
||||
'INVITE': 'user-plus',
|
||||
'ADD_TOPIC': 'book',
|
||||
'ADD_ARTICLE': 'book',
|
||||
'DELETE_TOPIC': 'book',
|
||||
|
@ -5,8 +5,7 @@ import ModalContainer from 'app-components/modal-container';
|
||||
|
||||
import Button from 'core-components/button';
|
||||
import Input from 'core-components/input';
|
||||
import Icon from 'core-components/icon';
|
||||
|
||||
import Loading from 'core-components/loading'
|
||||
|
||||
class AreYouSure extends React.Component {
|
||||
static propTypes = {
|
||||
@ -24,13 +23,14 @@ class AreYouSure extends React.Component {
|
||||
};
|
||||
|
||||
state = {
|
||||
loading: false,
|
||||
password: ''
|
||||
};
|
||||
|
||||
static openModal(description, onYes, type = 'default') {
|
||||
ModalContainer.openModal(
|
||||
<AreYouSure description={description} onYes={onYes} type={type}/>,
|
||||
true
|
||||
<AreYouSure description={description} onYes={onYes} type={type} />,
|
||||
{noPadding: true, closeButton: {showCloseButton: true, whiteColor: true}}
|
||||
);
|
||||
}
|
||||
|
||||
@ -39,28 +39,34 @@ class AreYouSure extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { loading } = this.state;
|
||||
const { description, type } = this.props;
|
||||
|
||||
return (
|
||||
<div className="are-you-sure" role="dialog" aria-labelledby="are-you-sure__header" aria-describedby="are-you-sure__description">
|
||||
<div className="are-you-sure__header" id="are-you-sure__header">
|
||||
{i18n('ARE_YOU_SURE')}
|
||||
</div>
|
||||
<span className="are-you-sure__close-icon" onClick={this.onNo.bind(this)}>
|
||||
<Icon name="times" size="2x"/>
|
||||
</span>
|
||||
<div className="are-you-sure__description" id="are-you-sure__description">
|
||||
{this.props.description || (this.props.type === 'secure' && i18n('PLEASE_CONFIRM_PASSWORD'))}
|
||||
{description || (type === 'secure' && i18n('PLEASE_CONFIRM_PASSWORD'))}
|
||||
</div>
|
||||
{(this.props.type === 'secure') ? this.renderPassword() : null}
|
||||
{(type === 'secure') ? this.renderPassword() : null}
|
||||
<span className="separator" />
|
||||
<div className="are-you-sure__buttons">
|
||||
<div className="are-you-sure__no-button">
|
||||
<Button type="link" size="auto" onClick={this.onNo.bind(this)} tabIndex="2">
|
||||
<Button disabled={loading} type="link" size="auto" onClick={this.onNo.bind(this)} tabIndex="2">
|
||||
{i18n('CANCEL')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="are-you-sure__yes-button">
|
||||
<Button type="secondary" size="small" onClick={this.onYes.bind(this)} ref="yesButton" tabIndex="2">
|
||||
{i18n('YES')}
|
||||
<Button
|
||||
type="secondary"
|
||||
size="small"
|
||||
onClick={this.onYes.bind(this)}
|
||||
ref="yesButton"
|
||||
tabIndex="2"
|
||||
disabled={loading}>
|
||||
{loading ? <Loading /> : i18n('YES')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@ -69,8 +75,20 @@ class AreYouSure extends React.Component {
|
||||
}
|
||||
|
||||
renderPassword() {
|
||||
const { password, loading } = this.state;
|
||||
|
||||
return (
|
||||
<Input className="are-you-sure__password" password placeholder="password" name="password" ref="password" size="medium" value={this.state.password} onChange={this.onPasswordChange.bind(this)} onKeyDown={this.onInputKeyDown.bind(this)}/>
|
||||
<Input
|
||||
className="are-you-sure__password"
|
||||
password
|
||||
placeholder="password"
|
||||
name="password"
|
||||
ref="password"
|
||||
size="medium"
|
||||
value={password}
|
||||
onChange={this.onPasswordChange.bind(this)}
|
||||
onKeyDown={this.onInputKeyDown.bind(this)}
|
||||
disabled={loading} />
|
||||
);
|
||||
}
|
||||
|
||||
@ -87,28 +105,59 @@ class AreYouSure extends React.Component {
|
||||
}
|
||||
|
||||
onYes() {
|
||||
if (this.props.type === 'secure' && !this.state.password) {
|
||||
const { password } = this.state;
|
||||
const { type, onYes } = this.props;
|
||||
|
||||
if(type === 'secure' && !password) {
|
||||
this.refs.password.focus()
|
||||
}
|
||||
|
||||
if (this.props.type === 'default' || this.state.password) {
|
||||
this.closeModal();
|
||||
|
||||
if (this.props.onYes) {
|
||||
this.props.onYes(this.state.password);
|
||||
if(type === 'default' || password) {
|
||||
if(onYes) {
|
||||
const result = onYes(password);
|
||||
if(this.isPromise(result)) {
|
||||
this.setState({
|
||||
loading: true
|
||||
});
|
||||
result
|
||||
.then(() => {
|
||||
this.setState({
|
||||
loading: false
|
||||
});
|
||||
this.closeModal();
|
||||
})
|
||||
.catch(() => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
});
|
||||
this.closeModal();
|
||||
})
|
||||
} else {
|
||||
this.closeModal();
|
||||
}
|
||||
} else {
|
||||
this.closeModal();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isPromise(object) {
|
||||
if(Promise && Promise.resolve) {
|
||||
return Promise.resolve(object) == object;
|
||||
} else {
|
||||
throw "Promise not supported in your environment"
|
||||
}
|
||||
}
|
||||
|
||||
onNo() {
|
||||
this.closeModal();
|
||||
}
|
||||
|
||||
closeModal() {
|
||||
if (this.context.closeModal) {
|
||||
this.context.closeModal();
|
||||
}
|
||||
const { closeModal } = this.context;
|
||||
|
||||
closeModal && closeModal();
|
||||
}
|
||||
}
|
||||
|
||||
export default AreYouSure;
|
||||
export default AreYouSure;
|
||||
|
@ -24,12 +24,16 @@
|
||||
&__buttons {
|
||||
margin-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
text-align: right;
|
||||
width: 100%;
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__yes-button,
|
||||
&__no-button {
|
||||
display: inline-block;
|
||||
display: block;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
|
@ -32,11 +32,11 @@ class ArticleAddModal extends React.Component {
|
||||
<Form onSubmit={this.onAddNewArticleFormSubmit.bind(this)} loading={this.state.loading}>
|
||||
<FormField name="title" label={i18n('TITLE')} field="input" fieldProps={{size: 'large'}} validation="TITLE" required/>
|
||||
<FormField name="content" label={i18n('CONTENT')} field="textarea" validation="TEXT_AREA" required fieldProps={{allowImages: this.props.allowAttachments}}/>
|
||||
<SubmitButton type="secondary">{i18n('ADD_ARTICLE')}</SubmitButton>
|
||||
<Button className="article-add-modal__cancel-button" type="link" onClick={(event) => {
|
||||
event.preventDefault();
|
||||
ModalContainer.closeModal();
|
||||
}}>{i18n('CANCEL')}</Button>
|
||||
<SubmitButton className="article-add-modal__submit-button" type="secondary">{i18n('ADD_ARTICLE')}</SubmitButton>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
|
@ -2,7 +2,11 @@
|
||||
width: 800px;
|
||||
|
||||
&__cancel-button {
|
||||
float: right;
|
||||
float: left;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
&__submit-button {
|
||||
float: right;
|
||||
}
|
||||
}
|
@ -36,11 +36,13 @@ class ArticlesList extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
if(this.props.errored) {
|
||||
return <Message type="error">{i18n('ERROR_RETRIEVING_ARTICLES')}</Message>;
|
||||
const { errored, loading } = this.props;
|
||||
|
||||
if(errored) {
|
||||
return <Message showCloseButton={false} type="error">{i18n('ERROR_RETRIEVING_ARTICLES')}</Message>;
|
||||
}
|
||||
|
||||
return (this.props.loading) ? <Loading /> : this.renderContent();
|
||||
return loading ? <Loading className="articles-list__loading" backgrounded size="large"/> : this.renderContent();
|
||||
}
|
||||
|
||||
renderContent() {
|
||||
@ -53,17 +55,19 @@ class ArticlesList extends React.Component {
|
||||
}
|
||||
|
||||
renderTopics() {
|
||||
const { topics, editable, articlePath } = this.props;
|
||||
|
||||
return (
|
||||
<div className="articles-list__topics">
|
||||
{this.props.topics.map((topic, index) => {
|
||||
{topics.map((topic, index) => {
|
||||
return (
|
||||
<div key={index}>
|
||||
<TopicViewer
|
||||
{...topic}
|
||||
id={topic.id * 1}
|
||||
editable={this.props.editable}
|
||||
editable={editable}
|
||||
onChange={this.retrieveArticles.bind(this)}
|
||||
articlePath={this.props.articlePath} />
|
||||
articlePath={articlePath} />
|
||||
<span className="separator" />
|
||||
</div>
|
||||
);
|
||||
@ -76,7 +80,7 @@ class ArticlesList extends React.Component {
|
||||
return (
|
||||
<div className="articles-list__add-topic-button">
|
||||
<Button onClick={() => ModalContainer.openModal(<TopicEditModal addForm onChange={this.retrieveArticles.bind(this)} />)} type="secondary" className="articles-list__add">
|
||||
<Icon name="plus-circle" size="2x" className="articles-list__add-icon"/> {i18n('ADD_TOPIC')}
|
||||
<Icon name="plus" className="articles-list__add-icon" /> {i18n('ADD_TOPIC')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
@ -88,9 +92,11 @@ class ArticlesList extends React.Component {
|
||||
}
|
||||
|
||||
export default connect((store) => {
|
||||
const { topics, errored, loading } = store.articles;
|
||||
|
||||
return {
|
||||
topics: store.articles.topics,
|
||||
errored: store.articles.errored,
|
||||
loading: store.articles.loading
|
||||
topics: topics.map((topic) => {return {...topic, private: topic.private === "1"}}),
|
||||
errored,
|
||||
loading
|
||||
};
|
||||
})(ArticlesList);
|
||||
|
@ -1,14 +1,19 @@
|
||||
@import "../scss/vars";
|
||||
|
||||
.articles-list {
|
||||
|
||||
&__add {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&__add-icon {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
margin-top: -4px;
|
||||
&__loading {
|
||||
min-width: 300px;
|
||||
min-height: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
&__add-topic-button {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
41
client/src/app-components/department-dropdown.js
Normal file
41
client/src/app-components/department-dropdown.js
Normal file
@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
|
||||
import DropDown from 'core-components/drop-down';
|
||||
import Icon from 'core-components/icon';
|
||||
|
||||
class DepartmentDropdown extends React.Component {
|
||||
static propTypes = {
|
||||
value: React.PropTypes.number,
|
||||
onChange: React.PropTypes.func,
|
||||
departments: React.PropTypes.array
|
||||
}
|
||||
|
||||
render() {
|
||||
return <DropDown {...this.props} onChange={this.onChange.bind(this)} items={this.getDepartments()} />
|
||||
}
|
||||
|
||||
getDepartments() {
|
||||
let departments = this.props.departments.map((department) => {
|
||||
if(department.private*1) {
|
||||
return {content: <span>{department.name} <Icon name='user-secret'/></span>};
|
||||
} else {
|
||||
return {content: department.name};
|
||||
}
|
||||
});
|
||||
|
||||
return departments;
|
||||
}
|
||||
|
||||
onChange(event) {
|
||||
if(this.props.onChange) {
|
||||
this.props.onChange({
|
||||
index: event.index,
|
||||
target: {
|
||||
value: event.index
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default DepartmentDropdown;
|
@ -3,7 +3,6 @@ import {connect} from 'react-redux';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import languageList from 'data/language-list';
|
||||
import i18n from 'lib-app/i18n';
|
||||
import DropDown from 'core-components/drop-down';
|
||||
|
||||
const languageCodes = Object.keys(languageList);
|
||||
|
@ -1,7 +1,6 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import keyCode from 'keycode';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import store from 'app/store';
|
||||
import ModalActions from 'actions/modal-actions';
|
||||
@ -9,11 +8,14 @@ import Modal from 'core-components/modal';
|
||||
|
||||
class ModalContainer extends React.Component {
|
||||
|
||||
static openModal(content, noPadding) {
|
||||
static openModal(
|
||||
content,
|
||||
options={noPadding: false, outsideClick: false, closeButton: {showCloseButton: false, whiteColor: false}}
|
||||
) {
|
||||
store.dispatch(
|
||||
ModalActions.openModal({
|
||||
content,
|
||||
noPadding
|
||||
options
|
||||
})
|
||||
);
|
||||
}
|
||||
@ -49,8 +51,16 @@ class ModalContainer extends React.Component {
|
||||
}
|
||||
|
||||
renderModal() {
|
||||
const { content, options } = this.props.modal;
|
||||
const { noPadding, outsideClick, closeButton } = options;
|
||||
|
||||
return (
|
||||
<Modal content={this.props.modal.content} noPadding={this.props.modal.noPadding}/>
|
||||
<Modal
|
||||
content={content}
|
||||
noPadding={noPadding}
|
||||
outsideClick={outsideClick}
|
||||
onOutsideClick={this.closeModal.bind(this)}
|
||||
closeButton={closeButton} />
|
||||
);
|
||||
}
|
||||
|
||||
@ -69,4 +79,4 @@ export default connect((store) => {
|
||||
return {
|
||||
modal: store.modal
|
||||
};
|
||||
})(ModalContainer);
|
||||
})(ModalContainer);
|
||||
|
41
client/src/app-components/page-size-dropdown.js
Normal file
41
client/src/app-components/page-size-dropdown.js
Normal file
@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
|
||||
import DropDown from 'core-components/drop-down';
|
||||
import i18n from 'lib-app/i18n';
|
||||
|
||||
class PageSizeDropdown extends React.Component {
|
||||
static propTypes = {
|
||||
value: React.PropTypes.number,
|
||||
onChange: React.PropTypes.func,
|
||||
pages: React.PropTypes.array
|
||||
}
|
||||
|
||||
state = {
|
||||
selectedIndex: 1
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<DropDown {...this.props} onChange={this.onChange.bind(this)} items={this.getPages()} selectedIndex={this.state.selectedIndex} />
|
||||
)
|
||||
}
|
||||
|
||||
getPages() {
|
||||
return this.props.pages.map((page) => {
|
||||
return {content: `${page} / ${i18n('TICKETS')}`}
|
||||
});
|
||||
}
|
||||
|
||||
onChange(event) {
|
||||
this.setState({
|
||||
selectedIndex: event.index
|
||||
})
|
||||
if(this.props.onChange) {
|
||||
this.props.onChange({
|
||||
pageSize: this.props.pages[event.index]
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default PageSizeDropdown;
|
@ -24,10 +24,21 @@ class PasswordRecovery extends React.Component {
|
||||
renderLogo: false
|
||||
};
|
||||
|
||||
state = {
|
||||
showRecoverSentMessage: true
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (!prevProps.recoverSent && this.props.recoverSent) {
|
||||
this.setState({showRecoverSentMessage : true});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { renderLogo, formProps, onBackToLoginClick } = this.props;
|
||||
const { renderLogo, formProps, onBackToLoginClick, style } = this.props;
|
||||
|
||||
return (
|
||||
<Widget {...this.props} className={this.getClass()} title={!renderLogo && i18n('RECOVER_PASSWORD')}>
|
||||
<Widget style={style} className={this.getClass()} title={!renderLogo ? i18n('RECOVER_PASSWORD') : ''}>
|
||||
{this.renderLogo()}
|
||||
<Form {...formProps}>
|
||||
<div className="password-recovery__inputs">
|
||||
@ -63,22 +74,29 @@ class PasswordRecovery extends React.Component {
|
||||
}
|
||||
|
||||
renderRecoverStatus() {
|
||||
let status = null;
|
||||
|
||||
if (this.props.recoverSent) {
|
||||
status = (
|
||||
<Message className="password-recovery__message" type="info" leftAligned>
|
||||
{i18n('RECOVER_SENT')}
|
||||
</Message>
|
||||
);
|
||||
}
|
||||
|
||||
return status;
|
||||
return (
|
||||
this.props.recoverSent ?
|
||||
<Message
|
||||
showMessage={this.state.showRecoverSentMessage}
|
||||
onCloseMessage={this.onCloseMessage.bind(this, "showRecoverSentMessage")}
|
||||
className="password-recovery__message"
|
||||
type="info"
|
||||
leftAligned>
|
||||
{i18n('RECOVER_SENT')}
|
||||
</Message> :
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
focusEmail() {
|
||||
this.refs.email.focus();
|
||||
}
|
||||
|
||||
onCloseMessage(showMessage) {
|
||||
this.setState({
|
||||
[showMessage]: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default PasswordRecovery;
|
||||
|
@ -24,3 +24,15 @@
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 320px) {
|
||||
.widget {
|
||||
margin-left: -12px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 992px) {
|
||||
.password-recovery__content {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
@ -61,4 +61,28 @@
|
||||
&__pagination {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 415px) {
|
||||
.people-list {
|
||||
&__item {
|
||||
height: unset;
|
||||
padding: unset;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: flex-end;
|
||||
|
||||
&-profile-pic-wrapper {
|
||||
top: 15px;
|
||||
left: 15px;
|
||||
}
|
||||
|
||||
&-block {
|
||||
width: unset;
|
||||
display: unset;
|
||||
padding: 15px 10px 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
48
client/src/app-components/popup-message.js
Normal file
48
client/src/app-components/popup-message.js
Normal file
@ -0,0 +1,48 @@
|
||||
import React from 'react';
|
||||
|
||||
import ModalContainer from 'app-components/modal-container';
|
||||
|
||||
import Button from 'core-components/button';
|
||||
import Message from 'core-components/message';
|
||||
|
||||
|
||||
class PopupMessage extends React.Component {
|
||||
static propTypes = Message.propTypes;
|
||||
|
||||
static contextTypes = {
|
||||
closeModal: React.PropTypes.func
|
||||
};
|
||||
|
||||
static open(props) {
|
||||
ModalContainer.openModal(
|
||||
<PopupMessage {...props} />,
|
||||
{noPadding: true, outsideClick: true, closeButton: {showCloseButton: false, whiteColor: false}}
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.refs.closeButton && this.refs.closeButton.focus();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="popup-message">
|
||||
<Message {...this.props} showCloseButton={false} className="popup-message__message" />
|
||||
<Button
|
||||
className="popup-message__close-button"
|
||||
iconName="times"
|
||||
type="clean"
|
||||
ref="closeButton"
|
||||
onClick={this.closeModal.bind(this)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
closeModal() {
|
||||
const { closeModal } = this.context;
|
||||
|
||||
closeModal && closeModal();
|
||||
}
|
||||
}
|
||||
|
||||
export default PopupMessage;
|
17
client/src/app-components/popup-message.scss
Normal file
17
client/src/app-components/popup-message.scss
Normal file
@ -0,0 +1,17 @@
|
||||
@import "../scss/vars";
|
||||
|
||||
.popup-message {
|
||||
min-width: 500px;
|
||||
position: relative;
|
||||
|
||||
&__close-button {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
color: $dark-grey;
|
||||
&:focus {
|
||||
outline: none;
|
||||
color: $grey;
|
||||
}
|
||||
}
|
||||
}
|
19
client/src/app-components/session-expired-modal.js
Normal file
19
client/src/app-components/session-expired-modal.js
Normal file
@ -0,0 +1,19 @@
|
||||
import React from "react";
|
||||
import _ from "lodash";
|
||||
|
||||
import i18n from "lib-app/i18n";
|
||||
|
||||
import Header from "core-components/header";
|
||||
|
||||
class SessionExpiredModal extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<Header
|
||||
title={i18n("SESSION_EXPIRED")}
|
||||
description={i18n("SESSION_EXPIRED_DESCRIPTION")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default SessionExpiredModal;
|
42
client/src/app-components/stat-card.js
Normal file
42
client/src/app-components/stat-card.js
Normal file
@ -0,0 +1,42 @@
|
||||
import React from 'react';
|
||||
|
||||
import Tooltip from 'core-components/tooltip';
|
||||
|
||||
class StatCard extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
label: React.PropTypes.string,
|
||||
description: React.PropTypes.string,
|
||||
value: React.PropTypes.number,
|
||||
isPercentage: React.PropTypes.bool
|
||||
};
|
||||
|
||||
state = {
|
||||
showTooltip: false
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
label,
|
||||
description,
|
||||
value,
|
||||
isPercentage
|
||||
} = this.props;
|
||||
|
||||
const displayValue = isNaN(value) ? "-" : (isPercentage ? value.toFixed(2) : value);
|
||||
return (
|
||||
<Tooltip content={description} show={this.state.showTooltip} >
|
||||
<div className="stat-card" onMouseEnter={() => this.setState({showTooltip: true})} onMouseLeave={() => this.setState({showTooltip: false})}>
|
||||
<div className="stat-card__wrapper">
|
||||
{label}
|
||||
<div className="stat-card__container">
|
||||
{displayValue}{isPercentage && !isNaN(value) ? "%" : ""}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default StatCard;
|
22
client/src/app-components/stat-card.scss
Normal file
22
client/src/app-components/stat-card.scss
Normal file
@ -0,0 +1,22 @@
|
||||
@import "../scss/vars";
|
||||
|
||||
.stat-card {
|
||||
margin: 0 4px;
|
||||
|
||||
&__wrapper {
|
||||
padding: 10px;
|
||||
min-width: 160px;
|
||||
margin: 8px auto;
|
||||
transition: 0.3s;
|
||||
box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2);
|
||||
text-align: center;
|
||||
&:hover {
|
||||
box-shadow: 0 8px 16px 0 rgba(0,0,0,0.2);
|
||||
}
|
||||
}
|
||||
|
||||
&__container {
|
||||
font-size: $font-size--lg;
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
@ -1,100 +0,0 @@
|
||||
import React from 'react';
|
||||
import {Line} from 'react-chartjs-2';
|
||||
|
||||
import i18n from 'lib-app/i18n';
|
||||
|
||||
class StatsChart extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
strokes: React.PropTypes.arrayOf(React.PropTypes.shape({
|
||||
name: React.PropTypes.string,
|
||||
values: React.PropTypes.arrayOf(React.PropTypes.shape({
|
||||
date: React.PropTypes.string,
|
||||
value: React.PropTypes.number
|
||||
}))
|
||||
})),
|
||||
period: React.PropTypes.number
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<Line data={this.getChartData()} options={this.getChartOptions()} width={800} height={400} redraw/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
getChartData() {
|
||||
let labels = this.getLabels();
|
||||
|
||||
let color = {
|
||||
'CLOSE': 'rgba(150, 20, 20, 0.6)',
|
||||
'CREATE_TICKET': 'rgba(20, 150, 20, 0.6)',
|
||||
'SIGNUP': 'rgba(20, 20, 150, 0.6)',
|
||||
'COMMENT': 'rgba(20, 200, 200, 0.6)',
|
||||
'ASSIGN': 'rgba(20, 150, 20, 0.6)'
|
||||
};
|
||||
|
||||
let strokes = this.props.strokes.slice();
|
||||
let datasets = strokes.map((stroke, index) => {
|
||||
return {
|
||||
label: i18n('CHART_' + stroke.name),
|
||||
data: stroke.values.map((val) => val.value),
|
||||
fill: false,
|
||||
borderWidth: this.getBorderWidth(),
|
||||
borderColor: color[stroke.name],
|
||||
pointBorderColor: color[stroke.name],
|
||||
pointRadius: 0,
|
||||
pointHoverRadius: 3,
|
||||
lineTension: 0.2,
|
||||
pointHoverBackgroundColor: color[stroke.name],
|
||||
hitRadius: this.hitRadius(index)
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
labels: labels,
|
||||
datasets: datasets
|
||||
};
|
||||
}
|
||||
|
||||
getBorderWidth() {
|
||||
return (this.props.period <= 90) ? 3 : 2;
|
||||
}
|
||||
|
||||
getLabels() {
|
||||
let months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
||||
let labels = [];
|
||||
|
||||
if (!this.props.strokes.length) {
|
||||
labels = Array.from('x'.repeat(this.props.period));
|
||||
}
|
||||
else {
|
||||
labels = this.props.strokes[0].values.map((item) => {
|
||||
let idx = item.date.slice(4, 6) - 1;
|
||||
|
||||
return item.date.slice(6, 8) + ' ' + months[idx];
|
||||
});
|
||||
}
|
||||
|
||||
return labels;
|
||||
}
|
||||
|
||||
hitRadius(index) {
|
||||
if (this.props.period <= 7) return 20;
|
||||
if (this.props.period <= 30) return 15;
|
||||
if (this.props.period <= 90) return 10;
|
||||
return 3;
|
||||
}
|
||||
|
||||
getChartOptions() {
|
||||
return {
|
||||
legend: {
|
||||
display: false
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default StatsChart;
|
@ -1,186 +0,0 @@
|
||||
import React from 'react';
|
||||
import _ from 'lodash';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import i18n from 'lib-app/i18n';
|
||||
import API from 'lib-app/api-call';
|
||||
|
||||
import DropDown from 'core-components/drop-down';
|
||||
import ToggleList from 'core-components/toggle-list';
|
||||
|
||||
import StatsChart from 'app-components/stats-chart';
|
||||
|
||||
const generalStrokes = ['CREATE_TICKET', 'CLOSE', 'SIGNUP', 'COMMENT'];
|
||||
const staffStrokes = ['ASSIGN', 'CLOSE'];
|
||||
const ID = {
|
||||
'CREATE_TICKET': 0,
|
||||
'ASSIGN': 0,
|
||||
'CLOSE': 1,
|
||||
'SIGNUP': 2,
|
||||
'COMMENT': 3
|
||||
};
|
||||
|
||||
const statsPeriod = {
|
||||
'WEEK': 7,
|
||||
'MONTH': 30
|
||||
};
|
||||
|
||||
class Stats extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
type: React.PropTypes.string,
|
||||
staffId: React.PropTypes.number
|
||||
};
|
||||
|
||||
state = {
|
||||
stats: this.getDefaultStats(),
|
||||
strokes: this.getStrokes().map((name) => {
|
||||
return {
|
||||
name: name,
|
||||
values: []
|
||||
}
|
||||
}),
|
||||
showed: [0],
|
||||
period: 0
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.retrieve('WEEK');
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className={this.getClass()}>
|
||||
<DropDown {...this.getDropDownProps()}/>
|
||||
<ToggleList {...this.getToggleListProps()} />
|
||||
<StatsChart {...this.getStatsChartProps()} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
getClass() {
|
||||
let classes = {
|
||||
'stats': true,
|
||||
'stats_staff': this.props.type === 'staff'
|
||||
};
|
||||
|
||||
return classNames(classes);
|
||||
}
|
||||
|
||||
getToggleListProps() {
|
||||
return {
|
||||
values: this.state.showed,
|
||||
className: 'stats__toggle-list',
|
||||
onChange: this.onToggleListChange.bind(this),
|
||||
type: this.props.type === 'general' ? 'default' : 'small',
|
||||
items: this.getStrokes().map((name) => {
|
||||
return {
|
||||
className: 'stats__toggle-list_' + name,
|
||||
content: (
|
||||
<div className="stats__toggle-list-item">
|
||||
<div className="stats__toggle-list-item-value">{this.state.stats[name]}</div>
|
||||
<div className="stats__toggle-list-item-name">{i18n('CHART_' + name)}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
onToggleListChange(event) {
|
||||
this.setState({
|
||||
showed: event.target.value
|
||||
});
|
||||
}
|
||||
|
||||
getDropDownProps() {
|
||||
return {
|
||||
items: Object.keys(statsPeriod).map(key => 'LAST_' + statsPeriod[key] + '_DAYS').map((name) => {
|
||||
return {
|
||||
content: i18n(name),
|
||||
icon: ''
|
||||
};
|
||||
}),
|
||||
onChange: this.onDropDownChange.bind(this),
|
||||
className: 'stats__dropdown'
|
||||
}
|
||||
}
|
||||
|
||||
onDropDownChange(event) {
|
||||
this.retrieve(Object.keys(statsPeriod)[event.index]);
|
||||
}
|
||||
|
||||
getStatsChartProps() {
|
||||
let showed = this.getShowedArray();
|
||||
|
||||
return {
|
||||
period: this.state.period,
|
||||
strokes: _.filter(this.state.strokes, (s, i) => showed[i])
|
||||
};
|
||||
}
|
||||
|
||||
retrieve(periodName) {
|
||||
API.call({
|
||||
path: '/system/get-stats',
|
||||
data: this.getApiCallData(periodName)
|
||||
}).then(this.onRetrieveSuccess.bind(this));
|
||||
}
|
||||
|
||||
onRetrieveSuccess(result) {
|
||||
let newStats = this.getDefaultStats();
|
||||
|
||||
let newStrokes = this.getStrokes().map((name) => {
|
||||
return {
|
||||
name: name,
|
||||
values: []
|
||||
};
|
||||
});
|
||||
|
||||
let realPeriod = result.data.length / this.getStrokes().length;
|
||||
|
||||
result.data.reverse().map((item) => {
|
||||
newStats[item.type] += item.value * 1;
|
||||
|
||||
newStrokes[ ID[item.type] ].values.push({
|
||||
date: item.date,
|
||||
value: item.value * 1
|
||||
});
|
||||
});
|
||||
|
||||
this.setState({stats: newStats, strokes: newStrokes, period: realPeriod});
|
||||
}
|
||||
|
||||
getShowedArray() {
|
||||
let showed = this.getStrokes().map(() => false);
|
||||
|
||||
for (let i = 0; i < showed.length; i++) {
|
||||
showed[this.state.showed[i]] = true;
|
||||
}
|
||||
|
||||
return showed;
|
||||
}
|
||||
|
||||
getStrokes() {
|
||||
return this.props.type === 'general' ? generalStrokes : staffStrokes;
|
||||
}
|
||||
|
||||
getDefaultStats() {
|
||||
return this.props.type === 'general' ?
|
||||
{
|
||||
'CREATE_TICKET': 0,
|
||||
'CLOSE': 0,
|
||||
'SIGNUP': 0,
|
||||
'COMMENT': 0
|
||||
} :
|
||||
{
|
||||
'ASSIGN': 0,
|
||||
'CLOSE': 0
|
||||
};
|
||||
}
|
||||
|
||||
getApiCallData(periodName) {
|
||||
return this.props.type === 'general' ? {period: periodName} : {period: periodName, staffId: this.props.staffId};
|
||||
}
|
||||
}
|
||||
|
||||
export default Stats;
|
@ -1,76 +0,0 @@
|
||||
@import '../scss/vars';
|
||||
|
||||
.stats {
|
||||
|
||||
&__dropdown {
|
||||
margin-left: auto;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
&__toggle-list {
|
||||
margin-bottom: 20px;
|
||||
user-select: none;
|
||||
|
||||
&-item {
|
||||
|
||||
&-value {
|
||||
font-size: $font-size--lg;
|
||||
line-height: 80px;
|
||||
}
|
||||
|
||||
&-name {
|
||||
font-size: $font-size--md;
|
||||
line-height: 20px;
|
||||
display: inline-flex;
|
||||
}
|
||||
}
|
||||
|
||||
&_CREATE_TICKET.toggle-list__selected {
|
||||
box-shadow: inset 0 -5px 0px 0px rgba(20, 150, 20, 0.6);
|
||||
}
|
||||
|
||||
&_CLOSE.toggle-list__selected {
|
||||
box-shadow: inset 0 -5px 0px 0px rgba(150, 20, 20, 0.6);
|
||||
}
|
||||
|
||||
&_SIGNUP.toggle-list__selected {
|
||||
box-shadow: inset 0 -5px 0px 0px rgba(20, 20, 150, 0.6);
|
||||
}
|
||||
|
||||
&_COMMENT.toggle-list__selected {
|
||||
box-shadow: inset 0 -5px 0px 0px rgba(20, 200, 200, 0.6);
|
||||
}
|
||||
|
||||
&_ASSIGN.toggle-list__selected {
|
||||
box-shadow: inset 0 -5px 0px 0px rgba(20, 150, 20, 0.6);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&_staff {
|
||||
.stats__dropdown {
|
||||
margin-left: auto;
|
||||
margin-bottom: 20px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.stats__toggle-list {
|
||||
margin-bottom: 20px;
|
||||
float: right;
|
||||
|
||||
&-item {
|
||||
|
||||
&-value {
|
||||
font-size: $font-size--md;
|
||||
line-height: 40px;
|
||||
}
|
||||
|
||||
&-name {
|
||||
font-size: $font-size--sm;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,12 +1,20 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import i18n from 'lib-app/i18n';
|
||||
import API from 'lib-app/api-call';
|
||||
|
||||
import DateTransformer from 'lib-core/date-transformer';
|
||||
import Icon from 'core-components/icon';
|
||||
import Tooltip from 'core-components/tooltip';
|
||||
import Button from 'core-components/button';
|
||||
import SubmitButton from 'core-components/submit-button';
|
||||
import Form from 'core-components/form';
|
||||
import FormField from 'core-components/form-field';
|
||||
|
||||
const VIEW_USER_PATH = "/admin/panel/users/view-user/";
|
||||
const VIEW_STAFF_PATH = "/admin/panel/staff/view-staff/";
|
||||
class TicketEvent extends React.Component {
|
||||
static propTypes = {
|
||||
type: React.PropTypes.oneOf([
|
||||
@ -16,12 +24,20 @@ class TicketEvent extends React.Component {
|
||||
'CLOSE',
|
||||
'RE_OPEN',
|
||||
'DEPARTMENT_CHANGED',
|
||||
'PRIORITY_CHANGED'
|
||||
]),
|
||||
author: React.PropTypes.object,
|
||||
content: React.PropTypes.string,
|
||||
date: React.PropTypes.string,
|
||||
private: React.PropTypes.string,
|
||||
edited: React.PropTypes.bool,
|
||||
edit: React.PropTypes.bool,
|
||||
onToggleEdit: React.PropTypes.func,
|
||||
isLastComment: React.PropTypes.bool,
|
||||
isTicketClosed: React.PropTypes.bool
|
||||
};
|
||||
|
||||
state = {
|
||||
content: this.props.content
|
||||
};
|
||||
|
||||
render() {
|
||||
@ -72,28 +88,85 @@ class TicketEvent extends React.Component {
|
||||
'CLOSE': this.renderClosed.bind(this),
|
||||
'RE_OPEN': this.renderReOpened.bind(this),
|
||||
'DEPARTMENT_CHANGED': this.renderDepartmentChange.bind(this),
|
||||
'PRIORITY_CHANGED': this.renderPriorityChange.bind(this)
|
||||
};
|
||||
|
||||
return renders[this.props.type]();
|
||||
}
|
||||
|
||||
renderComment() {
|
||||
const { author, date, edit, file } = this.props;
|
||||
const customFields = (author && author.customfields) || [];
|
||||
|
||||
return (
|
||||
<div className="ticket-event__comment">
|
||||
<span className="ticket-event__comment-pointer" />
|
||||
<div className="ticket-event__comment-author">
|
||||
<span className="ticket-event__comment-author-name">{this.props.author.name}</span>
|
||||
<span className="ticket-event__comment-author-type">{i18n((this.props.author.staff) ? 'STAFF' : 'CUSTOMER')}</span>
|
||||
{(this.props.private*1) ? <span className="ticket-event__comment-author-type">{i18n('PRIVATE')}</span> : null}
|
||||
{this.renderCommentAuthor()}
|
||||
<span className="ticket-event__comment-badge-container">
|
||||
<span className="ticket-event__comment-badge">{i18n((author.staff) ? 'STAFF' : 'CUSTOMER')}</span>
|
||||
</span>
|
||||
{customFields.map(this.renderCustomFieldValue.bind(this))}
|
||||
{(this.props.private*1) ? this.renderPrivateBadge() : null}
|
||||
</div>
|
||||
<div className="ticket-event__comment-date">{DateTransformer.transformToString(this.props.date)}</div>
|
||||
<div className="ticket-event__comment-content" dangerouslySetInnerHTML={{__html: this.props.content}}></div>
|
||||
{this.renderFileRow(this.props.file)}
|
||||
<div className="ticket-event__comment-date">{DateTransformer.transformToString(date)}</div>
|
||||
{!edit ? this.renderContent() : this.renderEditField()}
|
||||
{this.renderFooter(file)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderCommentAuthor() {
|
||||
const {
|
||||
author,
|
||||
level
|
||||
} = this.props;
|
||||
const commentAutorClass = "ticket-event__comment-author-name";
|
||||
let commentAuthor;
|
||||
|
||||
if(level === "3") {
|
||||
commentAuthor = (
|
||||
<a className={commentAutorClass} href={((author.staff) ? VIEW_STAFF_PATH : VIEW_USER_PATH)+author.id}>
|
||||
{author.name}
|
||||
</a>
|
||||
);
|
||||
} else if(level && !author.staff) {
|
||||
commentAuthor = <a className={commentAutorClass} href={VIEW_USER_PATH+author.id}>{author.name}</a>;
|
||||
} else {
|
||||
commentAuthor = <span className={commentAutorClass}>{author.name}</span>;
|
||||
}
|
||||
|
||||
return commentAuthor;
|
||||
}
|
||||
|
||||
renderContent() {
|
||||
const { content, author, userId, userStaff, isLastComment, isTicketClosed } = this.props;
|
||||
const { id, staff } = author;
|
||||
|
||||
return (
|
||||
<div className="ticket-event__comment-content ql-editor">
|
||||
<div dangerouslySetInnerHTML={{__html: content}}></div>
|
||||
{(id == userId && staff == userStaff && isLastComment && !isTicketClosed) ? this.renderEditIcon() : null }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
renderEditIcon() {
|
||||
return (
|
||||
<div className="ticket-event__comment-content__edit" >
|
||||
<Icon name="pencil" onClick={this.props.onToggleEdit} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
renderEditField() {
|
||||
return (
|
||||
<Form loading={this.props.loading} values={{content:this.state.content}} onChange={(form) => {this.setState({content:form.content})}} onSubmit={this.props.onEdit}>
|
||||
<FormField name="content" required field="textarea" validation="TEXT_AREA" fieldProps={{allowImages: this.props.allowAttachments}}/>
|
||||
<div className="ticket-event__submit-edited-comment" >
|
||||
<SubmitButton type="secondary" >{i18n('SUBMIT')}</SubmitButton>
|
||||
<Button size="medium" onClick={this.props.onToggleEdit}>{i18n('CLOSE')}</Button>
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
renderAssignment() {
|
||||
let assignedTo = this.props.content;
|
||||
let authorName = this.props.author.name;
|
||||
@ -161,19 +234,19 @@ class TicketEvent extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
renderPriorityChange() {
|
||||
renderPrivateBadge() {
|
||||
return (
|
||||
<div className="ticket-event__circled">
|
||||
<span className="ticket-event__circled-author">{this.props.author.name}</span>
|
||||
<span className="ticket-event__circled-text"> {i18n('ACTIVITY_PRIORITY_CHANGED_THIS')}</span>
|
||||
<span className="ticket-event__circled-indication"> {this.props.content}</span>
|
||||
<span className="ticket-event__circled-date"> {i18n('DATE_PREFIX')} {DateTransformer.transformToString(this.props.date)}</span>
|
||||
</div>
|
||||
<span className="ticket-event__comment-badge-container">
|
||||
<Tooltip content={i18n('PRIVATE_RESPONSE_DESCRIPTION')} openOnHover>
|
||||
<span className="ticket-event__comment-badge">{i18n('PRIVATE')}</span>
|
||||
</Tooltip>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
renderFileRow(file) {
|
||||
renderFooter(file) {
|
||||
let node = null;
|
||||
let edited = null;
|
||||
|
||||
if (file) {
|
||||
node = <span> {this.getFileLink(file)} <Icon name="paperclip" /> </span>;
|
||||
@ -181,11 +254,30 @@ class TicketEvent extends React.Component {
|
||||
node = i18n('NO_ATTACHMENT');
|
||||
}
|
||||
|
||||
if (this.props.edited && this.props.type === 'COMMENT') {
|
||||
edited = i18n('COMMENT_EDITED');
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="ticket-event__file">
|
||||
{node}
|
||||
<div className="ticket-event__items">
|
||||
<div className="ticket-event__edited">
|
||||
{edited}
|
||||
</div>
|
||||
<div className="ticket-event__file">
|
||||
{node}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
renderCustomFieldValue(customField) {
|
||||
return (
|
||||
<span className="ticket-event__comment-badge-container">
|
||||
<span className="ticket-event__comment-badge">
|
||||
{customField.customfield}: <span className="ticket-event__comment-badge-value">{customField.value}</span>
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
getClass() {
|
||||
@ -196,7 +288,6 @@ class TicketEvent extends React.Component {
|
||||
'CLOSE': true,
|
||||
'RE_OPEN': true,
|
||||
'DEPARTMENT_CHANGED': true,
|
||||
'PRIORITY_CHANGED': true
|
||||
};
|
||||
const classes = {
|
||||
'row': true,
|
||||
@ -207,7 +298,6 @@ class TicketEvent extends React.Component {
|
||||
'ticket-event_close': this.props.type === 'CLOSE',
|
||||
'ticket-event_reopen': this.props.type === 'RE_OPEN',
|
||||
'ticket-event_department': this.props.type === 'DEPARTMENT_CHANGED',
|
||||
'ticket-event_priority': this.props.type === 'PRIORITY_CHANGED',
|
||||
'ticket-event_private': this.props.private*1,
|
||||
};
|
||||
|
||||
@ -222,7 +312,6 @@ class TicketEvent extends React.Component {
|
||||
'CLOSE': 'lock',
|
||||
'RE_OPEN': 'unlock-alt',
|
||||
'DEPARTMENT_CHANGED': 'exchange',
|
||||
'PRIORITY_CHANGED': 'exclamation'
|
||||
};
|
||||
const iconSize = {
|
||||
'COMMENT': '2x',
|
||||
@ -231,7 +320,6 @@ class TicketEvent extends React.Component {
|
||||
'CLOSE': 'lg',
|
||||
'RE_OPEN': 'lg',
|
||||
'DEPARTMENT_CHANGED': 'lg',
|
||||
'PRIORITY_CHANGED': 'lg'
|
||||
};
|
||||
|
||||
return {
|
||||
@ -249,4 +337,7 @@ class TicketEvent extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default TicketEvent;
|
||||
export default connect((store) => {
|
||||
return { level: store.session.userLevel };
|
||||
})(TicketEvent);
|
||||
|
||||
|
@ -47,7 +47,7 @@
|
||||
|
||||
&__comment {
|
||||
position: relative;
|
||||
word-break: break-all;
|
||||
word-wrap: break-word;
|
||||
|
||||
&-pointer {
|
||||
right: 100%;
|
||||
@ -64,17 +64,19 @@
|
||||
position: relative;
|
||||
padding: 12px;
|
||||
color: $primary-black;
|
||||
}
|
||||
|
||||
&-type {
|
||||
font-size: 10.6px;
|
||||
padding-left: 10px;
|
||||
color: $secondary-blue;
|
||||
background-color: very-light-grey;
|
||||
border: 2px solid;
|
||||
border-radius: 4px;
|
||||
padding: 4px;
|
||||
margin-left: 12px;
|
||||
}
|
||||
&-badge-container {
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
&-badge {
|
||||
font-size: 10.6px;
|
||||
font-weight: bold;
|
||||
color: $primary-blue;
|
||||
background-color: $very-light-grey;
|
||||
padding: 6px;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
&-date {
|
||||
@ -82,7 +84,7 @@
|
||||
border: 2px solid $light-grey;
|
||||
border-bottom: none;
|
||||
padding: 12px;
|
||||
font-size: 10.6px;
|
||||
font-size: 12.5px;
|
||||
font-family: helvetica;
|
||||
background-color: $light-grey;
|
||||
|
||||
@ -92,27 +94,62 @@
|
||||
background-color: white;
|
||||
border: 2px solid $very-light-grey;
|
||||
border-top: none;
|
||||
padding: 20px 10px;
|
||||
padding: 28px 10px;
|
||||
text-align: left;
|
||||
position:relative;
|
||||
overflow-y: initial;
|
||||
|
||||
&:hover {
|
||||
.ticket-event__comment-content__edit {
|
||||
color: grey;
|
||||
cursor:pointer;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
max-width:100%;
|
||||
}
|
||||
|
||||
&__edit {
|
||||
position:absolute;
|
||||
top: 3px;
|
||||
right: 9px;
|
||||
align-self: right;
|
||||
color:white;
|
||||
}
|
||||
}
|
||||
}
|
||||
&__submit-edited-comment {
|
||||
display:flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
&__items {
|
||||
background-color: $very-light-grey;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
&__file {
|
||||
background-color: $very-light-grey;
|
||||
cursor: pointer;
|
||||
text-align: right;
|
||||
padding: 5px 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
&__edited{
|
||||
font-style: italic;
|
||||
}
|
||||
&__comment-badge-value {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
&_staff {
|
||||
.ticket-event__icon {
|
||||
background-color: $primary-blue;
|
||||
}
|
||||
|
||||
.ticket-event__comment-author-type {
|
||||
color: $primary-blue;
|
||||
}
|
||||
}
|
||||
|
||||
&_circled {
|
||||
@ -176,12 +213,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
&_priority {
|
||||
.ticket-event__icon {
|
||||
padding-left: 11px;
|
||||
padding-top: 5px;
|
||||
}
|
||||
}
|
||||
&_private {
|
||||
.ticket-event__comment-pointer {
|
||||
border-right-color: $light-yellow;
|
||||
@ -190,16 +221,9 @@
|
||||
background-color: $light-yellow;
|
||||
border-color: $light-yellow;
|
||||
}
|
||||
.ticket-event__comment-content {
|
||||
background-color: $very-light-yellow;
|
||||
border-color: $very-light-yellow;
|
||||
}
|
||||
.ticket-event__staff-pic {
|
||||
background-color: $light-yellow;
|
||||
border-color: $light-yellow;
|
||||
}
|
||||
.ticket-event__file {
|
||||
background-color: $light-yellow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -35,14 +35,6 @@ class TicketInfo extends React.Component {
|
||||
{(this.props.ticket.closed) ? 'closed' : 'open'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="ticket-info__properties__priority">
|
||||
<span className="ticket-info__properties__label">
|
||||
{i18n('PRIORITY')}:
|
||||
</span>
|
||||
<span className={this.getPriorityClass()}>
|
||||
{this.props.ticket.priority}
|
||||
</span>
|
||||
</div>
|
||||
<div className="ticket-info__properties__owner">
|
||||
<span className="ticket-info__properties__label">
|
||||
{i18n('OWNED')}:
|
||||
@ -80,15 +72,6 @@ class TicketInfo extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
getPriorityClass() {
|
||||
let priorityClasses = {
|
||||
'low': 'ticket-info__properties__badge-green',
|
||||
'medium': 'ticket-info__properties__badge-blue',
|
||||
'high': 'ticket-info__properties__badge-red'
|
||||
};
|
||||
|
||||
return priorityClasses[this.props.ticket.priority];
|
||||
}
|
||||
}
|
||||
|
||||
export default TicketInfo;
|
||||
|
@ -3,6 +3,7 @@
|
||||
.ticket-info {
|
||||
width: 300px;
|
||||
font-weight: normal;
|
||||
text-align: left;
|
||||
|
||||
&__title {
|
||||
color: $primary-black;
|
||||
@ -35,7 +36,6 @@
|
||||
|
||||
&__status,
|
||||
&__owner,
|
||||
&__priority,
|
||||
&__comments {
|
||||
display: inline-block;
|
||||
width: 50%;
|
||||
@ -86,4 +86,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +1,22 @@
|
||||
import React from 'react';
|
||||
import _ from 'lodash';
|
||||
import {connect} from 'react-redux';
|
||||
import queryString from 'query-string';
|
||||
|
||||
import i18n from 'lib-app/i18n';
|
||||
import DateTransformer from 'lib-core/date-transformer';
|
||||
|
||||
import TicketInfo from 'app-components/ticket-info';
|
||||
import DepartmentDropdown from 'app-components/department-dropdown';
|
||||
import Table from 'core-components/table';
|
||||
import Button from 'core-components/button';
|
||||
import Tooltip from 'core-components/tooltip';
|
||||
import DropDown from 'core-components/drop-down';
|
||||
import Checkbox from 'core-components/checkbox';
|
||||
import Tag from 'core-components/tag';
|
||||
import Icon from 'core-components/icon';
|
||||
import Message from 'core-components/message';
|
||||
import history from 'lib-app/history';
|
||||
import PageSizeDropdown from './page-size-dropdown';
|
||||
|
||||
class TicketList extends React.Component {
|
||||
static propTypes = {
|
||||
@ -21,7 +29,11 @@ class TicketList extends React.Component {
|
||||
type: React.PropTypes.oneOf([
|
||||
'primary',
|
||||
'secondary'
|
||||
])
|
||||
]),
|
||||
closedTicketsShown: React.PropTypes.bool,
|
||||
onClosedTicketsShownChange: React.PropTypes.func,
|
||||
onDepartmentChange: React.PropTypes.func,
|
||||
showPageSizeDropdown: React.PropTypes.bool
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
@ -30,7 +42,9 @@ class TicketList extends React.Component {
|
||||
tickets: [],
|
||||
departments: [],
|
||||
ticketPath: '/dashboard/ticket/',
|
||||
type: 'primary'
|
||||
type: 'primary',
|
||||
closedTicketsShown: false,
|
||||
showPageSizeDropdown: true
|
||||
};
|
||||
|
||||
state = {
|
||||
@ -38,61 +52,129 @@ class TicketList extends React.Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { type, showDepartmentDropdown, onClosedTicketsShownChange, showPageSizeDropdown } = this.props;
|
||||
const pages = [5, 10, 20, 50];
|
||||
|
||||
return (
|
||||
<div className="ticket-list">
|
||||
{(this.props.type === 'secondary' && this.props.showDepartmentDropdown) ? this.renderDepartmentsDropDown() : null}
|
||||
<div className="ticket-list__filters">
|
||||
<div className="ticket-list__main-filters">
|
||||
{(type === 'primary') ? this.renderMessage() : null}
|
||||
{
|
||||
((type === 'secondary') && showDepartmentDropdown) ?
|
||||
this.renderDepartmentsDropDown() :
|
||||
null
|
||||
}
|
||||
{onClosedTicketsShownChange ? this.renderFilterCheckbox() : null}
|
||||
</div>
|
||||
{
|
||||
showPageSizeDropdown ?
|
||||
<PageSizeDropdown className="ticket-list__page-dropdown" pages={pages} onChange={(event) => this.pageSizeChange(event)} /> :
|
||||
null
|
||||
}
|
||||
</div>
|
||||
<Table {...this.getTableProps()} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderFilterCheckbox() {
|
||||
return (
|
||||
<Checkbox
|
||||
className="ticket-list__checkbox"
|
||||
label={i18n("SHOW_CLOSED_TICKETS")}
|
||||
value={this.props.closedTicketsShown}
|
||||
onChange={this.props.onClosedTicketsShownChange}
|
||||
wrapInLabel
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderDepartmentsDropDown() {
|
||||
return (
|
||||
<div className="ticket-list__department-selector">
|
||||
<DropDown {...this.getDepartmentDropdownProps()} />
|
||||
<DepartmentDropdown {...this.getDepartmentDropdownProps()} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderMessage() {
|
||||
switch (queryString.parse(window.location.search)["message"]) {
|
||||
case 'success':
|
||||
return (
|
||||
<Message
|
||||
onCloseMessage={this.onCloseMessage}
|
||||
className="create-ticket-form__message"
|
||||
type="success">
|
||||
{i18n('TICKET_SENT')}
|
||||
</Message>
|
||||
);
|
||||
case 'fail':
|
||||
return (
|
||||
<Message
|
||||
onCloseMessage={this.onCloseMessage}
|
||||
className="create-ticket-form__message"
|
||||
type="error">
|
||||
{i18n('TICKET_SENT_ERROR')}
|
||||
</Message>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
pageSizeChange(event) {
|
||||
const { onPageSizeChange } = this.props;
|
||||
|
||||
onPageSizeChange && onPageSizeChange(event.pageSize);
|
||||
}
|
||||
|
||||
getDepartmentDropdownProps() {
|
||||
const { departments, onDepartmentChange } = this.props;
|
||||
|
||||
return {
|
||||
items: this.getDepartments(),
|
||||
departments: this.getDepartments(),
|
||||
onChange: (event) => {
|
||||
const departmentId = event.index && departments[event.index - 1].id;
|
||||
|
||||
this.setState({
|
||||
selectedDepartment: event.index && this.props.departments[event.index - 1].id
|
||||
selectedDepartment: departmentId
|
||||
});
|
||||
|
||||
onDepartmentChange && onDepartmentChange(departmentId || null);
|
||||
},
|
||||
size: 'medium'
|
||||
};
|
||||
}
|
||||
|
||||
getTableProps() {
|
||||
const { loading, page, pages, onPageChange } = this.props;
|
||||
|
||||
return {
|
||||
loading: this.props.loading,
|
||||
loading,
|
||||
headers: this.getTableHeaders(),
|
||||
rows: this.getTableRows(),
|
||||
pageSize: 10,
|
||||
comp: this.compareFunction,
|
||||
page: this.props.page,
|
||||
pages: this.props.pages,
|
||||
onPageChange: this.props.onPageChange
|
||||
pageSize: this.state.tickets,
|
||||
page,
|
||||
pages,
|
||||
onPageChange
|
||||
};
|
||||
}
|
||||
|
||||
getDepartments() {
|
||||
let departments = this.props.departments.map((department) => {
|
||||
return {content: department.name};
|
||||
});
|
||||
let departments = _.clone(this.props.departments);
|
||||
|
||||
departments.unshift({
|
||||
content: i18n('ALL_DEPARTMENTS')
|
||||
name: i18n('ALL_DEPARTMENTS')
|
||||
});
|
||||
|
||||
return departments;
|
||||
}
|
||||
|
||||
getTableHeaders() {
|
||||
if (this.props.type == 'primary' ) {
|
||||
const { type } = this.props;
|
||||
|
||||
if(type == 'primary' ) {
|
||||
return [
|
||||
{
|
||||
key: 'number',
|
||||
@ -111,11 +193,14 @@ class TicketList extends React.Component {
|
||||
},
|
||||
{
|
||||
key: 'date',
|
||||
value: i18n('DATE'),
|
||||
value: <div>
|
||||
{i18n('DATE')}
|
||||
{this.renderSortArrow('date')}
|
||||
</div>,
|
||||
className: 'ticket-list__date col-md-2'
|
||||
}
|
||||
];
|
||||
} else if (this.props.type == 'secondary') {
|
||||
} else if(type == 'secondary') {
|
||||
return [
|
||||
{
|
||||
key: 'number',
|
||||
@ -127,11 +212,6 @@ class TicketList extends React.Component {
|
||||
value: i18n('TITLE'),
|
||||
className: 'ticket-list__title col-md-4'
|
||||
},
|
||||
{
|
||||
key: 'priority',
|
||||
value: i18n('PRIORITY'),
|
||||
className: 'ticket-list__priority col-md-1'
|
||||
},
|
||||
{
|
||||
key: 'department',
|
||||
value: i18n('DEPARTMENT'),
|
||||
@ -144,107 +224,114 @@ class TicketList extends React.Component {
|
||||
},
|
||||
{
|
||||
key: 'date',
|
||||
value: i18n('DATE'),
|
||||
value: <div>
|
||||
{i18n('DATE')}
|
||||
{this.renderSortArrow('date')}
|
||||
</div>,
|
||||
className: 'ticket-list__date col-md-2'
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
renderSortArrow(header) {
|
||||
const { orderBy, showOrderArrows, onChangeOrderBy } = this.props;
|
||||
|
||||
return (
|
||||
showOrderArrows ?
|
||||
<Icon
|
||||
name={`arrow-${this.getIconName(header, orderBy)}`}
|
||||
className="ticket-list__order-icon"
|
||||
color={this.getIconColor(header, orderBy)}
|
||||
onClick={() => onChangeOrderBy(header)} /> :
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
getIconName(header, orderBy) {
|
||||
return (orderBy && orderBy.value === header && orderBy.asc) ? "up" : "down";
|
||||
}
|
||||
|
||||
getIconColor(header, orderBy) {
|
||||
return (orderBy && orderBy.value === header) ? "gray" : "white";
|
||||
}
|
||||
|
||||
getTableRows() {
|
||||
return this.getTickets().map(this.gerTicketTableObject.bind(this));
|
||||
return this.getTickets().map(this.getTicketTableObject.bind(this));
|
||||
}
|
||||
|
||||
getTickets() {
|
||||
return (this.state.selectedDepartment) ? _.filter(this.props.tickets, (ticket) => {
|
||||
return ticket.department.id == this.state.selectedDepartment
|
||||
}) : this.props.tickets;
|
||||
const { tickets } = this.props;
|
||||
const { selectedDepartment } = this.state;
|
||||
|
||||
return (
|
||||
(selectedDepartment) ?
|
||||
_.filter(tickets, (ticket) => { return ticket.department.id == selectedDepartment}) :
|
||||
tickets
|
||||
);
|
||||
}
|
||||
|
||||
gerTicketTableObject(ticket) {
|
||||
let titleText = (this.isTicketUnread(ticket)) ? ticket.title + ' (1)' : ticket.title;
|
||||
getTicketTableObject(ticket) {
|
||||
const { date, title, ticketNumber, closed, tags, department, author } = ticket;
|
||||
const dateTodayWithOutHoursAndMinutes = DateTransformer.getDateToday();
|
||||
const ticketDateWithOutHoursAndMinutes = Math.floor(DateTransformer.UTCDateToLocalNumericDate(JSON.stringify(date*1)) / 10000);
|
||||
const stringTicketLocalDateFormat = DateTransformer.transformToString(date, false, true);
|
||||
const ticketDate = (
|
||||
((dateTodayWithOutHoursAndMinutes - ticketDateWithOutHoursAndMinutes) > 1) ?
|
||||
stringTicketLocalDateFormat :
|
||||
`${(dateTodayWithOutHoursAndMinutes - ticketDateWithOutHoursAndMinutes) ? i18n("YESTERDAY_AT") : i18n("TODAY_AT")} ${stringTicketLocalDateFormat.slice(-5)}`
|
||||
);
|
||||
let titleText = (this.isTicketUnread(ticket)) ? title + ' (1)' : title;
|
||||
|
||||
return {
|
||||
number: (
|
||||
<Tooltip content={<TicketInfo ticket={ticket}/>} openOnHover>
|
||||
{'#' + ticket.ticketNumber}
|
||||
<Tooltip content={<TicketInfo ticket={ticket} />} openOnHover>
|
||||
{'#' + ticketNumber}
|
||||
</Tooltip>
|
||||
),
|
||||
title: (
|
||||
<Button className="ticket-list__title-link" type="clean" route={{to: this.props.ticketPath + ticket.ticketNumber}}>
|
||||
{titleText}
|
||||
</Button>
|
||||
<div>
|
||||
{closed ? <Icon size="sm" name="lock" /> : null}
|
||||
<Button className="ticket-list__title-link" type="clean" route={{to: this.props.ticketPath + ticketNumber}}>
|
||||
{titleText}
|
||||
</Button>
|
||||
{(tags || []).map((tagName,index) => {
|
||||
let tag = _.find(this.props.tags, {name:tagName});
|
||||
return <Tag size='small' name={tag && tag.name} color={tag && tag.color} key={index} />
|
||||
})}
|
||||
</div>
|
||||
|
||||
),
|
||||
priority: this.getTicketPriority(ticket.priority),
|
||||
department: ticket.department.name,
|
||||
author: ticket.author.name,
|
||||
date: DateTransformer.transformToString(ticket.date, false),
|
||||
department: department.name,
|
||||
author: author.name,
|
||||
date: ticketDate,
|
||||
unread: this.isTicketUnread(ticket),
|
||||
highlighted: this.isTicketUnread(ticket)
|
||||
};
|
||||
}
|
||||
|
||||
getTicketPriority(priority) {
|
||||
if(priority == 'high'){
|
||||
return (
|
||||
<span className="ticket-list__priority-high">{i18n('HIGH')}</span>
|
||||
);
|
||||
}
|
||||
if(priority == 'medium'){
|
||||
return (
|
||||
<span className="ticket-list__priority-medium">{i18n('MEDIUM')}</span>
|
||||
);
|
||||
}
|
||||
if(priority == 'low'){
|
||||
return (
|
||||
<span className="ticket-list__priority-low">{i18n('LOW')}</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
compareFunction(row1, row2) {
|
||||
if (row1.closed == row2.closed) {
|
||||
if (row1.unread == row2.unread) {
|
||||
let s1 = row1.date;
|
||||
let s2 = row2.date;
|
||||
|
||||
let y1 = s1.substring(0, 4);
|
||||
let y2 = s2.substring(0, 4);
|
||||
|
||||
if (y1 == y2) {
|
||||
let m1 = s1.substring(4, 6);
|
||||
let m2 = s2.substring(4, 6);
|
||||
|
||||
if (m1 == m2) {
|
||||
let d1 = s1.substring(6, 8);
|
||||
let d2 = s2.substring(6, 8);
|
||||
|
||||
if (d1 == d2) {
|
||||
return 0;
|
||||
}
|
||||
return d1 > d2 ? -1 : 1;
|
||||
}
|
||||
return m1 > m2 ? -1 : 1;
|
||||
}
|
||||
return y1 > y2 ? -1 : 1;
|
||||
}
|
||||
return row1.unread ? -1 : 1;
|
||||
}
|
||||
return row1.closed ? -1 : 1;
|
||||
}
|
||||
|
||||
isTicketUnread(ticket) {
|
||||
if(this.props.type === 'primary') {
|
||||
return ticket.unread;
|
||||
} else if(this.props.type === 'secondary') {
|
||||
if(ticket.author.id == this.props.userId && ticket.author.staff) {
|
||||
return ticket.unread;
|
||||
} else {
|
||||
return ticket.unreadStaff;
|
||||
}
|
||||
const { type, userId } = this.props;
|
||||
const { unread, author, unreadStaff } = ticket;
|
||||
|
||||
if(type === 'primary') {
|
||||
return unread;
|
||||
} else if(type === 'secondary') {
|
||||
if(author.id == userId && author.staff) {
|
||||
return unread;
|
||||
} else {
|
||||
return unreadStaff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onCloseMessage() {
|
||||
history.push(window.location.pathname);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default TicketList;
|
||||
export default connect((store) => {
|
||||
return {
|
||||
tags: store.config['tags']
|
||||
};
|
||||
})(TicketList);
|
||||
|
@ -1,9 +1,39 @@
|
||||
@import "../scss/vars";
|
||||
|
||||
.ticket-list {
|
||||
&__order-icon {
|
||||
padding-left: 5px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&__filters {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 25px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
&__main-filters {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__department-selector {
|
||||
margin-bottom: 25px;
|
||||
display: inline-block;
|
||||
margin-right: 25px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__page-dropdown {
|
||||
display: inline-block;
|
||||
margin-right: 25px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__checkbox {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
&__number {
|
||||
@ -32,24 +62,8 @@
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
&__priority-low,
|
||||
&__priority-medium,
|
||||
&__priority-high {
|
||||
display: inline-block;
|
||||
border-radius: 10px;
|
||||
width: 70px;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
&__priority-low {
|
||||
background-color: $primary-green;
|
||||
}
|
||||
|
||||
&__priority-medium {
|
||||
background-color: $secondary-blue;
|
||||
}
|
||||
|
||||
&__priority-high {
|
||||
background-color: $primary-red;
|
||||
}
|
||||
.create-ticket-form__message {
|
||||
width: 100%;
|
||||
}
|
350
client/src/app-components/ticket-query-filters.js
Normal file
350
client/src/app-components/ticket-query-filters.js
Normal file
@ -0,0 +1,350 @@
|
||||
import React from 'react';
|
||||
import _ from 'lodash';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import SearchFiltersActions from 'actions/search-filters-actions';
|
||||
|
||||
import i18n from 'lib-app/i18n';
|
||||
import API from 'lib-app/api-call';
|
||||
import history from 'lib-app/history';
|
||||
import searchTicketsUtils from 'lib-app/search-tickets-utils';
|
||||
import ticketUtils from 'lib-app/ticket-utils';
|
||||
|
||||
import Form from 'core-components/form';
|
||||
import SubmitButton from 'core-components/submit-button';
|
||||
import FormField from 'core-components/form-field';
|
||||
import Icon from 'core-components/icon';
|
||||
import Button from 'core-components/button';
|
||||
import Loading from 'core-components/loading';
|
||||
|
||||
|
||||
class TicketQueryFilters extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
filters: React.PropTypes.shape({
|
||||
query: React.PropTypes.string,
|
||||
departments: React.PropTypes.string,
|
||||
owners: React.PropTypes.string,
|
||||
tags: React.PropTypes.string,
|
||||
dateRange: React.PropTypes.string,
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
formState,
|
||||
filters,
|
||||
showFilters,
|
||||
ticketQueryListState,
|
||||
staffList
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className={"ticket-query-filters" + (showFilters ? "__open" : "") }>
|
||||
<Form
|
||||
loading={ticketQueryListState.loading}
|
||||
values={this.getFormValue(formState)}
|
||||
onChange={this.onChangeForm.bind(this)}
|
||||
onSubmit={this.onSubmitListConfig.bind(this)}>
|
||||
<div className="ticket-query-filters__search-box">
|
||||
<FormField name="query" field="search-box" fieldProps={{onSearch: this.onSubmitListConfig.bind(this)}} />
|
||||
</div>
|
||||
<div className="ticket-query-filters__second-row">
|
||||
<FormField
|
||||
label={i18n('PERIOD')}
|
||||
name="period"
|
||||
field="select"
|
||||
fieldProps={{
|
||||
items: [{content: i18n('LAST_7_DAYS')}, {content: i18n('LAST_30_DAYS')}, {content: i18n('LAST_90_DAYS')}, {content: i18n('LAST_365_DAYS')}],
|
||||
className: 'ticket-query-filters__drop-down'
|
||||
}} />
|
||||
<FormField
|
||||
label={i18n('DEPARTMENTS')}
|
||||
name="departments"
|
||||
field="autocomplete"
|
||||
fieldProps={{items: this.getDepartmentsItems()}} />
|
||||
<FormField
|
||||
label={i18n('OWNER')}
|
||||
name="owners"
|
||||
field="autocomplete"
|
||||
fieldProps={{items: ticketUtils.getStaffList({staffList}, 'toAutocomplete')}} />
|
||||
</div>
|
||||
<div className="ticket-query-filters__third-row">
|
||||
<FormField
|
||||
label={i18n('STATUS')}
|
||||
name="closed"
|
||||
field="select"
|
||||
fieldProps={{
|
||||
items: this.getStatusItems(),
|
||||
className: 'ticket-query-filters__drop-down'
|
||||
}} />
|
||||
<FormField
|
||||
label={i18n('TAGS')}
|
||||
name="tags"
|
||||
field="tag-selector"
|
||||
fieldProps={{
|
||||
items: this.getTags(filters.tags),
|
||||
onRemoveClick: this.removeTag.bind(this),
|
||||
onTagSelected: this.addTag.bind(this)
|
||||
}} />
|
||||
<FormField
|
||||
label={i18n('AUTHORS')}
|
||||
name="authors"
|
||||
field="autocomplete"
|
||||
fieldProps={{
|
||||
getItemListFromQuery: this.searchAuthors.bind(this),
|
||||
comparerFunction: this.autorsComparer.bind(this)
|
||||
}} />
|
||||
</div>
|
||||
<div className="ticket-query-filters__container">
|
||||
<Button
|
||||
className="ticket-query-filters__container__button ticket-query-filters__container__clear-button"
|
||||
size= "medium"
|
||||
disabled={ticketQueryListState.loading}
|
||||
onClick={this.clearFormValues.bind(this)}>
|
||||
{ticketQueryListState.loading ? <Loading /> : i18n('CLEAR')}
|
||||
</Button>
|
||||
<SubmitButton
|
||||
className="ticket-query-filters__container__button ticket-query-filters__container__search-button"
|
||||
type="secondary"
|
||||
size= "medium">
|
||||
{i18n('SEARCH')}
|
||||
</SubmitButton>
|
||||
</div>
|
||||
</Form>
|
||||
<span className="separator" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
searchAuthors(query, blacklist = []) {
|
||||
blacklist = blacklist.map(item => {return {isStaff: item.isStaff, id: item.id}});
|
||||
|
||||
return API.call({
|
||||
path: '/ticket/search-authors',
|
||||
data: {
|
||||
query: query,
|
||||
blackList: JSON.stringify(blacklist)
|
||||
}
|
||||
}).then(r => {
|
||||
return r.data.authors.map(author => {
|
||||
return {
|
||||
name: author.name,
|
||||
color: "gray",
|
||||
id: author.id*1,
|
||||
profilePic: author.profilePic,
|
||||
isStaff: author.isStaff * 1,
|
||||
content: author.profilePic !== undefined ? ticketUtils.renderStaffOption(author) : author.name,
|
||||
contentOnSelected: author.profilePic !== undefined ? ticketUtils.renderStaffSelected(author) : author.name
|
||||
}});
|
||||
});
|
||||
}
|
||||
|
||||
renderDepartmentOption(department) {
|
||||
return (
|
||||
<div className="ticket-query-filters__department-option" key={`department-option-${department.id}`}>
|
||||
{department.private*1 ?
|
||||
<Icon className="ticket-query-filters__department-option__icon" name='user-secret'/> :
|
||||
null}
|
||||
<span className="ticket-query-filters__department-option__name">{department.name}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderDeparmentSelected(department) {
|
||||
return (
|
||||
<div className="ticket-query-filters__department-selected" key={`department-selected-${department.id}`}>
|
||||
{department.private*1 ?
|
||||
<Icon className="ticket-query-filters__department-selected__icon" name='user-secret'/> :
|
||||
null}
|
||||
<span className="ticket-query-filters__department-selected__name">{department.name}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
addTag(tag) {
|
||||
const { formState } = this.props;
|
||||
this.onChangeFormState({...formState, tags: [...formState.tags, tag]});
|
||||
}
|
||||
|
||||
autorsComparer(autorList, autorSelectedList) {
|
||||
return autorList.filter(item => !_.some(autorSelectedList, {id: item.id, isStaff: item.isStaff}));
|
||||
}
|
||||
|
||||
clearFormValues(event) {
|
||||
event.preventDefault();
|
||||
this.props.dispatch(SearchFiltersActions.setDefaultFormValues());
|
||||
}
|
||||
|
||||
getDepartmentsItems() {
|
||||
const { departments, } = this.props;
|
||||
let departmentsList = departments.map(department => {
|
||||
return {
|
||||
id: JSON.parse(department.id),
|
||||
name: department.name.toLowerCase(),
|
||||
color: 'gray',
|
||||
contentOnSelected: this.renderDeparmentSelected(department),
|
||||
content: this.renderDepartmentOption(department),
|
||||
}
|
||||
});
|
||||
|
||||
return departmentsList;
|
||||
}
|
||||
|
||||
getSelectedDepartments(selectedDepartmentsId) {
|
||||
let selectedDepartments = [];
|
||||
|
||||
if(selectedDepartmentsId !== undefined) {
|
||||
selectedDepartments = selectedDepartmentsId.map(
|
||||
(departmentId) => this.getDepartmentsItems().find(_department => (_department.id === departmentId))
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
return selectedDepartments;
|
||||
}
|
||||
|
||||
getSelectedStaffs(selectedStaffsId) {
|
||||
const { staffList } = this.props;
|
||||
let selectedStaffs = [];
|
||||
|
||||
if(selectedStaffsId !== undefined) {
|
||||
selectedStaffs = selectedStaffsId.map(
|
||||
(staffId) => ticketUtils.getStaffList({staffList}, 'toAutocomplete').find(_staff => (_staff.id === staffId))
|
||||
);
|
||||
}
|
||||
|
||||
return selectedStaffs;
|
||||
}
|
||||
|
||||
getSelectedTagsName(selectedTagsId) {
|
||||
let selectedTagsName = [];
|
||||
|
||||
if(selectedTagsId !== undefined) {
|
||||
selectedTagsName = selectedTagsId.map(
|
||||
(tagId) => (this.getTags().find(_tag => (_tag.id === tagId)) || {}).name
|
||||
);
|
||||
}
|
||||
|
||||
return selectedTagsName;
|
||||
}
|
||||
|
||||
getStatusItems() {
|
||||
let items = [
|
||||
{id: 0, name: 'Any', content: i18n('ANY')},
|
||||
{id: 1, name: 'Opened', content: i18n('OPENED')},
|
||||
{id: 2, name: 'Closed', content: i18n('CLOSED')},
|
||||
];
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
getTags() {
|
||||
const { tags, } = this.props;
|
||||
let newTagList = tags.map(tag => {
|
||||
return {
|
||||
id: JSON.parse(tag.id),
|
||||
name: tag.name,
|
||||
color : tag.color
|
||||
}
|
||||
});
|
||||
|
||||
return newTagList;
|
||||
}
|
||||
|
||||
onChangeFormState(formValues) {
|
||||
this.props.dispatch(SearchFiltersActions.changeForm(formValues));
|
||||
}
|
||||
|
||||
onSubmitListConfig() {
|
||||
const {
|
||||
formState,
|
||||
filters,
|
||||
formEdited,
|
||||
} = this.props;
|
||||
const listConfigWithCompleteAuthorsList = searchTicketsUtils.formValueToListConfig(
|
||||
{...formState, orderBy: filters.orderBy, page: 1},
|
||||
true
|
||||
);
|
||||
|
||||
if(formEdited) {
|
||||
const filtersForAPI = searchTicketsUtils.getFiltersForAPI(listConfigWithCompleteAuthorsList.filters);
|
||||
const currentPath = window.location.pathname;
|
||||
const urlQuery = searchTicketsUtils.getFiltersForURL({
|
||||
filters: filtersForAPI,
|
||||
shouldRemoveCustomParam: true,
|
||||
shouldRemoveUseInitialValuesParam: true
|
||||
});
|
||||
urlQuery && history.push(`${currentPath}${urlQuery}`);
|
||||
}
|
||||
}
|
||||
|
||||
removeTag(tag) {
|
||||
const { formState } = this.props;
|
||||
|
||||
this.onChangeFormState({...formState, tags: formState.tags.filter(item => item !== tag)});
|
||||
}
|
||||
|
||||
tagsNametoTagsId(selectedTagsName) {
|
||||
let selectedTagsId = [];
|
||||
|
||||
if (selectedTagsName != undefined) {
|
||||
selectedTagsId = selectedTagsName.map(
|
||||
(tagName) => (this.getTags().find(_tag => (_tag.name === tagName)) || {}).id
|
||||
);
|
||||
}
|
||||
|
||||
return selectedTagsId;
|
||||
}
|
||||
|
||||
onChangeForm(data) {
|
||||
const departmentsId = data.departments.map(department => department.id);
|
||||
const staffsId = data.owners.map(staff => staff.id);
|
||||
const tagsName = this.tagsNametoTagsId(data.tags);
|
||||
const authors = data.authors.map(({name, id, isStaff, profilePic, color}) => ({name, id: id*1, isStaff, profilePic, color}));
|
||||
|
||||
this.onChangeFormState({
|
||||
...data,
|
||||
tags: tagsName,
|
||||
owners: staffsId,
|
||||
departments: departmentsId,
|
||||
authors: authors,
|
||||
});
|
||||
}
|
||||
|
||||
getFormValue(form) {
|
||||
return {
|
||||
...form,
|
||||
departments: this.getSelectedDepartments(form.departments),
|
||||
owners: this.getSelectedStaffs(form.owners),
|
||||
tags: this.getSelectedTagsName(form.tags),
|
||||
authors: this.getAuthors(form.authors),
|
||||
}
|
||||
}
|
||||
|
||||
getAuthors(authors = []) {
|
||||
return authors.map(author => ({
|
||||
name: author.name,
|
||||
color: "gray",
|
||||
id: author.id*1,
|
||||
isStaff: author.isStaff*1,
|
||||
profilePic: author.profilePic,
|
||||
content: author.profilePic !== undefined ? ticketUtils.renderStaffOption(author) : author.name,
|
||||
contentOnSelected: author.profilePic !== undefined ? ticketUtils.renderStaffSelected(author) : author.name
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect((store) => {
|
||||
return {
|
||||
tags: store.config.tags,
|
||||
departments: store.config.departments,
|
||||
staffList: store.adminData.staffMembers,
|
||||
formState: store.searchFilters.form,
|
||||
filters: store.searchFilters.listConfig.filters,
|
||||
showFilters: store.searchFilters.showFilters,
|
||||
formEdited: store.searchFilters.formEdited,
|
||||
ticketQueryListState: store.searchFilters.ticketQueryListState,
|
||||
};
|
||||
})(TicketQueryFilters);
|
118
client/src/app-components/ticket-query-filters.scss
Normal file
118
client/src/app-components/ticket-query-filters.scss
Normal file
@ -0,0 +1,118 @@
|
||||
@import '../scss/vars';
|
||||
|
||||
.ticket-query-filters {
|
||||
opacity: 0;
|
||||
max-height: 0;
|
||||
overflow-y: hidden;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&__open {
|
||||
max-height: 1000px;
|
||||
opacity: 1;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
&__container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
|
||||
&__button {
|
||||
margin: 0 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&__department-option {
|
||||
&__name {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&__department-selected {
|
||||
display: inline-block;
|
||||
border-radius: 3px;
|
||||
margin-left: 5px;
|
||||
padding: 1px;
|
||||
font-size: 13px;
|
||||
cursor: default;
|
||||
|
||||
&__icon {
|
||||
padding-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
&__first-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 10%;
|
||||
}
|
||||
|
||||
&__second-row,
|
||||
&__third-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
&__drop-down > .drop-down__current-item {
|
||||
background-color: $very-light-grey;
|
||||
|
||||
&:focus {
|
||||
background-color: $medium-grey;
|
||||
}
|
||||
}
|
||||
|
||||
&__search-box {
|
||||
width: 100%;
|
||||
padding: 0 50px;
|
||||
}
|
||||
|
||||
&__staff-option {
|
||||
&__profile-pic {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
&__name {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&__staff-selected {
|
||||
display: inline-block;
|
||||
border-radius: 3px;
|
||||
margin-left: 5px;
|
||||
padding: 1px;
|
||||
font-size: 13px;
|
||||
cursor: default;
|
||||
|
||||
&__profile-pic {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
border-radius: 50%;
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
&__title {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 992px) {
|
||||
.ticket-query-filters {
|
||||
&__first-row,
|
||||
&__second-row,
|
||||
&__third-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
padding: unset;
|
||||
}
|
||||
}
|
||||
}
|
82
client/src/app-components/ticket-query-list.js
Normal file
82
client/src/app-components/ticket-query-list.js
Normal file
@ -0,0 +1,82 @@
|
||||
import React from 'react';
|
||||
import _ from 'lodash';
|
||||
|
||||
import i18n from 'lib-app/i18n';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import TicketList from 'app-components/ticket-list';
|
||||
import Message from 'core-components/message';
|
||||
import searchFiltersActions from '../actions/search-filters-actions';
|
||||
import queryString from 'query-string';
|
||||
import searchTicketsUtils from 'lib-app/search-tickets-utils';
|
||||
import history from 'lib-app/history';
|
||||
|
||||
class TicketQueryList extends React.Component {
|
||||
|
||||
state = {
|
||||
tickets: [],
|
||||
page: 1,
|
||||
pages: 0,
|
||||
error: null,
|
||||
loading: true
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
this.state.error ?
|
||||
<Message showCloseButton={false} type="error">{i18n('ERROR_RETRIEVING_TICKETS')}</Message> :
|
||||
<TicketList {...this.getTicketListProps()} />
|
||||
);
|
||||
}
|
||||
|
||||
onPageChange(event) {
|
||||
const {
|
||||
dispatch,
|
||||
filters
|
||||
} = this.props;
|
||||
|
||||
dispatch(searchFiltersActions.changePage({
|
||||
...filters,
|
||||
page: event.target.value
|
||||
}));
|
||||
}
|
||||
|
||||
getTicketListProps () {
|
||||
const {
|
||||
filters,
|
||||
onChangeOrderBy,
|
||||
userId,
|
||||
ticketQueryListState
|
||||
} = this.props;
|
||||
const page = {
|
||||
...ticketQueryListState,
|
||||
...queryString.parse(window.location.search)
|
||||
}.page*1;
|
||||
|
||||
return {
|
||||
userId: userId,
|
||||
ticketPath: '/admin/panel/tickets/view-ticket/',
|
||||
tickets: ticketQueryListState.tickets,
|
||||
page: page,
|
||||
pages: ticketQueryListState.pages,
|
||||
loading: ticketQueryListState.loading,
|
||||
type: 'secondary',
|
||||
showDepartmentDropdown: false,
|
||||
closedTicketsShown: false,
|
||||
onPageChange:this.onPageChange.bind(this),
|
||||
orderBy: filters.orderBy ? JSON.parse(filters.orderBy) : filters.orderBy,
|
||||
showOrderArrows: true,
|
||||
onChangeOrderBy: onChangeOrderBy,
|
||||
showPageSizeDropdown: false
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect((store) => {
|
||||
return {
|
||||
userId: store.session.userId*1,
|
||||
filters: store.searchFilters.listConfig.filters,
|
||||
ticketQueryListState: store.searchFilters.ticketQueryListState
|
||||
};
|
||||
})(TicketQueryList);
|
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,6 @@
|
||||
@import "../scss/vars";
|
||||
|
||||
.ticket-viewer {
|
||||
|
||||
&__header {
|
||||
background-color: $primary-blue;
|
||||
border-top-right-radius: 4px;
|
||||
@ -9,34 +8,162 @@
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
padding: 6px 0;
|
||||
}
|
||||
&__private {
|
||||
display: flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
|
||||
&:hover {
|
||||
.ticket-viewer__edit-title-icon {
|
||||
color: $grey;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__buttons-column {
|
||||
padding-top: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
&__buttons-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
width: 250px;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
&__edited-title-text {
|
||||
font-style: italic;
|
||||
font-size: 14px;
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
&__edit-icon {
|
||||
right: 12px;
|
||||
margin: 0 10px;
|
||||
color: $light-grey;
|
||||
|
||||
&:hover {
|
||||
cursor:pointer;
|
||||
}
|
||||
}
|
||||
|
||||
&__edit-title-icon {
|
||||
color: $primary-blue;
|
||||
right: 12px;
|
||||
margin: 0 10px;
|
||||
|
||||
&:hover {
|
||||
cursor:pointer;
|
||||
}
|
||||
}
|
||||
|
||||
&__edit-status__buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
&___input-edit-title {
|
||||
color: black;
|
||||
align-items:center;
|
||||
justify-content: center;
|
||||
margin-bottom: 6px;
|
||||
margin-right: 6px;
|
||||
|
||||
.input__text {
|
||||
height: 30px;
|
||||
text-align: center;
|
||||
padding-top: 12px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
&__edit-title__buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
width: 160px;
|
||||
}
|
||||
|
||||
&__edit-title__button {
|
||||
width: 50px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&__number {
|
||||
color: white;
|
||||
margin-right: 10px;
|
||||
margin-right: 30px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&__title {
|
||||
display: inline-block;
|
||||
max-width: 375px;
|
||||
}
|
||||
|
||||
&__flag {
|
||||
margin-left: 10px;
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
&__info-row-header {
|
||||
&__info {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
background-color: $light-grey;
|
||||
font-weight: bold;
|
||||
}
|
||||
padding: 10px 15px 30px 15px;
|
||||
|
||||
&__info-row-values {
|
||||
background-color: $light-grey;
|
||||
color: $secondary-blue;
|
||||
padding-bottom: 10px;
|
||||
&-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
min-width: 150px;
|
||||
max-width: 250px;
|
||||
}
|
||||
|
||||
&-container-editable {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
min-width: 300px;
|
||||
|
||||
&:hover {
|
||||
.ticket-viewer__edit-icon {
|
||||
color: $primary-blue;
|
||||
}
|
||||
}
|
||||
|
||||
&__column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
min-width: 170px;
|
||||
}
|
||||
}
|
||||
|
||||
&-header {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&-value {
|
||||
color: $secondary-blue;
|
||||
padding-bottom: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&__editable-dropdown {
|
||||
@ -55,7 +182,15 @@
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&__reopen-close-buttons {
|
||||
width: 230px;
|
||||
display: flex;
|
||||
align-content: left;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&__response {
|
||||
width: 100%;
|
||||
margin-top: 20px;
|
||||
position: relative;
|
||||
|
||||
@ -74,24 +209,74 @@
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
&-custom {
|
||||
&-actions {
|
||||
background-color: $very-light-grey;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&-custom {
|
||||
padding: 20px 0 0 20px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
&-private {
|
||||
padding: 20px 20px 0 0;
|
||||
|
||||
&-info {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
&-container {
|
||||
display: flex;
|
||||
position: relative;
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
&-buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
align-items: flex-end;
|
||||
width: 50%;
|
||||
min-width: 50%;
|
||||
}
|
||||
|
||||
}
|
||||
&__actions {
|
||||
background-color: $very-light-grey;
|
||||
|
||||
display:flex;
|
||||
align-items:center;justify-content: space-between;
|
||||
@media screen and (max-width: 1151px) {
|
||||
.ticket-viewer__info {
|
||||
&-container {
|
||||
width: 200px;
|
||||
min-width: unset;
|
||||
max-width: unset;
|
||||
}
|
||||
|
||||
&-container-editable {
|
||||
min-width: 250px;
|
||||
max-width: 300px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 992px) {
|
||||
.ticket-viewer__info {
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 571px) {
|
||||
.ticket-viewer {
|
||||
&__number, &__edit-title-icon {
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
&__flag {
|
||||
padding: 0 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import FormField from 'core-components/form-field';
|
||||
import SubmitButton from 'core-components/submit-button';
|
||||
import IconSelector from 'core-components/icon-selector';
|
||||
import ColorSelector from 'core-components/color-selector';
|
||||
import InfoTooltip from 'core-components/info-tooltip';
|
||||
|
||||
class TopicEditModal extends React.Component {
|
||||
|
||||
@ -24,7 +25,7 @@ class TopicEditModal extends React.Component {
|
||||
};
|
||||
|
||||
state = {
|
||||
values: this.props.defaultValues || {title: '', icon: 'address-card', color: '#ff6900'},
|
||||
values: this.props.defaultValues || {title: '', icon: 'address-card', color: '#ff6900', private: false},
|
||||
loading: false
|
||||
};
|
||||
|
||||
@ -36,13 +37,16 @@ class TopicEditModal extends React.Component {
|
||||
<FormField name="title" label={i18n('TITLE')} fieldProps={{size: 'large'}} validation="TITLE" required />
|
||||
<FormField name="icon" className="topic-edit-modal__icon" label={i18n('ICON')} decorator={IconSelector} />
|
||||
<FormField name="color" className="topic-edit-modal__color" label={i18n('COLOR')} decorator={ColorSelector} />
|
||||
|
||||
<SubmitButton className="topic-edit-modal__save-button" type="secondary" size="small">
|
||||
{i18n('SAVE')}
|
||||
</SubmitButton>
|
||||
<Button className="topic-edit-modal__discard-button" onClick={this.onDiscardClick.bind(this)} size="small">
|
||||
{i18n('CANCEL')}
|
||||
</Button>
|
||||
<FormField className="topic-edit-modal__private" label={i18n('PRIVATE')} name="private" field="checkbox" />
|
||||
<InfoTooltip className="topic-edit-modal__private" text={i18n('PRIVATE_TOPIC_DESCRIPTION')} />
|
||||
<div className="topic-edit-modal__buttons-container">
|
||||
<Button className="topic-edit-modal__discard-button" onClick={this.onDiscardClick.bind(this)} size="small">
|
||||
{i18n('CANCEL')}
|
||||
</Button>
|
||||
<SubmitButton className="topic-edit-modal__save-button" type="secondary" size="small">
|
||||
{i18n('SAVE')}
|
||||
</SubmitButton>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
@ -59,7 +63,8 @@ class TopicEditModal extends React.Component {
|
||||
topicId: this.props.topicId,
|
||||
name: this.state.values['title'],
|
||||
icon: this.state.values['icon'],
|
||||
iconColor: this.state.values['color']
|
||||
iconColor: this.state.values['color'],
|
||||
private: this.state.values['private']*1
|
||||
}
|
||||
}).then(() => {
|
||||
this.context.closeModal();
|
||||
|
@ -13,7 +13,21 @@
|
||||
|
||||
}
|
||||
|
||||
&__discard-button {
|
||||
float: right;
|
||||
&__buttons-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20px 0 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__discard-button {
|
||||
float: left;
|
||||
}
|
||||
|
||||
&__private {
|
||||
display: inline-block;
|
||||
padding-right: 10px;
|
||||
}
|
||||
}
|
||||
|
@ -22,7 +22,8 @@ class TopicViewer extends React.Component {
|
||||
iconColor: React.PropTypes.string.isRequired,
|
||||
articles: React.PropTypes.array.isRequired,
|
||||
articlePath: React.PropTypes.string,
|
||||
editable: React.PropTypes.bool
|
||||
editable: React.PropTypes.bool,
|
||||
private: React.PropTypes.bool
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
@ -53,6 +54,8 @@ class TopicViewer extends React.Component {
|
||||
<span className="topic-viewer__title">{this.props.name}</span>
|
||||
{(this.props.editable) ? this.renderEditButton() : null}
|
||||
{(this.props.editable) ? this.renderDeleteButton() : null}
|
||||
{this.props.private*1 ? <Icon className="topic-viewer__private" name='user-secret' color='grey'/> : null}
|
||||
|
||||
</div>
|
||||
<ul className="topic-viewer__list">
|
||||
{this.state.articles.map(this.renderArticleItem.bind(this))}
|
||||
@ -121,13 +124,16 @@ class TopicViewer extends React.Component {
|
||||
}
|
||||
|
||||
renderEditModal() {
|
||||
let props = {
|
||||
topicId: this.props.id,
|
||||
onChange: this.props.onChange,
|
||||
const {id, onChange, name, icon, iconColor} = this.props;
|
||||
|
||||
const props = {
|
||||
topicId: id,
|
||||
onChange,
|
||||
defaultValues: {
|
||||
title: this.props.name,
|
||||
icon: this.props.icon,
|
||||
iconColor: this.props.iconColor
|
||||
title: name,
|
||||
icon,
|
||||
color: iconColor,
|
||||
private: this.props.private * 1
|
||||
}
|
||||
};
|
||||
|
||||
@ -148,7 +154,7 @@ class TopicViewer extends React.Component {
|
||||
<ArticleAddModal {...props}/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
getArticleLinkProps(article) {
|
||||
let classes = {
|
||||
'topic-viewer__list-item-button': true,
|
||||
@ -162,7 +168,7 @@ class TopicViewer extends React.Component {
|
||||
}
|
||||
|
||||
onDeleteClick() {
|
||||
API.call({
|
||||
return API.call({
|
||||
path: '/article/delete-topic',
|
||||
data: {
|
||||
topicId: this.props.id
|
||||
@ -265,4 +271,4 @@ class TopicViewer extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default TopicViewer;
|
||||
export default TopicViewer;
|
||||
|
@ -25,6 +25,10 @@
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
&__private {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
&__edit-icon {
|
||||
color: $grey;
|
||||
cursor: pointer;
|
||||
@ -78,4 +82,4 @@
|
||||
&__add-item {
|
||||
color: $dark-grey;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -50,7 +50,7 @@ class App extends React.Component {
|
||||
'application': true,
|
||||
'application_modal-opened': (this.props.modal.opened),
|
||||
'application_full-width': (this.props.config.layout === 'full-width' && !_.includes(this.props.location.pathname, '/admin')),
|
||||
'application_user-system': (this.props.config['user-system-enabled'])
|
||||
'application_mandatory-login': (this.props.config['mandatory-login'])
|
||||
};
|
||||
|
||||
return classNames(classes);
|
||||
@ -99,7 +99,7 @@ class App extends React.Component {
|
||||
history.push('/');
|
||||
}
|
||||
|
||||
if(props.config['user-system-enabled'] && _.includes(props.location.pathname, '/check-ticket')) {
|
||||
if(props.config['mandatory-login'] && _.includes(props.location.pathname, '/check-ticket')) {
|
||||
history.push('/');
|
||||
}
|
||||
|
||||
@ -111,7 +111,7 @@ class App extends React.Component {
|
||||
history.push('/admin');
|
||||
}
|
||||
|
||||
if(isProd && _.includes(props.location.pathname, '/components-demo')) {
|
||||
if(process.env.NODE_ENV === 'production' && _.includes(props.location.pathname, '/components-demo')) {
|
||||
history.push('/');
|
||||
}
|
||||
}
|
||||
|
@ -32,13 +32,14 @@ import AdminPanelMyAccount from 'app/admin/panel/dashboard/admin-panel-my-accoun
|
||||
|
||||
import AdminPanelMyTickets from 'app/admin/panel/tickets/admin-panel-my-tickets';
|
||||
import AdminPanelNewTickets from 'app/admin/panel/tickets/admin-panel-new-tickets';
|
||||
import AdminPanelAllTickets from 'app/admin/panel/tickets/admin-panel-all-tickets';
|
||||
import AdminPanelSearchTickets from 'app/admin/panel/tickets/admin-panel-search-tickets';
|
||||
import AdminPanelViewTicket from 'app/admin/panel/tickets/admin-panel-view-ticket';
|
||||
import AdminPanelCustomResponses from 'app/admin/panel/tickets/admin-panel-custom-responses';
|
||||
|
||||
import AdminPanelListUsers from 'app/admin/panel/users/admin-panel-list-users';
|
||||
import AdminPanelViewUser from 'app/admin/panel/users/admin-panel-view-user';
|
||||
import AdminPanelBanUsers from 'app/admin/panel/users/admin-panel-ban-users';
|
||||
import AdminPanelCustomFields from 'app/admin/panel/users/admin-panel-custom-fields';
|
||||
|
||||
import AdminPanelListArticles from 'app/admin/panel/articles/admin-panel-list-articles';
|
||||
import AdminPanelViewArticle from 'app/admin/panel/articles/admin-panel-view-article';
|
||||
@ -49,7 +50,8 @@ import AdminPanelViewStaff from 'app/admin/panel/staff/admin-panel-view-staff';
|
||||
|
||||
import AdminPanelSystemPreferences from 'app/admin/panel/settings/admin-panel-system-preferences';
|
||||
import AdminPanelAdvancedSettings from 'app/admin/panel/settings/admin-panel-advanced-settings';
|
||||
import AdminPanelEmailTemplates from 'app/admin/panel/settings/admin-panel-email-templates';
|
||||
import AdminPanelEmailSettings from 'app/admin/panel/settings/admin-panel-email-settings';
|
||||
import AdminPanelCustomTags from 'app/admin/panel/settings/admin-panel-custom-tags';
|
||||
|
||||
// INSTALLATION
|
||||
import InstallLayout from 'app/install/install-layout';
|
||||
@ -59,7 +61,7 @@ import InstallStep3Database from 'app/install/install-step-3-database';
|
||||
import InstallStep4UserSystem from 'app/install/install-step-4-user-system';
|
||||
import InstallStep5Settings from 'app/install/install-step-5-settings';
|
||||
import InstallStep6Admin from 'app/install/install-step-6-admin';
|
||||
import InstallStep7Completed from 'app/install/install-step-7-completed';
|
||||
import InstallCompleted from 'app/install/install-completed';
|
||||
|
||||
export default (
|
||||
<Router history={history}>
|
||||
@ -96,12 +98,12 @@ export default (
|
||||
<Route path="step-4" component={InstallStep4UserSystem} />
|
||||
<Route path="step-5" component={InstallStep5Settings} />
|
||||
<Route path="step-6" component={InstallStep6Admin} />
|
||||
<Route path="step-7" component={InstallStep7Completed} />
|
||||
<Route path="completed" component={InstallCompleted} />
|
||||
</Route>
|
||||
<Route path="admin">
|
||||
<IndexRoute component={AdminLoginPage} />
|
||||
<Route path="panel" component={AdminPanelLayout}>
|
||||
<IndexRedirect to="stats" />
|
||||
<IndexRedirect to="activity" />
|
||||
<Route path="stats" component={AdminPanelStats} />
|
||||
<Route path="activity" component={AdminPanelActivity} />
|
||||
<Route path="my-account" component={AdminPanelMyAccount} />
|
||||
@ -110,7 +112,7 @@ export default (
|
||||
<IndexRedirect to="my-tickets" />
|
||||
<Route path="my-tickets" component={AdminPanelMyTickets} />
|
||||
<Route path="new-tickets" component={AdminPanelNewTickets} />
|
||||
<Route path="all-tickets" component={AdminPanelAllTickets} />
|
||||
<Route path="search-tickets" component={AdminPanelSearchTickets} />
|
||||
<Route path="custom-responses" component={AdminPanelCustomResponses} />
|
||||
<Route path="view-ticket/:ticketNumber" component={AdminPanelViewTicket} />
|
||||
</Route>
|
||||
@ -120,6 +122,7 @@ export default (
|
||||
<Route path="list-users" component={AdminPanelListUsers} />
|
||||
<Route path="view-user/:userId" component={AdminPanelViewUser} />
|
||||
<Route path="ban-users" component={AdminPanelBanUsers} />
|
||||
<Route path="custom-fields" component={AdminPanelCustomFields} />
|
||||
</Route>
|
||||
|
||||
<Route path="articles">
|
||||
@ -139,7 +142,8 @@ export default (
|
||||
<IndexRedirect to="system-preferences" />
|
||||
<Route path="system-preferences" component={AdminPanelSystemPreferences} />
|
||||
<Route path="advanced-settings" component={AdminPanelAdvancedSettings} />
|
||||
<Route path="email-templates" component={AdminPanelEmailTemplates} />
|
||||
<Route path="email-settings" component={AdminPanelEmailSettings} />
|
||||
<Route path="custom-tags" component={AdminPanelCustomTags} />
|
||||
</Route>
|
||||
</Route>
|
||||
</Route>
|
||||
|
@ -1,5 +1,5 @@
|
||||
export default {
|
||||
dispatch: stub(),
|
||||
dispatch: stub().returns(new Promise(r => r())),
|
||||
getState: stub().returns({
|
||||
config: {},
|
||||
session: {},
|
||||
|
@ -16,6 +16,9 @@ import Message from 'core-components/message';
|
||||
import Widget from 'core-components/widget';
|
||||
import WidgetTransition from 'core-components/widget-transition';
|
||||
|
||||
import Captcha from 'app/main/captcha';
|
||||
|
||||
const MAX_FREE_LOGIN_ATTEMPTS = 3;
|
||||
class AdminLoginPage extends React.Component {
|
||||
|
||||
state = {
|
||||
@ -24,11 +27,14 @@ class AdminLoginPage extends React.Component {
|
||||
recoverFormErrors: {},
|
||||
recoverSent: false,
|
||||
loadingLogin: false,
|
||||
loadingRecover: false
|
||||
loadingRecover: false,
|
||||
showRecoverSentMessage: true,
|
||||
showEmailOrPassordErrorMessage: true
|
||||
};
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (!prevProps.session.failed && this.props.session.failed) {
|
||||
this.setState({showEmailOrPassordErrorMessage : true});
|
||||
this.refs.loginForm.refs.password.focus();
|
||||
}
|
||||
}
|
||||
@ -49,11 +55,33 @@ class AdminLoginPage extends React.Component {
|
||||
<div>
|
||||
<Widget className="admin-login-page__content">
|
||||
<div className="admin-login-page__image"><img width="100%" src={API.getURL() + '/images/logo.png'} alt="OpenSupports Admin Panel"/></div>
|
||||
<div className="admin-login-page__login-form">
|
||||
<Form onSubmit={this.onLoginFormSubmit.bind(this)} loading={this.props.session.pending}>
|
||||
<FormField name="email" label={i18n('EMAIL')} field="input" validation="EMAIL" fieldProps={{size:'large'}} required />
|
||||
<FormField name="password" label={i18n('PASSWORD')} field="input" fieldProps={{password:true, size:'large'}} />
|
||||
<SubmitButton>{i18n('LOG_IN')}</SubmitButton>
|
||||
<div className="admin-login-page__login-form-container">
|
||||
<Form {...this.getLoginFormProps()}>
|
||||
<div className="admin-login-page__login-form-container__login-form__fields">
|
||||
<FormField
|
||||
name="email"
|
||||
label={i18n('EMAIL')}
|
||||
className="admin-login-page__login-form-container__login-form__fields__email"
|
||||
field="input"
|
||||
validation="EMAIL"
|
||||
fieldProps={{size:'large'}}
|
||||
required />
|
||||
<FormField
|
||||
name="password"
|
||||
label={i18n('PASSWORD')}
|
||||
className="admin-login-page__login-form-container__login-form__fields__password"
|
||||
field="input"
|
||||
fieldProps={{password:true, size:'large'}} />
|
||||
<FormField
|
||||
name="remember"
|
||||
label={i18n('REMEMBER_ME')}
|
||||
className="admin-login-page__login-form-container__login-form__fields__remember"
|
||||
field="checkbox" />
|
||||
</div>
|
||||
{this.props.session.loginAttempts > MAX_FREE_LOGIN_ATTEMPTS ? this.renderLoginCaptcha() : null}
|
||||
<div className="admin-login-page__login-form-container__login-form__submit-button">
|
||||
<SubmitButton>{i18n('LOG_IN')}</SubmitButton>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
{this.renderRecoverStatus()}
|
||||
@ -66,46 +94,57 @@ class AdminLoginPage extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
renderLoginCaptcha() {
|
||||
return(
|
||||
<div className={`main-home-page__${this.props.sitekey ? "captcha" : "no-captcha"}`}>
|
||||
<Captcha ref="captcha" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderPasswordRecovery() {
|
||||
return (
|
||||
<div>
|
||||
<div className="admin-login-page__recovery-form-container">
|
||||
<PasswordRecovery recoverSent={this.state.recoverSent} formProps={this.getRecoverFormProps()} onBackToLoginClick={this.onBackToLoginClick.bind(this)} renderLogo={true}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderRecoverStatus() {
|
||||
let status = null;
|
||||
const { showRecoverSentMessage, recoverSent } = this.state;
|
||||
|
||||
if (this.state.recoverSent) {
|
||||
status = (
|
||||
<Message className="admin-login-page__message" type="info" leftAligned>
|
||||
{i18n('RECOVER_SENT')}
|
||||
</Message>
|
||||
);
|
||||
}
|
||||
|
||||
return status;
|
||||
return (
|
||||
recoverSent ?
|
||||
<Message
|
||||
showMessage={showRecoverSentMessage}
|
||||
onCloseMessage={this.onCloseMessage.bind(this, "showRecoverSentMessage")}
|
||||
className="admin-login-page__message"
|
||||
type="info"
|
||||
leftAligned>
|
||||
{i18n('RECOVER_SENT')}
|
||||
</Message> :
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
renderErrorStatus() {
|
||||
let status = null;
|
||||
|
||||
if (this.props.session.failed) {
|
||||
status = (
|
||||
<Message className="admin-login-page__error" type="error">
|
||||
{i18n('EMAIL_OR_PASSWORD')}
|
||||
</Message>
|
||||
);
|
||||
}
|
||||
|
||||
return status;
|
||||
return (
|
||||
this.props.session.failed ?
|
||||
<Message
|
||||
showMessage={this.state.showEmailOrPassordErrorMessage}
|
||||
onCloseMessage={this.onCloseMessage.bind(this, "showEmailOrPassordErrorMessage")}
|
||||
className="admin-login-page__error"
|
||||
type="error">
|
||||
{i18n('EMAIL_OR_PASSWORD')}
|
||||
</Message> :
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
getLoginFormProps() {
|
||||
return {
|
||||
loading: this.props.session.pending,
|
||||
className: 'admin-login-page__form',
|
||||
className: 'admin-login-page__login-form-container__login-form',
|
||||
ref: 'loginForm',
|
||||
onSubmit: this.onLoginFormSubmit.bind(this),
|
||||
errors: this.getLoginFormErrors(),
|
||||
@ -114,12 +153,14 @@ class AdminLoginPage extends React.Component {
|
||||
}
|
||||
|
||||
getRecoverFormProps() {
|
||||
const { loadingRecover, recoverFormErrors } = this.state;
|
||||
|
||||
return {
|
||||
loading: this.state.loadingRecover,
|
||||
className: 'admin-login-page__form',
|
||||
loading: loadingRecover,
|
||||
className: 'admin-login-page__recovery-form-container__recovery-form',
|
||||
ref: 'recoverForm',
|
||||
onSubmit: this.onForgotPasswordSubmit.bind(this),
|
||||
errors: this.state.recoverFormErrors,
|
||||
errors: recoverFormErrors,
|
||||
onValidateErrors: this.onRecoverFormErrorsValidation.bind(this)
|
||||
};
|
||||
}
|
||||
@ -184,7 +225,8 @@ class AdminLoginPage extends React.Component {
|
||||
onRecoverPasswordSent() {
|
||||
this.setState({
|
||||
loadingRecover: false,
|
||||
recoverSent: true
|
||||
recoverSent: true,
|
||||
showRecoverSentMessage: true
|
||||
});
|
||||
}
|
||||
|
||||
@ -198,10 +240,17 @@ class AdminLoginPage extends React.Component {
|
||||
this.refs.recoverForm.refs.email.focus();
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
onCloseMessage(showMessage) {
|
||||
this.setState({
|
||||
[showMessage]: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default connect((store) => {
|
||||
return {
|
||||
session: store.session
|
||||
session: store.session,
|
||||
sitekey: store.config.reCaptchaKey
|
||||
};
|
||||
})(AdminLoginPage);
|
||||
|
@ -19,12 +19,22 @@
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
&__login-form {
|
||||
&__login-form-container {
|
||||
margin: 0 auto;
|
||||
display: inline-block;
|
||||
|
||||
&__login-form__fields {
|
||||
padding: 10px 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__error {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
&__captcha {
|
||||
margin: 10px auto 20px;
|
||||
height: 78px;
|
||||
width: 304px;
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,7 @@
|
||||
import React from 'react';
|
||||
import store from 'app/store';
|
||||
|
||||
import ConfigActions from 'actions/config-actions';
|
||||
|
||||
import MainLayout from 'app/main/main-layout';
|
||||
import AdminPanelStaffWidget from 'app/admin/panel/admin-panel-staff-widget';
|
||||
@ -8,6 +11,10 @@ import Widget from 'core-components/widget';
|
||||
|
||||
class AdminPanel extends React.Component {
|
||||
|
||||
componentDidMount() {
|
||||
store.dispatch(ConfigActions.updateData());
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<MainLayout>
|
||||
@ -22,7 +29,7 @@ class AdminPanel extends React.Component {
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-md-12 admin-panel-layout__content">
|
||||
<Widget>
|
||||
<Widget className='admin-panel-layout__content__widget'>
|
||||
{this.props.children}
|
||||
</Widget>
|
||||
</div>
|
||||
@ -33,4 +40,4 @@ class AdminPanel extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default AdminPanel;
|
||||
export default AdminPanel;
|
||||
|
@ -4,4 +4,10 @@
|
||||
&__header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 415px) {
|
||||
.admin-panel-layout__content__widget {
|
||||
padding: 20px 5px;
|
||||
}
|
||||
}
|
||||
}
|
@ -2,10 +2,13 @@ import React from 'react';
|
||||
import _ from 'lodash';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {dispatch} from 'app/store';
|
||||
import i18n from 'lib-app/i18n';
|
||||
|
||||
import Menu from 'core-components/menu';
|
||||
import queryString from 'query-string';
|
||||
|
||||
const INITIAL_PAGE = 1;
|
||||
|
||||
|
||||
class AdminPanelMenu extends React.Component {
|
||||
static contextTypes = {
|
||||
@ -70,15 +73,32 @@ class AdminPanelMenu extends React.Component {
|
||||
|
||||
onGroupItemClick(index) {
|
||||
const group = this.getRoutes()[this.getGroupIndex()];
|
||||
const item = group.items[index];
|
||||
|
||||
this.context.router.push(group.items[index].path);
|
||||
this.context.router.push(item.path);
|
||||
item.onItemClick && item.onItemClick();
|
||||
}
|
||||
|
||||
getGroupItemIndex() {
|
||||
const { location } = this.props;
|
||||
const search = window.location.search;
|
||||
const filtersInURL = queryString.parse(search);
|
||||
const group = this.getRoutes()[this.getGroupIndex()];
|
||||
const pathname = this.props.location.pathname;
|
||||
const pathname = location.pathname + location.search;
|
||||
const SEARCH_TICKETS_PATH = '/admin/panel/tickets/search-tickets';
|
||||
|
||||
return _.findIndex(group.items, {path: pathname});
|
||||
return (
|
||||
_.findIndex(
|
||||
group.items,
|
||||
(item) => {
|
||||
if(location.pathname === SEARCH_TICKETS_PATH) {
|
||||
const customTicketsListNumber = queryString.parse(item.path.slice(SEARCH_TICKETS_PATH.length)).custom;
|
||||
return item.path.includes(SEARCH_TICKETS_PATH) && customTicketsListNumber === filtersInURL.custom;
|
||||
}
|
||||
return item.path === pathname;
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
getGroupIndex() {
|
||||
@ -90,8 +110,24 @@ class AdminPanelMenu extends React.Component {
|
||||
return (groupIndex === -1) ? 0 : groupIndex;
|
||||
}
|
||||
|
||||
getCustomlists() {
|
||||
if(window.customTicketList){
|
||||
return window.customTicketList.map((item, index) => {
|
||||
return {
|
||||
name: item.title,
|
||||
path: `/admin/panel/tickets/search-tickets?custom=${index}&page=${INITIAL_PAGE}&useInitialValues=true`,
|
||||
level: 1,
|
||||
}
|
||||
})
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
getRoutes() {
|
||||
return this.getItemsByFilteredByLevel([
|
||||
const customLists = this.getCustomlists();
|
||||
|
||||
return this.getItemsByFilteredByLevel(_.without([
|
||||
{
|
||||
groupName: i18n('DASHBOARD'),
|
||||
path: '/admin/panel',
|
||||
@ -99,13 +135,13 @@ class AdminPanelMenu extends React.Component {
|
||||
level: 1,
|
||||
items: this.getItemsByFilteredByLevel([
|
||||
{
|
||||
name: i18n('STATISTICS'),
|
||||
path: '/admin/panel/stats',
|
||||
name: i18n('LAST_ACTIVITY'),
|
||||
path: '/admin/panel/activity',
|
||||
level: 1
|
||||
},
|
||||
{
|
||||
name: i18n('LAST_ACTIVITY'),
|
||||
path: '/admin/panel/activity',
|
||||
name: i18n('STATISTICS'),
|
||||
path: '/admin/panel/stats',
|
||||
level: 1
|
||||
}
|
||||
])
|
||||
@ -127,15 +163,16 @@ class AdminPanelMenu extends React.Component {
|
||||
level: 1
|
||||
},
|
||||
{
|
||||
name: i18n('ALL_TICKETS'),
|
||||
path: '/admin/panel/tickets/all-tickets',
|
||||
level: 1
|
||||
name: i18n('SEARCH_TICKETS'),
|
||||
path: `/admin/panel/tickets/search-tickets?page=${INITIAL_PAGE}&useInitialValues=true`,
|
||||
level: 1,
|
||||
},
|
||||
{
|
||||
name: i18n('CUSTOM_RESPONSES'),
|
||||
path: '/admin/panel/tickets/custom-responses',
|
||||
level: 2
|
||||
}
|
||||
},
|
||||
...customLists
|
||||
])
|
||||
},
|
||||
{
|
||||
@ -153,6 +190,11 @@ class AdminPanelMenu extends React.Component {
|
||||
name: i18n('BAN_USERS'),
|
||||
path: '/admin/panel/users/ban-users',
|
||||
level: 1
|
||||
},
|
||||
{
|
||||
name: i18n('CUSTOM_FIELDS'),
|
||||
path: '/admin/panel/users/custom-fields',
|
||||
level: 1
|
||||
}
|
||||
])
|
||||
},
|
||||
@ -170,7 +212,6 @@ class AdminPanelMenu extends React.Component {
|
||||
])
|
||||
},
|
||||
{
|
||||
|
||||
groupName: i18n('STAFF'),
|
||||
path: '/admin/panel/staff',
|
||||
icon: 'users',
|
||||
@ -206,13 +247,18 @@ class AdminPanelMenu extends React.Component {
|
||||
level: 3
|
||||
},
|
||||
{
|
||||
name: i18n('EMAIL_TEMPLATES'),
|
||||
path: '/admin/panel/settings/email-templates',
|
||||
name: i18n('EMAIL_SETTINGS'),
|
||||
path: '/admin/panel/settings/email-settings',
|
||||
level: 3
|
||||
},
|
||||
{
|
||||
name: i18n('CUSTOM_TAGS'),
|
||||
path: '/admin/panel/settings/custom-tags',
|
||||
level: 3
|
||||
}
|
||||
])
|
||||
}
|
||||
]);
|
||||
], null));
|
||||
}
|
||||
|
||||
getItemsByFilteredByLevel(items) {
|
||||
@ -222,6 +268,8 @@ class AdminPanelMenu extends React.Component {
|
||||
|
||||
export default connect((store) => {
|
||||
return {
|
||||
level: store.session.userLevel
|
||||
level: store.session.userLevel,
|
||||
config: store.config,
|
||||
searchFilters: store.searchFilters,
|
||||
};
|
||||
})(AdminPanelMenu);
|
||||
|
@ -18,4 +18,4 @@ class AdminPanelListArticles extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default AdminPanelListArticles;
|
||||
export default AdminPanelListArticles;
|
||||
|
@ -3,4 +3,11 @@
|
||||
&__list {
|
||||
padding: 0 50px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 415px) {
|
||||
.admin-panel-list-articles__list {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import ArticlesActions from 'actions/articles-actions';
|
||||
import SessionStore from 'lib-app/session-store';
|
||||
import i18n from 'lib-app/i18n';
|
||||
import API from 'lib-app/api-call';
|
||||
import MentionsParser from 'lib-app/mentions-parser';
|
||||
import DateTransformer from 'lib-core/date-transformer';
|
||||
|
||||
import AreYouSure from 'app-components/are-you-sure';
|
||||
@ -17,6 +18,7 @@ import Form from 'core-components/form';
|
||||
import FormField from 'core-components/form-field';
|
||||
import SubmitButton from 'core-components/submit-button';
|
||||
import TextEditor from 'core-components/text-editor';
|
||||
import Icon from 'core-components/icon';
|
||||
|
||||
class AdminPanelViewArticle extends React.Component {
|
||||
|
||||
@ -64,23 +66,22 @@ class AdminPanelViewArticle extends React.Component {
|
||||
renderArticlePreview(article) {
|
||||
return (
|
||||
<div className="admin-panel-view-article__content">
|
||||
<div className="admin-panel-view-article__edit-buttons">
|
||||
<Button className="admin-panel-view-article__edit-button" size="medium" onClick={this.onEditClick.bind(this, article)} type="tertiary">
|
||||
{i18n('EDIT')}
|
||||
</Button>
|
||||
<Button size="medium" onClick={this.onDeleteClick.bind(this, article)}>
|
||||
{i18n('DELETE')}
|
||||
</Button>
|
||||
<div className="admin-panel-view-article__header-wrapper">
|
||||
<Header title={article.title} />
|
||||
<div className="admin-panel-view-article__header-buttons">
|
||||
<span onClick={this.onEditClick.bind(this, article)}>
|
||||
<Icon className="admin-panel-view-article__edit-icon" name="pencil" />
|
||||
</span>
|
||||
<span onClick={this.onDeleteClick.bind(this, article)} >
|
||||
<Icon className="admin-panel-view-article__edit-icon" name="trash" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="admin-panel-view-article__article">
|
||||
<Header title={article.title}/>
|
||||
|
||||
<div className="admin-panel-view-article__article-content">
|
||||
<div dangerouslySetInnerHTML={{__html: article.content}}/>
|
||||
</div>
|
||||
<div className="admin-panel-view-article__last-edited">
|
||||
{i18n('LAST_EDITED_IN', {date: DateTransformer.transformToString(article.lastEdited)})}
|
||||
</div>
|
||||
<div className="admin-panel-view-article__article-content ql-editor">
|
||||
<div dangerouslySetInnerHTML={{__html: MentionsParser.parse(article.content)}}/>
|
||||
</div>
|
||||
<div className="admin-panel-view-article__last-edited">
|
||||
{i18n('LAST_EDITED_IN', {date: DateTransformer.transformToString(article.lastEdited)})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -90,13 +91,13 @@ class AdminPanelViewArticle extends React.Component {
|
||||
return (
|
||||
<Form values={this.state.form} onChange={(form) => this.setState({form})} onSubmit={this.onFormSubmit.bind(this)}>
|
||||
<div className="admin-panel-view-article__buttons">
|
||||
<SubmitButton className="admin-panel-view-article__button" type="secondary" size="medium">{i18n('SAVE')}</SubmitButton>
|
||||
<Button className="admin-panel-view-article__button" size="medium" onClick={this.onFormCancel.bind(this)}>
|
||||
{i18n('CANCEL')}
|
||||
</Button>
|
||||
<SubmitButton className="admin-panel-view-article__button" type="secondary" size="medium">{i18n('SAVE')}</SubmitButton>
|
||||
</div>
|
||||
<FormField name="title" label={i18n('TITLE')} />
|
||||
<FormField name="content" label={i18n('CONTENT')} field="textarea" fieldProps={{allowImages: this.props.allowAttachments}}/>
|
||||
<FormField name="content" label={i18n('CONTENT')} field="textarea" validation="TEXT_AREA" required fieldProps={{allowImages: this.props.allowAttachments}}/>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
@ -152,7 +153,7 @@ class AdminPanelViewArticle extends React.Component {
|
||||
}
|
||||
|
||||
onArticleDeleted(article) {
|
||||
API.call({
|
||||
return API.call({
|
||||
path: '/article/delete',
|
||||
data: {
|
||||
articleId: article.id
|
||||
|
@ -1,12 +1,28 @@
|
||||
.admin-panel-view-article {
|
||||
|
||||
&__edit-buttons {
|
||||
text-align: left;
|
||||
margin-bottom: 20px;
|
||||
&__content {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
&__edit-button {
|
||||
margin-right: 20px;
|
||||
&__header-wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 35px;
|
||||
}
|
||||
|
||||
&__header-buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 50px;
|
||||
margin-bottom: 5px;
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
&__edit-icon {
|
||||
color: $grey;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&__last-edited {
|
||||
@ -19,8 +35,8 @@
|
||||
text-align: left;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
|
||||
&__button {
|
||||
margin-right: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -43,7 +43,7 @@ class AdminPanelActivity extends React.Component {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
getMenuProps() {
|
||||
return {
|
||||
className: 'admin-panel-activity__menu',
|
||||
@ -148,4 +148,4 @@ class AdminPanelActivity extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default AdminPanelActivity;
|
||||
export default AdminPanelActivity;
|
||||
|
@ -2,6 +2,10 @@
|
||||
|
||||
&__menu {
|
||||
margin: 0 auto 20px auto;
|
||||
width: 300px;
|
||||
min-width: 300px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
@ -3,7 +3,6 @@ import {connect} from 'react-redux';
|
||||
import _ from 'lodash';
|
||||
|
||||
import i18n from 'lib-app/i18n';
|
||||
import API from 'lib-app/api-call';
|
||||
import SessionActions from 'actions/session-actions';
|
||||
|
||||
import StaffEditor from 'app/admin/panel/staff/staff-editor';
|
||||
|
@ -1,20 +1,195 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import API from 'lib-app/api-call';
|
||||
import i18n from 'lib-app/i18n';
|
||||
import Stats from 'app-components/stats';
|
||||
import statsUtils from 'lib-app/stats-utils';
|
||||
import date from 'lib-app/date';
|
||||
|
||||
import Header from 'core-components/header';
|
||||
import Form from 'core-components/form';
|
||||
import FormField from 'core-components/form-field';
|
||||
import Icon from 'core-components/icon';
|
||||
import Loading from 'core-components/loading';
|
||||
import SubmitButton from 'core-components/submit-button';
|
||||
import Button from 'core-components/button';
|
||||
|
||||
class AdminPanelStats extends React.Component {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="admin-panel-stats">
|
||||
<Header title={i18n('STATISTICS')} description={i18n('STATISTICS_DESCRIPTION')}/>
|
||||
<Stats type="general"/>
|
||||
</div>
|
||||
);
|
||||
state = {
|
||||
loading: true,
|
||||
rawForm: {
|
||||
period: 0,
|
||||
departments: [],
|
||||
owners: [],
|
||||
tags: []
|
||||
},
|
||||
ticketData: {}
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
statsUtils.retrieveStats({
|
||||
rawForm: this.getFormWithDateRange(this.state.rawForm),
|
||||
tags: this.props.tags
|
||||
}).then(({data}) => {
|
||||
this.setState({ticketData: data, loading: false});
|
||||
}).catch((error) => {
|
||||
if (showLogs) console.error('ERROR: ', error);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { loading, rawForm, ticketData } = this.state;
|
||||
|
||||
return (
|
||||
<div className="admin-panel-stats">
|
||||
<Header title={i18n('STATISTICS')} description={i18n('STATISTICS_DESCRIPTION')} />
|
||||
<Form className="admin-panel-stats__form" loading={loading} values={rawForm} onChange={this.onFormChange.bind(this)} onSubmit={this.onFormSubmit.bind(this)}>
|
||||
<div className="admin-panel-stats__form__container">
|
||||
<div className="admin-panel-stats__form__container__row">
|
||||
<div className="admin-panel-stats__form__container__col">
|
||||
<FormField name="period" label={i18n('DATE')} field="select" fieldProps={{size: 'large', items: [{content: i18n('LAST_7_DAYS')}, {content: i18n('LAST_30_DAYS')}, {content: i18n('LAST_90_DAYS')}, {content: i18n('LAST_365_DAYS')}]}} />
|
||||
<FormField name="tags" label={i18n('TAGS')} field="tag-selector" fieldProps={{items: this.getTagItems()}} />
|
||||
</div>
|
||||
<div className="admin-panel-stats__form__container__col">
|
||||
<FormField name="departments" label={i18n('DEPARTMENTS')} field="autocomplete" fieldProps={{items: this.getDepartmentsItems()}} />
|
||||
<FormField name="owners" label={i18n('OWNER')} field="autocomplete" fieldProps={{items: this.getStaffItems()}} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="admin-panel-stats__container">
|
||||
<Button
|
||||
className="admin-panel-stats__container__button admin-panel-stats__container__clear-button"
|
||||
size= "medium"
|
||||
disabled={loading}
|
||||
onClick={this.clearFormValues.bind(this)}>
|
||||
{loading ? <Loading /> : i18n('CLEAR')}
|
||||
</Button>
|
||||
<SubmitButton
|
||||
className="admin-panel-stats__container__button admin-panel-stats__container__apply-button"
|
||||
type="secondary"
|
||||
size= "medium">
|
||||
{i18n('APPLY')}
|
||||
</SubmitButton>
|
||||
</div>
|
||||
</Form>
|
||||
<div className="row">
|
||||
<div className="col-md-12">
|
||||
<span className="separator" />
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
loading ?
|
||||
<div className="admin-panel-stats__loading"><Loading backgrounded size="large" /></div> :
|
||||
statsUtils.renderStatistics({showStatCards: true, showStatsByHours: true, showStatsByDays: true, ticketData})
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
clearFormValues(event) {
|
||||
event.preventDefault();
|
||||
this.setState({
|
||||
rawForm: {
|
||||
period: 0,
|
||||
departments: [],
|
||||
owners: [],
|
||||
tags: []
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getTagItems() {
|
||||
return this.props.tags.map((tag) => {
|
||||
return {
|
||||
id: JSON.parse(tag.id),
|
||||
name: tag.name,
|
||||
color : tag.color
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getStaffItems() {
|
||||
const getStaffProfilePic = (staff) => {
|
||||
return staff.profilePic ? API.getFileLink(staff.profilePic) : (API.getURL() + '/images/profile.png');
|
||||
}
|
||||
|
||||
const renderStaffItem = (staff, style) => {
|
||||
return (
|
||||
<div className={`admin-panel-stats__staff-${style}`} key={`staff-${style}-${staff.id}`}>
|
||||
<img className={`admin-panel-stats__staff-${style}__profile-pic`} src={getStaffProfilePic(staff)} />
|
||||
<span className={`admin-panel-stats__staff-${style}__name`}>{staff.name}</span>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
const { staffList } = this.props;
|
||||
let newStaffList = staffList.map(staff => {
|
||||
return {
|
||||
id: JSON.parse(staff.id),
|
||||
name: staff.name.toLowerCase(),
|
||||
color: 'gray',
|
||||
contentOnSelected: renderStaffItem(staff, 'selected'),
|
||||
content: renderStaffItem(staff, 'option'),
|
||||
}
|
||||
});
|
||||
|
||||
return newStaffList;
|
||||
}
|
||||
|
||||
getDepartmentsItems() {
|
||||
const renderDepartmentItem = (department, style) => {
|
||||
return (
|
||||
<div className={`admin-panel-stats__department-${style}`} key={`department-${style}-${department.id}`}>
|
||||
{department.private*1 ? <Icon className={`admin-panel-stats__department-${style}__icon`} name='user-secret' /> : null}
|
||||
<span className={`admin-panel-stats__department-${style}__name`}>{department.name}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return this.props.departments.map(department => {
|
||||
return {
|
||||
id: JSON.parse(department.id),
|
||||
name: department.name.toLowerCase(),
|
||||
color: 'gray',
|
||||
contentOnSelected: renderDepartmentItem(department, 'selected'),
|
||||
content: renderDepartmentItem(department, 'option'),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onFormChange(newFormState) {
|
||||
this.setState({rawForm: newFormState});
|
||||
}
|
||||
|
||||
onFormSubmit() {
|
||||
statsUtils.retrieveStats({
|
||||
rawForm: this.getFormWithDateRange(this.state.rawForm),
|
||||
tags: this.props.tags
|
||||
}).then(({data}) => {
|
||||
this.setState({ticketData: data, loading: false});
|
||||
}).catch((error) => {
|
||||
if (showLogs) console.error('ERROR: ', error);
|
||||
});
|
||||
}
|
||||
|
||||
getFormWithDateRange(form) {
|
||||
const {startDate, endDate} = statsUtils.getDateRangeFromPeriod(form.period);
|
||||
|
||||
return {
|
||||
...form,
|
||||
dateRange: {
|
||||
startDate,
|
||||
endDate
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default AdminPanelStats;
|
||||
export default connect((store) => {
|
||||
return {
|
||||
tags: store.config.tags,
|
||||
departments: store.config.departments,
|
||||
staffList: store.adminData.staffMembers
|
||||
};
|
||||
})(AdminPanelStats);
|
||||
|
@ -0,0 +1,110 @@
|
||||
@import "../../../../scss/vars";
|
||||
|
||||
.admin-panel-stats {
|
||||
text-align: left;
|
||||
|
||||
&__form {
|
||||
&__container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&__row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
&__col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
|
||||
&__button {
|
||||
margin: 0 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&__loading {
|
||||
min-height: 361px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: $grey;
|
||||
}
|
||||
|
||||
&__card-list {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
&__department-option { // Duplicated from ticket-query-filters, please REMOVE
|
||||
&__name {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&__department-selected { // Duplicated from ticket-query-filters, please REMOVE
|
||||
display: inline-block;
|
||||
border-radius: 3px;
|
||||
margin-left: 5px;
|
||||
padding: 1px;
|
||||
font-size: 13px;
|
||||
cursor: default;
|
||||
|
||||
&__icon {
|
||||
padding-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
&__staff-option { // Duplicated from ticket-query-filters, please REMOVE
|
||||
&__profile-pic {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
&__name {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&__staff-selected { // Duplicated from ticket-query-filters, please REMOVE
|
||||
display: inline-block;
|
||||
border-radius: 3px;
|
||||
margin-left: 5px;
|
||||
padding: 1px;
|
||||
font-size: 13px;
|
||||
cursor: default;
|
||||
|
||||
&__profile-pic {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
border-radius: 50%;
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 992px) {
|
||||
.admin-panel-stats__form__container__row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
@ -4,7 +4,6 @@ import {connect} from 'react-redux';
|
||||
import ConfigActions from 'actions/config-actions';
|
||||
import API from 'lib-app/api-call';
|
||||
import i18n from 'lib-app/i18n';
|
||||
import ToggleButton from 'app-components/toggle-button';
|
||||
import AreYouSure from 'app-components/are-you-sure';
|
||||
import ModalContainer from 'app-components/modal-container';
|
||||
|
||||
@ -17,6 +16,7 @@ import Listing from 'core-components/listing';
|
||||
import Form from 'core-components/form';
|
||||
import FormField from 'core-components/form-field';
|
||||
import SubmitButton from 'core-components/submit-button';
|
||||
import Checkbox from 'core-components/checkbox';
|
||||
|
||||
class AdminPanelAdvancedSettings extends React.Component {
|
||||
|
||||
@ -25,10 +25,11 @@ class AdminPanelAdvancedSettings extends React.Component {
|
||||
messageTitle: null,
|
||||
messageType: '',
|
||||
messageContent: '',
|
||||
keyName: '',
|
||||
keyCode: '',
|
||||
selectedAPIKey: -1,
|
||||
APIKeys: []
|
||||
APIKeys: [],
|
||||
error: '',
|
||||
showMessage: true,
|
||||
showAPIKeyMessage: true
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
@ -36,24 +37,33 @@ class AdminPanelAdvancedSettings extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { config } = this.props;
|
||||
const { messageType, error, selectedAPIKey, showAPIKeyMessage } = this.state;
|
||||
|
||||
return (
|
||||
<div className="admin-panel-advanced-settings">
|
||||
<Header title={i18n('ADVANCED_SETTINGS')} description={i18n('ADVANCED_SETTINGS_DESCRIPTION')}/>
|
||||
{(this.state.messageType) ? this.renderMessage() : null}
|
||||
<Header title={i18n('ADVANCED_SETTINGS')} description={i18n('ADVANCED_SETTINGS_DESCRIPTION')} />
|
||||
{messageType ? this.renderMessage() : null}
|
||||
<div className="row">
|
||||
<div className="col-md-12">
|
||||
<div className="col-md-6">
|
||||
<div className="admin-panel-advanced-settings__user-system-enabled">
|
||||
<span className="admin-panel-advanced-settings__text">
|
||||
{i18n('ENABLE_USER_SYSTEM')} <InfoTooltip text={i18n('ENABLE_USER_SYSTEM_DESCRIPTION')} />
|
||||
</span>
|
||||
<ToggleButton className="admin-panel-advanced-settings__toggle-button" value={this.props.config['user-system-enabled']} onChange={this.onToggleButtonUserSystemChange.bind(this)}/>
|
||||
</div>
|
||||
<div className="col-md-6 admin-panel-advanced-settings__mandatory-login">
|
||||
<Checkbox
|
||||
label={i18n('ENABLE_MANDATORY_LOGIN')}
|
||||
disabled={!config['registration']}
|
||||
className="admin-panel-advanced-settings__mandatory-login__checkbox"
|
||||
value={config['mandatory-login']}
|
||||
onChange={this.onCheckboxMandatoryLoginChange.bind(this)}
|
||||
wrapInLabel />
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<div className="admin-panel-advanced-settings__registration">
|
||||
<span className="admin-panel-advanced-settings__text">{i18n('ENABLE_USER_REGISTRATION')}</span>
|
||||
<ToggleButton className="admin-panel-advanced-settings__toggle-button" value={this.props.config['registration']} onChange={this.onToggleButtonRegistrationChange.bind(this)}/>
|
||||
<Checkbox
|
||||
label={i18n('ENABLE_USER_REGISTRATION')}
|
||||
disabled={!config['mandatory-login']}
|
||||
className="admin-panel-advanced-settings__registration__checkbox"
|
||||
value={config['registration']}
|
||||
onChange={this.onCheckboxRegistrationChange.bind(this)}
|
||||
wrapInLabel />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -65,7 +75,7 @@ class AdminPanelAdvancedSettings extends React.Component {
|
||||
<div className="admin-panel-advanced-settings__text">
|
||||
{i18n('INCLUDE_USERS_VIA_CSV')} <InfoTooltip text={i18n('CSV_DESCRIPTION')} />
|
||||
</div>
|
||||
<FileUploader className="admin-panel-advanced-settings__button" text="Upload" onChange={this.onImportCSV.bind(this)}/>
|
||||
<FileUploader className="admin-panel-advanced-settings__button" text="Upload" onChange={this.onImportCSV.bind(this)} />
|
||||
</div>
|
||||
<div className="col-md-4">
|
||||
<div className="admin-panel-advanced-settings__text">{i18n('BACKUP_DATABASE')}</div>
|
||||
@ -80,12 +90,21 @@ class AdminPanelAdvancedSettings extends React.Component {
|
||||
<span className="separator" />
|
||||
</div>
|
||||
<div className="col-md-12 admin-panel-advanced-settings__api-keys">
|
||||
<div className="col-md-12 admin-panel-advanced-settings__api-keys-title">{i18n('REGISTRATION_API_KEYS')}</div>
|
||||
<div className="col-md-12 admin-panel-advanced-settings__api-keys-title">{i18n('API_KEYS')}</div>
|
||||
<div className="col-md-4">
|
||||
<Listing {...this.getListingProps()} />
|
||||
</div>
|
||||
<div className="col-md-8">
|
||||
{(this.state.selectedAPIKey === -1) ? this.renderNoKey() : this.renderKey()}
|
||||
<div className="col-md-8 admin-panel-advanced-settings__api-keys__container">
|
||||
{
|
||||
error ?
|
||||
<Message
|
||||
showMessage={showAPIKeyMessage}
|
||||
onCloseMessage={this.onCloseMessage.bind(this, "showAPIKeyMessage")}
|
||||
type="error">
|
||||
{i18n(error)}
|
||||
</Message> :
|
||||
((selectedAPIKey === -1) ? this.renderNoKey() : this.renderKey())
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -94,8 +113,17 @@ class AdminPanelAdvancedSettings extends React.Component {
|
||||
}
|
||||
|
||||
renderMessage() {
|
||||
const { messageType, messageTitle, messageContent, showMessage } = this.state;
|
||||
|
||||
return (
|
||||
<Message type={this.state.messageType} title={this.state.messageTitle}>{this.state.messageContent}</Message>
|
||||
<Message
|
||||
showMessage={showMessage}
|
||||
onCloseMessage={this.onCloseMessage.bind(this, "showMessage")}
|
||||
className="admin-panel-advanced-settings__message"
|
||||
type={messageType}
|
||||
title={messageTitle}>
|
||||
{messageContent}
|
||||
</Message>
|
||||
);
|
||||
}
|
||||
|
||||
@ -108,14 +136,31 @@ class AdminPanelAdvancedSettings extends React.Component {
|
||||
}
|
||||
|
||||
renderKey() {
|
||||
let currentAPIKey = this.state.APIKeys[this.state.selectedAPIKey];
|
||||
const { APIKeys, selectedAPIKey } = this.state;
|
||||
const {
|
||||
name,
|
||||
token,
|
||||
canCreateTickets,
|
||||
shouldReturnTicketNumber,
|
||||
canCheckTickets,
|
||||
canCreateUser
|
||||
} = APIKeys[selectedAPIKey];
|
||||
|
||||
return (
|
||||
<div className="admin-panel-advanced-settings__api-keys-info">
|
||||
<div className="admin-panel-advanced-settings__api-keys__container-info">
|
||||
<div className="admin-panel-advanced-settings__api-keys-subtitle">{i18n('NAME_OF_KEY')}</div>
|
||||
<div className="admin-panel-advanced-settings__api-keys-data">{currentAPIKey.name}</div>
|
||||
<div className="admin-panel-advanced-settings__api-keys-data">{name}</div>
|
||||
<div className="admin-panel-advanced-settings__api-keys-subtitle">{i18n('KEY')}</div>
|
||||
<div className="admin-panel-advanced-settings__api-keys-data">{currentAPIKey.token}</div>
|
||||
<div className="admin-panel-advanced-settings__api-keys-data">{token}</div>
|
||||
<div className="admin-panel-advanced-settings__api-keys-subtitle">{i18n('PERMISSIONS')}</div>
|
||||
<div className="admin-panel-advanced-settings__api-keys__permissions">
|
||||
<FormField className="admin-panel-advanced-settings__api-keys__permissions__item" value={canCreateTickets*1} label={i18n('TICKET_CREATION_PERMISSION')} field='checkbox' />
|
||||
<FormField value={shouldReturnTicketNumber*1} label={i18n('TICKET_NUMBER_RETURN_PERMISSION')} field='checkbox' />
|
||||
</div>
|
||||
<div className="admin-panel-advanced-settings__api-keys__permissions">
|
||||
<FormField className="admin-panel-advanced-settings__api-keys__permissions__item" value={canCheckTickets*1} label={i18n('TICKET_CHECK_PERMISSION')} field='checkbox' />
|
||||
<FormField value={canCreateUser*1} label={i18n('USER_CREATION_PERMISSION')} field='checkbox' />
|
||||
</div>
|
||||
<Button className="admin-panel-advanced-settings__api-keys-button" size="medium" onClick={this.onDeleteKeyClick.bind(this)}>
|
||||
{i18n('DELETE')}
|
||||
</Button>
|
||||
@ -125,7 +170,7 @@ class AdminPanelAdvancedSettings extends React.Component {
|
||||
|
||||
getListingProps() {
|
||||
return {
|
||||
title: i18n('REGISTRATION_API_KEYS'),
|
||||
title: i18n('API_KEYS'),
|
||||
enableAddNew: true,
|
||||
items: this.state.APIKeys.map((item) => {
|
||||
return {
|
||||
@ -134,7 +179,7 @@ class AdminPanelAdvancedSettings extends React.Component {
|
||||
};
|
||||
}),
|
||||
selectedIndex: this.state.selectedAPIKey,
|
||||
onChange: index => this.setState({selectedAPIKey: index}),
|
||||
onChange: index => this.setState({selectedAPIKey: index, error:''}),
|
||||
onAddClick: this.openAPIKeyModal.bind(this)
|
||||
};
|
||||
}
|
||||
@ -142,18 +187,51 @@ class AdminPanelAdvancedSettings extends React.Component {
|
||||
openAPIKeyModal() {
|
||||
ModalContainer.openModal(
|
||||
<Form className="admin-panel-advanced-settings__api-keys-modal" onSubmit={this.addAPIKey.bind(this)}>
|
||||
<Header title={i18n('ADD_API_KEY')} description={i18n('ADD_API_KEY_DESCRIPTION')}/>
|
||||
<FormField name="name" label={i18n('NAME_OF_KEY')} validation="DEFAULT" required fieldProps={{size: 'large'}}/>
|
||||
<SubmitButton type="secondary">{i18n('SUBMIT')}</SubmitButton>
|
||||
</Form>
|
||||
<Header title={i18n('ADD_API_KEY')} description={i18n('ADD_API_KEY_DESCRIPTION')} />
|
||||
<FormField name="name" label={i18n('NAME_OF_KEY')} validation="DEFAULT" required fieldProps={{size: 'large'}} />
|
||||
<div className="admin-panel-advanced-settings__api-keys__permissions">
|
||||
<FormField className = "admin-panel-advanced-settings__api-keys__permissions__item" name="createTicketPermission" label={i18n('TICKET_CREATION_PERMISSION')} field='checkbox' />
|
||||
<FormField name="ticketNumberPermission" label={i18n('TICKET_NUMBER_RETURN_PERMISSION')} field='checkbox' />
|
||||
</div>
|
||||
<div className="admin-panel-advanced-settings__api-keys__permissions" >
|
||||
<FormField className = "admin-panel-advanced-settings__api-keys__permissions__item" name="checkTicketPermission" label={i18n('TICKET_CHECK_PERMISSION')} field='checkbox' />
|
||||
<FormField name="userPermission" label={i18n('USER_CREATION_PERMISSION')} field='checkbox' />
|
||||
</div>
|
||||
<div className="admin-panel-advanced-settings__api-keys__buttons-container">
|
||||
<Button
|
||||
className="admin-panel-advanced-settings__api-keys__cancel-button"
|
||||
onClick={(e) => {e.preventDefault(); ModalContainer.closeModal();}}
|
||||
type='link'
|
||||
size="medium">
|
||||
{i18n('CANCEL')}
|
||||
</Button>
|
||||
<SubmitButton className="admin-panel-advanced-settings__api-keys-modal__submit-button" type="secondary">{i18n('SUBMIT')}</SubmitButton>
|
||||
</div>
|
||||
</Form>,
|
||||
{
|
||||
closeButton: {
|
||||
showCloseButton: true
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
addAPIKey({name}) {
|
||||
addAPIKey({name,userPermission,createTicketPermission,checkTicketPermission,ticketNumberPermission}) {
|
||||
ModalContainer.closeModal();
|
||||
|
||||
this.setState({
|
||||
error: ''
|
||||
});
|
||||
|
||||
API.call({
|
||||
path: '/system/add-api-key',
|
||||
data: {name}
|
||||
data: {
|
||||
name,
|
||||
canCreateUsers: userPermission*1,
|
||||
canCreateTickets: createTicketPermission*1,
|
||||
canCheckTickets: checkTicketPermission*1,
|
||||
shouldReturnTicketNumber: ticketNumberPermission*1
|
||||
}
|
||||
}).then(this.getAllKeys.bind(this));
|
||||
}
|
||||
|
||||
@ -161,15 +239,20 @@ class AdminPanelAdvancedSettings extends React.Component {
|
||||
API.call({
|
||||
path: '/system/get-api-keys',
|
||||
data: {}
|
||||
}).then(this.onRetrieveSuccess.bind(this));
|
||||
}).then(this.onRetrieveSuccess.bind(this))
|
||||
}
|
||||
|
||||
onDeleteKeyClick() {
|
||||
const {
|
||||
APIKeys,
|
||||
selectedAPIKey
|
||||
} = this.state;
|
||||
|
||||
AreYouSure.openModal(null, () => {
|
||||
API.call({
|
||||
return API.call({
|
||||
path: '/system/delete-api-key',
|
||||
data: {
|
||||
name: this.state.APIKeys[this.state.selectedAPIKey].name
|
||||
name: APIKeys[selectedAPIKey].name
|
||||
}
|
||||
}).then(this.getAllKeys.bind(this));
|
||||
});
|
||||
@ -178,21 +261,24 @@ class AdminPanelAdvancedSettings extends React.Component {
|
||||
onRetrieveSuccess(result) {
|
||||
this.setState({
|
||||
APIKeys: result.data,
|
||||
selectedAPIKey: -1
|
||||
selectedAPIKey: -1,
|
||||
error: null
|
||||
});
|
||||
}
|
||||
|
||||
onToggleButtonUserSystemChange() {
|
||||
AreYouSure.openModal(null, this.onAreYouSureUserSystemOk.bind(this), 'secure');
|
||||
onCheckboxMandatoryLoginChange() {
|
||||
AreYouSure.openModal(null, this.onAreYouSureMandatoryLoginOk.bind(this), 'secure');
|
||||
}
|
||||
|
||||
onToggleButtonRegistrationChange() {
|
||||
onCheckboxRegistrationChange() {
|
||||
AreYouSure.openModal(null, this.onAreYouSureRegistrationOk.bind(this), 'secure');
|
||||
}
|
||||
|
||||
onAreYouSureUserSystemOk(password) {
|
||||
API.call({
|
||||
path: this.props.config['user-system-enabled'] ? '/system/disable-user-system' : '/system/enable-user-system',
|
||||
onAreYouSureMandatoryLoginOk(password) {
|
||||
const { config, dispatch } = this.props;
|
||||
|
||||
return API.call({
|
||||
path: config['mandatory-login'] ? '/system/disable-mandatory-login' : '/system/enable-mandatory-login',
|
||||
data: {
|
||||
password: password
|
||||
}
|
||||
@ -200,26 +286,30 @@ class AdminPanelAdvancedSettings extends React.Component {
|
||||
this.setState({
|
||||
messageType: 'success',
|
||||
messageTitle: null,
|
||||
messageContent: this.props.config['user-system-enabled'] ? i18n('USER_SYSTEM_DISABLED') : i18n('USER_SYSTEM_ENABLED')
|
||||
showMessage: true,
|
||||
messageContent: config['mandatory-login'] ? i18n('MANDATORY_LOGIN_DISABLED') : i18n('MANDATORY_LOGIN_ENABLED')
|
||||
});
|
||||
this.props.dispatch(ConfigActions.updateData());
|
||||
}).catch(() => this.setState({messageType: 'error', messageTitle: null, messageContent: i18n('ERROR_UPDATING_SETTINGS')}));
|
||||
dispatch(ConfigActions.updateData());
|
||||
}).catch(() => this.setState({messageType: 'error', showMessage: true, messageTitle: null, messageContent: i18n('ERROR_UPDATING_SETTINGS')}));
|
||||
}
|
||||
|
||||
onAreYouSureRegistrationOk(password) {
|
||||
API.call({
|
||||
path: this.props.config['registration'] ? '/system/disable-registration' : '/system/enable-registration',
|
||||
const { config, dispatch } = this.props;
|
||||
|
||||
return API.call({
|
||||
path: config['registration'] ? '/system/disable-registration' : '/system/enable-registration',
|
||||
data: {
|
||||
password: password
|
||||
}
|
||||
}).then(() => {
|
||||
this.setState({
|
||||
messageType: 'success',
|
||||
showMessage: true,
|
||||
messageTitle: null,
|
||||
messageContent: this.props.config['registration'] ? i18n('REGISTRATION_DISABLED') : i18n('REGISTRATION_ENABLED')
|
||||
messageContent: config['registration'] ? i18n('REGISTRATION_DISABLED') : i18n('REGISTRATION_ENABLED')
|
||||
});
|
||||
this.props.dispatch(ConfigActions.updateData());
|
||||
}).catch(() => this.setState({messageType: 'error', messageTitle: null, messageContent: i18n('ERROR_UPDATING_SETTINGS')}));
|
||||
dispatch(ConfigActions.updateData());
|
||||
}).catch(() => this.setState({messageType: 'error', showMessage: true, messageTitle: null, messageContent: i18n('ERROR_UPDATING_SETTINGS')}));
|
||||
}
|
||||
|
||||
onImportCSV(event) {
|
||||
@ -227,27 +317,35 @@ class AdminPanelAdvancedSettings extends React.Component {
|
||||
}
|
||||
|
||||
onAreYouSureCSVOk(file, password) {
|
||||
API.call({
|
||||
return API.call({
|
||||
path: '/system/csv-import',
|
||||
dataAsForm: true,
|
||||
data: {
|
||||
file: file,
|
||||
password: password
|
||||
file,
|
||||
password
|
||||
}
|
||||
})
|
||||
.then((result) => this.setState({
|
||||
messageType: 'success',
|
||||
messageType: 'success',
|
||||
showMessage: true,
|
||||
messageTitle: i18n('SUCCESS_IMPORTING_CSV_DESCRIPTION'),
|
||||
messageContent: (result.data.length) ? (
|
||||
<div>
|
||||
{i18n('ERRORS_FOUND')}
|
||||
<ul>
|
||||
{result.data.map((error) => <li>{error}</li>)}
|
||||
{result.data.map((error, index) => <li key={`csv-file__key-${index}`} >{error}</li>)}
|
||||
</ul>
|
||||
</div>
|
||||
) : null
|
||||
}))
|
||||
.catch(() => this.setState({messageType: 'error', messageTitle: null, messageContent: i18n('INVALID_FILE')}));
|
||||
.catch((error) => {
|
||||
this.setState({
|
||||
messageType: 'error',
|
||||
showMessage: true,
|
||||
messageTitle: null,
|
||||
messageContent: i18n(error.message)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
onBackupDatabase() {
|
||||
@ -270,17 +368,22 @@ class AdminPanelAdvancedSettings extends React.Component {
|
||||
}
|
||||
|
||||
onAreYouSureDeleteAllUsersOk(password) {
|
||||
API.call({
|
||||
return API.call({
|
||||
path: '/system/delete-all-users',
|
||||
data: {
|
||||
password: password
|
||||
}
|
||||
}).then(() => this.setState({messageType: 'success', messageTitle: null, messageContent: i18n('SUCCESS_DELETING_ALL_USERS')}
|
||||
)).catch(() => this.setState({messageType: 'error', messageTitle: null, messageContent: i18n('ERROR_DELETING_ALL_USERS')}));
|
||||
}).then(() => this.setState({messageType: 'success', showMessage: true, messageTitle: null, messageContent: i18n('SUCCESS_DELETING_ALL_USERS')}
|
||||
)).catch(() => this.setState({messageType: 'error', showMessage: true, messageTitle: null, messageContent: i18n('ERROR_DELETING_ALL_USERS')}));
|
||||
}
|
||||
|
||||
onCloseMessage(showMessage) {
|
||||
this.setState({
|
||||
[showMessage]: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default connect((store) => {
|
||||
return {
|
||||
config: store.config
|
||||
|
@ -1,7 +1,7 @@
|
||||
@import "../../../../scss/vars";
|
||||
|
||||
.admin-panel-advanced-settings {
|
||||
&__user-system-enabled {
|
||||
&__mandatory-login {
|
||||
|
||||
}
|
||||
|
||||
@ -9,13 +9,6 @@
|
||||
|
||||
}
|
||||
|
||||
&__toggle-button {
|
||||
display: inline-block;
|
||||
margin-left: 20px;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
&__text {
|
||||
margin-top: 30px;
|
||||
margin-bottom: 20px;
|
||||
@ -28,13 +21,20 @@
|
||||
|
||||
&__api-keys {
|
||||
|
||||
&__buttons-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
&-title {
|
||||
font-size: $font-size--bg;
|
||||
margin-bottom: 20px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
&-info {
|
||||
&__container-info {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
@ -52,8 +52,21 @@
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
&__permissions {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
margin-bottom: 20px;
|
||||
&__item {
|
||||
margin-right: 25px;
|
||||
}
|
||||
}
|
||||
|
||||
&-modal {
|
||||
min-width: 500px;
|
||||
min-width: 400px;
|
||||
|
||||
&__submit-button {
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
&-none {
|
||||
@ -61,4 +74,36 @@
|
||||
font-size: $font-size--md;
|
||||
}
|
||||
}
|
||||
|
||||
&__message {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 415px) {
|
||||
.admin-panel-advanced-settings {
|
||||
&__api-keys {
|
||||
|
||||
&-button {
|
||||
margin-bottom: 30px;
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
&__container {
|
||||
padding: 30px 0;
|
||||
|
||||
&-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
&-subtitle {
|
||||
margin: 10px 0 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,156 @@
|
||||
import React from 'react';
|
||||
|
||||
import i18n from 'lib-app/i18n';
|
||||
import API from 'lib-app/api-call';
|
||||
|
||||
import Button from 'core-components/button';
|
||||
import Header from 'core-components/header';
|
||||
import Form from 'core-components/form';
|
||||
import FormField from 'core-components/form-field';
|
||||
import SubmitButton from 'core-components/submit-button';
|
||||
import ColorSelector from 'core-components/color-selector';
|
||||
|
||||
class AdminPanelCustomTagsModal extends React.Component {
|
||||
static contextTypes = {
|
||||
closeModal: React.PropTypes.func,
|
||||
createTag: React.PropTypes.bool
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
defaultValues: React.PropTypes.object,
|
||||
onTagCreated: React.PropTypes.func
|
||||
};
|
||||
|
||||
state = {
|
||||
form: this.props.defaultValues || {name: '', color: '#ff6900'},
|
||||
loading: false
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
this.renderTagContentPopUp(this.props.createTag)
|
||||
);
|
||||
}
|
||||
|
||||
renderTagContentPopUp(create) {
|
||||
const {
|
||||
form,
|
||||
errors,
|
||||
loading,
|
||||
} = this.state;
|
||||
let title, description, nameRequired, submitFunction;
|
||||
|
||||
if(create) {
|
||||
title = i18n('ADD_CUSTOM_TAG');
|
||||
description = i18n('DESCRIPTION_ADD_CUSTOM_TAG');
|
||||
submitFunction = this.onSubmitNewTag.bind(this);
|
||||
nameRequired = true;
|
||||
} else {
|
||||
title = i18n('EDIT_CUSTOM_TAG');
|
||||
description = i18n('DESCRIPTION_EDIT_CUSTOM_TAG');
|
||||
nameRequired = false;
|
||||
submitFunction = this.onSubmitEditTag.bind(this);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='admin-panel-custom-tags-modal'>
|
||||
<Header title={title} description={description} />
|
||||
<Form
|
||||
values={form}
|
||||
onChange={this.onFormChange.bind(this)}
|
||||
onSubmit={submitFunction}
|
||||
errors={errors}
|
||||
onValidateErrors={errors => this.setState({errors})}
|
||||
loading={loading}>
|
||||
<FormField name="name" label={i18n('NAME')} fieldProps={{size: 'large'}} required={nameRequired} />
|
||||
<FormField name="color" label={i18n('COLOR')} decorator={ColorSelector} />
|
||||
<div className='admin-panel-custom-tags-modal__actions'>
|
||||
<SubmitButton type="secondary" size="small">
|
||||
{i18n('SAVE')}
|
||||
</SubmitButton>
|
||||
<Button onClick={this.onDiscardClick.bind(this)} size="small">
|
||||
{i18n('CANCEL')}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
onFormChange(form) {
|
||||
this.setState({
|
||||
form
|
||||
});
|
||||
}
|
||||
|
||||
onSubmitEditTag(form) {
|
||||
this.setState({
|
||||
loading: true
|
||||
});
|
||||
|
||||
API.call({
|
||||
path: '/ticket/edit-tag',
|
||||
data: {
|
||||
tagId: this.props.id,
|
||||
name: form.name,
|
||||
color: form.color,
|
||||
}
|
||||
}).then(() => {
|
||||
this.context.closeModal();
|
||||
|
||||
if(this.props.onTagChange) {
|
||||
this.props.onTagChange();
|
||||
}
|
||||
}).catch((result) => {
|
||||
|
||||
this.setState({
|
||||
loading: false,
|
||||
errors: {
|
||||
'name': result.message
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
onSubmitNewTag(form) {
|
||||
this.setState({
|
||||
loading: true
|
||||
});
|
||||
|
||||
API.call({
|
||||
path: '/ticket/create-tag',
|
||||
data: {
|
||||
name: form.name,
|
||||
color: form.color,
|
||||
}
|
||||
}).then(() => {
|
||||
this.context.closeModal();
|
||||
|
||||
if(this.props.onTagCreated) {
|
||||
this.props.onTagCreated();
|
||||
}
|
||||
|
||||
}).catch((result) => {
|
||||
|
||||
this.setState({
|
||||
loading: false,
|
||||
errors: {
|
||||
'name': result.message
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
onDiscardClick(event) {
|
||||
event.preventDefault();
|
||||
this.context.closeModal();
|
||||
this.setState({
|
||||
loading: false,
|
||||
errors: {}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default AdminPanelCustomTagsModal;
|
@ -0,0 +1,8 @@
|
||||
.admin-panel-custom-tags-modal {
|
||||
|
||||
&__actions{
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
109
client/src/app/admin/panel/settings/admin-panel-custom-tags.js
Normal file
109
client/src/app/admin/panel/settings/admin-panel-custom-tags.js
Normal file
@ -0,0 +1,109 @@
|
||||
import React from 'react';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import AdminPanelCustomTagsModal from 'app/admin/panel/settings/admin-panel-custom-tags-modal';
|
||||
|
||||
import i18n from 'lib-app/i18n';
|
||||
import API from 'lib-app/api-call';
|
||||
import ConfigActions from 'actions/config-actions';
|
||||
|
||||
import AreYouSure from 'app-components/are-you-sure';
|
||||
import ModalContainer from 'app-components/modal-container';
|
||||
|
||||
import Icon from 'core-components/icon';
|
||||
import Button from 'core-components/button';
|
||||
import Header from 'core-components/header';
|
||||
import Tag from 'core-components/tag';
|
||||
|
||||
class AdminPanelCustomTags extends React.Component {
|
||||
static propTypes = {
|
||||
tags: React.PropTypes.arrayOf(
|
||||
React.PropTypes.shape({
|
||||
name: React.PropTypes.string,
|
||||
color: React.PropTypes.string,
|
||||
id: React.PropTypes.number
|
||||
})
|
||||
),
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.retrieveCustomTags();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="admin-panel-custom-tags">
|
||||
<Header title={i18n('CUSTOM_TAGS')} description={i18n('CUSTOM_TAGS_DESCRIPTION')} />
|
||||
{this.renderContent()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderContent() {
|
||||
return (
|
||||
<div className="admin-panel-custom-tags__content">
|
||||
<div>
|
||||
<Button onClick={this.openTagModal.bind(this)} type="secondary">
|
||||
<Icon className="admin-panel-custom-tags__add-button-icon" name="plus" /> {i18n('ADD_CUSTOM_TAG')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="admin-panel-custom-tags__tag-list">
|
||||
{this.props.tags.map(this.renderTag.bind(this))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderTag(tag, index) {
|
||||
return (
|
||||
<div key={index} className="admin-panel-custom-tags__tag-container" >
|
||||
<Tag
|
||||
color={tag.color}
|
||||
name={tag.name}
|
||||
onEditClick={this.openEditTagModal.bind(this, tag.id, tag.name, tag.color)}
|
||||
onRemoveClick={this.onDeleteClick.bind(this, tag.id)}
|
||||
size='large'
|
||||
showEditButton
|
||||
showDeleteButton />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
openTagModal() {
|
||||
ModalContainer.openModal(
|
||||
<AdminPanelCustomTagsModal onTagCreated={this.retrieveCustomTags.bind(this)} createTag />
|
||||
);
|
||||
}
|
||||
|
||||
openEditTagModal(tagId,tagName,tagColor, event) {
|
||||
ModalContainer.openModal(
|
||||
<AdminPanelCustomTagsModal defaultValues={{name: tagName , color: tagColor}} id={tagId} onTagChange={this.retrieveCustomTags.bind(this)} />
|
||||
);
|
||||
}
|
||||
|
||||
onDeleteClick(tagId, event) {
|
||||
event.preventDefault();
|
||||
AreYouSure.openModal(i18n('WILL_DELETE_CUSTOM_TAG'), this.deleteCustomTag.bind(this, tagId));
|
||||
}
|
||||
|
||||
deleteCustomTag(tagId) {
|
||||
return API.call({
|
||||
path: '/ticket/delete-tag',
|
||||
data: {
|
||||
tagId,
|
||||
}
|
||||
}).then(() => {
|
||||
this.retrieveCustomTags()
|
||||
});
|
||||
}
|
||||
|
||||
retrieveCustomTags() {
|
||||
this.props.dispatch(ConfigActions.updateData());
|
||||
}
|
||||
}
|
||||
|
||||
export default connect((store) => {
|
||||
return {
|
||||
tags: store.config['tags'].map((tag) => {return {...tag, id: tag.id*1}})
|
||||
};
|
||||
})(AdminPanelCustomTags);
|
@ -0,0 +1,18 @@
|
||||
.admin-panel-custom-tags {
|
||||
|
||||
&__content {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
&__add-button-icon{
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
&__tag-list{
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
&__tag-container{
|
||||
margin-top:5px ;
|
||||
}
|
||||
}
|
@ -0,0 +1,560 @@
|
||||
import React from 'react';
|
||||
import _ from 'lodash';
|
||||
import {connect} from 'react-redux';
|
||||
import randomString from 'random-string';
|
||||
|
||||
import i18n from 'lib-app/i18n';
|
||||
import API from 'lib-app/api-call';
|
||||
|
||||
import AreYouSure from 'app-components/are-you-sure';
|
||||
import LanguageSelector from 'app-components/language-selector';
|
||||
import PopupMessage from 'app-components/popup-message';
|
||||
|
||||
import Button from 'core-components/button';
|
||||
import Header from 'core-components/header';
|
||||
import Listing from 'core-components/listing';
|
||||
import Loading from 'core-components/loading';
|
||||
import Form from 'core-components/form';
|
||||
import FormField from 'core-components/form-field';
|
||||
import SubmitButton from 'core-components/submit-button';
|
||||
import Message from 'core-components/message';
|
||||
|
||||
class AdminPanelEmailSettings extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
language: React.PropTypes.string,
|
||||
};
|
||||
|
||||
state = {
|
||||
headerImage: '',
|
||||
loadingHeaderImage: false,
|
||||
loadingList: true,
|
||||
loadingTemplate: false,
|
||||
templates: [],
|
||||
loadingForm: false,
|
||||
selectedIndex: -1,
|
||||
edited: false,
|
||||
errors: {},
|
||||
language: this.props.language,
|
||||
imapLoading: false,
|
||||
smtpLoading: false,
|
||||
form: {
|
||||
subject: '',
|
||||
text1: '',
|
||||
text2: '',
|
||||
text3: '',
|
||||
},
|
||||
emailForm: {
|
||||
['server-email']: '',
|
||||
},
|
||||
smtpForm: {
|
||||
['smtp-host']: '',
|
||||
['smtp-user']: '',
|
||||
['smtp-pass']: 'HIDDEN',
|
||||
},
|
||||
imapForm: {
|
||||
['imap-host']: '',
|
||||
['imap-user']: '',
|
||||
['imap-pass']: 'HIDDEN',
|
||||
['imap-token']: '',
|
||||
},
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.retrieveMailTemplateList();
|
||||
this.retrieveHeaderImage();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="admin-panel-email-settings">
|
||||
{(!this.state.loadingList) ? this.renderContent() : this.renderLoading()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderContent() {
|
||||
return (
|
||||
<div>
|
||||
{this.renderEmailSettings()}
|
||||
<Header title={i18n('EMAIL_TEMPLATES')} description={i18n('EMAIL_TEMPLATES_DESCRIPTION')} />
|
||||
<div className="row">
|
||||
<div className="col-md-3">
|
||||
<Listing {...this.getListingProps()} />
|
||||
</div>
|
||||
{(this.state.selectedIndex !== -1) ? this.renderForm() : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderLoading() {
|
||||
return (
|
||||
<div className="admin-panel-email-settings__loading">
|
||||
<Loading backgrounded size="large" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderEmailSettings() {
|
||||
return (
|
||||
<div>
|
||||
<Header title={i18n('EMAIL_SETTINGS')} description={i18n('EMAIL_SETTINGS_DESCRIPTION')} />
|
||||
<Form className="admin-panel-email-settings__email-form"
|
||||
onSubmit={this.submitEmailAddress.bind(this)}
|
||||
onChange={emailForm => this.setState({emailForm})}
|
||||
values={this.state.emailForm}>
|
||||
<div className="admin-panel-email-settings__email-container">
|
||||
<FormField className="admin-panel-email-settings__email-server-address"
|
||||
name="server-email"
|
||||
label={i18n('EMAIL_SERVER_ADDRESS')}
|
||||
fieldProps={{size: 'large'}}
|
||||
infoMessage={i18n('EMAIL_SERVER_ADDRESS_DESCRIPTION')} />
|
||||
<SubmitButton className="admin-panel-email-settings__submit" type="secondary"
|
||||
size="small">{i18n('SAVE')}</SubmitButton>
|
||||
</div>
|
||||
</Form>
|
||||
|
||||
<Form className="admin-panel-email-settings__image-form"
|
||||
values={{headerImage: this.state.headerImage}}
|
||||
onChange={form => this.setState({headerImage: form.headerImage})}
|
||||
onSubmit={this.onHeaderImageSubmit.bind(this)}>
|
||||
<div className="admin-panel-email-settings__image-container">
|
||||
<FormField className="admin-panel-email-settings__image-header-url"
|
||||
label={i18n('IMAGE_HEADER_URL')} name="headerImage" required
|
||||
infoMessage={i18n('IMAGE_HEADER_DESCRIPTION')}
|
||||
fieldProps={{size: 'large'}} />
|
||||
<SubmitButton className="admin-panel-email-settings__image-header-submit" type="secondary"
|
||||
size="small">{i18n('SAVE')}</SubmitButton>
|
||||
</div>
|
||||
</Form>
|
||||
|
||||
<div className="admin-panel-email-settings__servers">
|
||||
<div className="admin-panel-email-settings__box">
|
||||
<Header title={i18n('SMTP_SERVER')} description={i18n('SMTP_SERVER_DESCRIPTION')} />
|
||||
<Form onSubmit={this.submitSMTP.bind(this)} onChange={smtpForm => this.setState({smtpForm})}
|
||||
values={this.state.smtpForm} loading={this.state.smtpLoading}>
|
||||
<FormField name="smtp-host" label={i18n('SMTP_SERVER')} fieldProps={{size: 'large'}} />
|
||||
<FormField name="smtp-user" label={i18n('SMTP_USER')} fieldProps={{size: 'large'}} />
|
||||
<FormField name="smtp-pass" label={i18n('SMTP_PASSWORD')} fieldProps={{size: 'large', autoComplete: 'off'}} />
|
||||
<div className="admin-panel-email-settings__server-form-buttons">
|
||||
<SubmitButton type="tertiary" size="small" onClick={this.testSMTP.bind(this)}>
|
||||
{i18n('TEST')}
|
||||
</SubmitButton>
|
||||
<SubmitButton className="admin-panel-email-settings__submit" type="secondary" size="small">
|
||||
{i18n('SAVE')}
|
||||
</SubmitButton>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
|
||||
<div className="admin-panel-email-settings__box">
|
||||
<Header title={i18n('IMAP_SERVER')} description={i18n('IMAP_SERVER_DESCRIPTION')} />
|
||||
<Form onSubmit={this.submitIMAP.bind(this)} onChange={imapForm => this.setState({imapForm})}
|
||||
values={this.state.imapForm} loading={this.state.imapLoading}>
|
||||
<FormField name="imap-host" label={i18n('IMAP_SERVER')} fieldProps={{size: 'large'}} />
|
||||
<FormField name="imap-user" label={i18n('IMAP_USER')} fieldProps={{size: 'large'}} />
|
||||
<FormField name="imap-pass" label={i18n('IMAP_PASSWORD')} fieldProps={{size: 'large', autoComplete: 'off'}} />
|
||||
<FormField
|
||||
name="imap-token"
|
||||
label={i18n('IMAP_TOKEN')}
|
||||
infoMessage={i18n('IMAP_TOKEN_DESCRIPTION')}
|
||||
fieldProps={{size: 'large', icon: 'refresh', onIconClick: this.generateImapToken.bind(this)}} />
|
||||
<div className="admin-panel-email-settings__server-form-buttons">
|
||||
<SubmitButton type="tertiary" size="small" onClick={this.testIMAP.bind(this)}>
|
||||
{i18n('TEST')}
|
||||
</SubmitButton>
|
||||
<SubmitButton className="admin-panel-email-settings__submit" type="secondary" size="small">
|
||||
{i18n('SAVE')}
|
||||
</SubmitButton>
|
||||
</div>
|
||||
</Form>
|
||||
<Message showCloseButton={false} className="admin-panel-email-settings__imap-message" type="info">
|
||||
{i18n('IMAP_POLLING_DESCRIPTION', {url: `${apiRoot}/system/email-polling`})}
|
||||
</Message>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderForm() {
|
||||
const { form, language, selectedIndex, edited } = this.state;
|
||||
const { template, text2, text3} = form;
|
||||
|
||||
return (
|
||||
<div className="col-md-9">
|
||||
<FormField label={i18n('LANGUAGE')} decorator={LanguageSelector} value={language}
|
||||
onChange={event => this.onItemChange(selectedIndex, event.target.value)}
|
||||
fieldProps={{
|
||||
type: 'supported',
|
||||
size: 'medium'
|
||||
}} />
|
||||
<Form {...this.getFormProps()}>
|
||||
<div className="row">
|
||||
<div className="col-md-7">
|
||||
<FormField
|
||||
fieldProps={{size: 'large'}}
|
||||
label={i18n('SUBJECT')}
|
||||
name="subject"
|
||||
validation="TITLE"
|
||||
required />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
fieldProps={{className: 'admin-panel-email-settings__text-area'}}
|
||||
label={i18n('TEXT') + '1'}
|
||||
key="text1"
|
||||
name="text1"
|
||||
validation="TEXT_AREA"
|
||||
required
|
||||
decorator={'textarea'} />
|
||||
{
|
||||
(text2 || text2 === "") ?
|
||||
<FormField
|
||||
fieldProps={{className: 'admin-panel-email-settings__text-area'}}
|
||||
label={i18n('TEXT') + '2'}
|
||||
key="text2"
|
||||
name="text2"
|
||||
validation="TEXT_AREA"
|
||||
required
|
||||
decorator={'textarea'} /> :
|
||||
null
|
||||
}
|
||||
{
|
||||
((text3 || text3 === "") && (template !== "USER_PASSWORD" && template !== "USER_EMAIL")) ?
|
||||
<FormField
|
||||
fieldProps={{className: 'admin-panel-email-settings__text-area'}}
|
||||
label={i18n('TEXT') + '3'}
|
||||
key="text3"
|
||||
name="text3"
|
||||
validation={(template !== "USER_PASSWORD" && template !== "USER_EMAIL") ? "TEXT_AREA" : ""}
|
||||
required={(template !== "USER_PASSWORD" && template !== "USER_EMAIL")}
|
||||
decorator={'textarea'} /> :
|
||||
null
|
||||
}
|
||||
|
||||
<div className="admin-panel-email-settings__actions">
|
||||
<div className="admin-panel-email-settings__optional-buttons">
|
||||
<div className="admin-panel-email-settings__recover-button">
|
||||
<Button onClick={this.onRecoverClick.bind(this)} size="medium">
|
||||
{i18n('RECOVER_DEFAULT')}
|
||||
</Button>
|
||||
</div>
|
||||
{edited ? this.renderDiscardButton() : null}
|
||||
</div>
|
||||
<div className="admin-panel-email-settings__save-button">
|
||||
<SubmitButton key="submit-email-template" type="secondary" size="small">
|
||||
{i18n('SAVE')}
|
||||
</SubmitButton>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderDiscardButton() {
|
||||
return (
|
||||
<div className="admin-panel-email-settings__discard-button">
|
||||
<Button onClick={this.onDiscardChangesClick.bind(this)} size="medium">
|
||||
{i18n('DISCARD_CHANGES')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
getListingProps() {
|
||||
return {
|
||||
title: i18n('EMAIL_TEMPLATES'),
|
||||
items: this.getTemplateItems(),
|
||||
selectedIndex: this.state.selectedIndex,
|
||||
onChange: this.onItemChange.bind(this)
|
||||
};
|
||||
}
|
||||
|
||||
getFormProps() {
|
||||
const { form, errors, loadingForm } = this.state;
|
||||
|
||||
return {
|
||||
values: form,
|
||||
errors,
|
||||
loading: loadingForm,
|
||||
onChange: (form) => {
|
||||
this.setState({form, edited: true})
|
||||
},
|
||||
onValidateErrors: (errors) => {
|
||||
this.setState({errors})
|
||||
},
|
||||
onSubmit: this.onFormSubmit.bind(this, form)
|
||||
}
|
||||
}
|
||||
|
||||
getTemplateItems() {
|
||||
return this.state.templates.map((template) => {
|
||||
return {
|
||||
content: template
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
onItemChange(index, language) {
|
||||
if (this.state.edited) {
|
||||
AreYouSure.openModal(i18n('WILL_LOSE_CHANGES'), this.retrieveEmailTemplate.bind(this, index, language || this.state.language));
|
||||
} else {
|
||||
this.retrieveEmailTemplate(index, language || this.state.language);
|
||||
}
|
||||
}
|
||||
|
||||
onHeaderImageSubmit(form) {
|
||||
this.setState({
|
||||
loadingHeaderImage: true,
|
||||
});
|
||||
|
||||
API.call({
|
||||
path: '/system/edit-settings',
|
||||
data: {
|
||||
'mail-template-header-image': form['headerImage']
|
||||
}
|
||||
}).then(() => this.setState({
|
||||
loadingHeaderImage: false,
|
||||
}))
|
||||
}
|
||||
|
||||
onFormSubmit(form) {
|
||||
const {selectedIndex, language, templates} = this.state;
|
||||
|
||||
this.setState({loadingForm: true});
|
||||
|
||||
API.call({
|
||||
path: '/system/edit-mail-template',
|
||||
data: {
|
||||
template: templates[selectedIndex],
|
||||
language,
|
||||
subject: form.subject,
|
||||
text1: form.text1,
|
||||
text2: form.text2,
|
||||
text3: form.text3,
|
||||
}
|
||||
}).then(() => {
|
||||
this.setState({loadingForm: false, edited: false});
|
||||
}).catch(response => {
|
||||
this.setState({
|
||||
loadingForm: false,
|
||||
});
|
||||
|
||||
switch (response.message) {
|
||||
case 'INVALID_SUBJECT':
|
||||
this.setState({
|
||||
errors: {subject: i18n('INVALID_SYNTAX')}
|
||||
});
|
||||
break;
|
||||
case 'INVALID_TEXT_1':
|
||||
this.setState({
|
||||
errors: {text1: i18n('INVALID_SYNTAX')}
|
||||
});
|
||||
break;
|
||||
case 'INVALID_TEXT_2':
|
||||
this.setState({
|
||||
errors: {text2: i18n('INVALID_SYNTAX')}
|
||||
});
|
||||
break;
|
||||
case 'INVALID_TEXT_3':
|
||||
this.setState({
|
||||
errors: {text3: i18n('INVALID_SYNTAX')}
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onDiscardChangesClick(event) {
|
||||
event.preventDefault();
|
||||
this.onItemChange(this.state.selectedIndex, this.state.language);
|
||||
}
|
||||
|
||||
onRecoverClick(event) {
|
||||
event.preventDefault();
|
||||
AreYouSure.openModal(i18n('WILL_RECOVER_EMAIL_TEMPLATE'), this.recoverEmailTemplate.bind(this));
|
||||
}
|
||||
|
||||
generateImapToken() {
|
||||
this.setState({
|
||||
imapForm: {
|
||||
...this.state.imapForm,
|
||||
['imap-token']: randomString({length: 20}),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
submitEmailAddress(form) {
|
||||
this.editSettings(form, 'EMAIL_SUCCESS');
|
||||
}
|
||||
|
||||
submitSMTP(form) {
|
||||
this.setState({
|
||||
smtpLoading: true
|
||||
});
|
||||
|
||||
this.editSettings(form, 'SMTP_SUCCESS')
|
||||
.then(() => this.setState({
|
||||
smtpLoading: false
|
||||
}));
|
||||
}
|
||||
|
||||
submitIMAP(form) {
|
||||
this.setState({
|
||||
imapLoading: true
|
||||
});
|
||||
|
||||
this.editSettings(form, 'IMAP_SUCCESS')
|
||||
.then(() => this.setState({
|
||||
imapLoading: false
|
||||
}));
|
||||
}
|
||||
|
||||
editSettings(form, successMessage) {
|
||||
return API.call({
|
||||
path: '/system/edit-settings',
|
||||
data: this.parsePasswordField(form)
|
||||
}).then(() => PopupMessage.open({
|
||||
title: i18n('SETTINGS_UPDATED'),
|
||||
children: i18n(successMessage),
|
||||
type: 'success'
|
||||
})).catch(response => PopupMessage.open({
|
||||
title: i18n('ERROR_UPDATING_SETTINGS'),
|
||||
children: response.message,
|
||||
type: 'error'
|
||||
}));
|
||||
}
|
||||
|
||||
testSMTP(event) {
|
||||
event.preventDefault();
|
||||
|
||||
this.setState({
|
||||
smtpLoading: true
|
||||
});
|
||||
|
||||
API.call({
|
||||
path: '/system/test-smtp',
|
||||
data: this.parsePasswordField(this.state.smtpForm)
|
||||
}).then(() => PopupMessage.open({
|
||||
title: `${i18n('SUCCESSFUL_CONNECTION')}: SMTP`,
|
||||
children: i18n('SERVER_CREDENTIALS_WORKING'),
|
||||
type: 'success',
|
||||
})).catch(response => PopupMessage.open({
|
||||
title: `${i18n('UNSUCCESSFUL_CONNECTION')}: SMTP`,
|
||||
children: `${i18n('SERVER_ERROR')}: ${response.message}`,
|
||||
type: 'error',
|
||||
})).then(() => this.setState({
|
||||
smtpLoading: false
|
||||
}));
|
||||
}
|
||||
|
||||
testIMAP(event) {
|
||||
event.preventDefault();
|
||||
|
||||
this.setState({
|
||||
imapLoading: true
|
||||
});
|
||||
|
||||
API.call({
|
||||
path: '/system/test-imap',
|
||||
data: this.parsePasswordField(this.state.imapForm)
|
||||
}).then(() => PopupMessage.open({
|
||||
title: `${i18n('SUCCESSFUL_CONNECTION')}: IMAP`,
|
||||
children: i18n('SERVER_CREDENTIALS_WORKING'),
|
||||
type: 'success',
|
||||
})).catch(response => PopupMessage.open({
|
||||
title: `${i18n('UNSUCCESSFUL_CONNECTION')}: IMAP`,
|
||||
children: `${i18n('SERVER_ERROR')}: ${response.message}`,
|
||||
type: 'error',
|
||||
})).then(() => this.setState({
|
||||
imapLoading: false
|
||||
}));
|
||||
}
|
||||
|
||||
recoverEmailTemplate() {
|
||||
const {selectedIndex, language, templates} = this.state;
|
||||
|
||||
return API.call({
|
||||
path: '/system/recover-mail-template',
|
||||
data: {
|
||||
template: templates[selectedIndex],
|
||||
language
|
||||
}
|
||||
}).then(() => {
|
||||
this.retrieveEmailTemplate(this.state.selectedIndex, language);
|
||||
});
|
||||
}
|
||||
|
||||
retrieveEmailTemplate(index, language) {
|
||||
this.setState({
|
||||
loadingForm: true,
|
||||
});
|
||||
|
||||
return API.call({
|
||||
path: '/system/get-mail-template',
|
||||
data: {template: this.state.templates[index], language}
|
||||
}).then((result) => this.setState({
|
||||
language,
|
||||
selectedIndex: index,
|
||||
edited: false,
|
||||
loadingForm: false,
|
||||
form: result.data,
|
||||
errors: {},
|
||||
}));
|
||||
}
|
||||
|
||||
retrieveMailTemplateList() {
|
||||
API.call({
|
||||
path: '/system/get-mail-template-list',
|
||||
data: {}
|
||||
}).then((result) => this.setState({
|
||||
loadingList: false,
|
||||
templates: result.data
|
||||
}));
|
||||
}
|
||||
|
||||
retrieveHeaderImage() {
|
||||
API.call({
|
||||
path: '/system/get-settings',
|
||||
data: {allSettings: 1}
|
||||
}).then(result => this.setState({
|
||||
headerImage: result.data['mail-template-header-image'] || '',
|
||||
emailForm: {
|
||||
['server-email']: result.data['server-email'] || '',
|
||||
},
|
||||
smtpForm: {
|
||||
['smtp-host']: result.data['smtp-host'] || '',
|
||||
['smtp-user']: result.data['smtp-user'] || '',
|
||||
['smtp-pass']: 'HIDDEN',
|
||||
},
|
||||
imapForm: {
|
||||
['imap-host']: result.data['imap-host'] || '',
|
||||
['imap-user']: result.data['imap-user'] || '',
|
||||
['imap-pass']: 'HIDDEN',
|
||||
['imap-token']: result.data['imap-token'] || '',
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
parsePasswordField(form) {
|
||||
let parsedForm = _.extend({}, form);
|
||||
|
||||
delete parsedForm['smtp-pass'];
|
||||
delete parsedForm['imap-pass'];
|
||||
|
||||
return _.extend(parsedForm, {
|
||||
[ form['smtp-pass'] && form['smtp-pass'] !== 'HIDDEN' ? 'smtp-pass' : null]: form['smtp-pass'],
|
||||
[ form['imap-pass'] && form['imap-pass'] !== 'HIDDEN' ? 'imap-pass' : null]: form['imap-pass'],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default connect((store) => {
|
||||
return {
|
||||
language: store.config.language,
|
||||
};
|
||||
})(AdminPanelEmailSettings);
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user