Compare commits
1157 Commits
feature/cy
...
vite
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0595d58154 | ||
|
|
e0855d370b | ||
|
|
6e8d55cad9 | ||
|
|
4e03756e6e | ||
|
|
edc2a0f122 | ||
|
|
419ae32d32 | ||
|
|
530032b5ea | ||
|
|
3ca48350a6 | ||
|
|
c8979091fd | ||
|
|
7358dee3e0 | ||
|
|
ff151444d2 | ||
|
|
000a2e471e | ||
|
|
94bcfa2afc | ||
|
|
bcb319d571 | ||
|
|
ca246fa6fa | ||
|
|
2dcfa87fa6 | ||
|
|
7daf7540b3 | ||
|
|
ce6940629d | ||
|
|
b706b96d32 | ||
|
|
2427bc72f2 | ||
|
|
8bc1a9d9ee | ||
|
|
5f8c878bec | ||
|
|
fb47b45eb6 | ||
|
|
ba30225ba1 | ||
|
|
cb4a6e8774 | ||
|
|
23f640028d | ||
|
|
dc1c492c0b | ||
|
|
bdff9f62c0 | ||
|
|
3e5e6263fe | ||
|
|
48cef3e188 | ||
|
|
76b25a716c | ||
|
|
86c3806e28 | ||
|
|
8c0d6b2f6b | ||
|
|
22ee8acd0d | ||
|
|
548cfefc41 | ||
|
|
ae98566bbd | ||
|
|
afdcfb7bf6 | ||
|
|
c1f6d06128 | ||
|
|
120a8a4576 | ||
|
|
89224e871c | ||
|
|
fa0d472fb6 | ||
|
|
29d5465afc | ||
|
|
b0f4ad7e4f | ||
|
|
7503d86c69 | ||
|
|
efd1c17033 | ||
|
|
c7a0072f2d | ||
|
|
908942ec09 | ||
|
|
e4d3b53349 | ||
|
|
2b0ecbdd91 | ||
|
|
461bc726aa | ||
|
|
80483b617b | ||
|
|
03ce5458b5 | ||
|
|
f9528c5ff7 | ||
|
|
61c03ee206 | ||
|
|
0e4f5b8b2a | ||
|
|
b5d4944ad8 | ||
|
|
50f84d40e1 | ||
|
|
16b0381007 | ||
|
|
a394d6b37e | ||
|
|
f8408908b2 | ||
|
|
eb8519dc1d | ||
|
|
36dd97394f | ||
|
|
03d4e4dcd1 | ||
|
|
6489a8666f | ||
|
|
da5739be8f | ||
|
|
5ea64ed805 | ||
|
|
397ae72626 | ||
|
|
9668a01415 | ||
|
|
6d7f49a858 | ||
|
|
f138ab82fb | ||
|
|
56256be106 | ||
|
|
8dfe98d9d7 | ||
|
|
9e127d1c71 | ||
|
|
d740446ccb | ||
|
|
cdec9840bf | ||
|
|
d0a2bb7da0 | ||
|
|
5de4ef5d83 | ||
|
|
f59bdf9030 | ||
|
|
cfe0727447 | ||
|
|
09d112350a | ||
|
|
52f8eabd2b | ||
|
|
a162b275a3 | ||
|
|
2e7232bb65 | ||
|
|
82dc9e1c56 | ||
|
|
272a3f579a | ||
|
|
ff1ceb20cb | ||
|
|
343179d4fe | ||
|
|
eabbc2211b | ||
|
|
7f587680ca | ||
|
|
9deab7d5d5 | ||
|
|
d61ed03ef1 | ||
|
|
e46be6c12b | ||
|
|
166efdc877 | ||
|
|
e1c325c11d | ||
|
|
f02aa36b8d | ||
|
|
d7faf11e27 | ||
|
|
c72d474dc7 | ||
|
|
73058c4405 | ||
|
|
72e8aba546 | ||
|
|
039f1e6a91 | ||
|
|
ddea46ddef | ||
|
|
2ada4ac44b | ||
|
|
6937f33134 | ||
|
|
9274742520 | ||
|
|
9b4c85c9e3 | ||
|
|
66c64ce9e0 | ||
|
|
c9cbffdec8 | ||
|
|
ca3145ce0f | ||
|
|
06a8d4257a | ||
|
|
b478d26c4c | ||
|
|
44a13ca0d5 | ||
|
|
52f9106776 | ||
|
|
1a2cc5623d | ||
|
|
d06037df1f | ||
|
|
0665bade1b | ||
|
|
51483f62e1 | ||
|
|
cb8632641e | ||
|
|
48a08819f3 | ||
|
|
8dc41519ce | ||
|
|
e255f0a664 | ||
|
|
adbfcddd9d | ||
|
|
e3aea55e91 | ||
|
|
bf6b1c202f | ||
|
|
0cab47f984 | ||
|
|
829e611692 | ||
|
|
e0e62a52be | ||
|
|
23c0f8e383 | ||
|
|
7da5d1a4fd | ||
|
|
43621bc6a2 | ||
|
|
909f6e8eb5 | ||
|
|
1e78106224 | ||
|
|
bb872a2b18 | ||
|
|
3fb3773744 | ||
|
|
7a708e32e4 | ||
|
|
d1cd0ea126 | ||
|
|
ee23d83858 | ||
|
|
764a89e5bf | ||
|
|
f4908ed265 | ||
|
|
61d6fdee95 | ||
|
|
ae9bff7e75 | ||
|
|
fcd9c19f0b | ||
|
|
430823dde0 | ||
|
|
7e919a7221 | ||
|
|
8031b4f91a | ||
|
|
d7cb8d3753 | ||
|
|
eb3560bd34 | ||
|
|
2fc82aad62 | ||
|
|
4f93b5af13 | ||
|
|
71663d64e4 | ||
|
|
67e475ce21 | ||
|
|
86b84e75cb | ||
|
|
8de4ebd30a | ||
|
|
7e7f055c34 | ||
|
|
6e04ed7c83 | ||
|
|
5ec07f9620 | ||
|
|
d3da4a8a1e | ||
|
|
9ee10dc5f8 | ||
|
|
94dd7c6f69 | ||
|
|
bbbb7867a2 | ||
|
|
572963d987 | ||
|
|
1cb4b228ce | ||
|
|
eb96f6467f | ||
|
|
2699b80e1a | ||
|
|
c72b4a25cf | ||
|
|
4a9684ba87 | ||
|
|
636be8989e | ||
|
|
dea7fd71ef | ||
|
|
1a93e1de41 | ||
|
|
69791a3cdd | ||
|
|
c2d6c980ed | ||
|
|
be08ed8551 | ||
|
|
65ce588287 | ||
|
|
78a136e277 | ||
|
|
0cc367b25e | ||
|
|
2ce8549502 | ||
|
|
690e65df0b | ||
|
|
a38611a584 | ||
|
|
b6b445dc21 | ||
|
|
7245d4eab2 | ||
|
|
0d0f24802f | ||
|
|
cf9b03d073 | ||
|
|
7b61c24461 | ||
|
|
260607cb72 | ||
|
|
810738539b | ||
|
|
80539949fb | ||
|
|
ebe5c5b113 | ||
|
|
525182c2a7 | ||
|
|
3704c0cb12 | ||
|
|
c8c844cfba | ||
|
|
4c4e16b0c9 | ||
|
|
f3db5d83c7 | ||
|
|
63ae37e5a9 | ||
|
|
69f727c4e5 | ||
|
|
76bcfce312 | ||
|
|
04cff4acb1 | ||
|
|
4e4fcc3ae4 | ||
|
|
485f9d6025 | ||
|
|
a697ade93a | ||
|
|
7db07b5a94 | ||
|
|
9ec50875a2 | ||
|
|
02b6875eec | ||
|
|
34c332e077 | ||
|
|
d360bcbb71 | ||
|
|
ba32a71786 | ||
|
|
7d6e61043e | ||
|
|
e7e4c534bc | ||
|
|
9fc586434e | ||
|
|
e438fa1d99 | ||
|
|
4abbf50a46 | ||
|
|
ef0bc8c313 | ||
|
|
aa8a719154 | ||
|
|
5aa3612e52 | ||
|
|
7d16ae5194 | ||
|
|
663dfe0441 | ||
|
|
49131ba68b | ||
|
|
c2dbdbd6cf | ||
|
|
0635c651a2 | ||
|
|
05667dd322 | ||
|
|
d3654ec16e | ||
|
|
ab299619dd | ||
|
|
e9d1f3af67 | ||
|
|
46b58a6e1b | ||
|
|
3e9279d89a | ||
|
|
1305277c09 | ||
|
|
3dcc1fe7e0 | ||
|
|
c6012f7335 | ||
|
|
0c7b5087f1 | ||
|
|
7589f78fe1 | ||
|
|
2e589c44a6 | ||
|
|
0074c73c2a | ||
|
|
3c47c672d4 | ||
|
|
213a02d5f2 | ||
|
|
258d99cd41 | ||
|
|
83356fa4ef | ||
|
|
25429e78f8 | ||
|
|
6e377f98a7 | ||
|
|
3a3b3af13f | ||
|
|
de90bd1bb0 | ||
|
|
aa6cb4c1d2 | ||
|
|
e871ba600f | ||
|
|
fe3698980d | ||
|
|
1f14688199 | ||
|
|
89b640f71c | ||
|
|
d13a9cd04a | ||
|
|
c0dab92d0e | ||
|
|
bc50f5e983 | ||
|
|
9c897972ad | ||
|
|
307e244475 | ||
|
|
33a1ac9be4 | ||
|
|
2c8d1accea | ||
|
|
351d6f274b | ||
|
|
e43bfe0d3a | ||
|
|
61ed0087e7 | ||
|
|
9d3aca646b | ||
|
|
e6e61466df | ||
|
|
db7f9fe2ab | ||
|
|
ded798fdf1 | ||
|
|
bfe94e3068 | ||
|
|
823f07409a | ||
|
|
1a4bc720c2 | ||
|
|
73cacdec24 | ||
|
|
17791ac971 | ||
|
|
a0cb30f986 | ||
|
|
07b46ed92b | ||
|
|
79dce5d069 | ||
|
|
e5d8cc2bea | ||
|
|
7b83430c02 | ||
|
|
ed0da08326 | ||
|
|
18998c4dbe | ||
|
|
e236d6e912 | ||
|
|
523df670df | ||
|
|
ce58181fc3 | ||
|
|
bed0669f73 | ||
|
|
b0d077e104 | ||
|
|
bdb2951330 | ||
|
|
87a01208fb | ||
|
|
2daee84fbf | ||
|
|
cdbf58f3ac | ||
|
|
f6a59bdf55 | ||
|
|
e12edd977e | ||
|
|
ea72d44b42 | ||
|
|
b925c991eb | ||
|
|
1e7f43fe3d | ||
|
|
c8460f6092 | ||
|
|
d2e4b7d9ec | ||
|
|
d1b9b5546b | ||
|
|
6f21de1901 | ||
|
|
25e8eaa1d4 | ||
|
|
1c5f74e4f0 | ||
|
|
84f0affaed | ||
|
|
46d514ad1c | ||
|
|
6fe736ce06 | ||
|
|
482b03c2d1 | ||
|
|
ce3c72fc47 | ||
|
|
1ff5ed4141 | ||
|
|
fc4b5c6b1d | ||
|
|
10e3421572 | ||
|
|
0117237988 | ||
|
|
2c232a71d5 | ||
|
|
373fd817d0 | ||
|
|
0ef2d9646d | ||
|
|
c8ac417200 | ||
|
|
661bedbe5b | ||
|
|
0514fbe89d | ||
|
|
87391ff06a | ||
|
|
b2c8e45d5e | ||
|
|
1261e8001b | ||
|
|
83e4fb3dc4 | ||
|
|
2dd56590d3 | ||
|
|
a67fb3576c | ||
|
|
65157a094f | ||
|
|
25173b0903 | ||
|
|
98b760251c | ||
|
|
b97de32a44 | ||
|
|
64f56d20dd | ||
|
|
5b3c547316 | ||
|
|
16d040daf9 | ||
|
|
a22c4bdf8c | ||
|
|
9cb2a4a021 | ||
|
|
95c9978ee7 | ||
|
|
fe80256a40 | ||
|
|
b0d1a7b65e | ||
|
|
ad79344709 | ||
|
|
5c164f807d | ||
|
|
a043f7be24 | ||
|
|
8d9611333c | ||
|
|
02bb2c06eb | ||
|
|
008bcaf41b | ||
|
|
a9dbfbd231 | ||
|
|
1df870ee4e | ||
|
|
517a087186 | ||
|
|
51747c554e | ||
|
|
92c8b54f85 | ||
|
|
d8420f472c | ||
|
|
34d93c4de0 | ||
|
|
1c400cd456 | ||
|
|
f8e1758788 | ||
|
|
5c95c72f40 | ||
|
|
98f816b069 | ||
|
|
3ca6308dd2 | ||
|
|
a2c2aa11ac | ||
|
|
a3cf97fcab | ||
|
|
1a9dc7a377 | ||
|
|
806daebd3f | ||
|
|
dfd8845864 | ||
|
|
b5b772d0c2 | ||
|
|
170108b339 | ||
|
|
9f1f58a9c7 | ||
|
|
4d8a2e635c | ||
|
|
0852d55837 | ||
|
|
4c38ddf3cd | ||
|
|
e15edeadb5 | ||
|
|
422c7baada | ||
|
|
4288e2d986 | ||
|
|
4b289388bf | ||
|
|
2a2f8e51b3 | ||
|
|
ed0136090c | ||
|
|
0d70545b98 | ||
|
|
2252091b53 | ||
|
|
85b1875a22 | ||
|
|
386531884b | ||
|
|
afbe328aa7 | ||
|
|
b7c0fba48b | ||
|
|
68635b1629 | ||
|
|
3bd0058dc8 | ||
|
|
270a512585 | ||
|
|
ec2519eae4 | ||
|
|
d7f52d864a | ||
|
|
5cb2b3940e | ||
|
|
b5efaa944d | ||
|
|
bbcfc420d2 | ||
|
|
8ebf7baa71 | ||
|
|
742d2b5ff2 | ||
|
|
f96fefbfdc | ||
|
|
6570d38719 | ||
|
|
547b58f05a | ||
|
|
f299c685e2 | ||
|
|
59075ee610 | ||
|
|
432aa9f1e1 | ||
|
|
c5bed4f36d | ||
|
|
334306e3c9 | ||
|
|
0716920dfc | ||
|
|
07119e4e7e | ||
|
|
2a7606836c | ||
|
|
7dcdd64a17 | ||
|
|
f26b045727 | ||
|
|
79c966f9e4 | ||
|
|
8eff1dfc4c | ||
|
|
65f960db00 | ||
|
|
74da3ec1ca | ||
|
|
7e99a51495 | ||
|
|
8d170a5bb4 | ||
|
|
8670f386dc | ||
|
|
1f62108e57 | ||
|
|
8715ef4f24 | ||
|
|
85b137f0d6 | ||
|
|
14ebb280a3 | ||
|
|
f1953eef29 | ||
|
|
55842faedd | ||
|
|
9c50de85de | ||
|
|
e6df079431 | ||
|
|
36ce547579 | ||
|
|
ae5fef435a | ||
|
|
fe55701079 | ||
|
|
87b567e990 | ||
|
|
c2b3905c8e | ||
|
|
32072f1d6c | ||
|
|
e84d3bf53a | ||
|
|
67a7e4b865 | ||
|
|
3d7da0b28a | ||
|
|
9320587595 | ||
|
|
7bceba7ed5 | ||
|
|
0db72cd9e4 | ||
|
|
53fc5e361f | ||
|
|
54af163ddf | ||
|
|
c0d756fa38 | ||
|
|
54e673176c | ||
|
|
fc16190ec4 | ||
|
|
cf7f4f1b46 | ||
|
|
189c60a6d1 | ||
|
|
ba95a636cf | ||
|
|
2478fedf1a | ||
|
|
a718f2a012 | ||
|
|
bc6c889afc | ||
|
|
33bcf250d0 | ||
|
|
bad32e069b | ||
|
|
9acf20d4f3 | ||
|
|
26f1ee0d89 | ||
|
|
9c86be8250 | ||
|
|
4a06f9a686 | ||
|
|
98700f54b4 | ||
|
|
9a37cb5cb8 | ||
|
|
673d0bb7c5 | ||
|
|
04c7bc445b | ||
|
|
41782fe120 | ||
|
|
4e8e25a336 | ||
|
|
3c2d3156cb | ||
|
|
0dac15391f | ||
|
|
106534b59b | ||
|
|
19c0553746 | ||
|
|
5f1475d2ec | ||
|
|
84f4d5956a | ||
|
|
4330ddd926 | ||
|
|
0711210512 | ||
|
|
458ec76835 | ||
|
|
682ea860fb | ||
|
|
99977934e7 | ||
|
|
3e05b21c90 | ||
|
|
4e1dd52bea | ||
|
|
f6bcc743d8 | ||
|
|
7472285641 | ||
|
|
d747594e39 | ||
|
|
67ff9f30c6 | ||
|
|
a27092dbcc | ||
|
|
ca41bff446 | ||
|
|
cf8280590c | ||
|
|
b649ca1f00 | ||
|
|
b441301007 | ||
|
|
2e93238b5c | ||
|
|
ce4fe84536 | ||
|
|
9b7c0af025 | ||
|
|
dfdaf36ed1 | ||
|
|
cc8d1b3793 | ||
|
|
a33bfedbb8 | ||
|
|
eb359d83c5 | ||
|
|
ae13e9e36a | ||
|
|
4a62ac2a11 | ||
|
|
016a62b6d5 | ||
|
|
3e226b50ab | ||
|
|
ab84cb5ada | ||
|
|
7825aa4122 | ||
|
|
8d43fbfcd9 | ||
|
|
47a01628d3 | ||
|
|
c008660023 | ||
|
|
5da34cbeac | ||
|
|
5b29aec14b | ||
|
|
ca6aa682f6 | ||
|
|
eb3786cebf | ||
|
|
53843e22a4 | ||
|
|
e1693674ca | ||
|
|
2d2190e4fa | ||
|
|
02fd8097a8 | ||
|
|
fcfbc85683 | ||
|
|
802dd696f4 | ||
|
|
60a0222dd0 | ||
|
|
9114abd3ef | ||
|
|
f7fc0e6a6d | ||
|
|
ffebbe3b2a | ||
|
|
25b8c1b1eb | ||
|
|
17f8625108 | ||
|
|
e3c21f0373 | ||
|
|
859ff00277 | ||
|
|
e7c3be5231 | ||
|
|
85e3c5a433 | ||
|
|
d8d5cde3f1 | ||
|
|
6efa08fee3 | ||
|
|
636c13373c | ||
|
|
3659fbec84 | ||
|
|
05f1a9b280 | ||
|
|
34b4baac3d | ||
|
|
5884d5eba0 | ||
|
|
6b4709b76b | ||
|
|
4dd2137006 | ||
|
|
03315836a6 | ||
|
|
f703ba2cf9 | ||
|
|
dc05e4e166 | ||
|
|
4513acc640 | ||
|
|
612e359d4c | ||
|
|
c8fb1ce302 | ||
|
|
b29d8e1912 | ||
|
|
f1aa7944a3 | ||
|
|
74b6c2b6b5 | ||
|
|
4313cf471f | ||
|
|
fce8039dad | ||
|
|
085ae141ae | ||
|
|
fbc9ccc018 | ||
|
|
f2ede519d7 | ||
|
|
21c53473d3 | ||
|
|
fbc622fa04 | ||
|
|
6319fd20fa | ||
|
|
c998e4901f | ||
|
|
06c35a4ff8 | ||
|
|
64851047bf | ||
|
|
171c0b2b5a | ||
|
|
73fac34ef4 | ||
|
|
5ca34105ef | ||
|
|
fd4820336f | ||
|
|
fcfa1a9be8 | ||
|
|
41849644f3 | ||
|
|
46840266ee | ||
|
|
f36fb06dd6 | ||
|
|
af4c4a4fa3 | ||
|
|
d6045a9334 | ||
|
|
dcc29f23d4 | ||
|
|
638a9fc76b | ||
|
|
53e3b3fa03 | ||
|
|
56c1b6f992 | ||
|
|
22f9a7ee3d | ||
|
|
82a0d287d6 | ||
|
|
cfcad472fd | ||
|
|
1a622f1b2c | ||
|
|
4de604ef7c | ||
|
|
87e3adf579 | ||
|
|
aa7a4ccdd0 | ||
|
|
ec2c26ca69 | ||
|
|
28dc10f5a1 | ||
|
|
b2d615b9c1 | ||
|
|
1e40a22762 | ||
|
|
d1407162d9 | ||
|
|
7a1984d037 | ||
|
|
9bcc449f20 | ||
|
|
bc7d0ef171 | ||
|
|
3e9b046476 | ||
|
|
e0ccd62c82 | ||
|
|
fd0970aef2 | ||
|
|
b673bcae7a | ||
|
|
63673548a0 | ||
|
|
29b74a8c0e | ||
|
|
2658626c7e | ||
|
|
763b199646 | ||
|
|
026ce853e2 | ||
|
|
17905fa844 | ||
|
|
74a0b78a71 | ||
|
|
797a423702 | ||
|
|
2fce8c9644 | ||
|
|
9cd39c1c3e | ||
|
|
720b7f891b | ||
|
|
6264a2f45c | ||
|
|
94e47d14ad | ||
|
|
9319f492dd | ||
|
|
8f04c5a12c | ||
|
|
436a41405d | ||
|
|
b5332458ec | ||
|
|
a2150009db | ||
|
|
e1c785322f | ||
|
|
c1d71720ab | ||
|
|
89ff7740e2 | ||
|
|
4e69fe819e | ||
|
|
a8cc3fa190 | ||
|
|
10fceb7ddf | ||
|
|
6c1a0cff8d | ||
|
|
d92d2cca9a | ||
|
|
ea54820bc0 | ||
|
|
62a800a2c0 | ||
|
|
9c408d8bf5 | ||
|
|
45ad09c100 | ||
|
|
dd5cafcd42 | ||
|
|
eb48b56f47 | ||
|
|
a879e99e77 | ||
|
|
ddd816e7ca | ||
|
|
d646e5f285 | ||
|
|
c10517a11b | ||
|
|
ba683a2e8a | ||
|
|
6b66b76f84 | ||
|
|
c3fe763261 | ||
|
|
3c7b16412b | ||
|
|
5209c12b89 | ||
|
|
bf7aa17f65 | ||
|
|
a454c57bc9 | ||
|
|
cd6e0dcde3 | ||
|
|
a2822f5592 | ||
|
|
ca129fa4a0 | ||
|
|
cbe0c78553 | ||
|
|
2e763f1dd5 | ||
|
|
b8942c320e | ||
|
|
eee135f4ef | ||
|
|
de92b2d47e | ||
|
|
5d7384aa8b | ||
|
|
bf18e687da | ||
|
|
e69e844568 | ||
|
|
86ff7cbc69 | ||
|
|
2b5268fb77 | ||
|
|
eebe7edba8 | ||
|
|
1a5c74dc79 | ||
|
|
fbfdbc903c | ||
|
|
be62ab5ff9 | ||
|
|
85497eb815 | ||
|
|
5724d0129c | ||
|
|
9e64fdc985 | ||
|
|
fd579fc509 | ||
|
|
6e21b1bdf6 | ||
|
|
a7ad18fae2 | ||
|
|
d7c23297ab | ||
|
|
8bfa879485 | ||
|
|
ea774ff22b | ||
|
|
88101b0252 | ||
|
|
f6e095e0a6 | ||
|
|
60ec76701d | ||
|
|
6b52723ba9 | ||
|
|
d7ec2e717c | ||
|
|
910c2a0f9b | ||
|
|
6c93e600c4 | ||
|
|
e70edaec7c | ||
|
|
acaba96e3b | ||
|
|
12d1613b04 | ||
|
|
35fd74d3fe | ||
|
|
df878672fc | ||
|
|
076115253f | ||
|
|
6f58528de2 | ||
|
|
3defe7201f | ||
|
|
4ce75ead52 | ||
|
|
6de7ec00fe | ||
|
|
90ea2cd699 | ||
|
|
800552210b | ||
|
|
8325e2d9cf | ||
|
|
80abea56b4 | ||
|
|
480f081c40 | ||
|
|
9529335c96 | ||
|
|
cc3c1242f5 | ||
|
|
4334b3f419 | ||
|
|
94353bb342 | ||
|
|
5aad7acdd5 | ||
|
|
b2f4a5539c | ||
|
|
cd4f7ffb9c | ||
|
|
400dc79ed6 | ||
|
|
1dfb309223 | ||
|
|
29c9fb37a1 | ||
|
|
41d6f0a4bc | ||
|
|
af70c80e09 | ||
|
|
b02d4e0fdd | ||
|
|
27bf8d9ed6 | ||
|
|
582ad03e05 | ||
|
|
babdfe4cc5 | ||
|
|
8a7a94dd70 | ||
|
|
2654519277 | ||
|
|
02eddcbbf4 | ||
|
|
b967bb6d4e | ||
|
|
f60870a087 | ||
|
|
39aa21d985 | ||
|
|
512bb5e013 | ||
|
|
7581b8634e | ||
|
|
78f041a34f | ||
|
|
2c456cbf03 | ||
|
|
da76021802 | ||
|
|
6de06e084b | ||
|
|
cb49c91983 | ||
|
|
b0df5fa91c | ||
|
|
0652404334 | ||
|
|
bd7d8068df | ||
|
|
4dd868130c | ||
|
|
71860cf899 | ||
|
|
3512905264 | ||
|
|
2c072a9e7a | ||
|
|
fee5bee569 | ||
|
|
0a1cdbdfe3 | ||
|
|
8af79989ff | ||
|
|
5d2bdc7ee1 | ||
|
|
255d65e47d | ||
|
|
f0805e0a79 | ||
|
|
c875ade35c | ||
|
|
31b4f4e561 | ||
|
|
9b485bfe45 | ||
|
|
91279c27fe | ||
|
|
2c1844fb13 | ||
|
|
3812a0650e | ||
|
|
b1c5bbb01f | ||
|
|
bd59e40761 | ||
|
|
f440a2b022 | ||
|
|
6262b3ff83 | ||
|
|
0652114013 | ||
|
|
307c77b30c | ||
|
|
3150647ff6 | ||
|
|
f1d7a98fe8 | ||
|
|
be259317f9 | ||
|
|
046d104bfa | ||
|
|
e2258bb91f | ||
|
|
9c693a2b74 | ||
|
|
2be0f3de09 | ||
|
|
3dd4b3dd77 | ||
|
|
de8c2cd5a2 | ||
|
|
b791f9846f | ||
|
|
daa7631056 | ||
|
|
d2f7585ea5 | ||
|
|
14b8a2daef | ||
|
|
90b38d817d | ||
|
|
ace48e2890 | ||
|
|
fb810be5d5 | ||
|
|
50230e9f50 | ||
|
|
86e14967ca | ||
|
|
c45c3b4037 | ||
|
|
c4c11528b9 | ||
|
|
1f9c4e92f1 | ||
|
|
371e148e09 | ||
|
|
33af544ded | ||
|
|
6b8d0ec91c | ||
|
|
5a3ddfad0f | ||
|
|
043c44ed51 | ||
|
|
6d5dbf3145 | ||
|
|
6d8463265c | ||
|
|
0669282432 | ||
|
|
1af511be2f | ||
|
|
bf4dc7e158 | ||
|
|
14b38604a3 | ||
|
|
5de2036fdb | ||
|
|
1629663e15 | ||
|
|
e25f2db2b1 | ||
|
|
cbf5d268ea | ||
|
|
a92a95a9fa | ||
|
|
0be7bf2c8e | ||
|
|
56b810dd40 | ||
|
|
33cfa531b8 | ||
|
|
40c7b706aa | ||
|
|
000ded6649 | ||
|
|
72181e1ff7 | ||
|
|
d73b1d2220 | ||
|
|
88c03ce655 | ||
|
|
8645b434c8 | ||
|
|
38a13bd082 | ||
|
|
3bc5f5d626 | ||
|
|
86a2351316 | ||
|
|
de62e994bd | ||
|
|
d5b1496898 | ||
|
|
d0d8354395 | ||
|
|
6d2c3c81c7 | ||
|
|
3486e16d4e | ||
|
|
e2e02945cc | ||
|
|
404efcaf05 | ||
|
|
9b96460e4f | ||
|
|
618eff4973 | ||
|
|
109c34bebd | ||
|
|
1aab5aa740 | ||
|
|
484cb8e39e | ||
|
|
1ca483d4b0 | ||
|
|
3641363d3d | ||
|
|
094160ebf3 | ||
|
|
fde13436c9 | ||
|
|
e7be4c6e61 | ||
|
|
708fe63852 | ||
|
|
3f579f49b9 | ||
|
|
c15e69f079 | ||
|
|
ddb919e2cc | ||
|
|
27e0f497bb | ||
|
|
b9a9f07d7b | ||
|
|
3a06e813a8 | ||
|
|
c2aaf8844f | ||
|
|
488f79ddc8 | ||
|
|
5704e54e48 | ||
|
|
3c4902f71f | ||
|
|
5fb62aa16b | ||
|
|
f4473d11a8 | ||
|
|
dd469bad12 | ||
|
|
a716535795 | ||
|
|
c2ec476324 | ||
|
|
3ecd29c640 | ||
|
|
01443c478d | ||
|
|
209245187f | ||
|
|
3a9e989d70 | ||
|
|
8839fc0293 | ||
|
|
028bf3c7a0 | ||
|
|
ed05754368 | ||
|
|
7c3043988b | ||
|
|
a16f0df7de | ||
|
|
2562151f6e | ||
|
|
44f2287b07 | ||
|
|
5e96ccdd99 | ||
|
|
8f91416623 | ||
|
|
965af6da5f | ||
|
|
a18dbbb6c4 | ||
|
|
57d8ca5829 | ||
|
|
4c6a2d6d63 | ||
|
|
c5c47e9bfc | ||
|
|
d66fdfb2e0 | ||
|
|
5861d0e9b6 | ||
|
|
e36904794b | ||
|
|
1c89d12034 | ||
|
|
fb5c5561e9 | ||
|
|
5e36a4ae89 | ||
|
|
f553307587 | ||
|
|
2c80c81197 | ||
|
|
9d865cf130 | ||
|
|
8e119ce0dd | ||
|
|
fe49161718 | ||
|
|
040e366335 | ||
|
|
4655663dd8 | ||
|
|
6e1fbda79b | ||
|
|
b2f616f1eb | ||
|
|
4bc8ff26d2 | ||
|
|
76eec7bebc | ||
|
|
aa5f405e1b | ||
|
|
ca9752d119 | ||
|
|
45b4af5225 | ||
|
|
d2d310cf57 | ||
|
|
5d1a7657a9 | ||
|
|
5cb17994cd | ||
|
|
dab78e3dc9 | ||
|
|
1232f28b3d | ||
|
|
8e8d40d4b0 | ||
|
|
7fae408454 | ||
|
|
1cdafaa2cc | ||
|
|
b9ca7ef2e3 | ||
|
|
60867ae4dc | ||
|
|
b0ddb62ac0 | ||
|
|
39a4646339 | ||
|
|
584322819f | ||
|
|
051ee347a9 | ||
|
|
acf1b387de | ||
|
|
d350515c90 | ||
|
|
121e579388 | ||
|
|
cf5ebb8130 | ||
|
|
2dabf3c811 | ||
|
|
04509fa587 | ||
|
|
56fef0f43c | ||
|
|
42702ef015 | ||
|
|
25bee3cfdf | ||
|
|
baf06fee6c | ||
|
|
a3557bbc86 | ||
|
|
3e00e7981d | ||
|
|
12fa270a1a | ||
|
|
de250b152a | ||
|
|
c45741257f | ||
|
|
3988386c79 | ||
|
|
492032c1e2 | ||
|
|
8222e56485 | ||
|
|
49a61e1564 | ||
|
|
eed18aa1c5 | ||
|
|
2de3f8b022 | ||
|
|
91476c7ad3 | ||
|
|
00eb7926f9 | ||
|
|
854ad21b20 | ||
|
|
3c3f9521f6 | ||
|
|
561bcf10d9 | ||
|
|
75d9faa05b | ||
|
|
ac72177fbb | ||
|
|
088faf152c | ||
|
|
90cba9ed24 | ||
|
|
a0702785c5 | ||
|
|
6898d609fe | ||
|
|
d70893e2ba | ||
|
|
6155b8bf24 | ||
|
|
239dc5c62d | ||
|
|
2586855f11 | ||
|
|
b3b3c4c737 | ||
|
|
abe5fadeea | ||
|
|
08e5543536 | ||
|
|
f1a10e0df4 | ||
|
|
7dc3c00628 | ||
|
|
1c5c403d65 | ||
|
|
7594f53e88 | ||
|
|
b861957342 | ||
|
|
2bf24ff5a1 | ||
|
|
6e5fcbfdbd | ||
|
|
759a8ac58c | ||
|
|
833baca9cc | ||
|
|
889ef61185 | ||
|
|
add1eddbc1 | ||
|
|
1c63aa39c4 | ||
|
|
0cabd80b94 | ||
|
|
7f756bab88 | ||
|
|
99b847822f | ||
|
|
f66d9b8c09 | ||
|
|
5660de42af | ||
|
|
ccbe92c275 | ||
|
|
b0ea8a71fb | ||
|
|
060871306f | ||
|
|
2eb4e142ff | ||
|
|
adf8cf9e8d | ||
|
|
852fd9c388 | ||
|
|
e921f28105 | ||
|
|
6db68b76db | ||
|
|
51ebfd86e7 | ||
|
|
79a90bb9ee | ||
|
|
1664f9c935 | ||
|
|
23fcdd6375 | ||
|
|
ea7c22daec | ||
|
|
10ffb33ec9 | ||
|
|
f9e023f922 | ||
|
|
c7832bdd82 | ||
|
|
329bdbe22d | ||
|
|
2df046c39d | ||
|
|
46065f1986 | ||
|
|
66b3fb6988 | ||
|
|
5eda224393 | ||
|
|
e387abcd14 | ||
|
|
4c2d4e20a6 | ||
|
|
3d26c2e94e | ||
|
|
d9e2ef9300 | ||
|
|
b00fdadc1b | ||
|
|
4d5e06b9fc | ||
|
|
ad42dd1295 | ||
|
|
1f4c1c9e92 | ||
|
|
2938b9c94c | ||
|
|
2227acab3a | ||
|
|
bde17446ad | ||
|
|
081165b6f5 | ||
|
|
7e717c0b1f | ||
|
|
e242aaa9f5 | ||
|
|
fe60538acf | ||
|
|
2db88f57df | ||
|
|
7461e58000 | ||
|
|
cd0b7a4e56 | ||
|
|
2a3b4e89ab | ||
|
|
1d77eda3e2 | ||
|
|
490e1f696a | ||
|
|
b96c618f54 | ||
|
|
1afda01d34 | ||
|
|
b9d11580d4 | ||
|
|
27fadb9ae2 | ||
|
|
4c5a2cefe9 | ||
|
|
175692559c | ||
|
|
57b27f73c3 | ||
|
|
7badb09ba1 | ||
|
|
c39f1d824a | ||
|
|
132cf98a37 | ||
|
|
59f71d53cd | ||
|
|
f4b3a990d7 | ||
|
|
20371ea00d | ||
|
|
3086a654a1 | ||
|
|
60768c8847 | ||
|
|
a68f8d6880 | ||
|
|
0cad64ff6d | ||
|
|
522665256e | ||
|
|
d3fe2c9d06 | ||
|
|
f080e84985 | ||
|
|
2a0ad46eea | ||
|
|
3a83160b33 | ||
|
|
7725080a11 | ||
|
|
701c532e48 | ||
|
|
1932795f55 | ||
|
|
cc7bd1c792 | ||
|
|
7799d93f3d | ||
|
|
a7cf36d5f8 | ||
|
|
54cc02068c | ||
|
|
f575870685 | ||
|
|
171b61b92f | ||
|
|
de44116940 | ||
|
|
3c0a883326 | ||
|
|
14d6cc94dd | ||
|
|
c0d9bacf1d | ||
|
|
d787821345 | ||
|
|
ed6eab4c38 | ||
|
|
b12c9407d9 | ||
|
|
300aee5b02 | ||
|
|
357f40bdc2 | ||
|
|
0a93551db4 | ||
|
|
8602ccbb8a | ||
|
|
d7b0e3046b | ||
|
|
30689a8ca6 | ||
|
|
0e06b449cb | ||
|
|
8e8208dd9a | ||
|
|
79e2fecb24 | ||
|
|
86e909e4e9 | ||
|
|
c7ff893397 | ||
|
|
3981b8684c | ||
|
|
1fb856f95f | ||
|
|
c62c3fa938 | ||
|
|
554ec37ace | ||
|
|
be4feca990 | ||
|
|
ec45454b3d | ||
|
|
0e78cb47f9 | ||
|
|
e5b8d003ec | ||
|
|
2eb81dde37 | ||
|
|
614549a545 | ||
|
|
5717727d2a | ||
|
|
f3714cea1e | ||
|
|
a3375e6152 | ||
|
|
f48fb7130e | ||
|
|
1460fa6fd7 | ||
|
|
3d9a07bd39 | ||
|
|
bd4aa4027a | ||
|
|
9fa995f002 | ||
|
|
3ed48b26f1 | ||
|
|
d585cacdfc | ||
|
|
55ddaca328 | ||
|
|
3fc7af9780 | ||
|
|
146bf95e51 | ||
|
|
58defad2ea | ||
|
|
9c40a03a06 | ||
|
|
a1b6ccc23d | ||
|
|
9b4247d6f6 | ||
|
|
8a6d94f193 | ||
|
|
6e7d1abd70 | ||
|
|
6bd74aae87 | ||
|
|
a5e3985745 | ||
|
|
f21caa10fc | ||
|
|
3d164eb070 | ||
|
|
88ee4f13e1 | ||
|
|
88ab3a21e2 | ||
|
|
dde6f17029 | ||
|
|
45bc1893a0 | ||
|
|
1aceef9153 | ||
|
|
d053e682d7 | ||
|
|
984a4a4cf6 | ||
|
|
678892d134 | ||
|
|
9438ef9683 | ||
|
|
dc187bbf24 | ||
|
|
10a354e479 | ||
|
|
fa05d0b401 | ||
|
|
31092c20a9 | ||
|
|
09aae78715 | ||
|
|
86beaf049c | ||
|
|
cf017fb80b | ||
|
|
56c366e9e8 | ||
|
|
3bc0653230 | ||
|
|
b950b3f825 | ||
|
|
3891fbefdf | ||
|
|
07b7394fec | ||
|
|
73bcc72fc3 | ||
|
|
f3911859c7 | ||
|
|
0617d79d19 | ||
|
|
885e9c6958 | ||
|
|
6bf5f2fe77 | ||
|
|
a3cc5c2324 | ||
|
|
12e3d61cfb | ||
|
|
c42276ab3a | ||
|
|
4cba91e097 | ||
|
|
c695aea12e | ||
|
|
111f554dea | ||
|
|
fe2a731b5f | ||
|
|
61e6511547 | ||
|
|
85346e203b | ||
|
|
a44ed3c406 | ||
|
|
aa5110ae13 | ||
|
|
41d25cbc52 | ||
|
|
c565e2199d | ||
|
|
085c27ad20 | ||
|
|
8fa0946cfa | ||
|
|
a1ab254d6f | ||
|
|
38e6b5010e | ||
|
|
99d3943955 | ||
|
|
2415b4c2b4 | ||
|
|
99c7ba1fbc | ||
|
|
36e593f806 | ||
|
|
d825c04850 | ||
|
|
ca7dfacec4 | ||
|
|
c8ee9ca5a7 | ||
|
|
1138540518 | ||
|
|
242c275e7d | ||
|
|
59db305cb8 | ||
|
|
fea69fe3a5 | ||
|
|
43e4ff911e | ||
|
|
1dc6130fdf | ||
|
|
ae4cff98e7 | ||
|
|
3650cacb51 | ||
|
|
c2bf6841e1 | ||
|
|
910afbf48d | ||
|
|
f41b94d16d | ||
|
|
24da0207e5 | ||
|
|
bf34765e6b | ||
|
|
4c98a347f5 | ||
|
|
840e760619 | ||
|
|
9d9edfd674 | ||
|
|
739265ee6a | ||
|
|
62a5b49836 | ||
|
|
038aaf249e | ||
|
|
e0eb4657d2 | ||
|
|
02a6ccd481 | ||
|
|
79e75a5e73 | ||
|
|
e93e138f78 | ||
|
|
8d22248f4b | ||
|
|
bb8024ba9c | ||
|
|
4639e31e55 | ||
|
|
a960963e36 | ||
|
|
e8fde14f9b | ||
|
|
3be50b5067 | ||
|
|
90e87adc34 | ||
|
|
563c1d2402 | ||
|
|
81053b3cbf | ||
|
|
2108a4e96c | ||
|
|
f4290bf20c | ||
|
|
c04a690dc3 | ||
|
|
5371657aa4 | ||
|
|
20e84668a5 | ||
|
|
599f4e143c | ||
|
|
2e40583d31 | ||
|
|
07c307e17b | ||
|
|
94440e5c48 | ||
|
|
176774a888 | ||
|
|
aa5d6f2090 | ||
|
|
7b3dcf295e | ||
|
|
6bde1b1baf | ||
|
|
380dbd8b96 | ||
|
|
a34c2a5bc2 | ||
|
|
d8ba40979e | ||
|
|
5cd0527e16 | ||
|
|
25513ae5b5 | ||
|
|
d89acbd49d | ||
|
|
a54862a309 | ||
|
|
423157dfcc | ||
|
|
6607c80aca | ||
|
|
162aeca7c8 | ||
|
|
1583ed2d61 | ||
|
|
c78b13baa3 | ||
|
|
fc404b1f3b | ||
|
|
6528a0c700 | ||
|
|
79f032ecaf | ||
|
|
42b4534d21 | ||
|
|
32de0ddeb6 | ||
|
|
c6ba3fd8f0 | ||
|
|
76025b5db1 | ||
|
|
ea27fcd476 | ||
|
|
968816b4a6 | ||
|
|
9de076f060 | ||
|
|
08d334e93a | ||
|
|
119904ca2b | ||
|
|
af009a0bb3 | ||
|
|
f7e1b023df | ||
|
|
f8e74d9bad | ||
|
|
f6bf1ce793 | ||
|
|
9413bc60cf | ||
|
|
34f5fad365 | ||
|
|
d69ce2d2a9 | ||
|
|
5855569194 | ||
|
|
b0755a0cde | ||
|
|
4f852e7493 | ||
|
|
a551258895 | ||
|
|
deec40a89c | ||
|
|
145dd9bec6 | ||
|
|
b8e5d4412f | ||
|
|
277fb8f839 | ||
|
|
d3d5485846 | ||
|
|
55091d61d6 | ||
|
|
5b5df8a3a1 | ||
|
|
ccf48cfcf1 | ||
|
|
c89342b6ef | ||
|
|
e97ceb7cbe | ||
|
|
de34cbd937 | ||
|
|
cf7a1b0168 |
@@ -2,23 +2,21 @@ version: 2.1
|
|||||||
orbs:
|
orbs:
|
||||||
#snyk: snyk/snyk@0.0.8
|
#snyk: snyk/snyk@0.0.8
|
||||||
#cypress: cypress-io/cypress@1.23.0
|
#cypress: cypress-io/cypress@1.23.0
|
||||||
aws-s3: circleci/aws-s3@2.0.0
|
aws-s3: circleci/aws-s3@4.0.0
|
||||||
eb: circleci/aws-elastic-beanstalk@1.0.2
|
eb: circleci/aws-elastic-beanstalk@2.0.1
|
||||||
jira: circleci/jira@1.3.1
|
|
||||||
jobs:
|
jobs:
|
||||||
api-deploy:
|
api-deploy:
|
||||||
docker:
|
docker:
|
||||||
- image: "cimg/base:stable"
|
- image: cimg/node:18.18.2
|
||||||
steps:
|
steps:
|
||||||
- checkout
|
- checkout
|
||||||
- eb/setup
|
- eb/setup
|
||||||
- run:
|
- run:
|
||||||
command: |
|
command: |
|
||||||
eb init imex-online-production-api -r ca-central-1 -p "Node.js 16 running on 64bit Amazon Linux 2"
|
eb init imex-online-production-api -r ca-central-1 -p "Node.js 18 running on 64bit Amazon Linux 2"
|
||||||
eb status --verbose
|
eb status --verbose
|
||||||
eb deploy
|
eb deploy
|
||||||
eb status
|
eb status
|
||||||
- jira/notify
|
|
||||||
|
|
||||||
hasura-migrate:
|
hasura-migrate:
|
||||||
docker:
|
docker:
|
||||||
@@ -48,26 +46,36 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- checkout:
|
- checkout:
|
||||||
path: ~/repo
|
path: ~/repo
|
||||||
|
|
||||||
- restore_cache:
|
|
||||||
name: Restore Yarn Package Cache
|
|
||||||
keys:
|
|
||||||
- yarn-packages-{{ checksum "yarn.lock" }}
|
|
||||||
- run:
|
- run:
|
||||||
name: Install Dependencies
|
name: Install Dependencies
|
||||||
command: yarn install --frozen-lockfile --cache-folder ~/.cache/yarn
|
command: npm i
|
||||||
- save_cache:
|
|
||||||
name: Save Yarn Package Cache
|
|
||||||
key: yarn-packages-{{ checksum "yarn.lock" }}
|
|
||||||
paths:
|
|
||||||
- ~/.cache/yarn
|
|
||||||
|
|
||||||
- run: yarn run build
|
- run: npm run build
|
||||||
|
|
||||||
- aws-s3/sync:
|
- aws-s3/sync:
|
||||||
from: build
|
from: build
|
||||||
to: "s3://imex-online-production/"
|
to: "s3://imex-online-production/"
|
||||||
- jira/notify
|
arguments: "--exclude '*.map'"
|
||||||
|
|
||||||
|
app-beta-build:
|
||||||
|
docker:
|
||||||
|
- image: cimg/node:18.18.2
|
||||||
|
resource_class: xlarge
|
||||||
|
working_directory: ~/repo/client
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- checkout:
|
||||||
|
path: ~/repo
|
||||||
|
- run:
|
||||||
|
name: Install Dependencies
|
||||||
|
command: npm i
|
||||||
|
|
||||||
|
- run: npm run build
|
||||||
|
|
||||||
|
- aws-s3/sync:
|
||||||
|
from: build
|
||||||
|
to: "s3://imex-online-beta/"
|
||||||
|
arguments: "--exclude '*.map'"
|
||||||
|
|
||||||
test-hasura-migrate:
|
test-hasura-migrate:
|
||||||
docker:
|
docker:
|
||||||
@@ -98,26 +106,37 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- checkout:
|
- checkout:
|
||||||
path: ~/repo
|
path: ~/repo
|
||||||
|
|
||||||
- restore_cache:
|
|
||||||
name: Restore Yarn Package Cache
|
|
||||||
keys:
|
|
||||||
- yarn-packages-{{ checksum "yarn.lock" }}
|
|
||||||
- run:
|
- run:
|
||||||
name: Install Dependencies
|
name: Install Dependencies
|
||||||
command: yarn install --frozen-lockfile --cache-folder ~/.cache/yarn
|
command: npm i
|
||||||
- save_cache:
|
|
||||||
name: Save Yarn Package Cache
|
|
||||||
key: yarn-packages-{{ checksum "yarn.lock" }}
|
|
||||||
paths:
|
|
||||||
- ~/.cache/yarn
|
|
||||||
|
|
||||||
- run: yarn run build:test
|
- run: npm run build:test
|
||||||
|
|
||||||
- aws-s3/sync:
|
- aws-s3/sync:
|
||||||
from: build
|
from: build
|
||||||
to: "s3://imex-online-test/"
|
to: "s3://imex-online-test/"
|
||||||
- jira/notify
|
arguments: "--exclude '*.map'"
|
||||||
|
|
||||||
|
test-app-beta-build:
|
||||||
|
docker:
|
||||||
|
- image: cimg/node:18.18.2
|
||||||
|
resource_class: snaptsoft/pfic
|
||||||
|
working_directory: ~/repo/client
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- checkout:
|
||||||
|
path: ~/repo
|
||||||
|
|
||||||
|
- run:
|
||||||
|
name: Install Dependencies
|
||||||
|
command: npm i
|
||||||
|
|
||||||
|
- run: npm run build:test
|
||||||
|
|
||||||
|
- aws-s3/sync:
|
||||||
|
from: build
|
||||||
|
to: "s3://imex-online-test-beta/"
|
||||||
|
arguments: "--exclude '*.map'"
|
||||||
|
|
||||||
admin-app-build:
|
admin-app-build:
|
||||||
docker:
|
docker:
|
||||||
@@ -160,6 +179,10 @@ workflows:
|
|||||||
filters:
|
filters:
|
||||||
branches:
|
branches:
|
||||||
only: master
|
only: master
|
||||||
|
- app-beta-build:
|
||||||
|
filters:
|
||||||
|
branches:
|
||||||
|
only: master-beta
|
||||||
- hasura-migrate:
|
- hasura-migrate:
|
||||||
secret: ${HASURA_PROD_SECRET}
|
secret: ${HASURA_PROD_SECRET}
|
||||||
filters:
|
filters:
|
||||||
@@ -169,6 +192,10 @@ workflows:
|
|||||||
filters:
|
filters:
|
||||||
branches:
|
branches:
|
||||||
only: test
|
only: test
|
||||||
|
- test-app-beta-build:
|
||||||
|
filters:
|
||||||
|
branches:
|
||||||
|
only: test-beta
|
||||||
- test-hasura-migrate:
|
- test-hasura-migrate:
|
||||||
secret: ${HASURA_TEST_SECRET}
|
secret: ${HASURA_TEST_SECRET}
|
||||||
filters:
|
filters:
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -118,3 +118,5 @@ logs/oAuthClient-log.log
|
|||||||
.node-persist/**
|
.node-persist/**
|
||||||
|
|
||||||
/*.env.*
|
/*.env.*
|
||||||
|
.idea/*
|
||||||
|
.idea
|
||||||
11
README.MD
11
README.MD
@@ -1,14 +1,3 @@
|
|||||||
Yarn Dependency Management:
|
|
||||||
To force upgrades for some packages:
|
|
||||||
yarn upgrade-interactive --latest
|
|
||||||
|
|
||||||
To Start Hasura CLI:
|
|
||||||
npx hasura console
|
|
||||||
|
|
||||||
Migrating to Staging:
|
|
||||||
npx hasura migrate apply --endpoint https://db.imex.online/ --admin-secret 'Production-ImEXOnline!@#'
|
|
||||||
npx hasura migrate apply --endpoint https://db.test.bodyshop.app/ --admin-secret 'Test-ImEXOnlineBySnaptSoftware!'
|
|
||||||
|
|
||||||
NGROK TEsting:
|
NGROK TEsting:
|
||||||
./ngrok.exe http http://localhost:4000 -host-header="localhost:4000"
|
./ngrok.exe http http://localhost:4000 -host-header="localhost:4000"
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,13 +1,13 @@
|
|||||||
REACT_APP_GRAPHQL_ENDPOINT=https://db.dev.bodyshop.app/v1/graphql
|
VITE_APP_GRAPHQL_ENDPOINT=https://db.dev.bodyshop.app/v1/graphql
|
||||||
REACT_APP_GRAPHQL_ENDPOINT_WS=wss://db.dev.bodyshop.app/v1/graphql
|
VITE_APP_GRAPHQL_ENDPOINT_WS=wss://db.dev.bodyshop.app/v1/graphql
|
||||||
REACT_APP_GA_CODE=231099835
|
VITE_APP_GA_CODE=231099835
|
||||||
REACT_APP_FIREBASE_CONFIG={"apiKey":"AIzaSyDPLT8GiDHDR1R4nI66Qi0BY1aYviDPioc","authDomain":"imex-dev.firebaseapp.com","databaseURL":"https://imex-dev.firebaseio.com","projectId":"imex-dev","storageBucket":"imex-dev.appspot.com","messagingSenderId":"759548147434","appId":"1:759548147434:web:e8239868a48ceb36700993","measurementId":"G-K5XRBVVB4S"}
|
VITE_APP_FIREBASE_CONFIG={"apiKey":"AIzaSyDPLT8GiDHDR1R4nI66Qi0BY1aYviDPioc","authDomain":"imex-dev.firebaseapp.com","databaseURL":"https://imex-dev.firebaseio.com","projectId":"imex-dev","storageBucket":"imex-dev.appspot.com","messagingSenderId":"759548147434","appId":"1:759548147434:web:e8239868a48ceb36700993","measurementId":"G-K5XRBVVB4S"}
|
||||||
REACT_APP_CLOUDINARY_ENDPOINT_API=https://api.cloudinary.com/v1_1/io-test
|
VITE_APP_CLOUDINARY_ENDPOINT_API=https://api.cloudinary.com/v1_1/io-test
|
||||||
REACT_APP_CLOUDINARY_ENDPOINT=https://res.cloudinary.com/io-test
|
VITE_APP_CLOUDINARY_ENDPOINT=https://res.cloudinary.com/io-test
|
||||||
REACT_APP_CLOUDINARY_API_KEY=957865933348715
|
VITE_APP_CLOUDINARY_API_KEY=957865933348715
|
||||||
REACT_APP_CLOUDINARY_THUMB_TRANSFORMATIONS=c_fill,h_250,w_250
|
VITE_APP_CLOUDINARY_THUMB_TRANSFORMATIONS=c_fill,h_250,w_250
|
||||||
REACT_APP_FIREBASE_PUBLIC_VAPID_KEY='BG3tzU7L2BXlGZ_3VLK4PNaRceoEXEnmHfxcVbRMF5o5g05ejslhVPki9kBM9cBBT-08Ad9kN3HSpS6JmrWD6h4'
|
VITE_APP_FIREBASE_PUBLIC_VAPID_KEY='BG3tzU7L2BXlGZ_3VLK4PNaRceoEXEnmHfxcVbRMF5o5g05ejslhVPki9kBM9cBBT-08Ad9kN3HSpS6JmrWD6h4'
|
||||||
REACT_APP_STRIPE_PUBLIC_KEY=pk_test_51GqB4TJl3nQjrZ0wCQWAxAhlNF8jKe0tipIa6ExBaxwJGitwvFsIZUEua4dUzaMIAuXp4qwYHXx7lgjyQSwP0Pe900vzm38C7g
|
VITE_APP_STRIPE_PUBLIC_KEY=pk_test_51GqB4TJl3nQjrZ0wCQWAxAhlNF8jKe0tipIa6ExBaxwJGitwvFsIZUEua4dUzaMIAuXp4qwYHXx7lgjyQSwP0Pe900vzm38C7g
|
||||||
REACT_APP_AXIOS_BASE_API_URL=https://api.imex.online/
|
VITE_APP_AXIOS_BASE_API_URL=http://localhost:4000
|
||||||
REACT_APP_REPORTS_SERVER_URL=https://reports3.test.imex.online
|
VITE_APP_REPORTS_SERVER_URL=https://reports3.test.imex.online
|
||||||
REACT_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
|
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
GENERATE_SOURCEMAP=true
|
||||||
REACT_APP_GRAPHQL_ENDPOINT=https://db.imex.online/v1/graphql
|
REACT_APP_GRAPHQL_ENDPOINT=https://db.imex.online/v1/graphql
|
||||||
REACT_APP_GRAPHQL_ENDPOINT_WS=wss://db.imex.online/v1/graphql
|
REACT_APP_GRAPHQL_ENDPOINT_WS=wss://db.imex.online/v1/graphql
|
||||||
REACT_APP_GA_CODE=231103507
|
REACT_APP_GA_CODE=231103507
|
||||||
|
|||||||
3
client/.gitignore
vendored
Normal file
3
client/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
|
||||||
|
# Sentry Config File
|
||||||
|
.sentryclirc
|
||||||
1
client/.npmrc
Normal file
1
client/.npmrc
Normal file
@@ -0,0 +1 @@
|
|||||||
|
legacy-peer-deps=true
|
||||||
Binary file not shown.
@@ -1,73 +1,68 @@
|
|||||||
// craco.config.js
|
// craco.config.js
|
||||||
const TerserPlugin = require("terser-webpack-plugin");
|
const TerserPlugin = require("terser-webpack-plugin");
|
||||||
const CracoLessPlugin = require("craco-less");
|
const CracoLessPlugin = require("craco-less");
|
||||||
const SentryWebpackPlugin = require("@sentry/webpack-plugin");
|
const {convertLegacyToken} = require('@ant-design/compatible/lib');
|
||||||
|
const {theme} = require('antd/lib');
|
||||||
|
|
||||||
|
const {defaultAlgorithm, defaultSeed} = theme;
|
||||||
|
|
||||||
|
const mapToken = defaultAlgorithm(defaultSeed);
|
||||||
|
const v4Token = convertLegacyToken(mapToken);
|
||||||
|
|
||||||
|
// TODO, At the moment we are using less in the Dashboard. Once we remove this we can remove the less processor entirely.
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: [
|
plugins: [
|
||||||
{
|
// {
|
||||||
plugin: SentryWebpackPlugin,
|
// plugin: SentryWebpackPlugin,
|
||||||
options: {
|
// options: {
|
||||||
// sentry-cli configuration
|
// // sentry-cli configuration
|
||||||
authToken:
|
// authToken:
|
||||||
"6b45b028a02342db97a9a2f92c0959058665443d379d4a3a876430009e744260",
|
// "6b45b028a02342db97a9a2f92c0959058665443d379d4a3a876430009e744260",
|
||||||
org: "snapt-software",
|
// org: "snapt-software",
|
||||||
project: "imexonline",
|
// project: "imexonline",
|
||||||
release: process.env.REACT_APP_GIT_SHA,
|
// release: process.env.REACT_APP_GIT_SHA,
|
||||||
|
//
|
||||||
// webpack-specific configuration
|
// // webpack-specific configuration
|
||||||
include: ".",
|
// include: ".",
|
||||||
ignore: ["node_modules", "webpack.config.js"],
|
// ignore: ["node_modules", "webpack.config.js"],
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
{
|
{
|
||||||
plugin: CracoLessPlugin,
|
plugin: CracoLessPlugin,
|
||||||
options: {
|
options: {
|
||||||
lessLoaderOptions: {
|
lessLoaderOptions: {
|
||||||
lessOptions: {
|
lessOptions: {
|
||||||
modifyVars: {
|
modifyVars: {...v4Token},
|
||||||
...(process.env.NODE_ENV === "development"
|
javascriptEnabled: true,
|
||||||
? { "@primary-color": "#a51d1d" }
|
},
|
||||||
: {
|
},
|
||||||
//"@primary-color": "#1DA57A"
|
|
||||||
}),
|
|
||||||
// "@primary-color": " #1890ff", // primary color for all components
|
|
||||||
// "@link-color": "#1890ff", // link color
|
|
||||||
// "@success-color": "#52c41a", // success state color
|
|
||||||
// "@warning-color": "#faad14", // warning state color
|
|
||||||
// "@error-color": "#f5222d", // error state color
|
|
||||||
// "@font-size-base": "14px", // major text font size
|
|
||||||
// " @heading-color": "rgba(0, 0, 0, 0.85)", // heading text color
|
|
||||||
// "@text-color": "rgba(0, 0, 0, 0.65)", // major text color
|
|
||||||
// "@text-color-secondary": "rgba(0, 0, 0, 0.45)", // secondary text color
|
|
||||||
// "@disabled-color": "rgba(0, 0, 0, 0.25)", // disable state color
|
|
||||||
// "@border-radius-base": "2px", // major border radius
|
|
||||||
// "@border-color-base": "#d9d9d9", // major border color
|
|
||||||
// "@box-shadow-base":
|
|
||||||
// "0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 6px 16px 0 rgba(0, 0, 0, 0.08),0 9px 28px 8px rgba(0, 0, 0, 0.05); // major shadow for layers }",
|
|
||||||
},
|
},
|
||||||
javascriptEnabled: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
},
|
webpack: {
|
||||||
],
|
configure: (webpackConfig) => {
|
||||||
webpack: {
|
return {
|
||||||
configure: (webpackConfig) => ({
|
...webpackConfig,
|
||||||
...webpackConfig,
|
// Required for Dev Server
|
||||||
optimization: {
|
devServer: {
|
||||||
...webpackConfig.optimization,
|
...webpackConfig.devServer,
|
||||||
// Workaround for CircleCI bug caused by the number of CPUs shown
|
allowedHosts: 'all',
|
||||||
// https://github.com/facebook/create-react-app/issues/8320
|
},
|
||||||
minimizer: webpackConfig.optimization.minimizer.map((item) => {
|
optimization: {
|
||||||
if (item instanceof TerserPlugin) {
|
...webpackConfig.optimization,
|
||||||
item.options.parallel = 2;
|
// Workaround for CircleCI bug caused by the number of CPUs shown
|
||||||
}
|
// https://github.com/facebook/create-react-app/issues/8320
|
||||||
|
minimizer: webpackConfig.optimization.minimizer.map((item) => {
|
||||||
|
if (item instanceof TerserPlugin) {
|
||||||
|
item.options.parallel = 2;
|
||||||
|
}
|
||||||
|
|
||||||
return item;
|
return item;
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
}),
|
};
|
||||||
},
|
},
|
||||||
devtool: "source-map",
|
},
|
||||||
|
devtool: "source-map",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +1,17 @@
|
|||||||
const { defineConfig } = require("cypress");
|
const { defineConfig } = require('cypress')
|
||||||
|
|
||||||
module.exports = defineConfig({
|
module.exports = defineConfig({
|
||||||
e2e: {
|
experimentalStudio: true,
|
||||||
setupNodeEvents(on, config) {
|
env: {
|
||||||
// implement node event listeners here
|
FIREBASE_USERNAME: 'cypress@imex.test',
|
||||||
},
|
FIREBASE_PASSWORD: 'cypress',
|
||||||
},
|
},
|
||||||
});
|
e2e: {
|
||||||
|
// We've imported your old cypress plugins here.
|
||||||
|
// You may want to clean this up later by importing these.
|
||||||
|
setupNodeEvents(on, config) {
|
||||||
|
return require('./cypress/plugins/index.js')(on, config)
|
||||||
|
},
|
||||||
|
baseUrl: 'http://localhost:3000',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|||||||
23
client/cypress/e2e/01-General Render/01-home.cy.js
Normal file
23
client/cypress/e2e/01-General Render/01-home.cy.js
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
/// <reference types="Cypress" />
|
||||||
|
const { FIREBASE_USERNAME, FIREBASE_PASSWORcD } = Cypress.env();
|
||||||
|
describe("Renders the General Page", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit("/");
|
||||||
|
});
|
||||||
|
it("Renders Correctly", () => {});
|
||||||
|
it("Has the Slogan", () => {
|
||||||
|
cy.findByText("A whole x22new kind of shop management system.").should(
|
||||||
|
"exist"
|
||||||
|
);
|
||||||
|
/* ==== Generated with Cypress Studio ==== */
|
||||||
|
cy.get(
|
||||||
|
".ant-menu-item-active > .ant-menu-title-content > .header0-item-block"
|
||||||
|
).click();
|
||||||
|
cy.get("#email").clear();
|
||||||
|
cy.get("#email").type("patrick@imex.dev");
|
||||||
|
cy.get("#password").clear();
|
||||||
|
cy.get("#password").type("patrick123{enter}");
|
||||||
|
cy.get(".ant-form > .ant-btn").click();
|
||||||
|
/* ==== End Cypress Studio ==== */
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
/// <reference types="cypress" />
|
|
||||||
|
|
||||||
context("Actions", () => {
|
|
||||||
beforeEach(() => {});
|
|
||||||
|
|
||||||
// https://on.cypress.io/interacting-with-elements
|
|
||||||
|
|
||||||
it("Attempt a failed login.", () => {
|
|
||||||
cy.visit("http://localhost:3000");
|
|
||||||
cy.get(".ant-btn").contains("Sign In").click();
|
|
||||||
cy.get("#email").type("fake@email.com");
|
|
||||||
cy.get("#password").type("fakepassword");
|
|
||||||
cy.get(".ant-btn").contains("Login").click();
|
|
||||||
cy.should("have.text", "user");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Attempt a failed login.", () => {});
|
|
||||||
});
|
|
||||||
143
client/cypress/e2e/1-getting-started/todo.cy.js
Normal file
143
client/cypress/e2e/1-getting-started/todo.cy.js
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
|
// Welcome to Cypress!
|
||||||
|
//
|
||||||
|
// This spec file contains a variety of sample tests
|
||||||
|
// for a todo list app that are designed to demonstrate
|
||||||
|
// the power of writing tests in Cypress.
|
||||||
|
//
|
||||||
|
// To learn more about how Cypress works and
|
||||||
|
// what makes it such an awesome testing tool,
|
||||||
|
// please read our getting started guide:
|
||||||
|
// https://on.cypress.io/introduction-to-cypress
|
||||||
|
|
||||||
|
describe('example to-do app', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Cypress starts out with a blank slate for each test
|
||||||
|
// so we must tell it to visit our website with the `cy.visit()` command.
|
||||||
|
// Since we want to visit the same URL at the start of all our tests,
|
||||||
|
// we include it in our beforeEach function so that it runs before each test
|
||||||
|
cy.visit('https://example.cypress.io/todo')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('displays two todo items by default', () => {
|
||||||
|
// We use the `cy.get()` command to get all elements that match the selector.
|
||||||
|
// Then, we use `should` to assert that there are two matched items,
|
||||||
|
// which are the two default items.
|
||||||
|
cy.get('.todo-list li').should('have.length', 2)
|
||||||
|
|
||||||
|
// We can go even further and check that the default todos each contain
|
||||||
|
// the correct text. We use the `first` and `last` functions
|
||||||
|
// to get just the first and last matched elements individually,
|
||||||
|
// and then perform an assertion with `should`.
|
||||||
|
cy.get('.todo-list li').first().should('have.text', 'Pay electric bill')
|
||||||
|
cy.get('.todo-list li').last().should('have.text', 'Walk the dog')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can add new todo items', () => {
|
||||||
|
// We'll store our item text in a variable so we can reuse it
|
||||||
|
const newItem = 'Feed the cat'
|
||||||
|
|
||||||
|
// Let's get the input element and use the `type` command to
|
||||||
|
// input our new list item. After typing the content of our item,
|
||||||
|
// we need to type the enter key as well in order to submit the input.
|
||||||
|
// This input has a data-test attribute so we'll use that to select the
|
||||||
|
// element in accordance with best practices:
|
||||||
|
// https://on.cypress.io/selecting-elements
|
||||||
|
cy.get('[data-test=new-todo]').type(`${newItem}{enter}`)
|
||||||
|
|
||||||
|
// Now that we've typed our new item, let's check that it actually was added to the list.
|
||||||
|
// Since it's the newest item, it should exist as the last element in the list.
|
||||||
|
// In addition, with the two default items, we should have a total of 3 elements in the list.
|
||||||
|
// Since assertions yield the element that was asserted on,
|
||||||
|
// we can chain both of these assertions together into a single statement.
|
||||||
|
cy.get('.todo-list li')
|
||||||
|
.should('have.length', 3)
|
||||||
|
.last()
|
||||||
|
.should('have.text', newItem)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can check off an item as completed', () => {
|
||||||
|
// In addition to using the `get` command to get an element by selector,
|
||||||
|
// we can also use the `contains` command to get an element by its contents.
|
||||||
|
// However, this will yield the <label>, which is lowest-level element that contains the text.
|
||||||
|
// In order to check the item, we'll find the <input> element for this <label>
|
||||||
|
// by traversing up the dom to the parent element. From there, we can `find`
|
||||||
|
// the child checkbox <input> element and use the `check` command to check it.
|
||||||
|
cy.contains('Pay electric bill')
|
||||||
|
.parent()
|
||||||
|
.find('input[type=checkbox]')
|
||||||
|
.check()
|
||||||
|
|
||||||
|
// Now that we've checked the button, we can go ahead and make sure
|
||||||
|
// that the list element is now marked as completed.
|
||||||
|
// Again we'll use `contains` to find the <label> element and then use the `parents` command
|
||||||
|
// to traverse multiple levels up the dom until we find the corresponding <li> element.
|
||||||
|
// Once we get that element, we can assert that it has the completed class.
|
||||||
|
cy.contains('Pay electric bill')
|
||||||
|
.parents('li')
|
||||||
|
.should('have.class', 'completed')
|
||||||
|
})
|
||||||
|
|
||||||
|
context('with a checked task', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// We'll take the command we used above to check off an element
|
||||||
|
// Since we want to perform multiple tests that start with checking
|
||||||
|
// one element, we put it in the beforeEach hook
|
||||||
|
// so that it runs at the start of every test.
|
||||||
|
cy.contains('Pay electric bill')
|
||||||
|
.parent()
|
||||||
|
.find('input[type=checkbox]')
|
||||||
|
.check()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can filter for uncompleted tasks', () => {
|
||||||
|
// We'll click on the "active" button in order to
|
||||||
|
// display only incomplete items
|
||||||
|
cy.contains('Active').click()
|
||||||
|
|
||||||
|
// After filtering, we can assert that there is only the one
|
||||||
|
// incomplete item in the list.
|
||||||
|
cy.get('.todo-list li')
|
||||||
|
.should('have.length', 1)
|
||||||
|
.first()
|
||||||
|
.should('have.text', 'Walk the dog')
|
||||||
|
|
||||||
|
// For good measure, let's also assert that the task we checked off
|
||||||
|
// does not exist on the page.
|
||||||
|
cy.contains('Pay electric bill').should('not.exist')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can filter for completed tasks', () => {
|
||||||
|
// We can perform similar steps as the test above to ensure
|
||||||
|
// that only completed tasks are shown
|
||||||
|
cy.contains('Completed').click()
|
||||||
|
|
||||||
|
cy.get('.todo-list li')
|
||||||
|
.should('have.length', 1)
|
||||||
|
.first()
|
||||||
|
.should('have.text', 'Pay electric bill')
|
||||||
|
|
||||||
|
cy.contains('Walk the dog').should('not.exist')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can delete all completed tasks', () => {
|
||||||
|
// First, let's click the "Clear completed" button
|
||||||
|
// `contains` is actually serving two purposes here.
|
||||||
|
// First, it's ensuring that the button exists within the dom.
|
||||||
|
// This button only appears when at least one task is checked
|
||||||
|
// so this command is implicitly verifying that it does exist.
|
||||||
|
// Second, it selects the button so we can click it.
|
||||||
|
cy.contains('Clear completed').click()
|
||||||
|
|
||||||
|
// Then we can make sure that there is only one element
|
||||||
|
// in the list and our element does not exist
|
||||||
|
cy.get('.todo-list li')
|
||||||
|
.should('have.length', 1)
|
||||||
|
.should('not.have.text', 'Pay electric bill')
|
||||||
|
|
||||||
|
// Finally, make sure that the clear button no longer exists.
|
||||||
|
cy.contains('Clear completed').should('not.exist')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
299
client/cypress/e2e/2-advanced-examples/actions.cy.js
Normal file
299
client/cypress/e2e/2-advanced-examples/actions.cy.js
Normal file
@@ -0,0 +1,299 @@
|
|||||||
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
|
context('Actions', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit('https://example.cypress.io/commands/actions')
|
||||||
|
})
|
||||||
|
|
||||||
|
// https://on.cypress.io/interacting-with-elements
|
||||||
|
|
||||||
|
it('.type() - type into a DOM element', () => {
|
||||||
|
// https://on.cypress.io/type
|
||||||
|
cy.get('.action-email')
|
||||||
|
.type('fake@email.com').should('have.value', 'fake@email.com')
|
||||||
|
|
||||||
|
// .type() with special character sequences
|
||||||
|
.type('{leftarrow}{rightarrow}{uparrow}{downarrow}')
|
||||||
|
.type('{del}{selectall}{backspace}')
|
||||||
|
|
||||||
|
// .type() with key modifiers
|
||||||
|
.type('{alt}{option}') //these are equivalent
|
||||||
|
.type('{ctrl}{control}') //these are equivalent
|
||||||
|
.type('{meta}{command}{cmd}') //these are equivalent
|
||||||
|
.type('{shift}')
|
||||||
|
|
||||||
|
// Delay each keypress by 0.1 sec
|
||||||
|
.type('slow.typing@email.com', { delay: 100 })
|
||||||
|
.should('have.value', 'slow.typing@email.com')
|
||||||
|
|
||||||
|
cy.get('.action-disabled')
|
||||||
|
// Ignore error checking prior to type
|
||||||
|
// like whether the input is visible or disabled
|
||||||
|
.type('disabled error checking', { force: true })
|
||||||
|
.should('have.value', 'disabled error checking')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('.focus() - focus on a DOM element', () => {
|
||||||
|
// https://on.cypress.io/focus
|
||||||
|
cy.get('.action-focus').focus()
|
||||||
|
.should('have.class', 'focus')
|
||||||
|
.prev().should('have.attr', 'style', 'color: orange;')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('.blur() - blur off a DOM element', () => {
|
||||||
|
// https://on.cypress.io/blur
|
||||||
|
cy.get('.action-blur').type('About to blur').blur()
|
||||||
|
.should('have.class', 'error')
|
||||||
|
.prev().should('have.attr', 'style', 'color: red;')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('.clear() - clears an input or textarea element', () => {
|
||||||
|
// https://on.cypress.io/clear
|
||||||
|
cy.get('.action-clear').type('Clear this text')
|
||||||
|
.should('have.value', 'Clear this text')
|
||||||
|
.clear()
|
||||||
|
.should('have.value', '')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('.submit() - submit a form', () => {
|
||||||
|
// https://on.cypress.io/submit
|
||||||
|
cy.get('.action-form')
|
||||||
|
.find('[type="text"]').type('HALFOFF')
|
||||||
|
|
||||||
|
cy.get('.action-form').submit()
|
||||||
|
.next().should('contain', 'Your form has been submitted!')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('.click() - click on a DOM element', () => {
|
||||||
|
// https://on.cypress.io/click
|
||||||
|
cy.get('.action-btn').click()
|
||||||
|
|
||||||
|
// You can click on 9 specific positions of an element:
|
||||||
|
// -----------------------------------
|
||||||
|
// | topLeft top topRight |
|
||||||
|
// | |
|
||||||
|
// | |
|
||||||
|
// | |
|
||||||
|
// | left center right |
|
||||||
|
// | |
|
||||||
|
// | |
|
||||||
|
// | |
|
||||||
|
// | bottomLeft bottom bottomRight |
|
||||||
|
// -----------------------------------
|
||||||
|
|
||||||
|
// clicking in the center of the element is the default
|
||||||
|
cy.get('#action-canvas').click()
|
||||||
|
|
||||||
|
cy.get('#action-canvas').click('topLeft')
|
||||||
|
cy.get('#action-canvas').click('top')
|
||||||
|
cy.get('#action-canvas').click('topRight')
|
||||||
|
cy.get('#action-canvas').click('left')
|
||||||
|
cy.get('#action-canvas').click('right')
|
||||||
|
cy.get('#action-canvas').click('bottomLeft')
|
||||||
|
cy.get('#action-canvas').click('bottom')
|
||||||
|
cy.get('#action-canvas').click('bottomRight')
|
||||||
|
|
||||||
|
// .click() accepts an x and y coordinate
|
||||||
|
// that controls where the click occurs :)
|
||||||
|
|
||||||
|
cy.get('#action-canvas')
|
||||||
|
.click(80, 75) // click 80px on x coord and 75px on y coord
|
||||||
|
.click(170, 75)
|
||||||
|
.click(80, 165)
|
||||||
|
.click(100, 185)
|
||||||
|
.click(125, 190)
|
||||||
|
.click(150, 185)
|
||||||
|
.click(170, 165)
|
||||||
|
|
||||||
|
// click multiple elements by passing multiple: true
|
||||||
|
cy.get('.action-labels>.label').click({ multiple: true })
|
||||||
|
|
||||||
|
// Ignore error checking prior to clicking
|
||||||
|
cy.get('.action-opacity>.btn').click({ force: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('.dblclick() - double click on a DOM element', () => {
|
||||||
|
// https://on.cypress.io/dblclick
|
||||||
|
|
||||||
|
// Our app has a listener on 'dblclick' event in our 'scripts.js'
|
||||||
|
// that hides the div and shows an input on double click
|
||||||
|
cy.get('.action-div').dblclick().should('not.be.visible')
|
||||||
|
cy.get('.action-input-hidden').should('be.visible')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('.rightclick() - right click on a DOM element', () => {
|
||||||
|
// https://on.cypress.io/rightclick
|
||||||
|
|
||||||
|
// Our app has a listener on 'contextmenu' event in our 'scripts.js'
|
||||||
|
// that hides the div and shows an input on right click
|
||||||
|
cy.get('.rightclick-action-div').rightclick().should('not.be.visible')
|
||||||
|
cy.get('.rightclick-action-input-hidden').should('be.visible')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('.check() - check a checkbox or radio element', () => {
|
||||||
|
// https://on.cypress.io/check
|
||||||
|
|
||||||
|
// By default, .check() will check all
|
||||||
|
// matching checkbox or radio elements in succession, one after another
|
||||||
|
cy.get('.action-checkboxes [type="checkbox"]').not('[disabled]')
|
||||||
|
.check().should('be.checked')
|
||||||
|
|
||||||
|
cy.get('.action-radios [type="radio"]').not('[disabled]')
|
||||||
|
.check().should('be.checked')
|
||||||
|
|
||||||
|
// .check() accepts a value argument
|
||||||
|
cy.get('.action-radios [type="radio"]')
|
||||||
|
.check('radio1').should('be.checked')
|
||||||
|
|
||||||
|
// .check() accepts an array of values
|
||||||
|
cy.get('.action-multiple-checkboxes [type="checkbox"]')
|
||||||
|
.check(['checkbox1', 'checkbox2']).should('be.checked')
|
||||||
|
|
||||||
|
// Ignore error checking prior to checking
|
||||||
|
cy.get('.action-checkboxes [disabled]')
|
||||||
|
.check({ force: true }).should('be.checked')
|
||||||
|
|
||||||
|
cy.get('.action-radios [type="radio"]')
|
||||||
|
.check('radio3', { force: true }).should('be.checked')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('.uncheck() - uncheck a checkbox element', () => {
|
||||||
|
// https://on.cypress.io/uncheck
|
||||||
|
|
||||||
|
// By default, .uncheck() will uncheck all matching
|
||||||
|
// checkbox elements in succession, one after another
|
||||||
|
cy.get('.action-check [type="checkbox"]')
|
||||||
|
.not('[disabled]')
|
||||||
|
.uncheck().should('not.be.checked')
|
||||||
|
|
||||||
|
// .uncheck() accepts a value argument
|
||||||
|
cy.get('.action-check [type="checkbox"]')
|
||||||
|
.check('checkbox1')
|
||||||
|
.uncheck('checkbox1').should('not.be.checked')
|
||||||
|
|
||||||
|
// .uncheck() accepts an array of values
|
||||||
|
cy.get('.action-check [type="checkbox"]')
|
||||||
|
.check(['checkbox1', 'checkbox3'])
|
||||||
|
.uncheck(['checkbox1', 'checkbox3']).should('not.be.checked')
|
||||||
|
|
||||||
|
// Ignore error checking prior to unchecking
|
||||||
|
cy.get('.action-check [disabled]')
|
||||||
|
.uncheck({ force: true }).should('not.be.checked')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('.select() - select an option in a <select> element', () => {
|
||||||
|
// https://on.cypress.io/select
|
||||||
|
|
||||||
|
// at first, no option should be selected
|
||||||
|
cy.get('.action-select')
|
||||||
|
.should('have.value', '--Select a fruit--')
|
||||||
|
|
||||||
|
// Select option(s) with matching text content
|
||||||
|
cy.get('.action-select').select('apples')
|
||||||
|
// confirm the apples were selected
|
||||||
|
// note that each value starts with "fr-" in our HTML
|
||||||
|
cy.get('.action-select').should('have.value', 'fr-apples')
|
||||||
|
|
||||||
|
cy.get('.action-select-multiple')
|
||||||
|
.select(['apples', 'oranges', 'bananas'])
|
||||||
|
// when getting multiple values, invoke "val" method first
|
||||||
|
.invoke('val')
|
||||||
|
.should('deep.equal', ['fr-apples', 'fr-oranges', 'fr-bananas'])
|
||||||
|
|
||||||
|
// Select option(s) with matching value
|
||||||
|
cy.get('.action-select').select('fr-bananas')
|
||||||
|
// can attach an assertion right away to the element
|
||||||
|
.should('have.value', 'fr-bananas')
|
||||||
|
|
||||||
|
cy.get('.action-select-multiple')
|
||||||
|
.select(['fr-apples', 'fr-oranges', 'fr-bananas'])
|
||||||
|
.invoke('val')
|
||||||
|
.should('deep.equal', ['fr-apples', 'fr-oranges', 'fr-bananas'])
|
||||||
|
|
||||||
|
// assert the selected values include oranges
|
||||||
|
cy.get('.action-select-multiple')
|
||||||
|
.invoke('val').should('include', 'fr-oranges')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('.scrollIntoView() - scroll an element into view', () => {
|
||||||
|
// https://on.cypress.io/scrollintoview
|
||||||
|
|
||||||
|
// normally all of these buttons are hidden,
|
||||||
|
// because they're not within
|
||||||
|
// the viewable area of their parent
|
||||||
|
// (we need to scroll to see them)
|
||||||
|
cy.get('#scroll-horizontal button')
|
||||||
|
.should('not.be.visible')
|
||||||
|
|
||||||
|
// scroll the button into view, as if the user had scrolled
|
||||||
|
cy.get('#scroll-horizontal button').scrollIntoView()
|
||||||
|
.should('be.visible')
|
||||||
|
|
||||||
|
cy.get('#scroll-vertical button')
|
||||||
|
.should('not.be.visible')
|
||||||
|
|
||||||
|
// Cypress handles the scroll direction needed
|
||||||
|
cy.get('#scroll-vertical button').scrollIntoView()
|
||||||
|
.should('be.visible')
|
||||||
|
|
||||||
|
cy.get('#scroll-both button')
|
||||||
|
.should('not.be.visible')
|
||||||
|
|
||||||
|
// Cypress knows to scroll to the right and down
|
||||||
|
cy.get('#scroll-both button').scrollIntoView()
|
||||||
|
.should('be.visible')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('.trigger() - trigger an event on a DOM element', () => {
|
||||||
|
// https://on.cypress.io/trigger
|
||||||
|
|
||||||
|
// To interact with a range input (slider)
|
||||||
|
// we need to set its value & trigger the
|
||||||
|
// event to signal it changed
|
||||||
|
|
||||||
|
// Here, we invoke jQuery's val() method to set
|
||||||
|
// the value and trigger the 'change' event
|
||||||
|
cy.get('.trigger-input-range')
|
||||||
|
.invoke('val', 25)
|
||||||
|
.trigger('change')
|
||||||
|
.get('input[type=range]').siblings('p')
|
||||||
|
.should('have.text', '25')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cy.scrollTo() - scroll the window or element to a position', () => {
|
||||||
|
// https://on.cypress.io/scrollto
|
||||||
|
|
||||||
|
// You can scroll to 9 specific positions of an element:
|
||||||
|
// -----------------------------------
|
||||||
|
// | topLeft top topRight |
|
||||||
|
// | |
|
||||||
|
// | |
|
||||||
|
// | |
|
||||||
|
// | left center right |
|
||||||
|
// | |
|
||||||
|
// | |
|
||||||
|
// | |
|
||||||
|
// | bottomLeft bottom bottomRight |
|
||||||
|
// -----------------------------------
|
||||||
|
|
||||||
|
// if you chain .scrollTo() off of cy, we will
|
||||||
|
// scroll the entire window
|
||||||
|
cy.scrollTo('bottom')
|
||||||
|
|
||||||
|
cy.get('#scrollable-horizontal').scrollTo('right')
|
||||||
|
|
||||||
|
// or you can scroll to a specific coordinate:
|
||||||
|
// (x axis, y axis) in pixels
|
||||||
|
cy.get('#scrollable-vertical').scrollTo(250, 250)
|
||||||
|
|
||||||
|
// or you can scroll to a specific percentage
|
||||||
|
// of the (width, height) of the element
|
||||||
|
cy.get('#scrollable-both').scrollTo('75%', '25%')
|
||||||
|
|
||||||
|
// control the easing of the scroll (default is 'swing')
|
||||||
|
cy.get('#scrollable-vertical').scrollTo('center', { easing: 'linear' })
|
||||||
|
|
||||||
|
// control the duration of the scroll (in ms)
|
||||||
|
cy.get('#scrollable-both').scrollTo('center', { duration: 2000 })
|
||||||
|
})
|
||||||
|
})
|
||||||
39
client/cypress/e2e/2-advanced-examples/aliasing.cy.js
Normal file
39
client/cypress/e2e/2-advanced-examples/aliasing.cy.js
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
|
context('Aliasing', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit('https://example.cypress.io/commands/aliasing')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('.as() - alias a DOM element for later use', () => {
|
||||||
|
// https://on.cypress.io/as
|
||||||
|
|
||||||
|
// Alias a DOM element for use later
|
||||||
|
// We don't have to traverse to the element
|
||||||
|
// later in our code, we reference it with @
|
||||||
|
|
||||||
|
cy.get('.as-table').find('tbody>tr')
|
||||||
|
.first().find('td').first()
|
||||||
|
.find('button').as('firstBtn')
|
||||||
|
|
||||||
|
// when we reference the alias, we place an
|
||||||
|
// @ in front of its name
|
||||||
|
cy.get('@firstBtn').click()
|
||||||
|
|
||||||
|
cy.get('@firstBtn')
|
||||||
|
.should('have.class', 'btn-success')
|
||||||
|
.and('contain', 'Changed')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('.as() - alias a route for later use', () => {
|
||||||
|
// Alias the route to wait for its response
|
||||||
|
cy.intercept('GET', '**/comments/*').as('getComment')
|
||||||
|
|
||||||
|
// we have code that gets a comment when
|
||||||
|
// the button is clicked in scripts.js
|
||||||
|
cy.get('.network-btn').click()
|
||||||
|
|
||||||
|
// https://on.cypress.io/wait
|
||||||
|
cy.wait('@getComment').its('response.statusCode').should('eq', 200)
|
||||||
|
})
|
||||||
|
})
|
||||||
177
client/cypress/e2e/2-advanced-examples/assertions.cy.js
Normal file
177
client/cypress/e2e/2-advanced-examples/assertions.cy.js
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
|
context('Assertions', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit('https://example.cypress.io/commands/assertions')
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Implicit Assertions', () => {
|
||||||
|
it('.should() - make an assertion about the current subject', () => {
|
||||||
|
// https://on.cypress.io/should
|
||||||
|
cy.get('.assertion-table')
|
||||||
|
.find('tbody tr:last')
|
||||||
|
.should('have.class', 'success')
|
||||||
|
.find('td')
|
||||||
|
.first()
|
||||||
|
// checking the text of the <td> element in various ways
|
||||||
|
.should('have.text', 'Column content')
|
||||||
|
.should('contain', 'Column content')
|
||||||
|
.should('have.html', 'Column content')
|
||||||
|
// chai-jquery uses "is()" to check if element matches selector
|
||||||
|
.should('match', 'td')
|
||||||
|
// to match text content against a regular expression
|
||||||
|
// first need to invoke jQuery method text()
|
||||||
|
// and then match using regular expression
|
||||||
|
.invoke('text')
|
||||||
|
.should('match', /column content/i)
|
||||||
|
|
||||||
|
// a better way to check element's text content against a regular expression
|
||||||
|
// is to use "cy.contains"
|
||||||
|
// https://on.cypress.io/contains
|
||||||
|
cy.get('.assertion-table')
|
||||||
|
.find('tbody tr:last')
|
||||||
|
// finds first <td> element with text content matching regular expression
|
||||||
|
.contains('td', /column content/i)
|
||||||
|
.should('be.visible')
|
||||||
|
|
||||||
|
// for more information about asserting element's text
|
||||||
|
// see https://on.cypress.io/using-cypress-faq#How-do-I-get-an-element’s-text-contents
|
||||||
|
})
|
||||||
|
|
||||||
|
it('.and() - chain multiple assertions together', () => {
|
||||||
|
// https://on.cypress.io/and
|
||||||
|
cy.get('.assertions-link')
|
||||||
|
.should('have.class', 'active')
|
||||||
|
.and('have.attr', 'href')
|
||||||
|
.and('include', 'cypress.io')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Explicit Assertions', () => {
|
||||||
|
// https://on.cypress.io/assertions
|
||||||
|
it('expect - make an assertion about a specified subject', () => {
|
||||||
|
// We can use Chai's BDD style assertions
|
||||||
|
expect(true).to.be.true
|
||||||
|
const o = { foo: 'bar' }
|
||||||
|
|
||||||
|
expect(o).to.equal(o)
|
||||||
|
expect(o).to.deep.equal({ foo: 'bar' })
|
||||||
|
// matching text using regular expression
|
||||||
|
expect('FooBar').to.match(/bar$/i)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('pass your own callback function to should()', () => {
|
||||||
|
// Pass a function to should that can have any number
|
||||||
|
// of explicit assertions within it.
|
||||||
|
// The ".should(cb)" function will be retried
|
||||||
|
// automatically until it passes all your explicit assertions or times out.
|
||||||
|
cy.get('.assertions-p')
|
||||||
|
.find('p')
|
||||||
|
.should(($p) => {
|
||||||
|
// https://on.cypress.io/$
|
||||||
|
// return an array of texts from all of the p's
|
||||||
|
// @ts-ignore TS6133 unused variable
|
||||||
|
const texts = $p.map((i, el) => Cypress.$(el).text())
|
||||||
|
|
||||||
|
// jquery map returns jquery object
|
||||||
|
// and .get() convert this to simple array
|
||||||
|
const paragraphs = texts.get()
|
||||||
|
|
||||||
|
// array should have length of 3
|
||||||
|
expect(paragraphs, 'has 3 paragraphs').to.have.length(3)
|
||||||
|
|
||||||
|
// use second argument to expect(...) to provide clear
|
||||||
|
// message with each assertion
|
||||||
|
expect(paragraphs, 'has expected text in each paragraph').to.deep.eq([
|
||||||
|
'Some text from first p',
|
||||||
|
'More text from second p',
|
||||||
|
'And even more text from third p',
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('finds element by class name regex', () => {
|
||||||
|
cy.get('.docs-header')
|
||||||
|
.find('div')
|
||||||
|
// .should(cb) callback function will be retried
|
||||||
|
.should(($div) => {
|
||||||
|
expect($div).to.have.length(1)
|
||||||
|
|
||||||
|
const className = $div[0].className
|
||||||
|
|
||||||
|
expect(className).to.match(/heading-/)
|
||||||
|
})
|
||||||
|
// .then(cb) callback is not retried,
|
||||||
|
// it either passes or fails
|
||||||
|
.then(($div) => {
|
||||||
|
expect($div, 'text content').to.have.text('Introduction')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can throw any error', () => {
|
||||||
|
cy.get('.docs-header')
|
||||||
|
.find('div')
|
||||||
|
.should(($div) => {
|
||||||
|
if ($div.length !== 1) {
|
||||||
|
// you can throw your own errors
|
||||||
|
throw new Error('Did not find 1 element')
|
||||||
|
}
|
||||||
|
|
||||||
|
const className = $div[0].className
|
||||||
|
|
||||||
|
if (!className.match(/heading-/)) {
|
||||||
|
throw new Error(`Could not find class "heading-" in ${className}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('matches unknown text between two elements', () => {
|
||||||
|
/**
|
||||||
|
* Text from the first element.
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
let text
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes passed text,
|
||||||
|
* useful before comparing text with spaces and different capitalization.
|
||||||
|
* @param {string} s Text to normalize
|
||||||
|
*/
|
||||||
|
const normalizeText = (s) => s.replace(/\s/g, '').toLowerCase()
|
||||||
|
|
||||||
|
cy.get('.two-elements')
|
||||||
|
.find('.first')
|
||||||
|
.then(($first) => {
|
||||||
|
// save text from the first element
|
||||||
|
text = normalizeText($first.text())
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.get('.two-elements')
|
||||||
|
.find('.second')
|
||||||
|
.should(($div) => {
|
||||||
|
// we can massage text before comparing
|
||||||
|
const secondText = normalizeText($div.text())
|
||||||
|
|
||||||
|
expect(secondText, 'second text').to.equal(text)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('assert - assert shape of an object', () => {
|
||||||
|
const person = {
|
||||||
|
name: 'Joe',
|
||||||
|
age: 20,
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.isObject(person, 'value is object')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('retries the should callback until assertions pass', () => {
|
||||||
|
cy.get('#random-number')
|
||||||
|
.should(($div) => {
|
||||||
|
const n = parseFloat($div.text())
|
||||||
|
|
||||||
|
expect(n).to.be.gte(1).and.be.lte(10)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
97
client/cypress/e2e/2-advanced-examples/connectors.cy.js
Normal file
97
client/cypress/e2e/2-advanced-examples/connectors.cy.js
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
|
context('Connectors', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit('https://example.cypress.io/commands/connectors')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('.each() - iterate over an array of elements', () => {
|
||||||
|
// https://on.cypress.io/each
|
||||||
|
cy.get('.connectors-each-ul>li')
|
||||||
|
.each(($el, index, $list) => {
|
||||||
|
console.log($el, index, $list)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('.its() - get properties on the current subject', () => {
|
||||||
|
// https://on.cypress.io/its
|
||||||
|
cy.get('.connectors-its-ul>li')
|
||||||
|
// calls the 'length' property yielding that value
|
||||||
|
.its('length')
|
||||||
|
.should('be.gt', 2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('.invoke() - invoke a function on the current subject', () => {
|
||||||
|
// our div is hidden in our script.js
|
||||||
|
// $('.connectors-div').hide()
|
||||||
|
|
||||||
|
// https://on.cypress.io/invoke
|
||||||
|
cy.get('.connectors-div').should('be.hidden')
|
||||||
|
// call the jquery method 'show' on the 'div.container'
|
||||||
|
.invoke('show')
|
||||||
|
.should('be.visible')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('.spread() - spread an array as individual args to callback function', () => {
|
||||||
|
// https://on.cypress.io/spread
|
||||||
|
const arr = ['foo', 'bar', 'baz']
|
||||||
|
|
||||||
|
cy.wrap(arr).spread((foo, bar, baz) => {
|
||||||
|
expect(foo).to.eq('foo')
|
||||||
|
expect(bar).to.eq('bar')
|
||||||
|
expect(baz).to.eq('baz')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('.then()', () => {
|
||||||
|
it('invokes a callback function with the current subject', () => {
|
||||||
|
// https://on.cypress.io/then
|
||||||
|
cy.get('.connectors-list > li')
|
||||||
|
.then(($lis) => {
|
||||||
|
expect($lis, '3 items').to.have.length(3)
|
||||||
|
expect($lis.eq(0), 'first item').to.contain('Walk the dog')
|
||||||
|
expect($lis.eq(1), 'second item').to.contain('Feed the cat')
|
||||||
|
expect($lis.eq(2), 'third item').to.contain('Write JavaScript')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('yields the returned value to the next command', () => {
|
||||||
|
cy.wrap(1)
|
||||||
|
.then((num) => {
|
||||||
|
expect(num).to.equal(1)
|
||||||
|
|
||||||
|
return 2
|
||||||
|
})
|
||||||
|
.then((num) => {
|
||||||
|
expect(num).to.equal(2)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('yields the original subject without return', () => {
|
||||||
|
cy.wrap(1)
|
||||||
|
.then((num) => {
|
||||||
|
expect(num).to.equal(1)
|
||||||
|
// note that nothing is returned from this callback
|
||||||
|
})
|
||||||
|
.then((num) => {
|
||||||
|
// this callback receives the original unchanged value 1
|
||||||
|
expect(num).to.equal(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('yields the value yielded by the last Cypress command inside', () => {
|
||||||
|
cy.wrap(1)
|
||||||
|
.then((num) => {
|
||||||
|
expect(num).to.equal(1)
|
||||||
|
// note how we run a Cypress command
|
||||||
|
// the result yielded by this Cypress command
|
||||||
|
// will be passed to the second ".then"
|
||||||
|
cy.wrap(2)
|
||||||
|
})
|
||||||
|
.then((num) => {
|
||||||
|
// this callback receives the value yielded by "cy.wrap(2)"
|
||||||
|
expect(num).to.equal(2)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
77
client/cypress/e2e/2-advanced-examples/cookies.cy.js
Normal file
77
client/cypress/e2e/2-advanced-examples/cookies.cy.js
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
|
context('Cookies', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
Cypress.Cookies.debug(true)
|
||||||
|
|
||||||
|
cy.visit('https://example.cypress.io/commands/cookies')
|
||||||
|
|
||||||
|
// clear cookies again after visiting to remove
|
||||||
|
// any 3rd party cookies picked up such as cloudflare
|
||||||
|
cy.clearCookies()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cy.getCookie() - get a browser cookie', () => {
|
||||||
|
// https://on.cypress.io/getcookie
|
||||||
|
cy.get('#getCookie .set-a-cookie').click()
|
||||||
|
|
||||||
|
// cy.getCookie() yields a cookie object
|
||||||
|
cy.getCookie('token').should('have.property', 'value', '123ABC')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cy.getCookies() - get browser cookies', () => {
|
||||||
|
// https://on.cypress.io/getcookies
|
||||||
|
cy.getCookies().should('be.empty')
|
||||||
|
|
||||||
|
cy.get('#getCookies .set-a-cookie').click()
|
||||||
|
|
||||||
|
// cy.getCookies() yields an array of cookies
|
||||||
|
cy.getCookies().should('have.length', 1).should((cookies) => {
|
||||||
|
// each cookie has these properties
|
||||||
|
expect(cookies[0]).to.have.property('name', 'token')
|
||||||
|
expect(cookies[0]).to.have.property('value', '123ABC')
|
||||||
|
expect(cookies[0]).to.have.property('httpOnly', false)
|
||||||
|
expect(cookies[0]).to.have.property('secure', false)
|
||||||
|
expect(cookies[0]).to.have.property('domain')
|
||||||
|
expect(cookies[0]).to.have.property('path')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cy.setCookie() - set a browser cookie', () => {
|
||||||
|
// https://on.cypress.io/setcookie
|
||||||
|
cy.getCookies().should('be.empty')
|
||||||
|
|
||||||
|
cy.setCookie('foo', 'bar')
|
||||||
|
|
||||||
|
// cy.getCookie() yields a cookie object
|
||||||
|
cy.getCookie('foo').should('have.property', 'value', 'bar')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cy.clearCookie() - clear a browser cookie', () => {
|
||||||
|
// https://on.cypress.io/clearcookie
|
||||||
|
cy.getCookie('token').should('be.null')
|
||||||
|
|
||||||
|
cy.get('#clearCookie .set-a-cookie').click()
|
||||||
|
|
||||||
|
cy.getCookie('token').should('have.property', 'value', '123ABC')
|
||||||
|
|
||||||
|
// cy.clearCookies() yields null
|
||||||
|
cy.clearCookie('token').should('be.null')
|
||||||
|
|
||||||
|
cy.getCookie('token').should('be.null')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cy.clearCookies() - clear browser cookies', () => {
|
||||||
|
// https://on.cypress.io/clearcookies
|
||||||
|
cy.getCookies().should('be.empty')
|
||||||
|
|
||||||
|
cy.get('#clearCookies .set-a-cookie').click()
|
||||||
|
|
||||||
|
cy.getCookies().should('have.length', 1)
|
||||||
|
|
||||||
|
// cy.clearCookies() yields null
|
||||||
|
cy.clearCookies()
|
||||||
|
|
||||||
|
cy.getCookies().should('be.empty')
|
||||||
|
})
|
||||||
|
})
|
||||||
202
client/cypress/e2e/2-advanced-examples/cypress_api.cy.js
Normal file
202
client/cypress/e2e/2-advanced-examples/cypress_api.cy.js
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
|
context('Cypress.Commands', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit('https://example.cypress.io/cypress-api')
|
||||||
|
})
|
||||||
|
|
||||||
|
// https://on.cypress.io/custom-commands
|
||||||
|
|
||||||
|
it('.add() - create a custom command', () => {
|
||||||
|
Cypress.Commands.add('console', {
|
||||||
|
prevSubject: true,
|
||||||
|
}, (subject, method) => {
|
||||||
|
// the previous subject is automatically received
|
||||||
|
// and the commands arguments are shifted
|
||||||
|
|
||||||
|
// allow us to change the console method used
|
||||||
|
method = method || 'log'
|
||||||
|
|
||||||
|
// log the subject to the console
|
||||||
|
// @ts-ignore TS7017
|
||||||
|
console[method]('The subject is', subject)
|
||||||
|
|
||||||
|
// whatever we return becomes the new subject
|
||||||
|
// we don't want to change the subject so
|
||||||
|
// we return whatever was passed in
|
||||||
|
return subject
|
||||||
|
})
|
||||||
|
|
||||||
|
// @ts-ignore TS2339
|
||||||
|
cy.get('button').console('info').then(($button) => {
|
||||||
|
// subject is still $button
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
context('Cypress.Cookies', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit('https://example.cypress.io/cypress-api')
|
||||||
|
})
|
||||||
|
|
||||||
|
// https://on.cypress.io/cookies
|
||||||
|
it('.debug() - enable or disable debugging', () => {
|
||||||
|
Cypress.Cookies.debug(true)
|
||||||
|
|
||||||
|
// Cypress will now log in the console when
|
||||||
|
// cookies are set or cleared
|
||||||
|
cy.setCookie('fakeCookie', '123ABC')
|
||||||
|
cy.clearCookie('fakeCookie')
|
||||||
|
cy.setCookie('fakeCookie', '123ABC')
|
||||||
|
cy.clearCookie('fakeCookie')
|
||||||
|
cy.setCookie('fakeCookie', '123ABC')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('.preserveOnce() - preserve cookies by key', () => {
|
||||||
|
// normally cookies are reset after each test
|
||||||
|
cy.getCookie('fakeCookie').should('not.be.ok')
|
||||||
|
|
||||||
|
// preserving a cookie will not clear it when
|
||||||
|
// the next test starts
|
||||||
|
cy.setCookie('lastCookie', '789XYZ')
|
||||||
|
Cypress.Cookies.preserveOnce('lastCookie')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('.defaults() - set defaults for all cookies', () => {
|
||||||
|
// now any cookie with the name 'session_id' will
|
||||||
|
// not be cleared before each new test runs
|
||||||
|
Cypress.Cookies.defaults({
|
||||||
|
preserve: 'session_id',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
context('Cypress.arch', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit('https://example.cypress.io/cypress-api')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Get CPU architecture name of underlying OS', () => {
|
||||||
|
// https://on.cypress.io/arch
|
||||||
|
expect(Cypress.arch).to.exist
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
context('Cypress.config()', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit('https://example.cypress.io/cypress-api')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Get and set configuration options', () => {
|
||||||
|
// https://on.cypress.io/config
|
||||||
|
let myConfig = Cypress.config()
|
||||||
|
|
||||||
|
expect(myConfig).to.have.property('animationDistanceThreshold', 5)
|
||||||
|
expect(myConfig).to.have.property('baseUrl', null)
|
||||||
|
expect(myConfig).to.have.property('defaultCommandTimeout', 4000)
|
||||||
|
expect(myConfig).to.have.property('requestTimeout', 5000)
|
||||||
|
expect(myConfig).to.have.property('responseTimeout', 30000)
|
||||||
|
expect(myConfig).to.have.property('viewportHeight', 660)
|
||||||
|
expect(myConfig).to.have.property('viewportWidth', 1000)
|
||||||
|
expect(myConfig).to.have.property('pageLoadTimeout', 60000)
|
||||||
|
expect(myConfig).to.have.property('waitForAnimations', true)
|
||||||
|
|
||||||
|
expect(Cypress.config('pageLoadTimeout')).to.eq(60000)
|
||||||
|
|
||||||
|
// this will change the config for the rest of your tests!
|
||||||
|
Cypress.config('pageLoadTimeout', 20000)
|
||||||
|
|
||||||
|
expect(Cypress.config('pageLoadTimeout')).to.eq(20000)
|
||||||
|
|
||||||
|
Cypress.config('pageLoadTimeout', 60000)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
context('Cypress.dom', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit('https://example.cypress.io/cypress-api')
|
||||||
|
})
|
||||||
|
|
||||||
|
// https://on.cypress.io/dom
|
||||||
|
it('.isHidden() - determine if a DOM element is hidden', () => {
|
||||||
|
let hiddenP = Cypress.$('.dom-p p.hidden').get(0)
|
||||||
|
let visibleP = Cypress.$('.dom-p p.visible').get(0)
|
||||||
|
|
||||||
|
// our first paragraph has css class 'hidden'
|
||||||
|
expect(Cypress.dom.isHidden(hiddenP)).to.be.true
|
||||||
|
expect(Cypress.dom.isHidden(visibleP)).to.be.false
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
context('Cypress.env()', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit('https://example.cypress.io/cypress-api')
|
||||||
|
})
|
||||||
|
|
||||||
|
// We can set environment variables for highly dynamic values
|
||||||
|
|
||||||
|
// https://on.cypress.io/environment-variables
|
||||||
|
it('Get environment variables', () => {
|
||||||
|
// https://on.cypress.io/env
|
||||||
|
// set multiple environment variables
|
||||||
|
Cypress.env({
|
||||||
|
host: 'veronica.dev.local',
|
||||||
|
api_server: 'http://localhost:8888/v1/',
|
||||||
|
})
|
||||||
|
|
||||||
|
// get environment variable
|
||||||
|
expect(Cypress.env('host')).to.eq('veronica.dev.local')
|
||||||
|
|
||||||
|
// set environment variable
|
||||||
|
Cypress.env('api_server', 'http://localhost:8888/v2/')
|
||||||
|
expect(Cypress.env('api_server')).to.eq('http://localhost:8888/v2/')
|
||||||
|
|
||||||
|
// get all environment variable
|
||||||
|
expect(Cypress.env()).to.have.property('host', 'veronica.dev.local')
|
||||||
|
expect(Cypress.env()).to.have.property('api_server', 'http://localhost:8888/v2/')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
context('Cypress.log', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit('https://example.cypress.io/cypress-api')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Control what is printed to the Command Log', () => {
|
||||||
|
// https://on.cypress.io/cypress-log
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
context('Cypress.platform', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit('https://example.cypress.io/cypress-api')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Get underlying OS name', () => {
|
||||||
|
// https://on.cypress.io/platform
|
||||||
|
expect(Cypress.platform).to.be.exist
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
context('Cypress.version', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit('https://example.cypress.io/cypress-api')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Get current version of Cypress being run', () => {
|
||||||
|
// https://on.cypress.io/version
|
||||||
|
expect(Cypress.version).to.be.exist
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
context('Cypress.spec', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit('https://example.cypress.io/cypress-api')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Get current spec information', () => {
|
||||||
|
// https://on.cypress.io/spec
|
||||||
|
// wrap the object so we can inspect it easily by clicking in the command log
|
||||||
|
cy.wrap(Cypress.spec).should('include.keys', ['name', 'relative', 'absolute'])
|
||||||
|
})
|
||||||
|
})
|
||||||
88
client/cypress/e2e/2-advanced-examples/files.cy.js
Normal file
88
client/cypress/e2e/2-advanced-examples/files.cy.js
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
|
/// JSON fixture file can be loaded directly using
|
||||||
|
// the built-in JavaScript bundler
|
||||||
|
// @ts-ignore
|
||||||
|
const requiredExample = require('../../fixtures/example')
|
||||||
|
|
||||||
|
context('Files', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit('https://example.cypress.io/commands/files')
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// load example.json fixture file and store
|
||||||
|
// in the test context object
|
||||||
|
cy.fixture('example.json').as('example')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cy.fixture() - load a fixture', () => {
|
||||||
|
// https://on.cypress.io/fixture
|
||||||
|
|
||||||
|
// Instead of writing a response inline you can
|
||||||
|
// use a fixture file's content.
|
||||||
|
|
||||||
|
// when application makes an Ajax request matching "GET **/comments/*"
|
||||||
|
// Cypress will intercept it and reply with the object in `example.json` fixture
|
||||||
|
cy.intercept('GET', '**/comments/*', { fixture: 'example.json' }).as('getComment')
|
||||||
|
|
||||||
|
// we have code that gets a comment when
|
||||||
|
// the button is clicked in scripts.js
|
||||||
|
cy.get('.fixture-btn').click()
|
||||||
|
|
||||||
|
cy.wait('@getComment').its('response.body')
|
||||||
|
.should('have.property', 'name')
|
||||||
|
.and('include', 'Using fixtures to represent data')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cy.fixture() or require - load a fixture', function () {
|
||||||
|
// we are inside the "function () { ... }"
|
||||||
|
// callback and can use test context object "this"
|
||||||
|
// "this.example" was loaded in "beforeEach" function callback
|
||||||
|
expect(this.example, 'fixture in the test context')
|
||||||
|
.to.deep.equal(requiredExample)
|
||||||
|
|
||||||
|
// or use "cy.wrap" and "should('deep.equal', ...)" assertion
|
||||||
|
cy.wrap(this.example)
|
||||||
|
.should('deep.equal', requiredExample)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cy.readFile() - read file contents', () => {
|
||||||
|
// https://on.cypress.io/readfile
|
||||||
|
|
||||||
|
// You can read a file and yield its contents
|
||||||
|
// The filePath is relative to your project's root.
|
||||||
|
cy.readFile('cypress.json').then((json) => {
|
||||||
|
expect(json).to.be.an('object')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cy.writeFile() - write to a file', () => {
|
||||||
|
// https://on.cypress.io/writefile
|
||||||
|
|
||||||
|
// You can write to a file
|
||||||
|
|
||||||
|
// Use a response from a request to automatically
|
||||||
|
// generate a fixture file for use later
|
||||||
|
cy.request('https://jsonplaceholder.cypress.io/users')
|
||||||
|
.then((response) => {
|
||||||
|
cy.writeFile('cypress/fixtures/users.json', response.body)
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.fixture('users').should((users) => {
|
||||||
|
expect(users[0].name).to.exist
|
||||||
|
})
|
||||||
|
|
||||||
|
// JavaScript arrays and objects are stringified
|
||||||
|
// and formatted into text.
|
||||||
|
cy.writeFile('cypress/fixtures/profile.json', {
|
||||||
|
id: 8739,
|
||||||
|
name: 'Jane',
|
||||||
|
email: 'jane@example.com',
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.fixture('profile').should((profile) => {
|
||||||
|
expect(profile.name).to.eq('Jane')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
52
client/cypress/e2e/2-advanced-examples/local_storage.cy.js
Normal file
52
client/cypress/e2e/2-advanced-examples/local_storage.cy.js
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
|
context('Local Storage', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit('https://example.cypress.io/commands/local-storage')
|
||||||
|
})
|
||||||
|
// Although local storage is automatically cleared
|
||||||
|
// in between tests to maintain a clean state
|
||||||
|
// sometimes we need to clear the local storage manually
|
||||||
|
|
||||||
|
it('cy.clearLocalStorage() - clear all data in local storage', () => {
|
||||||
|
// https://on.cypress.io/clearlocalstorage
|
||||||
|
cy.get('.ls-btn').click().should(() => {
|
||||||
|
expect(localStorage.getItem('prop1')).to.eq('red')
|
||||||
|
expect(localStorage.getItem('prop2')).to.eq('blue')
|
||||||
|
expect(localStorage.getItem('prop3')).to.eq('magenta')
|
||||||
|
})
|
||||||
|
|
||||||
|
// clearLocalStorage() yields the localStorage object
|
||||||
|
cy.clearLocalStorage().should((ls) => {
|
||||||
|
expect(ls.getItem('prop1')).to.be.null
|
||||||
|
expect(ls.getItem('prop2')).to.be.null
|
||||||
|
expect(ls.getItem('prop3')).to.be.null
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.get('.ls-btn').click().should(() => {
|
||||||
|
expect(localStorage.getItem('prop1')).to.eq('red')
|
||||||
|
expect(localStorage.getItem('prop2')).to.eq('blue')
|
||||||
|
expect(localStorage.getItem('prop3')).to.eq('magenta')
|
||||||
|
})
|
||||||
|
|
||||||
|
// Clear key matching string in Local Storage
|
||||||
|
cy.clearLocalStorage('prop1').should((ls) => {
|
||||||
|
expect(ls.getItem('prop1')).to.be.null
|
||||||
|
expect(ls.getItem('prop2')).to.eq('blue')
|
||||||
|
expect(ls.getItem('prop3')).to.eq('magenta')
|
||||||
|
})
|
||||||
|
|
||||||
|
cy.get('.ls-btn').click().should(() => {
|
||||||
|
expect(localStorage.getItem('prop1')).to.eq('red')
|
||||||
|
expect(localStorage.getItem('prop2')).to.eq('blue')
|
||||||
|
expect(localStorage.getItem('prop3')).to.eq('magenta')
|
||||||
|
})
|
||||||
|
|
||||||
|
// Clear keys matching regex in Local Storage
|
||||||
|
cy.clearLocalStorage(/prop1|2/).should((ls) => {
|
||||||
|
expect(ls.getItem('prop1')).to.be.null
|
||||||
|
expect(ls.getItem('prop2')).to.be.null
|
||||||
|
expect(ls.getItem('prop3')).to.eq('magenta')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
32
client/cypress/e2e/2-advanced-examples/location.cy.js
Normal file
32
client/cypress/e2e/2-advanced-examples/location.cy.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
|
context('Location', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit('https://example.cypress.io/commands/location')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cy.hash() - get the current URL hash', () => {
|
||||||
|
// https://on.cypress.io/hash
|
||||||
|
cy.hash().should('be.empty')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cy.location() - get window.location', () => {
|
||||||
|
// https://on.cypress.io/location
|
||||||
|
cy.location().should((location) => {
|
||||||
|
expect(location.hash).to.be.empty
|
||||||
|
expect(location.href).to.eq('https://example.cypress.io/commands/location')
|
||||||
|
expect(location.host).to.eq('example.cypress.io')
|
||||||
|
expect(location.hostname).to.eq('example.cypress.io')
|
||||||
|
expect(location.origin).to.eq('https://example.cypress.io')
|
||||||
|
expect(location.pathname).to.eq('/commands/location')
|
||||||
|
expect(location.port).to.eq('')
|
||||||
|
expect(location.protocol).to.eq('https:')
|
||||||
|
expect(location.search).to.be.empty
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cy.url() - get the current URL', () => {
|
||||||
|
// https://on.cypress.io/url
|
||||||
|
cy.url().should('eq', 'https://example.cypress.io/commands/location')
|
||||||
|
})
|
||||||
|
})
|
||||||
104
client/cypress/e2e/2-advanced-examples/misc.cy.js
Normal file
104
client/cypress/e2e/2-advanced-examples/misc.cy.js
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
|
context('Misc', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit('https://example.cypress.io/commands/misc')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('.end() - end the command chain', () => {
|
||||||
|
// https://on.cypress.io/end
|
||||||
|
|
||||||
|
// cy.end is useful when you want to end a chain of commands
|
||||||
|
// and force Cypress to re-query from the root element
|
||||||
|
cy.get('.misc-table').within(() => {
|
||||||
|
// ends the current chain and yields null
|
||||||
|
cy.contains('Cheryl').click().end()
|
||||||
|
|
||||||
|
// queries the entire table again
|
||||||
|
cy.contains('Charles').click()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cy.exec() - execute a system command', () => {
|
||||||
|
// execute a system command.
|
||||||
|
// so you can take actions necessary for
|
||||||
|
// your test outside the scope of Cypress.
|
||||||
|
// https://on.cypress.io/exec
|
||||||
|
|
||||||
|
// we can use Cypress.platform string to
|
||||||
|
// select appropriate command
|
||||||
|
// https://on.cypress/io/platform
|
||||||
|
cy.log(`Platform ${Cypress.platform} architecture ${Cypress.arch}`)
|
||||||
|
|
||||||
|
// on CircleCI Windows build machines we have a failure to run bash shell
|
||||||
|
// https://github.com/cypress-io/cypress/issues/5169
|
||||||
|
// so skip some of the tests by passing flag "--env circle=true"
|
||||||
|
const isCircleOnWindows = Cypress.platform === 'win32' && Cypress.env('circle')
|
||||||
|
|
||||||
|
if (isCircleOnWindows) {
|
||||||
|
cy.log('Skipping test on CircleCI')
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// cy.exec problem on Shippable CI
|
||||||
|
// https://github.com/cypress-io/cypress/issues/6718
|
||||||
|
const isShippable = Cypress.platform === 'linux' && Cypress.env('shippable')
|
||||||
|
|
||||||
|
if (isShippable) {
|
||||||
|
cy.log('Skipping test on ShippableCI')
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cy.exec('echo Jane Lane')
|
||||||
|
.its('stdout').should('contain', 'Jane Lane')
|
||||||
|
|
||||||
|
if (Cypress.platform === 'win32') {
|
||||||
|
cy.exec('print cypress.json')
|
||||||
|
.its('stderr').should('be.empty')
|
||||||
|
} else {
|
||||||
|
cy.exec('cat cypress.json')
|
||||||
|
.its('stderr').should('be.empty')
|
||||||
|
|
||||||
|
cy.exec('pwd')
|
||||||
|
.its('code').should('eq', 0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cy.focused() - get the DOM element that has focus', () => {
|
||||||
|
// https://on.cypress.io/focused
|
||||||
|
cy.get('.misc-form').find('#name').click()
|
||||||
|
cy.focused().should('have.id', 'name')
|
||||||
|
|
||||||
|
cy.get('.misc-form').find('#description').click()
|
||||||
|
cy.focused().should('have.id', 'description')
|
||||||
|
})
|
||||||
|
|
||||||
|
context('Cypress.Screenshot', function () {
|
||||||
|
it('cy.screenshot() - take a screenshot', () => {
|
||||||
|
// https://on.cypress.io/screenshot
|
||||||
|
cy.screenshot('my-image')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Cypress.Screenshot.defaults() - change default config of screenshots', function () {
|
||||||
|
Cypress.Screenshot.defaults({
|
||||||
|
blackout: ['.foo'],
|
||||||
|
capture: 'viewport',
|
||||||
|
clip: { x: 0, y: 0, width: 200, height: 200 },
|
||||||
|
scale: false,
|
||||||
|
disableTimersAndAnimations: true,
|
||||||
|
screenshotOnRunFailure: true,
|
||||||
|
onBeforeScreenshot () { },
|
||||||
|
onAfterScreenshot () { },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cy.wrap() - wrap an object', () => {
|
||||||
|
// https://on.cypress.io/wrap
|
||||||
|
cy.wrap({ foo: 'bar' })
|
||||||
|
.should('have.property', 'foo')
|
||||||
|
.and('include', 'bar')
|
||||||
|
})
|
||||||
|
})
|
||||||
56
client/cypress/e2e/2-advanced-examples/navigation.cy.js
Normal file
56
client/cypress/e2e/2-advanced-examples/navigation.cy.js
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
|
context('Navigation', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit('https://example.cypress.io')
|
||||||
|
cy.get('.navbar-nav').contains('Commands').click()
|
||||||
|
cy.get('.dropdown-menu').contains('Navigation').click()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cy.go() - go back or forward in the browser\'s history', () => {
|
||||||
|
// https://on.cypress.io/go
|
||||||
|
|
||||||
|
cy.location('pathname').should('include', 'navigation')
|
||||||
|
|
||||||
|
cy.go('back')
|
||||||
|
cy.location('pathname').should('not.include', 'navigation')
|
||||||
|
|
||||||
|
cy.go('forward')
|
||||||
|
cy.location('pathname').should('include', 'navigation')
|
||||||
|
|
||||||
|
// clicking back
|
||||||
|
cy.go(-1)
|
||||||
|
cy.location('pathname').should('not.include', 'navigation')
|
||||||
|
|
||||||
|
// clicking forward
|
||||||
|
cy.go(1)
|
||||||
|
cy.location('pathname').should('include', 'navigation')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cy.reload() - reload the page', () => {
|
||||||
|
// https://on.cypress.io/reload
|
||||||
|
cy.reload()
|
||||||
|
|
||||||
|
// reload the page without using the cache
|
||||||
|
cy.reload(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cy.visit() - visit a remote url', () => {
|
||||||
|
// https://on.cypress.io/visit
|
||||||
|
|
||||||
|
// Visit any sub-domain of your current domain
|
||||||
|
|
||||||
|
// Pass options to the visit
|
||||||
|
cy.visit('https://example.cypress.io/commands/navigation', {
|
||||||
|
timeout: 50000, // increase total time for the visit to resolve
|
||||||
|
onBeforeLoad (contentWindow) {
|
||||||
|
// contentWindow is the remote page's window object
|
||||||
|
expect(typeof contentWindow === 'object').to.be.true
|
||||||
|
},
|
||||||
|
onLoad (contentWindow) {
|
||||||
|
// contentWindow is the remote page's window object
|
||||||
|
expect(typeof contentWindow === 'object').to.be.true
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
163
client/cypress/e2e/2-advanced-examples/network_requests.cy.js
Normal file
163
client/cypress/e2e/2-advanced-examples/network_requests.cy.js
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
|
context('Network Requests', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit('https://example.cypress.io/commands/network-requests')
|
||||||
|
})
|
||||||
|
|
||||||
|
// Manage HTTP requests in your app
|
||||||
|
|
||||||
|
it('cy.request() - make an XHR request', () => {
|
||||||
|
// https://on.cypress.io/request
|
||||||
|
cy.request('https://jsonplaceholder.cypress.io/comments')
|
||||||
|
.should((response) => {
|
||||||
|
expect(response.status).to.eq(200)
|
||||||
|
// the server sometimes gets an extra comment posted from another machine
|
||||||
|
// which gets returned as 1 extra object
|
||||||
|
expect(response.body).to.have.property('length').and.be.oneOf([500, 501])
|
||||||
|
expect(response).to.have.property('headers')
|
||||||
|
expect(response).to.have.property('duration')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cy.request() - verify response using BDD syntax', () => {
|
||||||
|
cy.request('https://jsonplaceholder.cypress.io/comments')
|
||||||
|
.then((response) => {
|
||||||
|
// https://on.cypress.io/assertions
|
||||||
|
expect(response).property('status').to.equal(200)
|
||||||
|
expect(response).property('body').to.have.property('length').and.be.oneOf([500, 501])
|
||||||
|
expect(response).to.include.keys('headers', 'duration')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cy.request() with query parameters', () => {
|
||||||
|
// will execute request
|
||||||
|
// https://jsonplaceholder.cypress.io/comments?postId=1&id=3
|
||||||
|
cy.request({
|
||||||
|
url: 'https://jsonplaceholder.cypress.io/comments',
|
||||||
|
qs: {
|
||||||
|
postId: 1,
|
||||||
|
id: 3,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.its('body')
|
||||||
|
.should('be.an', 'array')
|
||||||
|
.and('have.length', 1)
|
||||||
|
.its('0') // yields first element of the array
|
||||||
|
.should('contain', {
|
||||||
|
postId: 1,
|
||||||
|
id: 3,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cy.request() - pass result to the second request', () => {
|
||||||
|
// first, let's find out the userId of the first user we have
|
||||||
|
cy.request('https://jsonplaceholder.cypress.io/users?_limit=1')
|
||||||
|
.its('body') // yields the response object
|
||||||
|
.its('0') // yields the first element of the returned list
|
||||||
|
// the above two commands its('body').its('0')
|
||||||
|
// can be written as its('body.0')
|
||||||
|
// if you do not care about TypeScript checks
|
||||||
|
.then((user) => {
|
||||||
|
expect(user).property('id').to.be.a('number')
|
||||||
|
// make a new post on behalf of the user
|
||||||
|
cy.request('POST', 'https://jsonplaceholder.cypress.io/posts', {
|
||||||
|
userId: user.id,
|
||||||
|
title: 'Cypress Test Runner',
|
||||||
|
body: 'Fast, easy and reliable testing for anything that runs in a browser.',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
// note that the value here is the returned value of the 2nd request
|
||||||
|
// which is the new post object
|
||||||
|
.then((response) => {
|
||||||
|
expect(response).property('status').to.equal(201) // new entity created
|
||||||
|
expect(response).property('body').to.contain({
|
||||||
|
title: 'Cypress Test Runner',
|
||||||
|
})
|
||||||
|
|
||||||
|
// we don't know the exact post id - only that it will be > 100
|
||||||
|
// since JSONPlaceholder has built-in 100 posts
|
||||||
|
expect(response.body).property('id').to.be.a('number')
|
||||||
|
.and.to.be.gt(100)
|
||||||
|
|
||||||
|
// we don't know the user id here - since it was in above closure
|
||||||
|
// so in this test just confirm that the property is there
|
||||||
|
expect(response.body).property('userId').to.be.a('number')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cy.request() - save response in the shared test context', () => {
|
||||||
|
// https://on.cypress.io/variables-and-aliases
|
||||||
|
cy.request('https://jsonplaceholder.cypress.io/users?_limit=1')
|
||||||
|
.its('body').its('0') // yields the first element of the returned list
|
||||||
|
.as('user') // saves the object in the test context
|
||||||
|
.then(function () {
|
||||||
|
// NOTE 👀
|
||||||
|
// By the time this callback runs the "as('user')" command
|
||||||
|
// has saved the user object in the test context.
|
||||||
|
// To access the test context we need to use
|
||||||
|
// the "function () { ... }" callback form,
|
||||||
|
// otherwise "this" points at a wrong or undefined object!
|
||||||
|
cy.request('POST', 'https://jsonplaceholder.cypress.io/posts', {
|
||||||
|
userId: this.user.id,
|
||||||
|
title: 'Cypress Test Runner',
|
||||||
|
body: 'Fast, easy and reliable testing for anything that runs in a browser.',
|
||||||
|
})
|
||||||
|
.its('body').as('post') // save the new post from the response
|
||||||
|
})
|
||||||
|
.then(function () {
|
||||||
|
// When this callback runs, both "cy.request" API commands have finished
|
||||||
|
// and the test context has "user" and "post" objects set.
|
||||||
|
// Let's verify them.
|
||||||
|
expect(this.post, 'post has the right user id').property('userId').to.equal(this.user.id)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cy.intercept() - route responses to matching requests', () => {
|
||||||
|
// https://on.cypress.io/intercept
|
||||||
|
|
||||||
|
let message = 'whoa, this comment does not exist'
|
||||||
|
|
||||||
|
// Listen to GET to comments/1
|
||||||
|
cy.intercept('GET', '**/comments/*').as('getComment')
|
||||||
|
|
||||||
|
// we have code that gets a comment when
|
||||||
|
// the button is clicked in scripts.js
|
||||||
|
cy.get('.network-btn').click()
|
||||||
|
|
||||||
|
// https://on.cypress.io/wait
|
||||||
|
cy.wait('@getComment').its('response.statusCode').should('be.oneOf', [200, 304])
|
||||||
|
|
||||||
|
// Listen to POST to comments
|
||||||
|
cy.intercept('POST', '**/comments').as('postComment')
|
||||||
|
|
||||||
|
// we have code that posts a comment when
|
||||||
|
// the button is clicked in scripts.js
|
||||||
|
cy.get('.network-post').click()
|
||||||
|
cy.wait('@postComment').should(({ request, response }) => {
|
||||||
|
expect(request.body).to.include('email')
|
||||||
|
expect(request.headers).to.have.property('content-type')
|
||||||
|
expect(response && response.body).to.have.property('name', 'Using POST in cy.intercept()')
|
||||||
|
})
|
||||||
|
|
||||||
|
// Stub a response to PUT comments/ ****
|
||||||
|
cy.intercept({
|
||||||
|
method: 'PUT',
|
||||||
|
url: '**/comments/*',
|
||||||
|
}, {
|
||||||
|
statusCode: 404,
|
||||||
|
body: { error: message },
|
||||||
|
headers: { 'access-control-allow-origin': '*' },
|
||||||
|
delayMs: 500,
|
||||||
|
}).as('putComment')
|
||||||
|
|
||||||
|
// we have code that puts a comment when
|
||||||
|
// the button is clicked in scripts.js
|
||||||
|
cy.get('.network-put').click()
|
||||||
|
|
||||||
|
cy.wait('@putComment')
|
||||||
|
|
||||||
|
// our 404 statusCode logic in scripts.js executed
|
||||||
|
cy.get('.network-put-comment').should('contain', message)
|
||||||
|
})
|
||||||
|
})
|
||||||
114
client/cypress/e2e/2-advanced-examples/querying.cy.js
Normal file
114
client/cypress/e2e/2-advanced-examples/querying.cy.js
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
|
context('Querying', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit('https://example.cypress.io/commands/querying')
|
||||||
|
})
|
||||||
|
|
||||||
|
// The most commonly used query is 'cy.get()', you can
|
||||||
|
// think of this like the '$' in jQuery
|
||||||
|
|
||||||
|
it('cy.get() - query DOM elements', () => {
|
||||||
|
// https://on.cypress.io/get
|
||||||
|
|
||||||
|
cy.get('#query-btn').should('contain', 'Button')
|
||||||
|
|
||||||
|
cy.get('.query-btn').should('contain', 'Button')
|
||||||
|
|
||||||
|
cy.get('#querying .well>button:first').should('contain', 'Button')
|
||||||
|
// ↲
|
||||||
|
// Use CSS selectors just like jQuery
|
||||||
|
|
||||||
|
cy.get('[data-test-id="test-example"]').should('have.class', 'example')
|
||||||
|
|
||||||
|
// 'cy.get()' yields jQuery object, you can get its attribute
|
||||||
|
// by invoking `.attr()` method
|
||||||
|
cy.get('[data-test-id="test-example"]')
|
||||||
|
.invoke('attr', 'data-test-id')
|
||||||
|
.should('equal', 'test-example')
|
||||||
|
|
||||||
|
// or you can get element's CSS property
|
||||||
|
cy.get('[data-test-id="test-example"]')
|
||||||
|
.invoke('css', 'position')
|
||||||
|
.should('equal', 'static')
|
||||||
|
|
||||||
|
// or use assertions directly during 'cy.get()'
|
||||||
|
// https://on.cypress.io/assertions
|
||||||
|
cy.get('[data-test-id="test-example"]')
|
||||||
|
.should('have.attr', 'data-test-id', 'test-example')
|
||||||
|
.and('have.css', 'position', 'static')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cy.contains() - query DOM elements with matching content', () => {
|
||||||
|
// https://on.cypress.io/contains
|
||||||
|
cy.get('.query-list')
|
||||||
|
.contains('bananas')
|
||||||
|
.should('have.class', 'third')
|
||||||
|
|
||||||
|
// we can pass a regexp to `.contains()`
|
||||||
|
cy.get('.query-list')
|
||||||
|
.contains(/^b\w+/)
|
||||||
|
.should('have.class', 'third')
|
||||||
|
|
||||||
|
cy.get('.query-list')
|
||||||
|
.contains('apples')
|
||||||
|
.should('have.class', 'first')
|
||||||
|
|
||||||
|
// passing a selector to contains will
|
||||||
|
// yield the selector containing the text
|
||||||
|
cy.get('#querying')
|
||||||
|
.contains('ul', 'oranges')
|
||||||
|
.should('have.class', 'query-list')
|
||||||
|
|
||||||
|
cy.get('.query-button')
|
||||||
|
.contains('Save Form')
|
||||||
|
.should('have.class', 'btn')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('.within() - query DOM elements within a specific element', () => {
|
||||||
|
// https://on.cypress.io/within
|
||||||
|
cy.get('.query-form').within(() => {
|
||||||
|
cy.get('input:first').should('have.attr', 'placeholder', 'Email')
|
||||||
|
cy.get('input:last').should('have.attr', 'placeholder', 'Password')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cy.root() - query the root DOM element', () => {
|
||||||
|
// https://on.cypress.io/root
|
||||||
|
|
||||||
|
// By default, root is the document
|
||||||
|
cy.root().should('match', 'html')
|
||||||
|
|
||||||
|
cy.get('.query-ul').within(() => {
|
||||||
|
// In this within, the root is now the ul DOM element
|
||||||
|
cy.root().should('have.class', 'query-ul')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('best practices - selecting elements', () => {
|
||||||
|
// https://on.cypress.io/best-practices#Selecting-Elements
|
||||||
|
cy.get('[data-cy=best-practices-selecting-elements]').within(() => {
|
||||||
|
// Worst - too generic, no context
|
||||||
|
cy.get('button').click()
|
||||||
|
|
||||||
|
// Bad. Coupled to styling. Highly subject to change.
|
||||||
|
cy.get('.btn.btn-large').click()
|
||||||
|
|
||||||
|
// Average. Coupled to the `name` attribute which has HTML semantics.
|
||||||
|
cy.get('[name=submission]').click()
|
||||||
|
|
||||||
|
// Better. But still coupled to styling or JS event listeners.
|
||||||
|
cy.get('#main').click()
|
||||||
|
|
||||||
|
// Slightly better. Uses an ID but also ensures the element
|
||||||
|
// has an ARIA role attribute
|
||||||
|
cy.get('#main[role=button]').click()
|
||||||
|
|
||||||
|
// Much better. But still coupled to text content that may change.
|
||||||
|
cy.contains('Submit').click()
|
||||||
|
|
||||||
|
// Best. Insulated from all changes.
|
||||||
|
cy.get('[data-cy=submit]').click()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
205
client/cypress/e2e/2-advanced-examples/spies_stubs_clocks.cy.js
Normal file
205
client/cypress/e2e/2-advanced-examples/spies_stubs_clocks.cy.js
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
/// <reference types="cypress" />
|
||||||
|
// remove no check once Cypress.sinon is typed
|
||||||
|
// https://github.com/cypress-io/cypress/issues/6720
|
||||||
|
|
||||||
|
context('Spies, Stubs, and Clock', () => {
|
||||||
|
it('cy.spy() - wrap a method in a spy', () => {
|
||||||
|
// https://on.cypress.io/spy
|
||||||
|
cy.visit('https://example.cypress.io/commands/spies-stubs-clocks')
|
||||||
|
|
||||||
|
const obj = {
|
||||||
|
foo () {},
|
||||||
|
}
|
||||||
|
|
||||||
|
const spy = cy.spy(obj, 'foo').as('anyArgs')
|
||||||
|
|
||||||
|
obj.foo()
|
||||||
|
|
||||||
|
expect(spy).to.be.called
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cy.spy() retries until assertions pass', () => {
|
||||||
|
cy.visit('https://example.cypress.io/commands/spies-stubs-clocks')
|
||||||
|
|
||||||
|
const obj = {
|
||||||
|
/**
|
||||||
|
* Prints the argument passed
|
||||||
|
* @param x {any}
|
||||||
|
*/
|
||||||
|
foo (x) {
|
||||||
|
console.log('obj.foo called with', x)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cy.spy(obj, 'foo').as('foo')
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
obj.foo('first')
|
||||||
|
}, 500)
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
obj.foo('second')
|
||||||
|
}, 2500)
|
||||||
|
|
||||||
|
cy.get('@foo').should('have.been.calledTwice')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cy.stub() - create a stub and/or replace a function with stub', () => {
|
||||||
|
// https://on.cypress.io/stub
|
||||||
|
cy.visit('https://example.cypress.io/commands/spies-stubs-clocks')
|
||||||
|
|
||||||
|
const obj = {
|
||||||
|
/**
|
||||||
|
* prints both arguments to the console
|
||||||
|
* @param a {string}
|
||||||
|
* @param b {string}
|
||||||
|
*/
|
||||||
|
foo (a, b) {
|
||||||
|
console.log('a', a, 'b', b)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const stub = cy.stub(obj, 'foo').as('foo')
|
||||||
|
|
||||||
|
obj.foo('foo', 'bar')
|
||||||
|
|
||||||
|
expect(stub).to.be.called
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cy.clock() - control time in the browser', () => {
|
||||||
|
// https://on.cypress.io/clock
|
||||||
|
|
||||||
|
// create the date in UTC so its always the same
|
||||||
|
// no matter what local timezone the browser is running in
|
||||||
|
const now = new Date(Date.UTC(2017, 2, 14)).getTime()
|
||||||
|
|
||||||
|
cy.clock(now)
|
||||||
|
cy.visit('https://example.cypress.io/commands/spies-stubs-clocks')
|
||||||
|
cy.get('#clock-div').click()
|
||||||
|
.should('have.text', '1489449600')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cy.tick() - move time in the browser', () => {
|
||||||
|
// https://on.cypress.io/tick
|
||||||
|
|
||||||
|
// create the date in UTC so its always the same
|
||||||
|
// no matter what local timezone the browser is running in
|
||||||
|
const now = new Date(Date.UTC(2017, 2, 14)).getTime()
|
||||||
|
|
||||||
|
cy.clock(now)
|
||||||
|
cy.visit('https://example.cypress.io/commands/spies-stubs-clocks')
|
||||||
|
cy.get('#tick-div').click()
|
||||||
|
.should('have.text', '1489449600')
|
||||||
|
|
||||||
|
cy.tick(10000) // 10 seconds passed
|
||||||
|
cy.get('#tick-div').click()
|
||||||
|
.should('have.text', '1489449610')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cy.stub() matches depending on arguments', () => {
|
||||||
|
// see all possible matchers at
|
||||||
|
// https://sinonjs.org/releases/latest/matchers/
|
||||||
|
const greeter = {
|
||||||
|
/**
|
||||||
|
* Greets a person
|
||||||
|
* @param {string} name
|
||||||
|
*/
|
||||||
|
greet (name) {
|
||||||
|
return `Hello, ${name}!`
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cy.stub(greeter, 'greet')
|
||||||
|
.callThrough() // if you want non-matched calls to call the real method
|
||||||
|
.withArgs(Cypress.sinon.match.string).returns('Hi')
|
||||||
|
.withArgs(Cypress.sinon.match.number).throws(new Error('Invalid name'))
|
||||||
|
|
||||||
|
expect(greeter.greet('World')).to.equal('Hi')
|
||||||
|
// @ts-ignore
|
||||||
|
expect(() => greeter.greet(42)).to.throw('Invalid name')
|
||||||
|
expect(greeter.greet).to.have.been.calledTwice
|
||||||
|
|
||||||
|
// non-matched calls goes the actual method
|
||||||
|
// @ts-ignore
|
||||||
|
expect(greeter.greet()).to.equal('Hello, undefined!')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('matches call arguments using Sinon matchers', () => {
|
||||||
|
// see all possible matchers at
|
||||||
|
// https://sinonjs.org/releases/latest/matchers/
|
||||||
|
const calculator = {
|
||||||
|
/**
|
||||||
|
* returns the sum of two arguments
|
||||||
|
* @param a {number}
|
||||||
|
* @param b {number}
|
||||||
|
*/
|
||||||
|
add (a, b) {
|
||||||
|
return a + b
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const spy = cy.spy(calculator, 'add').as('add')
|
||||||
|
|
||||||
|
expect(calculator.add(2, 3)).to.equal(5)
|
||||||
|
|
||||||
|
// if we want to assert the exact values used during the call
|
||||||
|
expect(spy).to.be.calledWith(2, 3)
|
||||||
|
|
||||||
|
// let's confirm "add" method was called with two numbers
|
||||||
|
expect(spy).to.be.calledWith(Cypress.sinon.match.number, Cypress.sinon.match.number)
|
||||||
|
|
||||||
|
// alternatively, provide the value to match
|
||||||
|
expect(spy).to.be.calledWith(Cypress.sinon.match(2), Cypress.sinon.match(3))
|
||||||
|
|
||||||
|
// match any value
|
||||||
|
expect(spy).to.be.calledWith(Cypress.sinon.match.any, 3)
|
||||||
|
|
||||||
|
// match any value from a list
|
||||||
|
expect(spy).to.be.calledWith(Cypress.sinon.match.in([1, 2, 3]), 3)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the given number is event
|
||||||
|
* @param {number} x
|
||||||
|
*/
|
||||||
|
const isEven = (x) => x % 2 === 0
|
||||||
|
|
||||||
|
// expect the value to pass a custom predicate function
|
||||||
|
// the second argument to "sinon.match(predicate, message)" is
|
||||||
|
// shown if the predicate does not pass and assertion fails
|
||||||
|
expect(spy).to.be.calledWith(Cypress.sinon.match(isEven, 'isEven'), 3)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a function that checks if a given number is larger than the limit
|
||||||
|
* @param {number} limit
|
||||||
|
* @returns {(x: number) => boolean}
|
||||||
|
*/
|
||||||
|
const isGreaterThan = (limit) => (x) => x > limit
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a function that checks if a given number is less than the limit
|
||||||
|
* @param {number} limit
|
||||||
|
* @returns {(x: number) => boolean}
|
||||||
|
*/
|
||||||
|
const isLessThan = (limit) => (x) => x < limit
|
||||||
|
|
||||||
|
// you can combine several matchers using "and", "or"
|
||||||
|
expect(spy).to.be.calledWith(
|
||||||
|
Cypress.sinon.match.number,
|
||||||
|
Cypress.sinon.match(isGreaterThan(2), '> 2').and(Cypress.sinon.match(isLessThan(4), '< 4')),
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(spy).to.be.calledWith(
|
||||||
|
Cypress.sinon.match.number,
|
||||||
|
Cypress.sinon.match(isGreaterThan(200), '> 200').or(Cypress.sinon.match(3)),
|
||||||
|
)
|
||||||
|
|
||||||
|
// matchers can be used from BDD assertions
|
||||||
|
cy.get('@add').should('have.been.calledWith',
|
||||||
|
Cypress.sinon.match.number, Cypress.sinon.match(3))
|
||||||
|
|
||||||
|
// you can alias matchers for shorter test code
|
||||||
|
const { match: M } = Cypress.sinon
|
||||||
|
|
||||||
|
cy.get('@add').should('have.been.calledWith', M.number, M(3))
|
||||||
|
})
|
||||||
|
})
|
||||||
121
client/cypress/e2e/2-advanced-examples/traversal.cy.js
Normal file
121
client/cypress/e2e/2-advanced-examples/traversal.cy.js
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
|
context('Traversal', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit('https://example.cypress.io/commands/traversal')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('.children() - get child DOM elements', () => {
|
||||||
|
// https://on.cypress.io/children
|
||||||
|
cy.get('.traversal-breadcrumb')
|
||||||
|
.children('.active')
|
||||||
|
.should('contain', 'Data')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('.closest() - get closest ancestor DOM element', () => {
|
||||||
|
// https://on.cypress.io/closest
|
||||||
|
cy.get('.traversal-badge')
|
||||||
|
.closest('ul')
|
||||||
|
.should('have.class', 'list-group')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('.eq() - get a DOM element at a specific index', () => {
|
||||||
|
// https://on.cypress.io/eq
|
||||||
|
cy.get('.traversal-list>li')
|
||||||
|
.eq(1).should('contain', 'siamese')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('.filter() - get DOM elements that match the selector', () => {
|
||||||
|
// https://on.cypress.io/filter
|
||||||
|
cy.get('.traversal-nav>li')
|
||||||
|
.filter('.active').should('contain', 'About')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('.find() - get descendant DOM elements of the selector', () => {
|
||||||
|
// https://on.cypress.io/find
|
||||||
|
cy.get('.traversal-pagination')
|
||||||
|
.find('li').find('a')
|
||||||
|
.should('have.length', 7)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('.first() - get first DOM element', () => {
|
||||||
|
// https://on.cypress.io/first
|
||||||
|
cy.get('.traversal-table td')
|
||||||
|
.first().should('contain', '1')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('.last() - get last DOM element', () => {
|
||||||
|
// https://on.cypress.io/last
|
||||||
|
cy.get('.traversal-buttons .btn')
|
||||||
|
.last().should('contain', 'Submit')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('.next() - get next sibling DOM element', () => {
|
||||||
|
// https://on.cypress.io/next
|
||||||
|
cy.get('.traversal-ul')
|
||||||
|
.contains('apples').next().should('contain', 'oranges')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('.nextAll() - get all next sibling DOM elements', () => {
|
||||||
|
// https://on.cypress.io/nextall
|
||||||
|
cy.get('.traversal-next-all')
|
||||||
|
.contains('oranges')
|
||||||
|
.nextAll().should('have.length', 3)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('.nextUntil() - get next sibling DOM elements until next el', () => {
|
||||||
|
// https://on.cypress.io/nextuntil
|
||||||
|
cy.get('#veggies')
|
||||||
|
.nextUntil('#nuts').should('have.length', 3)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('.not() - remove DOM elements from set of DOM elements', () => {
|
||||||
|
// https://on.cypress.io/not
|
||||||
|
cy.get('.traversal-disabled .btn')
|
||||||
|
.not('[disabled]').should('not.contain', 'Disabled')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('.parent() - get parent DOM element from DOM elements', () => {
|
||||||
|
// https://on.cypress.io/parent
|
||||||
|
cy.get('.traversal-mark')
|
||||||
|
.parent().should('contain', 'Morbi leo risus')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('.parents() - get parent DOM elements from DOM elements', () => {
|
||||||
|
// https://on.cypress.io/parents
|
||||||
|
cy.get('.traversal-cite')
|
||||||
|
.parents().should('match', 'blockquote')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('.parentsUntil() - get parent DOM elements from DOM elements until el', () => {
|
||||||
|
// https://on.cypress.io/parentsuntil
|
||||||
|
cy.get('.clothes-nav')
|
||||||
|
.find('.active')
|
||||||
|
.parentsUntil('.clothes-nav')
|
||||||
|
.should('have.length', 2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('.prev() - get previous sibling DOM element', () => {
|
||||||
|
// https://on.cypress.io/prev
|
||||||
|
cy.get('.birds').find('.active')
|
||||||
|
.prev().should('contain', 'Lorikeets')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('.prevAll() - get all previous sibling DOM elements', () => {
|
||||||
|
// https://on.cypress.io/prevall
|
||||||
|
cy.get('.fruits-list').find('.third')
|
||||||
|
.prevAll().should('have.length', 2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('.prevUntil() - get all previous sibling DOM elements until el', () => {
|
||||||
|
// https://on.cypress.io/prevuntil
|
||||||
|
cy.get('.foods-list').find('#nuts')
|
||||||
|
.prevUntil('#veggies').should('have.length', 3)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('.siblings() - get all sibling DOM elements', () => {
|
||||||
|
// https://on.cypress.io/siblings
|
||||||
|
cy.get('.traversal-pills .active')
|
||||||
|
.siblings().should('have.length', 2)
|
||||||
|
})
|
||||||
|
})
|
||||||
110
client/cypress/e2e/2-advanced-examples/utilities.cy.js
Normal file
110
client/cypress/e2e/2-advanced-examples/utilities.cy.js
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
|
context('Utilities', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit('https://example.cypress.io/utilities')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Cypress._ - call a lodash method', () => {
|
||||||
|
// https://on.cypress.io/_
|
||||||
|
cy.request('https://jsonplaceholder.cypress.io/users')
|
||||||
|
.then((response) => {
|
||||||
|
let ids = Cypress._.chain(response.body).map('id').take(3).value()
|
||||||
|
|
||||||
|
expect(ids).to.deep.eq([1, 2, 3])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Cypress.$ - call a jQuery method', () => {
|
||||||
|
// https://on.cypress.io/$
|
||||||
|
let $li = Cypress.$('.utility-jquery li:first')
|
||||||
|
|
||||||
|
cy.wrap($li)
|
||||||
|
.should('not.have.class', 'active')
|
||||||
|
.click()
|
||||||
|
.should('have.class', 'active')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Cypress.Blob - blob utilities and base64 string conversion', () => {
|
||||||
|
// https://on.cypress.io/blob
|
||||||
|
cy.get('.utility-blob').then(($div) => {
|
||||||
|
// https://github.com/nolanlawson/blob-util#imgSrcToDataURL
|
||||||
|
// get the dataUrl string for the javascript-logo
|
||||||
|
return Cypress.Blob.imgSrcToDataURL('https://example.cypress.io/assets/img/javascript-logo.png', undefined, 'anonymous')
|
||||||
|
.then((dataUrl) => {
|
||||||
|
// create an <img> element and set its src to the dataUrl
|
||||||
|
let img = Cypress.$('<img />', { src: dataUrl })
|
||||||
|
|
||||||
|
// need to explicitly return cy here since we are initially returning
|
||||||
|
// the Cypress.Blob.imgSrcToDataURL promise to our test
|
||||||
|
// append the image
|
||||||
|
$div.append(img)
|
||||||
|
|
||||||
|
cy.get('.utility-blob img').click()
|
||||||
|
.should('have.attr', 'src', dataUrl)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Cypress.minimatch - test out glob patterns against strings', () => {
|
||||||
|
// https://on.cypress.io/minimatch
|
||||||
|
let matching = Cypress.minimatch('/users/1/comments', '/users/*/comments', {
|
||||||
|
matchBase: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(matching, 'matching wildcard').to.be.true
|
||||||
|
|
||||||
|
matching = Cypress.minimatch('/users/1/comments/2', '/users/*/comments', {
|
||||||
|
matchBase: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(matching, 'comments').to.be.false
|
||||||
|
|
||||||
|
// ** matches against all downstream path segments
|
||||||
|
matching = Cypress.minimatch('/foo/bar/baz/123/quux?a=b&c=2', '/foo/**', {
|
||||||
|
matchBase: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(matching, 'comments').to.be.true
|
||||||
|
|
||||||
|
// whereas * matches only the next path segment
|
||||||
|
|
||||||
|
matching = Cypress.minimatch('/foo/bar/baz/123/quux?a=b&c=2', '/foo/*', {
|
||||||
|
matchBase: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(matching, 'comments').to.be.false
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Cypress.Promise - instantiate a bluebird promise', () => {
|
||||||
|
// https://on.cypress.io/promise
|
||||||
|
let waited = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Bluebird<string>
|
||||||
|
*/
|
||||||
|
function waitOneSecond () {
|
||||||
|
// return a promise that resolves after 1 second
|
||||||
|
// @ts-ignore TS2351 (new Cypress.Promise)
|
||||||
|
return new Cypress.Promise((resolve, reject) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
// set waited to true
|
||||||
|
waited = true
|
||||||
|
|
||||||
|
// resolve with 'foo' string
|
||||||
|
resolve('foo')
|
||||||
|
}, 1000)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
cy.then(() => {
|
||||||
|
// return a promise to cy.then() that
|
||||||
|
// is awaited until it resolves
|
||||||
|
// @ts-ignore TS7006
|
||||||
|
return waitOneSecond().then((str) => {
|
||||||
|
expect(str).to.eq('foo')
|
||||||
|
expect(waited).to.be.true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
59
client/cypress/e2e/2-advanced-examples/viewport.cy.js
Normal file
59
client/cypress/e2e/2-advanced-examples/viewport.cy.js
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
|
context('Viewport', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit('https://example.cypress.io/commands/viewport')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cy.viewport() - set the viewport size and dimension', () => {
|
||||||
|
// https://on.cypress.io/viewport
|
||||||
|
|
||||||
|
cy.get('#navbar').should('be.visible')
|
||||||
|
cy.viewport(320, 480)
|
||||||
|
|
||||||
|
// the navbar should have collapse since our screen is smaller
|
||||||
|
cy.get('#navbar').should('not.be.visible')
|
||||||
|
cy.get('.navbar-toggle').should('be.visible').click()
|
||||||
|
cy.get('.nav').find('a').should('be.visible')
|
||||||
|
|
||||||
|
// lets see what our app looks like on a super large screen
|
||||||
|
cy.viewport(2999, 2999)
|
||||||
|
|
||||||
|
// cy.viewport() accepts a set of preset sizes
|
||||||
|
// to easily set the screen to a device's width and height
|
||||||
|
|
||||||
|
// We added a cy.wait() between each viewport change so you can see
|
||||||
|
// the change otherwise it is a little too fast to see :)
|
||||||
|
|
||||||
|
cy.viewport('macbook-15')
|
||||||
|
cy.wait(200)
|
||||||
|
cy.viewport('macbook-13')
|
||||||
|
cy.wait(200)
|
||||||
|
cy.viewport('macbook-11')
|
||||||
|
cy.wait(200)
|
||||||
|
cy.viewport('ipad-2')
|
||||||
|
cy.wait(200)
|
||||||
|
cy.viewport('ipad-mini')
|
||||||
|
cy.wait(200)
|
||||||
|
cy.viewport('iphone-6+')
|
||||||
|
cy.wait(200)
|
||||||
|
cy.viewport('iphone-6')
|
||||||
|
cy.wait(200)
|
||||||
|
cy.viewport('iphone-5')
|
||||||
|
cy.wait(200)
|
||||||
|
cy.viewport('iphone-4')
|
||||||
|
cy.wait(200)
|
||||||
|
cy.viewport('iphone-3')
|
||||||
|
cy.wait(200)
|
||||||
|
|
||||||
|
// cy.viewport() accepts an orientation for all presets
|
||||||
|
// the default orientation is 'portrait'
|
||||||
|
cy.viewport('ipad-2', 'portrait')
|
||||||
|
cy.wait(200)
|
||||||
|
cy.viewport('iphone-4', 'landscape')
|
||||||
|
cy.wait(200)
|
||||||
|
|
||||||
|
// The viewport will be reset back to the default dimensions
|
||||||
|
// in between tests (the default can be set in cypress.json)
|
||||||
|
})
|
||||||
|
})
|
||||||
31
client/cypress/e2e/2-advanced-examples/waiting.cy.js
Normal file
31
client/cypress/e2e/2-advanced-examples/waiting.cy.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
|
context('Waiting', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit('https://example.cypress.io/commands/waiting')
|
||||||
|
})
|
||||||
|
// BE CAREFUL of adding unnecessary wait times.
|
||||||
|
// https://on.cypress.io/best-practices#Unnecessary-Waiting
|
||||||
|
|
||||||
|
// https://on.cypress.io/wait
|
||||||
|
it('cy.wait() - wait for a specific amount of time', () => {
|
||||||
|
cy.get('.wait-input1').type('Wait 1000ms after typing')
|
||||||
|
cy.wait(1000)
|
||||||
|
cy.get('.wait-input2').type('Wait 1000ms after typing')
|
||||||
|
cy.wait(1000)
|
||||||
|
cy.get('.wait-input3').type('Wait 1000ms after typing')
|
||||||
|
cy.wait(1000)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cy.wait() - wait for a specific route', () => {
|
||||||
|
// Listen to GET to comments/1
|
||||||
|
cy.intercept('GET', '**/comments/*').as('getComment')
|
||||||
|
|
||||||
|
// we have code that gets a comment when
|
||||||
|
// the button is clicked in scripts.js
|
||||||
|
cy.get('.network-btn').click()
|
||||||
|
|
||||||
|
// wait for GET comments/1
|
||||||
|
cy.wait('@getComment').its('response.statusCode').should('be.oneOf', [200, 304])
|
||||||
|
})
|
||||||
|
})
|
||||||
22
client/cypress/e2e/2-advanced-examples/window.cy.js
Normal file
22
client/cypress/e2e/2-advanced-examples/window.cy.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
|
context('Window', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit('https://example.cypress.io/commands/window')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cy.window() - get the global window object', () => {
|
||||||
|
// https://on.cypress.io/window
|
||||||
|
cy.window().should('have.property', 'top')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cy.document() - get the document object', () => {
|
||||||
|
// https://on.cypress.io/document
|
||||||
|
cy.document().should('have.property', 'charset').and('eq', 'UTF-8')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cy.title() - get the title', () => {
|
||||||
|
// https://on.cypress.io/title
|
||||||
|
cy.title().should('include', 'Kitchen Sink')
|
||||||
|
})
|
||||||
|
})
|
||||||
5
client/cypress/fixtures/profile.json
Normal file
5
client/cypress/fixtures/profile.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"id": 8739,
|
||||||
|
"name": "Jane",
|
||||||
|
"email": "jane@example.com"
|
||||||
|
}
|
||||||
1
client/cypress/fixtures/users.json
Normal file
1
client/cypress/fixtures/users.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
[]
|
||||||
22
client/cypress/plugins/index.js
Normal file
22
client/cypress/plugins/index.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
/// <reference types="cypress" />
|
||||||
|
// ***********************************************************
|
||||||
|
// This example plugins/index.js can be used to load plugins
|
||||||
|
//
|
||||||
|
// You can change the location of this file or turn off loading
|
||||||
|
// the plugins file with the 'pluginsFile' configuration option.
|
||||||
|
//
|
||||||
|
// You can read more here:
|
||||||
|
// https://on.cypress.io/plugins-guide
|
||||||
|
// ***********************************************************
|
||||||
|
|
||||||
|
// This function is called when a project is opened or re-opened (e.g. due to
|
||||||
|
// the project's config changing)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {Cypress.PluginConfig}
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
module.exports = (on, config) => {
|
||||||
|
// `on` is used to hook into various events Cypress emits
|
||||||
|
// `config` is the resolved Cypress config
|
||||||
|
}
|
||||||
@@ -23,3 +23,5 @@
|
|||||||
//
|
//
|
||||||
// -- This will overwrite an existing command --
|
// -- This will overwrite an existing command --
|
||||||
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
|
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
|
||||||
|
|
||||||
|
import "@testing-library/cypress/add-commands";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// ***********************************************************
|
// ***********************************************************
|
||||||
// This example support/e2e.js is processed and
|
// This example support/index.js is processed and
|
||||||
// loaded automatically before your test files.
|
// loaded automatically before your test files.
|
||||||
//
|
//
|
||||||
// This is a great place to put global configuration and
|
// This is a great place to put global configuration and
|
||||||
|
|||||||
8
client/cypress/tsconfig.json
Normal file
8
client/cypress/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"allowJs": true,
|
||||||
|
"baseUrl": "../node_modules",
|
||||||
|
"types": ["cypress"]
|
||||||
|
},
|
||||||
|
"include": ["**/*.*"]
|
||||||
|
}
|
||||||
@@ -2,12 +2,12 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<link rel="icon" href="%PUBLIC_URL%/favicon.png" />
|
<link rel="icon" href="/favicon.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta name="theme-color" content="#002366" />
|
<meta name="theme-color" content="#002366" />
|
||||||
<meta name="description" content="ImEX Online" />
|
<meta name="description" content="ImEX Online" />
|
||||||
<!-- <link rel="apple-touch-icon" href="logo192.png" /> -->
|
<!-- <link rel="apple-touch-icon" href="logo192.png" /> -->
|
||||||
<link rel="apple-touch-icon" href="logo192.png" />
|
<link rel="apple-touch-icon" href="public/logo192.png" />
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
window.$crisp = [];
|
window.$crisp = [];
|
||||||
window.CRISP_WEBSITE_ID = "36724f62-2eb0-4b29-9cdd-9905fb99913e";
|
window.CRISP_WEBSITE_ID = "36724f62-2eb0-4b29-9cdd-9905fb99913e";
|
||||||
@@ -67,7 +67,7 @@
|
|||||||
manifest.json provides metadata used when your web app is installed on a
|
manifest.json provides metadata used when your web app is installed on a
|
||||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||||
-->
|
-->
|
||||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
<link rel="manifest" href="/manifest.json" />
|
||||||
<!--
|
<!--
|
||||||
Notice the use of %PUBLIC_URL% in the tags above.
|
Notice the use of %PUBLIC_URL% in the tags above.
|
||||||
It will be replaced with the URL of the `public` folder during the build.
|
It will be replaced with the URL of the `public` folder during the build.
|
||||||
@@ -82,5 +82,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|
||||||
|
<script type="module" src="src/index.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -8872,13 +8872,13 @@
|
|||||||
│ ├─ email: luis@luisrudge.net
|
│ ├─ email: luis@luisrudge.net
|
||||||
│ ├─ path: /Users/pfic/Documents/Development/bodyshop/client/node_modules/postcss-flexbugs-fixes
|
│ ├─ path: /Users/pfic/Documents/Development/bodyshop/client/node_modules/postcss-flexbugs-fixes
|
||||||
│ └─ licenseFile: /Users/pfic/Documents/Development/bodyshop/client/node_modules/postcss-flexbugs-fixes/LICENSE
|
│ └─ licenseFile: /Users/pfic/Documents/Development/bodyshop/client/node_modules/postcss-flexbugs-fixes/LICENSE
|
||||||
├─ postcss-focus-visible@4.0.0
|
├─ postcss-focus-open@4.0.0
|
||||||
│ ├─ licenses: CC0-1.0
|
│ ├─ licenses: CC0-1.0
|
||||||
│ ├─ repository: https://github.com/jonathantneal/postcss-focus-visible
|
│ ├─ repository: https://github.com/jonathantneal/postcss-focus-open
|
||||||
│ ├─ publisher: Jonathan Neal
|
│ ├─ publisher: Jonathan Neal
|
||||||
│ ├─ email: jonathantneal@hotmail.com
|
│ ├─ email: jonathantneal@hotmail.com
|
||||||
│ ├─ path: /Users/pfic/Documents/Development/bodyshop/client/node_modules/postcss-focus-visible
|
│ ├─ path: /Users/pfic/Documents/Development/bodyshop/client/node_modules/postcss-focus-open
|
||||||
│ └─ licenseFile: /Users/pfic/Documents/Development/bodyshop/client/node_modules/postcss-focus-visible/LICENSE.md
|
│ └─ licenseFile: /Users/pfic/Documents/Development/bodyshop/client/node_modules/postcss-focus-open/LICENSE.md
|
||||||
├─ postcss-focus-within@3.0.0
|
├─ postcss-focus-within@3.0.0
|
||||||
│ ├─ licenses: CC0-1.0
|
│ ├─ licenses: CC0-1.0
|
||||||
│ ├─ repository: https://github.com/jonathantneal/postcss-focus-within
|
│ ├─ repository: https://github.com/jonathantneal/postcss-focus-within
|
||||||
|
|||||||
16334
client/package-lock.json
generated
Normal file
16334
client/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,97 +1,112 @@
|
|||||||
{
|
{
|
||||||
"name": "bodyshop",
|
"name": "bodyshop",
|
||||||
"version": "0.2.1",
|
"version": "0.2.1",
|
||||||
|
"engines": {
|
||||||
|
"node": "18.18.2"
|
||||||
|
},
|
||||||
|
"type": "commonjs",
|
||||||
"private": true,
|
"private": true,
|
||||||
"proxy": "http://localhost:4000",
|
"proxy": "http://localhost:4000",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apollo/client": "^3.6.9",
|
"@ant-design/compatible": "^5.1.2",
|
||||||
|
"@ant-design/pro-layout": "^7.17.16",
|
||||||
|
"@apollo/client": "^3.9.0",
|
||||||
"@asseinfo/react-kanban": "^2.2.0",
|
"@asseinfo/react-kanban": "^2.2.0",
|
||||||
"@craco/craco": "^6.4.5",
|
"@craco/craco": "^7.1.0",
|
||||||
"@fingerprintjs/fingerprintjs": "^3.3.3",
|
"@fingerprintjs/fingerprintjs": "^4.2.2",
|
||||||
"@jsreport/browser-client": "^3.1.0",
|
"@jsreport/browser-client": "^3.1.0",
|
||||||
"@sentry/react": "^7.7.0",
|
"@reduxjs/toolkit": "^2.1.0",
|
||||||
"@sentry/tracing": "^7.7.0",
|
"@sentry/cli": "^2.27.0",
|
||||||
"@splitsoftware/splitio-react": "^1.6.0",
|
"@sentry/react": "^7.99.0",
|
||||||
"@stripe/react-stripe-js": "^1.9.0",
|
"@sentry/tracing": "^7.99.0",
|
||||||
"@stripe/stripe-js": "^1.32.0",
|
"@splitsoftware/splitio-react": "^1.11.0",
|
||||||
"@tanem/react-nprogress": "^5.0.8",
|
"@tanem/react-nprogress": "^5.0.51",
|
||||||
"antd": "^4.22.3",
|
"@vitejs/plugin-legacy": "^5.3.0",
|
||||||
"apollo-link-logger": "^2.0.0",
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
"axios": "^0.27.2",
|
"@vitejs/plugin-react-refresh": "^1.3.6",
|
||||||
"craco-less": "^1.20.0",
|
"@vitejs/plugin-react-swc": "^3.6.0",
|
||||||
|
"antd": "^5.13.3",
|
||||||
|
"apollo-link-logger": "^2.0.1",
|
||||||
|
"apollo-link-sentry": "^3.3.0",
|
||||||
|
"axios": "^1.6.7",
|
||||||
|
"consola": "^3.2.3",
|
||||||
|
"dayjs": "^1.11.10",
|
||||||
|
"dayjs-business-days2": "^1.2.2",
|
||||||
"dinero.js": "^1.9.1",
|
"dinero.js": "^1.9.1",
|
||||||
|
"dotenv": "^16.4.1",
|
||||||
"enquire-js": "^0.2.1",
|
"enquire-js": "^0.2.1",
|
||||||
"env-cmd": "^10.1.0",
|
"env-cmd": "^10.1.0",
|
||||||
|
"esbuild": "^0.20.0",
|
||||||
"exifr": "^7.1.3",
|
"exifr": "^7.1.3",
|
||||||
"firebase": "^9.9.1",
|
"firebase": "^10.7.2",
|
||||||
"graphql": "^16.5.0",
|
"graphql": "^16.6.0",
|
||||||
"i18next": "^21.8.14",
|
"i18next": "^23.8.1",
|
||||||
"i18next-browser-languagedetector": "^6.1.4",
|
"i18next-browser-languagedetector": "^7.0.2",
|
||||||
"jsoneditor": "^9.9.0",
|
"jsoneditor": "^10.0.0",
|
||||||
"jsreport-browser-client-dist": "^1.3.0",
|
"jsreport-browser-client-dist": "^1.3.0",
|
||||||
"libphonenumber-js": "^1.10.9",
|
"libphonenumber-js": "^1.10.54",
|
||||||
"logrocket": "^3.0.1",
|
"logrocket": "^7.0.0",
|
||||||
"markerjs2": "^2.22.0",
|
"markerjs2": "^2.32.0",
|
||||||
"moment-business-days": "^1.2.0",
|
"normalize-url": "^8.0.0",
|
||||||
"moment-timezone": "^0.5.34",
|
"phone": "^3.1.42",
|
||||||
"normalize-url": "^7.0.3",
|
|
||||||
"phone": "^3.1.23",
|
|
||||||
"preval.macro": "^5.0.0",
|
"preval.macro": "^5.0.0",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"query-string": "^7.1.1",
|
"query-string": "^8.1.0",
|
||||||
"rc-queue-anim": "^2.0.0",
|
"rc-queue-anim": "^2.0.0",
|
||||||
"rc-scroll-anim": "^2.7.6",
|
"rc-scroll-anim": "^2.7.6",
|
||||||
"react": "^17.0.2",
|
"react": "^18.2.0",
|
||||||
"react-big-calendar": "^1.5.0",
|
"react-big-calendar": "^1.8.7",
|
||||||
"react-color": "^2.19.3",
|
"react-color": "^2.19.3",
|
||||||
"react-cookie": "^4.1.1",
|
"react-cookie": "^7.0.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^18.2.0",
|
||||||
"react-drag-listview": "^0.2.1",
|
"react-drag-listview": "^2.0.0",
|
||||||
"react-grid-gallery": "^0.5.5",
|
"react-grid-gallery": "^1.0.0",
|
||||||
"react-grid-layout": "^1.3.4",
|
"react-grid-layout": "1.3.4",
|
||||||
"react-i18next": "^11.18.1",
|
"react-i18next": "^14.0.1",
|
||||||
"react-icons": "^4.4.0",
|
"react-icons": "^5.0.1",
|
||||||
"react-number-format": "^4.9.3",
|
"react-image-lightbox": "^5.1.4",
|
||||||
"react-redux": "^7.2.8",
|
"react-intersection-observer": "^9.5.3",
|
||||||
"react-resizable": "^3.0.4",
|
"react-markdown": "^9.0.1",
|
||||||
"react-router-dom": "^5.3.0",
|
"react-number-format": "^5.1.4",
|
||||||
"react-scripts": "^4.0.3",
|
"react-redux": "^9.1.0",
|
||||||
|
"react-resizable": "^3.0.5",
|
||||||
|
"react-router-dom": "^6.21.2",
|
||||||
"react-sticky": "^6.0.3",
|
"react-sticky": "^6.0.3",
|
||||||
"react-sublime-video": "^0.2.5",
|
"react-sublime-video": "^0.2.5",
|
||||||
"react-virtualized": "^9.22.3",
|
"react-virtualized": "^9.22.5",
|
||||||
"recharts": "^2.1.12",
|
"recharts": "^2.11.0",
|
||||||
"redux": "^4.2.0",
|
"redux": "^5.0.1",
|
||||||
"redux-persist": "^6.0.0",
|
"redux-persist": "^6.0.0",
|
||||||
"redux-saga": "^1.1.3",
|
"redux-saga": "^1.3.0",
|
||||||
"redux-state-sync": "^3.1.4",
|
"redux-state-sync": "^3.1.4",
|
||||||
"reselect": "^4.1.6",
|
"reselect": "^5.1.0",
|
||||||
"sass": "^1.54.0",
|
"sass": "^1.70.0",
|
||||||
"socket.io-client": "^4.5.1",
|
"socket.io-client": "^4.7.4",
|
||||||
"styled-components": "^5.3.5",
|
"styled-components": "^6.1.8",
|
||||||
"subscriptions-transport-ws": "^0.11.0",
|
"subscriptions-transport-ws": "^0.11.0",
|
||||||
"web-vitals": "^2.1.4",
|
"terser-webpack-plugin": "^5.3.10",
|
||||||
"workbox-background-sync": "^6.5.3",
|
"vite-plugin-svgr": "^4.2.0",
|
||||||
"workbox-broadcast-update": "^6.5.3",
|
"web-vitals": "^3.5.2",
|
||||||
"workbox-cacheable-response": "^6.5.3",
|
"workbox-core": "^7.0.0",
|
||||||
"workbox-core": "^6.5.3",
|
"workbox-expiration": "^7.0.0",
|
||||||
"workbox-expiration": "^6.5.3",
|
"workbox-navigation-preload": "^7.0.0",
|
||||||
"workbox-google-analytics": "^6.5.3",
|
"workbox-precaching": "^7.0.0",
|
||||||
"workbox-navigation-preload": "^6.5.3",
|
"workbox-routing": "^7.0.0",
|
||||||
"workbox-precaching": "^6.5.3",
|
"workbox-strategies": "^7.0.0",
|
||||||
"workbox-range-requests": "^6.5.3",
|
"yauzl": "^2.10.0"
|
||||||
"workbox-routing": "^6.5.3",
|
|
||||||
"workbox-strategies": "^6.5.3",
|
|
||||||
"workbox-streams": "^6.5.3"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"analyze": "source-map-explorer 'build/static/js/*.js'",
|
"analyze": "source-map-explorer 'build/static/js/*.js'",
|
||||||
"start": "craco start",
|
"start": "vite",
|
||||||
"build": "REACT_APP_GIT_SHA=`git rev-parse --short HEAD` craco build",
|
"build": "cross-env-shell VITE_APP_GIT_SHA=\\\"`git rev-parse --short HEAD`\\\" vite build && npm run sentry:sourcemaps",
|
||||||
"build:test": "env-cmd -f .env.test yarn run build",
|
"build:test": "env-cmd -f .env.test npm run build",
|
||||||
"build-deploy:test": "yarn run build:test && s3cmd sync build/* s3://imex-online-test && echo '🚀 TESTING Deployed!'",
|
"build-deploy:test": "npm run build:test && s3cmd sync build/* s3://imex-online-test && echo '🚀 TESTING Deployed!'",
|
||||||
"buildcra": "REACT_APP_GIT_SHA=`git rev-parse --short HEAD` craco build",
|
"buildcra": "cross-env-shell VITE_APP_GIT_SHA=\\\"`git rev-parse --short HEAD`\\\" vite build",
|
||||||
"test": "cypress open",
|
"test": "cypress open",
|
||||||
"eject": "react-scripts eject"
|
"eject": "react-scripts eject",
|
||||||
|
"madge": "madge --image ./madge-graph.svg --extensions js,jsx,ts,tsx --circular .",
|
||||||
|
"eulaize": "node src/utils/eulaize.js",
|
||||||
|
"sentry:sourcemaps": "sentry-cli sourcemaps inject --org imex --project imexonline ./build && sentry-cli sourcemaps upload --org imex --project imexonline ./build"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"extends": [
|
"extends": [
|
||||||
@@ -116,11 +131,29 @@
|
|||||||
"react-error-overlay": "6.0.9"
|
"react-error-overlay": "6.0.9"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sentry/webpack-plugin": "^1.19.0",
|
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||||
"cypress": "^12.2.0",
|
"@babel/preset-react": "^7.23.3",
|
||||||
"eslint-plugin-cypress": "^2.12.1",
|
"@emotion/babel-plugin": "^11.11.0",
|
||||||
|
"@emotion/react": "^11.11.3",
|
||||||
|
"@sentry/webpack-plugin": "^2.10.3",
|
||||||
|
"@swc/core": "^1.3.107",
|
||||||
|
"@swc/plugin-styled-components": "^1.5.108",
|
||||||
|
"@testing-library/cypress": "^10.0.1",
|
||||||
|
"browserslist": "^4.22.3",
|
||||||
|
"browserslist-to-esbuild": "^2.1.1",
|
||||||
|
"craco-less": "^3.0.1",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"cypress": "^13.6.4",
|
||||||
|
"eslint-plugin-cypress": "^2.15.1",
|
||||||
|
"memfs": "^4.6.0",
|
||||||
|
"os-browserify": "^0.3.0",
|
||||||
"react-error-overlay": "6.0.11",
|
"react-error-overlay": "6.0.11",
|
||||||
"redux-logger": "^3.0.6",
|
"redux-logger": "^3.0.6",
|
||||||
"source-map-explorer": "^2.5.2"
|
"source-map-explorer": "^2.5.3",
|
||||||
|
"vite": "^5.0.11",
|
||||||
|
"vite-plugin-babel": "^1.2.0",
|
||||||
|
"vite-plugin-legacy": "^2.1.0",
|
||||||
|
"vite-plugin-node-polyfills": "^0.19.0",
|
||||||
|
"vite-plugin-style-import": "^2.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
13087
client/patches/peerjs+1.3.2.patch
Normal file
13087
client/patches/peerjs+1.3.2.patch
Normal file
File diff suppressed because one or more lines are too long
@@ -190,7 +190,7 @@ This package contains the following license and notice below:
|
|||||||
# @firebase/logger
|
# @firebase/logger
|
||||||
|
|
||||||
This package serves as the base of all logging in the JS SDK. Any logging that
|
This package serves as the base of all logging in the JS SDK. Any logging that
|
||||||
is intended to be visible to Firebase end developers should go through this
|
is intended to be open to Firebase end developers should go through this
|
||||||
module.
|
module.
|
||||||
|
|
||||||
## Basic Usage
|
## Basic Usage
|
||||||
@@ -9375,7 +9375,7 @@ parties to make or receive copies. Mere interaction with a user through
|
|||||||
a computer network, with no transfer of a copy, is not conveying.
|
a computer network, with no transfer of a copy, is not conveying.
|
||||||
|
|
||||||
An interactive user interface displays "Appropriate Legal Notices"
|
An interactive user interface displays "Appropriate Legal Notices"
|
||||||
to the extent that it includes a convenient and prominently visible
|
to the extent that it includes a convenient and prominently open
|
||||||
feature that (1) displays an appropriate copyright notice, and (2)
|
feature that (1) displays an appropriate copyright notice, and (2)
|
||||||
tells the user that there is no warranty for the work (except to the
|
tells the user that there is no warranty for the work (except to the
|
||||||
extent that warranties are provided), that licensees may convey the
|
extent that warranties are provided), that licensees may convey the
|
||||||
|
|||||||
@@ -1029,7 +1029,7 @@ The following NPM packages may be included in this product:
|
|||||||
- postcss-dir-pseudo-class@5.0.0
|
- postcss-dir-pseudo-class@5.0.0
|
||||||
- postcss-double-position-gradients@1.0.0
|
- postcss-double-position-gradients@1.0.0
|
||||||
- postcss-env-function@2.0.2
|
- postcss-env-function@2.0.2
|
||||||
- postcss-focus-visible@4.0.0
|
- postcss-focus-open@4.0.0
|
||||||
- postcss-focus-within@3.0.0
|
- postcss-focus-within@3.0.0
|
||||||
- postcss-gap-properties@2.0.0
|
- postcss-gap-properties@2.0.0
|
||||||
- postcss-image-set-function@3.0.1
|
- postcss-image-set-function@3.0.1
|
||||||
@@ -1699,7 +1699,7 @@ This package contains the following license and notice below:
|
|||||||
# @firebase/logger
|
# @firebase/logger
|
||||||
|
|
||||||
This package serves as the base of all logging in the JS SDK. Any logging that
|
This package serves as the base of all logging in the JS SDK. Any logging that
|
||||||
is intended to be visible to Firebase end developers should go through this
|
is intended to be open to Firebase end developers should go through this
|
||||||
module.
|
module.
|
||||||
|
|
||||||
## Basic Usage
|
## Basic Usage
|
||||||
@@ -24029,7 +24029,7 @@ parties to make or receive copies. Mere interaction with a user through
|
|||||||
a computer network, with no transfer of a copy, is not conveying.
|
a computer network, with no transfer of a copy, is not conveying.
|
||||||
|
|
||||||
An interactive user interface displays "Appropriate Legal Notices"
|
An interactive user interface displays "Appropriate Legal Notices"
|
||||||
to the extent that it includes a convenient and prominently visible
|
to the extent that it includes a convenient and prominently open
|
||||||
feature that (1) displays an appropriate copyright notice, and (2)
|
feature that (1) displays an appropriate copyright notice, and (2)
|
||||||
tells the user that there is no warranty for the work (except to the
|
tells the user that there is no warranty for the work (except to the
|
||||||
extent that warranties are provided), that licensees may convey the
|
extent that warranties are provided), that licensees may convey the
|
||||||
|
|||||||
@@ -1,44 +1,53 @@
|
|||||||
import { ApolloProvider } from "@apollo/client";
|
import {ApolloProvider} from "@apollo/client";
|
||||||
import { SplitFactory, SplitSdk } from "@splitsoftware/splitio-react";
|
import {SplitFactoryProvider, SplitSdk,} from '@splitsoftware/splitio-react';
|
||||||
import { ConfigProvider } from "antd";
|
import {ConfigProvider} from "antd";
|
||||||
import enLocale from "antd/es/locale/en_US";
|
import enLocale from "antd/es/locale/en_US";
|
||||||
import moment from "moment";
|
import dayjs from "../utils/day";
|
||||||
|
import 'dayjs/locale/en';
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import {useTranslation} from "react-i18next";
|
||||||
import GlobalLoadingBar from "../components/global-loading-bar/global-loading-bar.component";
|
import GlobalLoadingBar from "../components/global-loading-bar/global-loading-bar.component";
|
||||||
import client from "../utils/GraphQLClient";
|
import client from "../utils/GraphQLClient";
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
|
import * as Sentry from "@sentry/react";
|
||||||
|
|
||||||
moment.locale("en-US");
|
import themeProvider from "./themeProvider";
|
||||||
|
|
||||||
export const factory = SplitSdk({
|
dayjs.locale("en");
|
||||||
core: {
|
|
||||||
authorizationKey: process.env.REACT_APP_SPLIT_API,
|
|
||||||
key: "anon",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default function AppContainer() {
|
const config = {
|
||||||
const { t } = useTranslation();
|
core: {
|
||||||
|
authorizationKey: import.meta.env.VITE_APP_SPLIT_API,
|
||||||
|
key: "anon",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export const factory = SplitSdk(config);
|
||||||
|
|
||||||
return (
|
|
||||||
<ApolloProvider client={client}>
|
function AppContainer() {
|
||||||
<ConfigProvider
|
const {t} = useTranslation();
|
||||||
//componentSize="small"
|
|
||||||
input={{ autoComplete: "new-password" }}
|
return (
|
||||||
locale={enLocale}
|
<ApolloProvider client={client}>
|
||||||
form={{
|
<ConfigProvider
|
||||||
validateMessages: {
|
//componentSize="small"
|
||||||
// eslint-disable-next-line no-template-curly-in-string
|
input={{autoComplete: "new-password"}}
|
||||||
required: t("general.validation.required", { label: "${label}" }),
|
locale={enLocale}
|
||||||
},
|
theme={themeProvider}
|
||||||
}}
|
form={{
|
||||||
>
|
validateMessages: {
|
||||||
<GlobalLoadingBar />
|
// eslint-disable-next-line no-template-curly-in-string
|
||||||
<SplitFactory factory={factory}>
|
required: t("general.validation.required", {label: "${label}"}),
|
||||||
<App />
|
},
|
||||||
</SplitFactory>
|
}}
|
||||||
</ConfigProvider>
|
>
|
||||||
</ApolloProvider>
|
<GlobalLoadingBar/>
|
||||||
);
|
<SplitFactoryProvider factory={factory}>
|
||||||
|
<App/>
|
||||||
|
</SplitFactoryProvider>
|
||||||
|
</ConfigProvider>
|
||||||
|
</ApolloProvider>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default Sentry.withProfiler(AppContainer);
|
||||||
|
|||||||
@@ -1,161 +1,156 @@
|
|||||||
import { useClient } from "@splitsoftware/splitio-react";
|
import {useSplitClient} from "@splitsoftware/splitio-react";
|
||||||
import { Button, Result } from "antd";
|
import {Button, Result} from "antd";
|
||||||
import LogRocket from "logrocket";
|
import LogRocket from "logrocket";
|
||||||
import React, { lazy, Suspense, useEffect } from "react";
|
import React, {lazy, Suspense, useEffect, useState} from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import {useTranslation} from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import {connect} from "react-redux";
|
||||||
import { Route, Switch } from "react-router-dom";
|
import {Route, Routes} from "react-router-dom";
|
||||||
import { createStructuredSelector } from "reselect";
|
import {createStructuredSelector} from "reselect";
|
||||||
import DocumentEditorContainer from "../components/document-editor/document-editor.container";
|
import DocumentEditorContainer from "../components/document-editor/document-editor.container";
|
||||||
import ErrorBoundary from "../components/error-boundary/error-boundary.component";
|
import ErrorBoundary from "../components/error-boundary/error-boundary.component";
|
||||||
|
|
||||||
//Component Imports
|
//Component Imports
|
||||||
import LoadingSpinner from "../components/loading-spinner/loading-spinner.component";
|
import LoadingSpinner from "../components/loading-spinner/loading-spinner.component";
|
||||||
import DisclaimerPage from "../pages/disclaimer/disclaimer.page";
|
import DisclaimerPage from "../pages/disclaimer/disclaimer.page";
|
||||||
import LandingPage from "../pages/landing/landing.page";
|
import LandingPage from "../pages/landing/landing.page";
|
||||||
import TechPageContainer from "../pages/tech/tech.page.container";
|
import TechPageContainer from "../pages/tech/tech.page.container";
|
||||||
import { setOnline } from "../redux/application/application.actions";
|
import {setOnline} from "../redux/application/application.actions";
|
||||||
import { selectOnline } from "../redux/application/application.selectors";
|
import {selectOnline} from "../redux/application/application.selectors";
|
||||||
import { checkUserSession } from "../redux/user/user.actions";
|
import {checkUserSession} from "../redux/user/user.actions";
|
||||||
import {
|
import {selectBodyshop, selectCurrentEula, selectCurrentUser,} from "../redux/user/user.selectors";
|
||||||
selectBodyshop,
|
import PrivateRoute from "../components/PrivateRoute";
|
||||||
selectCurrentUser,
|
|
||||||
} from "../redux/user/user.selectors";
|
|
||||||
import PrivateRoute from "../utils/private-route";
|
|
||||||
import "./App.styles.scss";
|
import "./App.styles.scss";
|
||||||
|
import handleBeta from "../utils/betaHandler";
|
||||||
|
import Eula from "../components/eula/eula.component";
|
||||||
|
|
||||||
const ResetPassword = lazy(() =>
|
const ResetPassword = lazy(() =>
|
||||||
import("../pages/reset-password/reset-password.component")
|
import("../pages/reset-password/reset-password.component")
|
||||||
);
|
);
|
||||||
const ManagePage = lazy(() => import("../pages/manage/manage.page.container"));
|
const ManagePage = lazy(() => import("../pages/manage/manage.page.container"));
|
||||||
const SignInPage = lazy(() => import("../pages/sign-in/sign-in.page"));
|
const SignInPage = lazy(() => import("../pages/sign-in/sign-in.page"));
|
||||||
|
|
||||||
const CsiPage = lazy(() => import("../pages/csi/csi.container.page"));
|
const CsiPage = lazy(() => import("../pages/csi/csi.container.page"));
|
||||||
const MobilePaymentContainer = lazy(() =>
|
const MobilePaymentContainer = lazy(() =>
|
||||||
import("../pages/mobile-payment/mobile-payment.container")
|
import("../pages/mobile-payment/mobile-payment.container")
|
||||||
);
|
);
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
currentUser: selectCurrentUser,
|
currentUser: selectCurrentUser,
|
||||||
online: selectOnline,
|
online: selectOnline,
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
|
currentEula: selectCurrentEula
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
checkUserSession: () => dispatch(checkUserSession()),
|
checkUserSession: () => dispatch(checkUserSession()),
|
||||||
setOnline: (isOnline) => dispatch(setOnline(isOnline)),
|
setOnline: (isOnline) => dispatch(setOnline(isOnline)),
|
||||||
});
|
});
|
||||||
|
|
||||||
export function App({
|
export function App({bodyshop, checkUserSession, currentUser, online, setOnline, currentEula}) {
|
||||||
bodyshop,
|
const client = useSplitClient().client;
|
||||||
checkUserSession,
|
const [listenersAdded, setListenersAdded] = useState(false)
|
||||||
currentUser,
|
const {t} = useTranslation();
|
||||||
online,
|
|
||||||
setOnline,
|
|
||||||
}) {
|
|
||||||
const client = useClient();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!navigator.onLine) {
|
|
||||||
setOnline(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
checkUserSession();
|
useEffect(() => {
|
||||||
}, [checkUserSession, setOnline]);
|
if (!navigator.onLine) {
|
||||||
|
setOnline(false);
|
||||||
//const b = Grid.useBreakpoint();
|
|
||||||
// console.log("Breakpoints:", b);
|
|
||||||
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
window.addEventListener("offline", function (e) {
|
|
||||||
setOnline(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener("online", function (e) {
|
|
||||||
setOnline(true);
|
|
||||||
});
|
|
||||||
useEffect(() => {
|
|
||||||
if (currentUser.authorized && bodyshop) {
|
|
||||||
client.setAttribute("imexshopid", bodyshop.imexshopid);
|
|
||||||
|
|
||||||
if (client.getTreatment("LogRocket_Tracking") === "on") {
|
|
||||||
console.log("LR Start");
|
|
||||||
LogRocket.init("gvfvfw/bodyshopapp");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [bodyshop, client, currentUser.authorized]);
|
|
||||||
|
|
||||||
if (currentUser.authorized === null) {
|
|
||||||
return <LoadingSpinner message={t("general.labels.loggingin")} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!online)
|
|
||||||
return (
|
|
||||||
<Result
|
|
||||||
status="warning"
|
|
||||||
title={t("general.labels.nointernet")}
|
|
||||||
subTitle={t("general.labels.nointernet_sub")}
|
|
||||||
extra={
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
onClick={() => {
|
|
||||||
window.location.reload();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("general.actions.refresh")}
|
|
||||||
</Button>
|
|
||||||
}
|
}
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
checkUserSession();
|
||||||
<Switch>
|
}, [checkUserSession, setOnline]);
|
||||||
<Suspense fallback={<LoadingSpinner message="ImEX Online" />}>
|
|
||||||
<ErrorBoundary>
|
//const b = Grid.useBreakpoint();
|
||||||
<Route exact path="/" component={LandingPage} />
|
// console.log("Breakpoints:", b);
|
||||||
</ErrorBoundary>
|
|
||||||
<ErrorBoundary>
|
// Associate event listeners, memoize to prevent multiple listeners being added
|
||||||
<Route exact path="/signin" component={SignInPage} />
|
useEffect(() => {
|
||||||
</ErrorBoundary>
|
const offlineListener = (e) => {
|
||||||
<ErrorBoundary>
|
setOnline(false);
|
||||||
<Route exact path="/resetpassword" component={ResetPassword} />
|
}
|
||||||
</ErrorBoundary>
|
|
||||||
<ErrorBoundary>
|
const onlineListener = (e) => {
|
||||||
<Route exact path="/csi/:surveyId" component={CsiPage} />
|
setOnline(true);
|
||||||
</ErrorBoundary>
|
}
|
||||||
<ErrorBoundary>
|
|
||||||
<Route exact path="/disclaimer" component={DisclaimerPage} />
|
if (!listenersAdded) {
|
||||||
</ErrorBoundary>
|
console.log('Added events for offline and online');
|
||||||
<ErrorBoundary>
|
window.addEventListener("offline", offlineListener);
|
||||||
<Route
|
window.addEventListener("online", onlineListener);
|
||||||
exact
|
setListenersAdded(true);
|
||||||
path="/mp/:paymentIs"
|
}
|
||||||
component={MobilePaymentContainer}
|
|
||||||
/>
|
return () => {
|
||||||
</ErrorBoundary>
|
window.removeEventListener("offline", offlineListener);
|
||||||
<ErrorBoundary>
|
window.removeEventListener("online", onlineListener);
|
||||||
<PrivateRoute
|
}
|
||||||
isAuthorized={currentUser.authorized}
|
}, [setOnline, listenersAdded]);
|
||||||
path="/manage"
|
|
||||||
component={ManagePage}
|
useEffect(() => {
|
||||||
/>
|
if (currentUser.authorized && bodyshop) {
|
||||||
</ErrorBoundary>
|
client.setAttribute("imexshopid", bodyshop.imexshopid);
|
||||||
<ErrorBoundary>
|
|
||||||
<PrivateRoute
|
if (
|
||||||
isAuthorized={currentUser.authorized}
|
client.getTreatment("LogRocket_Tracking") === "on" ||
|
||||||
path="/tech"
|
window.location.hostname === 'beta.imex.online'
|
||||||
component={TechPageContainer}
|
) {
|
||||||
/>
|
console.log("LR Start");
|
||||||
</ErrorBoundary>
|
LogRocket.init("gvfvfw/bodyshopapp");
|
||||||
<ErrorBoundary>
|
}
|
||||||
<PrivateRoute
|
}
|
||||||
isAuthorized={currentUser.authorized}
|
}, [bodyshop, client, currentUser.authorized]);
|
||||||
path="/edit"
|
|
||||||
component={DocumentEditorContainer}
|
if (currentUser.authorized === null) {
|
||||||
/>
|
return <LoadingSpinner message={t("general.labels.loggingin")}/>;
|
||||||
</ErrorBoundary>
|
}
|
||||||
</Suspense>
|
|
||||||
</Switch>
|
handleBeta();
|
||||||
);
|
|
||||||
|
if (!online)
|
||||||
|
return (
|
||||||
|
<Result
|
||||||
|
status="warning"
|
||||||
|
title={t("general.labels.nointernet")}
|
||||||
|
subTitle={t("general.labels.nointernet_sub")}
|
||||||
|
extra={
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={() => {
|
||||||
|
window.location.reload();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("general.actions.refresh")}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (currentEula && !currentUser.eulaIsAccepted) {
|
||||||
|
return <Eula/>
|
||||||
|
}
|
||||||
|
|
||||||
|
// Any route that is not assigned and matched will default to the Landing Page component
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<LoadingSpinner message="ImEX Online"/>}>
|
||||||
|
<Routes>
|
||||||
|
<Route path="*" element={<ErrorBoundary><LandingPage/></ErrorBoundary>}/>
|
||||||
|
<Route path="/signin" element={<ErrorBoundary><SignInPage/></ErrorBoundary>}/>
|
||||||
|
<Route path="/resetpassword" element={<ErrorBoundary><ResetPassword/></ErrorBoundary>}/>
|
||||||
|
<Route path="/csi/:surveyId" element={<ErrorBoundary><CsiPage/></ErrorBoundary>}/>
|
||||||
|
<Route path="/disclaimer" element={<ErrorBoundary><DisclaimerPage/></ErrorBoundary>}/>
|
||||||
|
<Route path="/mp/:paymentIs" element={<ErrorBoundary><MobilePaymentContainer/></ErrorBoundary>}/>
|
||||||
|
<Route path="/manage/*"
|
||||||
|
element={<ErrorBoundary><PrivateRoute isAuthorized={currentUser.authorized}/></ErrorBoundary>}>
|
||||||
|
<Route path="*" element={<ManagePage/>}/>
|
||||||
|
</Route>
|
||||||
|
<Route path="/tech/*"
|
||||||
|
element={<ErrorBoundary><PrivateRoute isAuthorized={currentUser.authorized}/></ErrorBoundary>}>
|
||||||
|
<Route path="*" element={<TechPageContainer/>}/>
|
||||||
|
</Route>
|
||||||
|
<Route path="/edit/*" element={<PrivateRoute isAuthorized={currentUser.authorized}/>}>
|
||||||
|
<Route path="*" element={<DocumentEditorContainer/>}/>
|
||||||
|
</Route>
|
||||||
|
</Routes>
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(App);
|
export default connect(mapStateToProps, mapDispatchToProps)(App);
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
//Global Styles.
|
//Global Styles.
|
||||||
@import "react-big-calendar/lib/sass/styles";
|
@import "react-big-calendar/lib/sass/styles";
|
||||||
|
|
||||||
|
.ant-menu-item-divider {
|
||||||
|
border-bottom: 1px solid #74695c !important;
|
||||||
|
}
|
||||||
|
|
||||||
.imex-table-header {
|
.imex-table-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -143,8 +147,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.job-line-manual {
|
||||||
//Update row highlighting on production board.
|
color: tomato;
|
||||||
.ant-table-tbody > tr.ant-table-row:hover > td {
|
font-style: italic;
|
||||||
background: #eaeaea !important;
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.ant-table-tbody > tr.ant-table-row:nth-child(2n) > td {
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rowWithColor > td {
|
||||||
|
background-color: var(--bgColor) !important;
|
||||||
}
|
}
|
||||||
60
client/src/App/themeProvider.js
Normal file
60
client/src/App/themeProvider.js
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import {defaultsDeep} from "lodash";
|
||||||
|
import {theme} from "antd";
|
||||||
|
|
||||||
|
const {defaultAlgorithm, darkAlgorithm} = theme;
|
||||||
|
|
||||||
|
let isDarkMode = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default theme
|
||||||
|
* @type {{components: {Menu: {itemDividerBorderColor: string}}}}
|
||||||
|
*/
|
||||||
|
const defaultTheme = {
|
||||||
|
components: {
|
||||||
|
Table: {
|
||||||
|
rowHoverBg: '#e7f3ff',
|
||||||
|
rowSelectedBg: '#e6f7ff',
|
||||||
|
headerSortHoverBg: 'transparent',
|
||||||
|
},
|
||||||
|
Menu: {
|
||||||
|
darkItemHoverBg: '#1677ff',
|
||||||
|
itemHoverBg: '#1677ff',
|
||||||
|
horizontalItemHoverBg: '#1677ff',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
token: {
|
||||||
|
colorPrimary: '#1677ff'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Development theme
|
||||||
|
* @type {{components: {Menu: {itemHoverBg: string, darkItemHoverBg: string, horizontalItemHoverBg: string}}, token: {colorPrimary: string}}}
|
||||||
|
*/
|
||||||
|
const devTheme = {
|
||||||
|
components: {
|
||||||
|
Menu: {
|
||||||
|
darkItemHoverBg: '#a51d1d',
|
||||||
|
itemHoverBg: '#a51d1d',
|
||||||
|
horizontalItemHoverBg: '#a51d1d',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
token: {
|
||||||
|
colorPrimary: '#a51d1d'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Production theme
|
||||||
|
* @type {{components: {Menu: {itemHoverBg: string, darkItemHoverBg: string, horizontalItemHoverBg: string}}, token: {colorPrimary: string}}}
|
||||||
|
*/
|
||||||
|
const prodTheme = {};
|
||||||
|
|
||||||
|
const currentTheme = import.meta.env.DEV ? devTheme
|
||||||
|
: prodTheme;
|
||||||
|
|
||||||
|
const finaltheme = {
|
||||||
|
algorithm: isDarkMode ? darkAlgorithm : defaultAlgorithm,
|
||||||
|
...defaultsDeep(currentTheme, defaultTheme)
|
||||||
|
}
|
||||||
|
export default finaltheme;
|
||||||
17
client/src/components/PrivateRoute.js
Normal file
17
client/src/components/PrivateRoute.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import React, {useEffect} from "react";
|
||||||
|
import {Outlet, useLocation, useNavigate} from "react-router-dom";
|
||||||
|
|
||||||
|
function PrivateRoute({component: Component, isAuthorized, ...rest}) {
|
||||||
|
const location = useLocation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isAuthorized) {
|
||||||
|
navigate(`/signin?redirect=${location.pathname}`);
|
||||||
|
}
|
||||||
|
}, [isAuthorized, navigate,location]);
|
||||||
|
|
||||||
|
return <Outlet/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PrivateRoute;
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
import {
|
|
||||||
PaymentRequestButtonElement,
|
|
||||||
useStripe,
|
|
||||||
} from "@stripe/react-stripe-js";
|
|
||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import { connect } from "react-redux";
|
|
||||||
import { createStructuredSelector } from "reselect";
|
|
||||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
|
||||||
import { setEmailOptions } from "../../redux/email/email.actions";
|
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
|
||||||
import { TemplateList } from "../../utils/TemplateConstants";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
|
||||||
bodyshop: selectBodyshop,
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
|
||||||
setEmailOptions: (e) => dispatch(setEmailOptions(e)),
|
|
||||||
});
|
|
||||||
|
|
||||||
function Test({ bodyshop, setEmailOptions }) {
|
|
||||||
const stripe = useStripe();
|
|
||||||
|
|
||||||
const [paymentRequest, setPaymentRequest] = useState(null);
|
|
||||||
useEffect(() => {
|
|
||||||
if (stripe) {
|
|
||||||
const pr = stripe.paymentRequest({
|
|
||||||
country: "CA",
|
|
||||||
displayItems: [{ label: "Deductible", amount: 1099 }],
|
|
||||||
currency: "cad",
|
|
||||||
total: {
|
|
||||||
label: "Demo total",
|
|
||||||
amount: 1099,
|
|
||||||
},
|
|
||||||
requestPayerName: true,
|
|
||||||
requestPayerEmail: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check the availability of the Payment Request API.
|
|
||||||
pr.canMakePayment().then((result) => {
|
|
||||||
if (result) {
|
|
||||||
setPaymentRequest(pr);
|
|
||||||
} else {
|
|
||||||
// var details = {
|
|
||||||
// total: { label: "", amount: { currency: "CAD", value: "0.00" } },
|
|
||||||
// };
|
|
||||||
new PaymentRequest(
|
|
||||||
[{ supportedMethods: ["basic-card"] }],
|
|
||||||
{}
|
|
||||||
// details
|
|
||||||
).show();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [stripe]);
|
|
||||||
|
|
||||||
if (paymentRequest) {
|
|
||||||
return (
|
|
||||||
<div style={{ height: "300px" }}>
|
|
||||||
<PaymentRequestButtonElement options={{ paymentRequest }} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
setEmailOptions({
|
|
||||||
messageOptions: {
|
|
||||||
to: ["patrickwf@gmail.com"],
|
|
||||||
replyTo: bodyshop.email,
|
|
||||||
},
|
|
||||||
template: {
|
|
||||||
name: TemplateList().parts_order.key,
|
|
||||||
variables: {
|
|
||||||
id: "a7c2d4e1-f519-42a9-a071-c48cf0f22979",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
send email
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
logImEXEvent("IMEXEVENT", { somethignArThare: 5 });
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Log an ImEX Event.
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(Test);
|
|
||||||
63
client/src/components/_test/payment_response.json
Normal file
63
client/src/components/_test/payment_response.json
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
{
|
||||||
|
"status": 24201299,
|
||||||
|
"custid": 19607899,
|
||||||
|
"paymentid": 24201299,
|
||||||
|
"response": "A",
|
||||||
|
"authcode": "498680",
|
||||||
|
"declinereason": "Approved",
|
||||||
|
"fee": 0,
|
||||||
|
"invoice": "",
|
||||||
|
"account": "john",
|
||||||
|
"amount": 1000,
|
||||||
|
"amountincludesfee": false,
|
||||||
|
"total": 1000,
|
||||||
|
"paymenttype": "C",
|
||||||
|
"methodhint": "VI ***1111",
|
||||||
|
"cardbrand": "Visa",
|
||||||
|
"cardnumdisplay": "***1111",
|
||||||
|
"receiptelements": {
|
||||||
|
"authcode": "498680",
|
||||||
|
"cust_srv_ph_num": "1-555-555-5555",
|
||||||
|
"rcpt_pg_ftr_txt": "Thank You\nPlease Come Again",
|
||||||
|
"rcpt_currency": "USD",
|
||||||
|
"responsecode": "A",
|
||||||
|
"rcpt_pay_mthd": "Visa",
|
||||||
|
"transid": "C00 915799",
|
||||||
|
"merch_disp_nm": "CP Devel Test",
|
||||||
|
"rcpt_input_mthd": "Keyed",
|
||||||
|
"rcpt_pg_hdr_txt": "Welcome!",
|
||||||
|
"rcpt_tran_time": "Thursday February 23 2023, 11:25:36 pm +08",
|
||||||
|
"rcpt_trans_type": "Normal Transaction (Sale)",
|
||||||
|
"message": "Approved",
|
||||||
|
"rcpt_dba_addr": "1234 Storefront Ave\nSome City, UT 84111",
|
||||||
|
"avsdata": "N",
|
||||||
|
"receiptrequirements": "S",
|
||||||
|
"rcpt_cardnum": "************1111",
|
||||||
|
"cv2result": "M",
|
||||||
|
"rfnd_policy_txt": "<b>No Refunds</b>\nStore Credit Only",
|
||||||
|
"labels": {
|
||||||
|
"tranref": "REF#",
|
||||||
|
"tid": "TID",
|
||||||
|
"validationcode": "ValCode",
|
||||||
|
"emvapplicationid": "AID",
|
||||||
|
"emvatc": "ATC",
|
||||||
|
"rcpt_pay_mthd": "Pay Method",
|
||||||
|
"transid": "TransID",
|
||||||
|
"rcpt_input_mthd": "IMode",
|
||||||
|
"emvtsi": "TSI",
|
||||||
|
"emvac": "AC",
|
||||||
|
"rcpt_trans_type": "TranType",
|
||||||
|
"emvapplicationname": "PApp",
|
||||||
|
"visarewards": "RewardsProg"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"receipttoken": "H4sIAAAAAAAAACXMTQuCMBgA4P/ynh3tw_3dBI/ipQ8NOtRN53QiblpBRfTfCzo/8LwhxGAdZCCwFYoJJFQjI2kvHdGu74lVkgmrWyWNhASW5jW7cB87yHjKKePGJODnxrrnMl7dDTKmEJlSOqV/_N30XPpyj2Eddq57_KKZ8FLzmh_G1VQnVfhjiXGK1XYTc/h8AVOkf4qUAAAA",
|
||||||
|
"call": "card_payment",
|
||||||
|
"nonce": "488b5568-b5c1-4f38-8b2f-3b050f3abb11P",
|
||||||
|
"hmac": "JyPAJ9Yx0SlYBTtqns1OxAFRt+xF3l2UiLPO5zTDRBE=",
|
||||||
|
"paymentreferenceid": "C19607899P24201299",
|
||||||
|
"cardnum": "...1111",
|
||||||
|
"email": "",
|
||||||
|
"nameOnCard": "John Allen",
|
||||||
|
"cardType": "visa"
|
||||||
|
}
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
|
|
||||||
export default function Test() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<QboAuthorizeComponent />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
31
client/src/components/_test/test.page.jsx
Normal file
31
client/src/components/_test/test.page.jsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { Button } from "antd";
|
||||||
|
import React from "react";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||||
|
|
||||||
|
const mapStateToProps = createStructuredSelector({});
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
setRefundPaymentContext: (context) =>
|
||||||
|
dispatch(setModalContext({ context: context, modal: "refund_payment" })),
|
||||||
|
});
|
||||||
|
|
||||||
|
function Test({ setRefundPaymentContext, refundPaymentModal }) {
|
||||||
|
console.log("refundPaymentModal", refundPaymentModal);
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
onClick={() =>
|
||||||
|
setRefundPaymentContext({
|
||||||
|
context: {},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Open Modal
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(Test);
|
||||||
@@ -15,6 +15,7 @@ import { createStructuredSelector } from "reselect";
|
|||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
|
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
|
||||||
import BillMarkSelectedExported from "../payable-mark-selected-exported/payable-mark-selected-exported.component";
|
import BillMarkSelectedExported from "../payable-mark-selected-exported/payable-mark-selected-exported.component";
|
||||||
|
import {pageLimit} from "../../utils/config";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -210,7 +211,7 @@ export function AccountingPayablesTableComponent({
|
|||||||
<Table
|
<Table
|
||||||
loading={loading}
|
loading={loading}
|
||||||
dataSource={dataSource}
|
dataSource={dataSource}
|
||||||
pagination={{ position: "top", pageSize: 50 }}
|
pagination={{ position: "top", pageSize: pageLimit }}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
onChange={handleTableChange}
|
onChange={handleTableChange}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import PaymentExportButton from "../payment-export-button/payment-export-button.
|
|||||||
import PaymentMarkSelectedExported from "../payment-mark-selected-exported/payment-mark-selected-exported.component";
|
import PaymentMarkSelectedExported from "../payment-mark-selected-exported/payment-mark-selected-exported.component";
|
||||||
import PaymentsExportAllButton from "../payments-export-all-button/payments-export-all-button.component";
|
import PaymentsExportAllButton from "../payments-export-all-button/payments-export-all-button.component";
|
||||||
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
|
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
|
||||||
|
import {pageLimit} from "../../utils/config";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -107,11 +108,6 @@ export function AccountingPayablesTableComponent({
|
|||||||
dataIndex: "transactionid",
|
dataIndex: "transactionid",
|
||||||
key: "transactionid",
|
key: "transactionid",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: t("payments.fields.stripeid"),
|
|
||||||
dataIndex: "stripeid",
|
|
||||||
key: "stripeid",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: t("payments.fields.created_at"),
|
title: t("payments.fields.created_at"),
|
||||||
dataIndex: "created_at",
|
dataIndex: "created_at",
|
||||||
@@ -214,7 +210,7 @@ export function AccountingPayablesTableComponent({
|
|||||||
<Table
|
<Table
|
||||||
loading={loading}
|
loading={loading}
|
||||||
dataSource={dataSource}
|
dataSource={dataSource}
|
||||||
pagination={{ position: "top", pageSize: 50 }}
|
pagination={{ position: "top", pageSize: pageLimit }}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
onChange={handleTableChange}
|
onChange={handleTableChange}
|
||||||
|
|||||||
@@ -2,5 +2,5 @@ import { Alert } from "antd";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
export default function AlertComponent(props) {
|
export default function AlertComponent(props) {
|
||||||
return <Alert {...props} />;
|
return <Alert {...props} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export function AllocationsAssignmentComponent({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover content={popContent} visible={visibility}>
|
<Popover content={popContent} open={visibility}>
|
||||||
<Button onClick={() => setVisibility(true)}>
|
<Button onClick={() => setVisibility(true)}>
|
||||||
{t("allocations.actions.assign")}
|
{t("allocations.actions.assign")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ export default connect(
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover content={popContent} visible={visibility}>
|
<Popover content={popContent} open={visibility}>
|
||||||
<Button disabled={disabled} onClick={() => setVisibility(true)}>
|
<Button disabled={disabled} onClick={() => setVisibility(true)}>
|
||||||
{t("allocations.actions.assign")}
|
{t("allocations.actions.assign")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { alphaSort } from "../../utils/sorters";
|
|||||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import AuditTrailValuesComponent from "../audit-trail-values/audit-trail-values.component";
|
import AuditTrailValuesComponent from "../audit-trail-values/audit-trail-values.component";
|
||||||
|
import {pageLimit} from "../../utils/config";
|
||||||
|
|
||||||
export default function AuditTrailListComponent({ loading, data }) {
|
export default function AuditTrailListComponent({ loading, data }) {
|
||||||
const [state, setState] = useState({
|
const [state, setState] = useState({
|
||||||
@@ -74,7 +75,7 @@ export default function AuditTrailListComponent({ loading, data }) {
|
|||||||
<Table
|
<Table
|
||||||
{...formItemLayout}
|
{...formItemLayout}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
pagination={{ position: "top", defaultPageSize: 25 }}
|
pagination={{ position: "top", defaultPageSize: pageLimit }}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
dataSource={data}
|
dataSource={data}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import React, { useState } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||||
import { alphaSort } from "../../utils/sorters";
|
import { alphaSort } from "../../utils/sorters";
|
||||||
|
import {pageLimit} from "../../utils/config";
|
||||||
|
|
||||||
export default function EmailAuditTrailListComponent({ loading, data }) {
|
export default function EmailAuditTrailListComponent({ loading, data }) {
|
||||||
const [state, setState] = useState({
|
const [state, setState] = useState({
|
||||||
@@ -53,7 +54,7 @@ export default function EmailAuditTrailListComponent({ loading, data }) {
|
|||||||
<Table
|
<Table
|
||||||
{...formItemLayout}
|
{...formItemLayout}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
pagination={{ position: "top", defaultPageSize: 25 }}
|
pagination={{ position: "top", defaultPageSize: pageLimit }}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
dataSource={data}
|
dataSource={data}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { DELETE_BILL } from "../../graphql/bills.queries";
|
import { DELETE_BILL } from "../../graphql/bills.queries";
|
||||||
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
|
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
|
||||||
|
|
||||||
export default function BillDeleteButton({ bill }) {
|
export default function BillDeleteButton({ bill, callback }) {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [deleteBill] = useMutation(DELETE_BILL);
|
const [deleteBill] = useMutation(DELETE_BILL);
|
||||||
@@ -36,6 +36,8 @@ export default function BillDeleteButton({ bill }) {
|
|||||||
|
|
||||||
if (!!!result.errors) {
|
if (!!!result.errors) {
|
||||||
notification["success"]({ message: t("bills.successes.deleted") });
|
notification["success"]({ message: t("bills.successes.deleted") });
|
||||||
|
|
||||||
|
if (callback && typeof callback === "function") callback(bill.id);
|
||||||
} else {
|
} else {
|
||||||
//Check if it's an fkey violation.
|
//Check if it's an fkey violation.
|
||||||
const error = JSON.stringify(result.errors);
|
const error = JSON.stringify(result.errors);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useMutation, useQuery } from "@apollo/client";
|
import {useMutation, useQuery} from "@apollo/client";
|
||||||
import { Button, Form, PageHeader, Popconfirm, Space } from "antd";
|
import {Button, Form, Popconfirm, Space} from "antd";
|
||||||
import moment from "moment";
|
import dayjs from "../../utils/day";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -20,148 +20,148 @@ import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
|||||||
import AlertComponent from "../alert/alert.component";
|
import AlertComponent from "../alert/alert.component";
|
||||||
import BillFormContainer from "../bill-form/bill-form.container";
|
import BillFormContainer from "../bill-form/bill-form.container";
|
||||||
import BillMarkExportedButton from "../bill-mark-exported-button/bill-mark-exported-button.component";
|
import BillMarkExportedButton from "../bill-mark-exported-button/bill-mark-exported-button.component";
|
||||||
|
import BillPrintButton from "../bill-print-button/bill-print-button.component";
|
||||||
import BillReeportButtonComponent from "../bill-reexport-button/bill-reexport-button.component";
|
import BillReeportButtonComponent from "../bill-reexport-button/bill-reexport-button.component";
|
||||||
import JobDocumentsGallery from "../jobs-documents-gallery/jobs-documents-gallery.container";
|
import JobDocumentsGallery from "../jobs-documents-gallery/jobs-documents-gallery.container";
|
||||||
import JobsDocumentsLocalGallery from "../jobs-documents-local-gallery/jobs-documents-local-gallery.container";
|
import JobsDocumentsLocalGallery from "../jobs-documents-local-gallery/jobs-documents-local-gallery.container";
|
||||||
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
||||||
import BillDetailEditReturn from "./bill-detail-edit-return.component";
|
import BillDetailEditReturn from "./bill-detail-edit-return.component";
|
||||||
|
import {PageHeader} from "@ant-design/pro-layout";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
setPartsOrderContext: (context) =>
|
setPartsOrderContext: (context) =>
|
||||||
dispatch(setModalContext({ context: context, modal: "partsOrder" })),
|
dispatch(setModalContext({context: context, modal: "partsOrder"})),
|
||||||
insertAuditTrail: ({ jobid, operation }) =>
|
insertAuditTrail: ({jobid, operation}) =>
|
||||||
dispatch(insertAuditTrail({ jobid, operation })),
|
dispatch(insertAuditTrail({jobid, operation})),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
mapStateToProps,
|
mapStateToProps,
|
||||||
mapDispatchToProps
|
mapDispatchToProps
|
||||||
)(BillDetailEditcontainer);
|
)(BillDetailEditcontainer);
|
||||||
|
|
||||||
export function BillDetailEditcontainer({
|
export function BillDetailEditcontainer({setPartsOrderContext, insertAuditTrail, bodyshop,}) {
|
||||||
setPartsOrderContext,
|
const search = queryString.parse(useLocation().search);
|
||||||
insertAuditTrail,
|
|
||||||
bodyshop,
|
|
||||||
}) {
|
|
||||||
const search = queryString.parse(useLocation().search);
|
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const {t} = useTranslation();
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [visible, setVisible] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [updateLoading, setUpdateLoading] = useState(false);
|
const [updateLoading, setUpdateLoading] = useState(false);
|
||||||
const [update_bill] = useMutation(UPDATE_BILL);
|
const [update_bill] = useMutation(UPDATE_BILL);
|
||||||
const [insertBillLine] = useMutation(INSERT_NEW_BILL_LINES);
|
const [insertBillLine] = useMutation(INSERT_NEW_BILL_LINES);
|
||||||
const [updateBillLine] = useMutation(UPDATE_BILL_LINE);
|
const [updateBillLine] = useMutation(UPDATE_BILL_LINE);
|
||||||
const [deleteBillLine] = useMutation(DELETE_BILL_LINE);
|
const [deleteBillLine] = useMutation(DELETE_BILL_LINE);
|
||||||
|
|
||||||
const { loading, error, data, refetch } = useQuery(QUERY_BILL_BY_PK, {
|
const {loading, error, data, refetch} = useQuery(QUERY_BILL_BY_PK, {
|
||||||
variables: { billid: search.billid },
|
variables: {billid: search.billid},
|
||||||
skip: !!!search.billid,
|
skip: !!!search.billid,
|
||||||
fetchPolicy: "network-only",
|
fetchPolicy: "network-only",
|
||||||
nextFetchPolicy: "network-only",
|
nextFetchPolicy: "network-only",
|
||||||
});
|
|
||||||
|
|
||||||
const handleSave = () => {
|
|
||||||
//It's got a previously deducted bill line!
|
|
||||||
if (
|
|
||||||
data.bills_by_pk.billlines.filter((b) => b.deductedfromlbr).length > 0 ||
|
|
||||||
form.getFieldValue("billlines").filter((b) => b.deductedfromlbr).length >
|
|
||||||
0
|
|
||||||
)
|
|
||||||
setVisible(true);
|
|
||||||
else {
|
|
||||||
form.submit();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFinish = async (values) => {
|
|
||||||
setUpdateLoading(true);
|
|
||||||
//let adjustmentsToInsert = {};
|
|
||||||
|
|
||||||
const { billlines, upload, ...bill } = values;
|
|
||||||
const updates = [];
|
|
||||||
updates.push(
|
|
||||||
update_bill({
|
|
||||||
variables: { billId: search.billid, bill: bill },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
billlines.forEach((l) => {
|
|
||||||
delete l.selected;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
//Find bill lines that were deleted.
|
// ... rest of the code remains the same
|
||||||
const deletedJobLines = [];
|
|
||||||
|
|
||||||
data.bills_by_pk.billlines.forEach((a) => {
|
const handleSave = () => {
|
||||||
const matchingRecord = billlines.find((b) => b.id === a.id);
|
//It's got a previously deducted bill line!
|
||||||
if (!matchingRecord) {
|
if (
|
||||||
deletedJobLines.push(a);
|
data.bills_by_pk.billlines.filter((b) => b.deductedfromlbr).length > 0 ||
|
||||||
}
|
form.getFieldValue("billlines").filter((b) => b.deductedfromlbr).length >
|
||||||
});
|
0
|
||||||
|
)
|
||||||
|
setOpen(true);
|
||||||
|
else {
|
||||||
|
form.submit();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
deletedJobLines.forEach((d) => {
|
const handleFinish = async (values) => {
|
||||||
updates.push(deleteBillLine({ variables: { id: d.id } }));
|
setUpdateLoading(true);
|
||||||
});
|
//let adjustmentsToInsert = {};
|
||||||
|
|
||||||
billlines.forEach((billline) => {
|
const {billlines, upload, ...bill} = values;
|
||||||
const { deductedfromlbr, inventories, jobline, ...il } = billline;
|
const updates = [];
|
||||||
delete il.__typename;
|
|
||||||
|
|
||||||
if (il.id) {
|
|
||||||
updates.push(
|
updates.push(
|
||||||
updateBillLine({
|
update_bill({
|
||||||
variables: {
|
variables: {billId: search.billid, bill: bill},
|
||||||
billLineId: il.id,
|
})
|
||||||
billLine: {
|
|
||||||
...il,
|
|
||||||
deductedfromlbr: deductedfromlbr,
|
|
||||||
joblineid: il.joblineid === "noline" ? null : il.joblineid,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
//It's a new line, have to insert it.
|
|
||||||
updates.push(
|
|
||||||
insertBillLine({
|
|
||||||
variables: {
|
|
||||||
billLines: [
|
|
||||||
{
|
|
||||||
...il,
|
|
||||||
deductedfromlbr: deductedfromlbr,
|
|
||||||
billid: search.billid,
|
|
||||||
joblineid: il.joblineid === "noline" ? null : il.joblineid,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await Promise.all(updates);
|
billlines.forEach((l) => {
|
||||||
|
delete l.selected;
|
||||||
|
});
|
||||||
|
|
||||||
insertAuditTrail({
|
//Find bill lines that were deleted.
|
||||||
jobid: bill.jobid,
|
const deletedJobLines = [];
|
||||||
billid: search.billid,
|
|
||||||
operation: AuditTrailMapping.billupdated(bill.invoice_number),
|
|
||||||
});
|
|
||||||
|
|
||||||
await refetch();
|
data.bills_by_pk.billlines.forEach((a) => {
|
||||||
form.setFieldsValue(transformData(data));
|
const matchingRecord = billlines.find((b) => b.id === a.id);
|
||||||
form.resetFields();
|
if (!matchingRecord) {
|
||||||
setVisible(false);
|
deletedJobLines.push(a);
|
||||||
setUpdateLoading(false);
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
deletedJobLines.forEach((d) => {
|
||||||
if (!search.billid) return <></>; //<div>{t("bills.labels.noneselected")}</div>;
|
updates.push(deleteBillLine({variables: {id: d.id}}));
|
||||||
|
});
|
||||||
|
|
||||||
const exported = data && data.bills_by_pk && data.bills_by_pk.exported;
|
billlines.forEach((billline) => {
|
||||||
|
const {deductedfromlbr, inventories, jobline, ...il} = billline;
|
||||||
|
delete il.__typename;
|
||||||
|
|
||||||
|
if (il.id) {
|
||||||
|
updates.push(
|
||||||
|
updateBillLine({
|
||||||
|
variables: {
|
||||||
|
billLineId: il.id,
|
||||||
|
billLine: {
|
||||||
|
...il,
|
||||||
|
deductedfromlbr: deductedfromlbr,
|
||||||
|
joblineid: il.joblineid === "noline" ? null : il.joblineid,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
//It's a new line, have to insert it.
|
||||||
|
updates.push(
|
||||||
|
insertBillLine({
|
||||||
|
variables: {
|
||||||
|
billLines: [
|
||||||
|
{
|
||||||
|
...il,
|
||||||
|
deductedfromlbr: deductedfromlbr,
|
||||||
|
billid: search.billid,
|
||||||
|
joblineid: il.joblineid === "noline" ? null : il.joblineid,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(updates);
|
||||||
|
|
||||||
|
insertAuditTrail({
|
||||||
|
jobid: bill.jobid,
|
||||||
|
billid: search.billid,
|
||||||
|
operation: AuditTrailMapping.billupdated(bill.invoice_number),
|
||||||
|
});
|
||||||
|
|
||||||
|
await refetch();
|
||||||
|
form.setFieldsValue(transformData(data));
|
||||||
|
form.resetFields();
|
||||||
|
setOpen(false);
|
||||||
|
setUpdateLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (error) return <AlertComponent message={error.message} type="error"/>;
|
||||||
|
if (!search.billid) return <></>; //<div>{t("bills.labels.noneselected")}</div>;
|
||||||
|
|
||||||
|
const exported = data && data.bills_by_pk && data.bills_by_pk.exported;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -176,11 +176,11 @@ export function BillDetailEditcontainer({
|
|||||||
extra={
|
extra={
|
||||||
<Space>
|
<Space>
|
||||||
<BillDetailEditReturn data={data} />
|
<BillDetailEditReturn data={data} />
|
||||||
|
<BillPrintButton billid={search.billid} />
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
visible={visible}
|
open={open}
|
||||||
onConfirm={() => form.submit()}
|
onConfirm={() => form.submit()}
|
||||||
onCancel={() => setVisible(false)}
|
onCancel={() => setOpen(false)}
|
||||||
okButtonProps={{ loading: updateLoading }}
|
okButtonProps={{ loading: updateLoading }}
|
||||||
title={t("bills.labels.editadjwarning")}
|
title={t("bills.labels.editadjwarning")}
|
||||||
>
|
>
|
||||||
@@ -207,45 +207,45 @@ export function BillDetailEditcontainer({
|
|||||||
>
|
>
|
||||||
<BillFormContainer form={form} billEdit disabled={exported} />
|
<BillFormContainer form={form} billEdit disabled={exported} />
|
||||||
|
|
||||||
{bodyshop.uselocalmediaserver ? (
|
{bodyshop.uselocalmediaserver ? (
|
||||||
<JobsDocumentsLocalGallery
|
<JobsDocumentsLocalGallery
|
||||||
job={{ id: data ? data.bills_by_pk.jobid : null }}
|
job={{id: data ? data.bills_by_pk.jobid : null}}
|
||||||
invoice_number={data ? data.bills_by_pk.invoice_number : null}
|
invoice_number={data ? data.bills_by_pk.invoice_number : null}
|
||||||
vendorid={data ? data.bills_by_pk.vendorid : null}
|
vendorid={data ? data.bills_by_pk.vendorid : null}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<JobDocumentsGallery
|
<JobDocumentsGallery
|
||||||
jobId={data ? data.bills_by_pk.jobid : null}
|
jobId={data ? data.bills_by_pk.jobid : null}
|
||||||
billId={search.billid}
|
billId={search.billid}
|
||||||
documentsList={data ? data.bills_by_pk.documents : []}
|
documentsList={data ? data.bills_by_pk.documents : []}
|
||||||
billsCallback={refetch}
|
billsCallback={refetch}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
</Form>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</Form>
|
|
||||||
</>
|
</>
|
||||||
)}
|
);
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const transformData = (data) => {
|
const transformData = (data) => {
|
||||||
return data
|
return data
|
||||||
? {
|
? {
|
||||||
...data.bills_by_pk,
|
...data.bills_by_pk,
|
||||||
|
|
||||||
billlines: data.bills_by_pk.billlines.map((i) => {
|
billlines: data.bills_by_pk.billlines.map((i) => {
|
||||||
return {
|
return {
|
||||||
...i,
|
...i,
|
||||||
joblineid: !!i.joblineid ? i.joblineid : "noline",
|
joblineid: !!i.joblineid ? i.joblineid : "noline",
|
||||||
applicable_taxes: {
|
applicable_taxes: {
|
||||||
federal:
|
federal:
|
||||||
(i.applicable_taxes && i.applicable_taxes.federal) || false,
|
(i.applicable_taxes && i.applicable_taxes.federal) || false,
|
||||||
state: (i.applicable_taxes && i.applicable_taxes.state) || false,
|
state: (i.applicable_taxes && i.applicable_taxes.state) || false,
|
||||||
local: (i.applicable_taxes && i.applicable_taxes.local) || false,
|
local: (i.applicable_taxes && i.applicable_taxes.local) || false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
date: data.bills_by_pk ? moment(data.bills_by_pk.date) : null,
|
date: data.bills_by_pk ? dayjs(data.bills_by_pk.date) : null,
|
||||||
}
|
}
|
||||||
: {};
|
: {};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import queryString from "query-string";
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { useHistory, useLocation } from "react-router-dom";
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||||
@@ -33,10 +33,10 @@ export function BillDetailEditReturn({
|
|||||||
disabled,
|
disabled,
|
||||||
}) {
|
}) {
|
||||||
const search = queryString.parse(useLocation().search);
|
const search = queryString.parse(useLocation().search);
|
||||||
const history = useHistory();
|
const history = useNavigate();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [visible, setVisible] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const handleFinish = ({ billlines }) => {
|
const handleFinish = ({ billlines }) => {
|
||||||
const selectedLines = billlines.filter((l) => l.selected).map((l) => l.id);
|
const selectedLines = billlines.filter((l) => l.selected).map((l) => l.id);
|
||||||
@@ -67,18 +67,18 @@ export function BillDetailEditReturn({
|
|||||||
});
|
});
|
||||||
delete search.billid;
|
delete search.billid;
|
||||||
|
|
||||||
history.push({ search: queryString.stringify(search) });
|
history({ search: queryString.stringify(search) });
|
||||||
setVisible(false);
|
setOpen(false);
|
||||||
};
|
};
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (visible === false) form.resetFields();
|
if (open === false) form.resetFields();
|
||||||
}, [visible, form]);
|
}, [open, form]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Modal
|
<Modal
|
||||||
visible={visible}
|
open={open}
|
||||||
onCancel={() => setVisible(false)}
|
onCancel={() => setOpen(false)}
|
||||||
destroyOnClose
|
destroyOnClose
|
||||||
title={t("bills.actions.return")}
|
title={t("bills.actions.return")}
|
||||||
onOk={() => form.submit()}
|
onOk={() => form.submit()}
|
||||||
@@ -175,7 +175,7 @@ export function BillDetailEditReturn({
|
|||||||
<Button
|
<Button
|
||||||
disabled={data.bills_by_pk.is_credit_memo || disabled}
|
disabled={data.bills_by_pk.is_credit_memo || disabled}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setVisible(true);
|
setOpen(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("bills.actions.return")}
|
{t("bills.actions.return")}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { Drawer, Grid } from "antd";
|
import { Drawer, Grid } from "antd";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useHistory, useLocation } from "react-router-dom";
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
import BillDetailEditComponent from "./bill-detail-edit-component";
|
import BillDetailEditComponent from "./bill-detail-edit-component";
|
||||||
|
|
||||||
export default function BillDetailEditcontainer() {
|
export default function BillDetailEditcontainer() {
|
||||||
const search = queryString.parse(useLocation().search);
|
const search = queryString.parse(useLocation().search);
|
||||||
const history = useHistory();
|
const history = useNavigate();
|
||||||
|
|
||||||
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
|
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
|
||||||
.filter((screen) => !!screen[1])
|
.filter((screen) => !!screen[1])
|
||||||
@@ -29,10 +29,10 @@ export default function BillDetailEditcontainer() {
|
|||||||
width={drawerPercentage}
|
width={drawerPercentage}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
delete search.billid;
|
delete search.billid;
|
||||||
history.push({ search: queryString.stringify(search) });
|
history({ search: queryString.stringify(search) });
|
||||||
}}
|
}}
|
||||||
destroyOnClose
|
destroyOnClose
|
||||||
visible={search.billid}
|
open={search.billid}
|
||||||
>
|
>
|
||||||
<BillDetailEditComponent />
|
<BillDetailEditComponent />
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
import { useApolloClient, useMutation } from "@apollo/client";
|
import { useApolloClient, useMutation } from "@apollo/client";
|
||||||
import { Button, Checkbox, Form, Modal, notification, Space } from "antd";
|
import { Button, Checkbox, Form, Modal, Space, notification } from "antd";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import React, { useEffect, useState, useMemo } from "react";
|
import React, { useEffect, useMemo, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { INSERT_NEW_BILL } from "../../graphql/bills.queries";
|
import { INSERT_NEW_BILL } from "../../graphql/bills.queries";
|
||||||
|
import { UPDATE_INVENTORY_LINES } from "../../graphql/inventory.queries";
|
||||||
import { UPDATE_JOB_LINE } from "../../graphql/jobs-lines.queries";
|
import { UPDATE_JOB_LINE } from "../../graphql/jobs-lines.queries";
|
||||||
import {
|
import {
|
||||||
QUERY_JOB_LBR_ADJUSTMENTS,
|
QUERY_JOB_LBR_ADJUSTMENTS,
|
||||||
UPDATE_JOB,
|
UPDATE_JOB,
|
||||||
} from "../../graphql/jobs.queries";
|
} from "../../graphql/jobs.queries";
|
||||||
import { MUTATION_MARK_RETURN_RECEIVED } from "../../graphql/parts-orders.queries";
|
import { MUTATION_MARK_RETURN_RECEIVED } from "../../graphql/parts-orders.queries";
|
||||||
import { UPDATE_INVENTORY_LINES } from "../../graphql/inventory.queries";
|
|
||||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||||
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
||||||
import { selectBillEnterModal } from "../../redux/modals/modals.selectors";
|
import { selectBillEnterModal } from "../../redux/modals/modals.selectors";
|
||||||
@@ -20,15 +20,15 @@ import {
|
|||||||
selectBodyshop,
|
selectBodyshop,
|
||||||
selectCurrentUser,
|
selectCurrentUser,
|
||||||
} from "../../redux/user/user.selectors";
|
} from "../../redux/user/user.selectors";
|
||||||
import confirmDialog from "../../utils/asyncConfirm";
|
|
||||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||||
import BillFormContainer from "../bill-form/bill-form.container";
|
|
||||||
import { CalculateBillTotal } from "../bill-form/bill-form.totals.utility";
|
|
||||||
import { handleUpload } from "../documents-upload/documents-upload.utility";
|
|
||||||
import { handleUpload as handleLocalUpload } from "../documents-local-upload/documents-local-upload.utility";
|
|
||||||
import useLocalStorage from "../../utils/useLocalStorage";
|
|
||||||
import { GenerateDocument } from "../../utils/RenderTemplate";
|
import { GenerateDocument } from "../../utils/RenderTemplate";
|
||||||
import { TemplateList } from "../../utils/TemplateConstants";
|
import { TemplateList } from "../../utils/TemplateConstants";
|
||||||
|
import confirmDialog from "../../utils/asyncConfirm";
|
||||||
|
import useLocalStorage from "../../utils/useLocalStorage";
|
||||||
|
import BillFormContainer from "../bill-form/bill-form.container";
|
||||||
|
import { CalculateBillTotal } from "../bill-form/bill-form.totals.utility";
|
||||||
|
import { handleUpload as handleLocalUpload } from "../documents-local-upload/documents-local-upload.utility";
|
||||||
|
import { handleUpload } from "../documents-upload/documents-upload.utility";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
billEnterModal: selectBillEnterModal,
|
billEnterModal: selectBillEnterModal,
|
||||||
@@ -37,8 +37,8 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
toggleModalVisible: () => dispatch(toggleModalVisible("billEnter")),
|
toggleModalVisible: () => dispatch(toggleModalVisible("billEnter")),
|
||||||
insertAuditTrail: ({ jobid, operation }) =>
|
insertAuditTrail: ({ jobid, billid, operation }) =>
|
||||||
dispatch(insertAuditTrail({ jobid, operation })),
|
dispatch(insertAuditTrail({ jobid, billid, operation })),
|
||||||
});
|
});
|
||||||
|
|
||||||
const Templates = TemplateList("job_special");
|
const Templates = TemplateList("job_special");
|
||||||
@@ -126,13 +126,24 @@ function BillEnterModalContainer({
|
|||||||
deductedfromlbr: deductedfromlbr,
|
deductedfromlbr: deductedfromlbr,
|
||||||
lbr_adjustment,
|
lbr_adjustment,
|
||||||
joblineid: i.joblineid === "noline" ? null : i.joblineid,
|
joblineid: i.joblineid === "noline" ? null : i.joblineid,
|
||||||
|
applicable_taxes: {
|
||||||
|
federal:
|
||||||
|
(i.applicable_taxes && i.applicable_taxes.federal) ||
|
||||||
|
false,
|
||||||
|
state:
|
||||||
|
(i.applicable_taxes && i.applicable_taxes.state) ||
|
||||||
|
false,
|
||||||
|
local:
|
||||||
|
(i.applicable_taxes && i.applicable_taxes.local) ||
|
||||||
|
false,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
refetchQueries: ["QUERY_PARTS_BILLS_BY_JOBID"],
|
refetchQueries: ["QUERY_PARTS_BILLS_BY_JOBID", "GET_JOB_BY_PK"],
|
||||||
});
|
});
|
||||||
|
|
||||||
const adjKeys = Object.keys(adjustmentsToInsert);
|
const adjKeys = Object.keys(adjustmentsToInsert);
|
||||||
@@ -305,7 +316,9 @@ function BillEnterModalContainer({
|
|||||||
insertAuditTrail({
|
insertAuditTrail({
|
||||||
jobid: values.jobid,
|
jobid: values.jobid,
|
||||||
billid: billId,
|
billid: billId,
|
||||||
operation: AuditTrailMapping.billposted(remainingValues.invoice_number),
|
operation: AuditTrailMapping.billposted(
|
||||||
|
r1.data.insert_bills.returning[0].invoice_number
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (enterAgain) {
|
if (enterAgain) {
|
||||||
@@ -333,18 +346,18 @@ function BillEnterModalContainer({
|
|||||||
}, [enterAgain, form]);
|
}, [enterAgain, form]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (billEnterModal.visible) {
|
if (billEnterModal.open) {
|
||||||
form.setFieldsValue(formValues);
|
form.setFieldsValue(formValues);
|
||||||
} else {
|
} else {
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
}
|
}
|
||||||
}, [billEnterModal.visible, form, formValues]);
|
}, [billEnterModal.open, form, formValues]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title={t("bills.labels.new")}
|
title={t("bills.labels.new")}
|
||||||
width={"98%"}
|
width={"98%"}
|
||||||
visible={billEnterModal.visible}
|
open={billEnterModal.open}
|
||||||
okText={t("general.actions.save")}
|
okText={t("general.actions.save")}
|
||||||
keyboard="false"
|
keyboard="false"
|
||||||
onOk={() => form.submit()}
|
onOk={() => form.submit()}
|
||||||
|
|||||||
@@ -1,25 +1,18 @@
|
|||||||
import Icon, { UploadOutlined } from "@ant-design/icons";
|
import Icon, {UploadOutlined} from "@ant-design/icons";
|
||||||
import { useApolloClient } from "@apollo/client";
|
import {useApolloClient} from "@apollo/client";
|
||||||
import { MdOpenInNew } from "react-icons/md";
|
import {useSplitTreatments} from "@splitsoftware/splitio-react";
|
||||||
import {
|
import {Alert, Divider, Form, Input, Select, Space, Statistic, Switch, Upload,} from "antd";
|
||||||
Alert,
|
import dayjs from "../../utils/day";
|
||||||
Divider,
|
import React, {useEffect, useState} from "react";
|
||||||
Form,
|
import {useTranslation} from "react-i18next";
|
||||||
Input,
|
import {MdOpenInNew} from "react-icons/md";
|
||||||
Select,
|
import {connect} from "react-redux";
|
||||||
Space,
|
import {Link} from "react-router-dom";
|
||||||
Statistic,
|
import {createStructuredSelector} from "reselect";
|
||||||
Switch,
|
import {CHECK_BILL_INVOICE_NUMBER} from "../../graphql/bills.queries";
|
||||||
Upload,
|
import {selectBodyshop} from "../../redux/user/user.selectors";
|
||||||
} from "antd";
|
|
||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { connect } from "react-redux";
|
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
import { createStructuredSelector } from "reselect";
|
|
||||||
import { CHECK_BILL_INVOICE_NUMBER } from "../../graphql/bills.queries";
|
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
|
||||||
import AlertComponent from "../alert/alert.component";
|
import AlertComponent from "../alert/alert.component";
|
||||||
|
import BillFormLinesExtended from "../bill-form-lines-extended/bill-form-lines-extended.component";
|
||||||
import FormDatePicker from "../form-date-picker/form-date-picker.component";
|
import FormDatePicker from "../form-date-picker/form-date-picker.component";
|
||||||
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
|
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
|
||||||
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
||||||
@@ -27,273 +20,307 @@ import JobSearchSelect from "../job-search-select/job-search-select.component";
|
|||||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||||
import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component";
|
import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component";
|
||||||
import BillFormLines from "./bill-form.lines.component";
|
import BillFormLines from "./bill-form.lines.component";
|
||||||
import { CalculateBillTotal } from "./bill-form.totals.utility";
|
import {CalculateBillTotal} from "./bill-form.totals.utility";
|
||||||
import { useTreatments } from "@splitsoftware/splitio-react";
|
|
||||||
import BillFormLinesExtended from "../bill-form-lines-extended/bill-form-lines-extended.component";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({});
|
const mapDispatchToProps = (dispatch) => ({});
|
||||||
|
|
||||||
export function BillFormComponent({
|
export function BillFormComponent({bodyshop, disabled, form, vendorAutoCompleteOptions, lineData, responsibilityCenters, loadLines, billEdit, disableInvNumber, job, loadOutstandingReturns, loadInventory, preferredMake}) {
|
||||||
bodyshop,
|
|
||||||
disabled,
|
|
||||||
form,
|
|
||||||
vendorAutoCompleteOptions,
|
|
||||||
lineData,
|
|
||||||
responsibilityCenters,
|
|
||||||
loadLines,
|
|
||||||
billEdit,
|
|
||||||
disableInvNumber,
|
|
||||||
job,
|
|
||||||
loadOutstandingReturns,
|
|
||||||
loadInventory,
|
|
||||||
}) {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const client = useApolloClient();
|
|
||||||
const [discount, setDiscount] = useState(0);
|
|
||||||
const { Extended_Bill_Posting } = useTreatments(
|
|
||||||
["Extended_Bill_Posting"],
|
|
||||||
{},
|
|
||||||
bodyshop.imexshopid
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleVendorSelect = (props, opt) => {
|
const {t} = useTranslation();
|
||||||
setDiscount(opt.discount);
|
const client = useApolloClient();
|
||||||
|
const [discount, setDiscount] = useState(0);
|
||||||
|
|
||||||
opt &&
|
const { treatments: {Extended_Bill_Posting, ClosingPeriod} } = useSplitTreatments({
|
||||||
!billEdit &&
|
attributes: {},
|
||||||
loadOutstandingReturns({
|
names: ["Extended_Bill_Posting", "ClosingPeriod"],
|
||||||
variables: {
|
splitKey: bodyshop.imexshopid,
|
||||||
jobId: form.getFieldValue("jobid"),
|
});
|
||||||
vendorId: opt.value,
|
|
||||||
},
|
|
||||||
});
|
const handleVendorSelect = (props, opt) => {
|
||||||
|
setDiscount(opt.discount);
|
||||||
|
|
||||||
|
opt &&
|
||||||
|
!billEdit &&
|
||||||
|
loadOutstandingReturns({
|
||||||
|
variables: {
|
||||||
|
jobId: form.getFieldValue("jobid"),
|
||||||
|
vendorId: opt.value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFederalTaxExemptSwitchToggle = (checked) => {
|
||||||
|
// Early gate
|
||||||
|
if (!checked) return;
|
||||||
|
const values = form.getFieldsValue("billlines");
|
||||||
|
// Gate bill lines
|
||||||
|
if (!values?.billlines?.length) return;
|
||||||
|
|
||||||
|
const billlines = values.billlines.map((b) => {
|
||||||
|
b.applicable_taxes.federal = false;
|
||||||
|
return b;
|
||||||
|
});
|
||||||
|
form.setFieldsValue({ billlines });
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (job) form.validateFields(["is_credit_memo"]);
|
if (job) form.validateFields(["is_credit_memo"]);
|
||||||
}, [job, form]);
|
}, [job, form]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const vendorId = form.getFieldValue("vendorid");
|
const vendorId = form.getFieldValue("vendorid");
|
||||||
if (vendorId && vendorAutoCompleteOptions) {
|
if (vendorId && vendorAutoCompleteOptions) {
|
||||||
const matchingVendors = vendorAutoCompleteOptions.filter(
|
const matchingVendors = vendorAutoCompleteOptions.filter(
|
||||||
(v) => v.id === vendorId
|
(v) => v.id === vendorId
|
||||||
);
|
);
|
||||||
if (matchingVendors.length === 1) {
|
if (matchingVendors.length === 1) {
|
||||||
setDiscount(matchingVendors[0].discount);
|
setDiscount(matchingVendors[0].discount);
|
||||||
}
|
|
||||||
}
|
|
||||||
const jobId = form.getFieldValue("jobid");
|
|
||||||
if (jobId) {
|
|
||||||
loadLines({ variables: { id: jobId } });
|
|
||||||
if (form.getFieldValue("is_credit_memo") && vendorId && !billEdit) {
|
|
||||||
loadOutstandingReturns({
|
|
||||||
variables: {
|
|
||||||
jobId: jobId,
|
|
||||||
vendorId: vendorId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (vendorId === bodyshop.inhousevendorid && !billEdit) {
|
|
||||||
loadInventory();
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
form,
|
|
||||||
billEdit,
|
|
||||||
loadOutstandingReturns,
|
|
||||||
loadInventory,
|
|
||||||
setDiscount,
|
|
||||||
vendorAutoCompleteOptions,
|
|
||||||
loadLines,
|
|
||||||
bodyshop.inhousevendorid,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<FormFieldsChanged form={form} />
|
|
||||||
<Form.Item
|
|
||||||
style={{ display: "none" }}
|
|
||||||
name="isinhouse"
|
|
||||||
valuePropName="checked"
|
|
||||||
>
|
|
||||||
<Switch />
|
|
||||||
</Form.Item>
|
|
||||||
<LayoutFormRow grow>
|
|
||||||
<Form.Item
|
|
||||||
name="jobid"
|
|
||||||
label={t("bills.fields.ro_number")}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<JobSearchSelect
|
|
||||||
disabled={billEdit || disabled}
|
|
||||||
convertedOnly
|
|
||||||
notExported={false}
|
|
||||||
onBlur={() => {
|
|
||||||
if (form.getFieldValue("jobid") !== null) {
|
|
||||||
loadLines({ variables: { id: form.getFieldValue("jobid") } });
|
|
||||||
if (form.getFieldValue("vendorid") !== null) {
|
|
||||||
loadOutstandingReturns({
|
|
||||||
variables: {
|
|
||||||
jobId: form.getFieldValue("jobid"),
|
|
||||||
vendorId: form.getFieldValue("vendorid"),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t("bills.fields.vendor")}
|
|
||||||
name="vendorid"
|
|
||||||
// style={{ display: billEdit ? "none" : null }}
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
},
|
|
||||||
({ getFieldValue }) => ({
|
|
||||||
validator(rule, value) {
|
|
||||||
if (
|
|
||||||
value &&
|
|
||||||
!getFieldValue(["isinhouse"]) &&
|
|
||||||
value === bodyshop.inhousevendorid
|
|
||||||
) {
|
|
||||||
return Promise.reject(t("bills.validation.manualinhouse"));
|
|
||||||
}
|
|
||||||
return Promise.resolve();
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<VendorSearchSelect
|
|
||||||
disabled={disabled}
|
|
||||||
options={vendorAutoCompleteOptions}
|
|
||||||
onSelect={handleVendorSelect}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
</LayoutFormRow>
|
|
||||||
{job &&
|
|
||||||
job.ious &&
|
|
||||||
job.ious.length > 0 &&
|
|
||||||
job.ious.map((iou) => (
|
|
||||||
<Alert
|
|
||||||
key={iou.id}
|
|
||||||
type="warning"
|
|
||||||
message={
|
|
||||||
<Space>
|
|
||||||
{t("bills.labels.iouexists")}
|
|
||||||
<Link
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
to={`/manage/jobs/${iou.id}?tab=repairdata`}
|
|
||||||
>
|
|
||||||
<Space>
|
|
||||||
{iou.ro_number}
|
|
||||||
<Icon component={MdOpenInNew} />
|
|
||||||
</Space>
|
|
||||||
</Link>
|
|
||||||
</Space>
|
|
||||||
}
|
}
|
||||||
/>
|
}
|
||||||
))}
|
const jobId = form.getFieldValue("jobid");
|
||||||
<LayoutFormRow>
|
if (jobId) {
|
||||||
<Form.Item
|
loadLines({variables: {id: jobId}});
|
||||||
label={t("bills.fields.invoice_number")}
|
if (form.getFieldValue("is_credit_memo") && vendorId && !billEdit) {
|
||||||
name="invoice_number"
|
loadOutstandingReturns({
|
||||||
validateTrigger="onBlur"
|
|
||||||
hasFeedback
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
},
|
|
||||||
({ getFieldValue }) => ({
|
|
||||||
async validator(rule, value) {
|
|
||||||
const vendorid = getFieldValue("vendorid");
|
|
||||||
if (vendorid && value) {
|
|
||||||
const response = await client.query({
|
|
||||||
query: CHECK_BILL_INVOICE_NUMBER,
|
|
||||||
variables: {
|
variables: {
|
||||||
invoice_number: value,
|
jobId: jobId,
|
||||||
vendorid: vendorid,
|
vendorId: vendorId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (response.data.bills_aggregate.aggregate.count === 0) {
|
if (vendorId === bodyshop.inhousevendorid && !billEdit) {
|
||||||
return Promise.resolve();
|
loadInventory();
|
||||||
} else if (
|
}
|
||||||
response.data.bills_aggregate.nodes.length === 1 &&
|
}, [
|
||||||
response.data.bills_aggregate.nodes[0].id ===
|
form,
|
||||||
form.getFieldValue("id")
|
billEdit,
|
||||||
) {
|
loadOutstandingReturns,
|
||||||
return Promise.resolve();
|
loadInventory,
|
||||||
}
|
setDiscount,
|
||||||
return Promise.reject(
|
vendorAutoCompleteOptions,
|
||||||
t("bills.validation.unique_invoice_number")
|
loadLines,
|
||||||
);
|
bodyshop.inhousevendorid,
|
||||||
} else {
|
]);
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Input disabled={disabled || disableInvNumber} />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t("bills.fields.date")}
|
|
||||||
name="date"
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<FormDatePicker disabled={disabled} />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item
|
|
||||||
label={t("bills.fields.is_credit_memo")}
|
|
||||||
name="is_credit_memo"
|
|
||||||
valuePropName="checked"
|
|
||||||
rules={[
|
|
||||||
({ getFieldValue }) => ({
|
|
||||||
validator(rule, value) {
|
|
||||||
if (
|
|
||||||
value === true &&
|
|
||||||
getFieldValue("jobid") &&
|
|
||||||
getFieldValue("vendorid")
|
|
||||||
) {
|
|
||||||
//Removed as this would cause an additional reload when validating the form on submit and clear the values.
|
|
||||||
// loadOutstandingReturns({
|
|
||||||
// variables: {
|
|
||||||
// jobId: form.getFieldValue("jobid"),
|
|
||||||
// vendorId: form.getFieldValue("vendorid"),
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
return (
|
||||||
!bodyshop.bill_allow_post_to_closed &&
|
<div>
|
||||||
job &&
|
<FormFieldsChanged form={form}/>
|
||||||
(job.status === bodyshop.md_ro_statuses.default_invoiced ||
|
<Form.Item
|
||||||
job.status === bodyshop.md_ro_statuses.default_exported ||
|
style={{display: "none"}}
|
||||||
job.status === bodyshop.md_ro_statuses.default_void) &&
|
name="isinhouse"
|
||||||
(value === false || !value)
|
valuePropName="checked"
|
||||||
) {
|
>
|
||||||
return Promise.reject(t("bills.labels.onlycmforinvoiced"));
|
<Switch/>
|
||||||
}
|
</Form.Item>
|
||||||
|
<LayoutFormRow grow>
|
||||||
|
<Form.Item
|
||||||
|
name="jobid"
|
||||||
|
label={t("bills.fields.ro_number")}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<JobSearchSelect
|
||||||
|
disabled={billEdit || disabled}
|
||||||
|
convertedOnly
|
||||||
|
notExported={false}
|
||||||
|
onBlur={() => {
|
||||||
|
if (form.getFieldValue("jobid") !== null) {
|
||||||
|
loadLines({variables: {id: form.getFieldValue("jobid")}});
|
||||||
|
if (form.getFieldValue("vendorid") !== null) {
|
||||||
|
loadOutstandingReturns({
|
||||||
|
variables: {
|
||||||
|
jobId: form.getFieldValue("jobid"),
|
||||||
|
vendorId: form.getFieldValue("vendorid"),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("bills.fields.vendor")}
|
||||||
|
name="vendorid"
|
||||||
|
// style={{ display: billEdit ? "none" : null }}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
},
|
||||||
|
({getFieldValue}) => ({
|
||||||
|
validator(rule, value) {
|
||||||
|
if (
|
||||||
|
value &&
|
||||||
|
!getFieldValue(["isinhouse"]) &&
|
||||||
|
value === bodyshop.inhousevendorid
|
||||||
|
) {
|
||||||
|
return Promise.reject(t("bills.validation.manualinhouse"));
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<VendorSearchSelect
|
||||||
|
disabled={disabled}
|
||||||
|
options={vendorAutoCompleteOptions}
|
||||||
|
preferredMake={preferredMake}
|
||||||
|
onSelect={handleVendorSelect}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</LayoutFormRow>
|
||||||
|
{job &&
|
||||||
|
job.ious &&
|
||||||
|
job.ious.length > 0 &&
|
||||||
|
job.ious.map((iou) => (
|
||||||
|
<Alert
|
||||||
|
key={iou.id}
|
||||||
|
type="warning"
|
||||||
|
message={
|
||||||
|
<Space>
|
||||||
|
{t("bills.labels.iouexists")}
|
||||||
|
<Link
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
to={`/manage/jobs/${iou.id}?tab=repairdata`}
|
||||||
|
>
|
||||||
|
<Space>
|
||||||
|
{iou.ro_number}
|
||||||
|
<Icon component={MdOpenInNew}/>
|
||||||
|
</Space>
|
||||||
|
</Link>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<LayoutFormRow>
|
||||||
|
<Form.Item
|
||||||
|
label={t("bills.fields.invoice_number")}
|
||||||
|
name="invoice_number"
|
||||||
|
validateTrigger="onBlur"
|
||||||
|
hasFeedback
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
},
|
||||||
|
({getFieldValue}) => ({
|
||||||
|
async validator(rule, value) {
|
||||||
|
const vendorid = getFieldValue("vendorid");
|
||||||
|
if (vendorid && value) {
|
||||||
|
const response = await client.query({
|
||||||
|
query: CHECK_BILL_INVOICE_NUMBER,
|
||||||
|
variables: {
|
||||||
|
invoice_number: value,
|
||||||
|
vendorid: vendorid,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.bills_aggregate.aggregate.count === 0) {
|
||||||
|
return Promise.resolve();
|
||||||
|
} else if (
|
||||||
|
response.data.bills_aggregate.nodes.length === 1 &&
|
||||||
|
response.data.bills_aggregate.nodes[0].id ===
|
||||||
|
form.getFieldValue("id")
|
||||||
|
) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
return Promise.reject(
|
||||||
|
t("bills.validation.unique_invoice_number")
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input disabled={disabled || disableInvNumber}/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("bills.fields.date")}
|
||||||
|
name="date"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
},
|
||||||
|
({getFieldValue}) => ({
|
||||||
|
validator(rule, value) {
|
||||||
|
if (
|
||||||
|
ClosingPeriod.treatment === "on" &&
|
||||||
|
bodyshop.accountingconfig.ClosingPeriod
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
dayjs(value)
|
||||||
|
.startOf("day")
|
||||||
|
.isSameOrAfter(
|
||||||
|
dayjs(
|
||||||
|
bodyshop.accountingconfig.ClosingPeriod[0]
|
||||||
|
).startOf("day")
|
||||||
|
) &&
|
||||||
|
dayjs(value)
|
||||||
|
.startOf("day")
|
||||||
|
.isSameOrBefore(
|
||||||
|
dayjs(
|
||||||
|
bodyshop.accountingconfig.ClosingPeriod[1]
|
||||||
|
).endOf("day")
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return Promise.resolve();
|
||||||
|
} else {
|
||||||
|
return Promise.reject(t("bills.validation.closingperiod"));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<FormDatePicker disabled={disabled}/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("bills.fields.is_credit_memo")}
|
||||||
|
name="is_credit_memo"
|
||||||
|
valuePropName="checked"
|
||||||
|
rules={[
|
||||||
|
({getFieldValue}) => ({
|
||||||
|
validator(rule, value) {
|
||||||
|
if (
|
||||||
|
value === true &&
|
||||||
|
getFieldValue("jobid") &&
|
||||||
|
getFieldValue("vendorid")
|
||||||
|
) {
|
||||||
|
//Removed as this would cause an additional reload when validating the form on submit and clear the values.
|
||||||
|
// loadOutstandingReturns({
|
||||||
|
// variables: {
|
||||||
|
// jobId: form.getFieldValue("jobid"),
|
||||||
|
// vendorId: form.getFieldValue("vendorid"),
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!bodyshop.bill_allow_post_to_closed &&
|
||||||
|
job &&
|
||||||
|
(job.status === bodyshop.md_ro_statuses.default_invoiced ||
|
||||||
|
job.status === bodyshop.md_ro_statuses.default_exported ||
|
||||||
|
job.status === bodyshop.md_ro_statuses.default_void) &&
|
||||||
|
(value === false || !value)
|
||||||
|
) {
|
||||||
|
return Promise.reject(t("bills.labels.onlycmforinvoiced"));
|
||||||
|
}
|
||||||
|
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
},
|
},
|
||||||
@@ -348,7 +375,16 @@ export function BillFormComponent({
|
|||||||
>
|
>
|
||||||
<CurrencyInput min={0} />
|
<CurrencyInput min={0} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item shouldUpdate span={15}>
|
{bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid ? (
|
||||||
|
<Form.Item
|
||||||
|
span={2}
|
||||||
|
label={t("bills.labels.federal_tax_exempt")}
|
||||||
|
name="federal_tax_exempt"
|
||||||
|
>
|
||||||
|
<Switch onChange={handleFederalTaxExemptSwitchToggle} />
|
||||||
|
</Form.Item>
|
||||||
|
) : null}
|
||||||
|
<Form.Item shouldUpdate span={13}>
|
||||||
{() => {
|
{() => {
|
||||||
const values = form.getFieldsValue([
|
const values = form.getFieldsValue([
|
||||||
"billlines",
|
"billlines",
|
||||||
@@ -366,7 +402,7 @@ export function BillFormComponent({
|
|||||||
totals = CalculateBillTotal(values);
|
totals = CalculateBillTotal(values);
|
||||||
if (!!totals)
|
if (!!totals)
|
||||||
return (
|
return (
|
||||||
<div>
|
<div align="right">
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
<Statistic
|
<Statistic
|
||||||
title={t("bills.labels.subtotal")}
|
title={t("bills.labels.subtotal")}
|
||||||
@@ -424,55 +460,55 @@ export function BillFormComponent({
|
|||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
<Divider orientation="left">{t("bills.labels.bill_lines")}</Divider>
|
<Divider orientation="left">{t("bills.labels.bill_lines")}</Divider>
|
||||||
|
|
||||||
{Extended_Bill_Posting.treatment === "on" ? (
|
{Extended_Bill_Posting.treatment === "on" ? (
|
||||||
<BillFormLinesExtended
|
<BillFormLinesExtended
|
||||||
lineData={lineData}
|
lineData={lineData}
|
||||||
discount={discount}
|
discount={discount}
|
||||||
form={form}
|
form={form}
|
||||||
responsibilityCenters={responsibilityCenters}
|
responsibilityCenters={responsibilityCenters}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<BillFormLines
|
<BillFormLines
|
||||||
lineData={lineData}
|
lineData={lineData}
|
||||||
discount={discount}
|
discount={discount}
|
||||||
form={form}
|
form={form}
|
||||||
responsibilityCenters={responsibilityCenters}
|
responsibilityCenters={responsibilityCenters}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
billEdit={billEdit}
|
billEdit={billEdit}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="upload"
|
name="upload"
|
||||||
label="Upload"
|
label="Upload"
|
||||||
style={{ display: billEdit ? "none" : null }}
|
style={{display: billEdit ? "none" : null}}
|
||||||
valuePropName="fileList"
|
valuePropName="fileList"
|
||||||
getValueFromEvent={(e) => {
|
getValueFromEvent={(e) => {
|
||||||
if (Array.isArray(e)) {
|
if (Array.isArray(e)) {
|
||||||
return e;
|
return e;
|
||||||
}
|
}
|
||||||
return e && e.fileList;
|
return e && e.fileList;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Upload.Dragger
|
<Upload.Dragger
|
||||||
multiple={true}
|
multiple={true}
|
||||||
name="logo"
|
name="logo"
|
||||||
beforeUpload={() => false}
|
beforeUpload={() => false}
|
||||||
listType="picture"
|
listType="picture"
|
||||||
>
|
>
|
||||||
<>
|
<>
|
||||||
<p className="ant-upload-drag-icon">
|
<p className="ant-upload-drag-icon">
|
||||||
<UploadOutlined />
|
<UploadOutlined/>
|
||||||
</p>
|
</p>
|
||||||
<p className="ant-upload-text">
|
<p className="ant-upload-text">
|
||||||
Click or drag files to this area to upload.
|
Click or drag files to this area to upload.
|
||||||
</p>
|
</p>
|
||||||
</>
|
</>
|
||||||
</Upload.Dragger>
|
</Upload.Dragger>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(BillFormComponent);
|
export default connect(mapStateToProps, mapDispatchToProps)(BillFormComponent);
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import { useLazyQuery, useQuery } from "@apollo/client";
|
import { useLazyQuery, useQuery } from "@apollo/client";
|
||||||
|
import {useSplitTreatments} from "@splitsoftware/splitio-react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { QUERY_OUTSTANDING_INVENTORY } from "../../graphql/inventory.queries";
|
||||||
import { GET_JOB_LINES_TO_ENTER_BILL } from "../../graphql/jobs-lines.queries";
|
import { GET_JOB_LINES_TO_ENTER_BILL } from "../../graphql/jobs-lines.queries";
|
||||||
|
import { QUERY_UNRECEIVED_LINES } from "../../graphql/parts-orders.queries";
|
||||||
import { SEARCH_VENDOR_AUTOCOMPLETE } from "../../graphql/vendors.queries";
|
import { SEARCH_VENDOR_AUTOCOMPLETE } from "../../graphql/vendors.queries";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import BillFormComponent from "./bill-form.component";
|
|
||||||
import BillCmdReturnsTableComponent from "../bill-cm-returns-table/bill-cm-returns-table.component";
|
import BillCmdReturnsTableComponent from "../bill-cm-returns-table/bill-cm-returns-table.component";
|
||||||
import { QUERY_UNRECEIVED_LINES } from "../../graphql/parts-orders.queries";
|
|
||||||
import BillInventoryTable from "../bill-inventory-table/bill-inventory-table.component";
|
import BillInventoryTable from "../bill-inventory-table/bill-inventory-table.component";
|
||||||
import { QUERY_OUTSTANDING_INVENTORY } from "../../graphql/inventory.queries";
|
import BillFormComponent from "./bill-form.component";
|
||||||
import { useTreatments } from "@splitsoftware/splitio-react";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -23,11 +23,11 @@ export function BillFormContainer({
|
|||||||
disabled,
|
disabled,
|
||||||
disableInvNumber,
|
disableInvNumber,
|
||||||
}) {
|
}) {
|
||||||
const { Simple_Inventory } = useTreatments(
|
const { treatments: {Simple_Inventory} } = useSplitTreatments({
|
||||||
["Simple_Inventory"],
|
attributes: {},
|
||||||
{},
|
names: ["Simple_Inventory"],
|
||||||
bodyshop && bodyshop.imexshopid
|
splitKey: bodyshop && bodyshop.imexshopid,
|
||||||
);
|
});
|
||||||
|
|
||||||
const { data: VendorAutoCompleteData } = useQuery(
|
const { data: VendorAutoCompleteData } = useQuery(
|
||||||
SEARCH_VENDOR_AUTOCOMPLETE,
|
SEARCH_VENDOR_AUTOCOMPLETE,
|
||||||
@@ -59,6 +59,7 @@ export function BillFormContainer({
|
|||||||
disableInvNumber={disableInvNumber}
|
disableInvNumber={disableInvNumber}
|
||||||
loadOutstandingReturns={loadOutstandingReturns}
|
loadOutstandingReturns={loadOutstandingReturns}
|
||||||
loadInventory={loadInventory}
|
loadInventory={loadInventory}
|
||||||
|
preferredMake={lineData ? lineData.jobs_by_pk.v_make_desc : null}
|
||||||
/>
|
/>
|
||||||
{!billEdit && (
|
{!billEdit && (
|
||||||
<BillCmdReturnsTableComponent
|
<BillCmdReturnsTableComponent
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
import { DeleteFilled, DollarCircleFilled } from "@ant-design/icons";
|
import { DeleteFilled, DollarCircleFilled } from "@ant-design/icons";
|
||||||
import { useTreatments } from "@splitsoftware/splitio-react";
|
import {useSplitTreatments} from "@splitsoftware/splitio-react";
|
||||||
import {
|
import {
|
||||||
Button, Form,
|
Button,
|
||||||
|
Form,
|
||||||
Input,
|
Input,
|
||||||
InputNumber,
|
InputNumber,
|
||||||
Select,
|
Select,
|
||||||
Space,
|
Space,
|
||||||
Switch,
|
Switch,
|
||||||
Table,
|
Table,
|
||||||
Tooltip
|
Tooltip,
|
||||||
} from "antd";
|
} from "antd";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -40,11 +41,14 @@ export function BillEnterModalLinesComponent({
|
|||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { setFieldsValue, getFieldsValue, getFieldValue } = form;
|
const { setFieldsValue, getFieldsValue, getFieldValue } = form;
|
||||||
const { Simple_Inventory } = useTreatments(
|
|
||||||
["Simple_Inventory"],
|
const { treatments: {Simple_Inventory} } = useSplitTreatments({
|
||||||
{},
|
attributes: {},
|
||||||
bodyshop && bodyshop.imexshopid
|
names: ["Simple_Inventory"],
|
||||||
);
|
splitKey: bodyshop && bodyshop.imexshopid,
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
const columns = (remove) => {
|
const columns = (remove) => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -466,7 +470,8 @@ export function BillEnterModalLinesComponent({
|
|||||||
return {
|
return {
|
||||||
key: `${field.index}fedtax`,
|
key: `${field.index}fedtax`,
|
||||||
valuePropName: "checked",
|
valuePropName: "checked",
|
||||||
initialValue: true,
|
initialValue:
|
||||||
|
form.getFieldValue("federal_tax_exempt") === true ? false : true,
|
||||||
name: [field.name, "applicable_taxes", "federal"],
|
name: [field.name, "applicable_taxes", "federal"],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ const BillLineSearchSelect = (
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
showSearch
|
showSearch
|
||||||
dropdownMatchSelectWidth={false}
|
popupMatchSelectWidth={false}
|
||||||
|
optionLabelProp={"name"}
|
||||||
// optionFilterProp="line_desc"
|
// optionFilterProp="line_desc"
|
||||||
filterOption={(inputValue, option) => {
|
filterOption={(inputValue, option) => {
|
||||||
return (
|
return (
|
||||||
@@ -57,6 +58,9 @@ const BillLineSearchSelect = (
|
|||||||
style={{
|
style={{
|
||||||
...(item.removed ? { textDecoration: "line-through" } : {}),
|
...(item.removed ? { textDecoration: "line-through" } : {}),
|
||||||
}}
|
}}
|
||||||
|
name={`${item.removed ? `(REMOVED) ` : ""}${item.line_desc}${
|
||||||
|
item.oem_partno ? ` - ${item.oem_partno}` : ""
|
||||||
|
}${item.alt_partno ? ` (${item.alt_partno})` : ""}`.trim()}
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
{`${item.removed ? `(REMOVED) ` : ""}${item.line_desc}${
|
{`${item.removed ? `(REMOVED) ` : ""}${item.line_desc}${
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { Button, Space } from "antd";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { GenerateDocument } from "../../utils/RenderTemplate";
|
||||||
|
import { TemplateList } from "../../utils/TemplateConstants";
|
||||||
|
|
||||||
|
export default function BillPrintButton({ billid }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const Templates = TemplateList("job_special");
|
||||||
|
|
||||||
|
const submitHandler = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
await GenerateDocument(
|
||||||
|
{
|
||||||
|
name: Templates.parts_invoice_label_single.key,
|
||||||
|
variables: {
|
||||||
|
id: billid,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
"p"
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Warning: Error generating a document.");
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Space wrap>
|
||||||
|
<Button loading={loading} onClick={submitHandler}>
|
||||||
|
{t("bills.labels.printlabels")}
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ import { FileAddFilled } from "@ant-design/icons";
|
|||||||
import { useMutation } from "@apollo/client";
|
import { useMutation } from "@apollo/client";
|
||||||
import { Button, notification, Tooltip } from "antd";
|
import { Button, notification, Tooltip } from "antd";
|
||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
import moment from "moment";
|
import dayjs from "./../../utils/day";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
@@ -36,7 +36,6 @@ export function BilllineAddInventory({
|
|||||||
}) {
|
}) {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const { billid } = queryString.parse(useLocation().search);
|
const { billid } = queryString.parse(useLocation().search);
|
||||||
|
|
||||||
const [insertInventoryLine] = useMutation(INSERT_INVENTORY_AND_CREDIT);
|
const [insertInventoryLine] = useMutation(INSERT_INVENTORY_AND_CREDIT);
|
||||||
|
|
||||||
const addToInventory = async () => {
|
const addToInventory = async () => {
|
||||||
@@ -50,7 +49,7 @@ export function BilllineAddInventory({
|
|||||||
jobid: jobid,
|
jobid: jobid,
|
||||||
isinhouse: true,
|
isinhouse: true,
|
||||||
is_credit_memo: true,
|
is_credit_memo: true,
|
||||||
date: moment().format("YYYY-MM-DD"),
|
date: dayjs().format("YYYY-MM-DD"),
|
||||||
federal_tax_rate: bodyshop.bill_tax_rates.federal_tax_rate,
|
federal_tax_rate: bodyshop.bill_tax_rates.federal_tax_rate,
|
||||||
state_tax_rate: bodyshop.bill_tax_rates.state_tax_rate,
|
state_tax_rate: bodyshop.bill_tax_rates.state_tax_rate,
|
||||||
local_tax_rate: bodyshop.bill_tax_rates.local_tax_rate,
|
local_tax_rate: bodyshop.bill_tax_rates.local_tax_rate,
|
||||||
@@ -92,7 +91,7 @@ export function BilllineAddInventory({
|
|||||||
pol: {
|
pol: {
|
||||||
returnfrombill: billid,
|
returnfrombill: billid,
|
||||||
vendorid: bodyshop.inhousevendorid,
|
vendorid: bodyshop.inhousevendorid,
|
||||||
deliver_by: moment().format("YYYY-MM-DD"),
|
deliver_by: dayjs().format("YYYY-MM-DD"),
|
||||||
parts_order_lines: {
|
parts_order_lines: {
|
||||||
data: [
|
data: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React, { useState } from "react";
|
|||||||
import { QUERY_ALL_VENDORS } from "../../graphql/vendors.queries";
|
import { QUERY_ALL_VENDORS } from "../../graphql/vendors.queries";
|
||||||
import { useQuery } from "@apollo/client";
|
import { useQuery } from "@apollo/client";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import { useHistory, useLocation } from "react-router-dom";
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
import { Table, Input } from "antd";
|
import { Table, Input } from "antd";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { alphaSort } from "../../utils/sorters";
|
import { alphaSort } from "../../utils/sorters";
|
||||||
@@ -10,7 +10,7 @@ import AlertComponent from "../alert/alert.component";
|
|||||||
|
|
||||||
export default function BillsVendorsList() {
|
export default function BillsVendorsList() {
|
||||||
const search = queryString.parse(useLocation().search);
|
const search = queryString.parse(useLocation().search);
|
||||||
const history = useHistory();
|
const history = useNavigate();
|
||||||
|
|
||||||
const { loading, error, data } = useQuery(QUERY_ALL_VENDORS, {
|
const { loading, error, data } = useQuery(QUERY_ALL_VENDORS, {
|
||||||
fetchPolicy: "network-only",
|
fetchPolicy: "network-only",
|
||||||
|
|||||||
@@ -1,46 +1,64 @@
|
|||||||
import { HomeFilled } from "@ant-design/icons";
|
import {HomeFilled} from "@ant-design/icons";
|
||||||
import { Breadcrumb, Row, Col } from "antd";
|
import {Breadcrumb, Col, Row} from "antd";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { connect } from "react-redux";
|
import {connect} from "react-redux";
|
||||||
import { Link } from "react-router-dom";
|
import {Link} from "react-router-dom";
|
||||||
import { createStructuredSelector } from "reselect";
|
import {createStructuredSelector} from "reselect";
|
||||||
import { selectBreadcrumbs } from "../../redux/application/application.selectors";
|
import {selectBreadcrumbs} from "../../redux/application/application.selectors";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import {selectBodyshop} from "../../redux/user/user.selectors";
|
||||||
import GlobalSearch from "../global-search/global-search.component";
|
import GlobalSearch from "../global-search/global-search.component";
|
||||||
|
import GlobalSearchOs from "../global-search/global-search-os.component";
|
||||||
import "./breadcrumbs.styles.scss";
|
import "./breadcrumbs.styles.scss";
|
||||||
|
import {useSplitTreatments} from "@splitsoftware/splitio-react";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
breadcrumbs: selectBreadcrumbs,
|
breadcrumbs: selectBreadcrumbs,
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
});
|
});
|
||||||
|
|
||||||
export function BreadCrumbs({ breadcrumbs, bodyshop }) {
|
export function BreadCrumbs({breadcrumbs, bodyshop}) {
|
||||||
return (
|
|
||||||
<Row className="breadcrumb-container">
|
const {treatments: {OpenSearch}} = useSplitTreatments({
|
||||||
<Col xs={24} sm={24} md={16}>
|
attributes: {},
|
||||||
<Breadcrumb separator=">">
|
names: ["OpenSearch"],
|
||||||
<Breadcrumb.Item>
|
splitKey: bodyshop && bodyshop.imexshopid,
|
||||||
<Link to={`/manage`}>
|
});
|
||||||
<HomeFilled />{" "}
|
// TODO - Client Update - Technically key is not doing anything here
|
||||||
{(bodyshop && bodyshop.shopname && `(${bodyshop.shopname})`) ||
|
return (
|
||||||
""}
|
<Row className="breadcrumb-container">
|
||||||
</Link>
|
<Col xs={24} sm={24} md={16}>
|
||||||
</Breadcrumb.Item>
|
<Breadcrumb
|
||||||
{breadcrumbs.map((item) =>
|
separator=">"
|
||||||
item.link ? (
|
items={[
|
||||||
<Breadcrumb.Item key={item.label}>
|
{
|
||||||
<Link to={item.link}>{item.label} </Link>
|
key: "home",
|
||||||
</Breadcrumb.Item>
|
title: (
|
||||||
) : (
|
<Link to={`/manage/`}>
|
||||||
<Breadcrumb.Item key={item.label}>{item.label}</Breadcrumb.Item>
|
<HomeFilled/>{" "}
|
||||||
)
|
{(bodyshop && bodyshop.shopname && `(${bodyshop.shopname})`) ||
|
||||||
)}
|
""}
|
||||||
</Breadcrumb>
|
</Link>
|
||||||
</Col>
|
),
|
||||||
<Col xs={24} sm={24} md={8}>
|
},
|
||||||
<GlobalSearch />
|
...breadcrumbs.map((item) =>
|
||||||
</Col>
|
item.link
|
||||||
</Row>
|
? {
|
||||||
);
|
key: item.label,
|
||||||
|
title: <Link to={item.link}>{item.label}</Link>,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
key: item.label,
|
||||||
|
title: item.label,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} sm={24} md={8}>
|
||||||
|
{OpenSearch.treatment === "on" ? <GlobalSearchOs/> : <GlobalSearch/>}
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(mapStateToProps, null)(BreadCrumbs);
|
export default connect(mapStateToProps, null)(BreadCrumbs);
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export function ContractsFindModalContainer({
|
|||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { visible } = caBcEtfTableModal;
|
const { open } = caBcEtfTableModal;
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const EtfTemplate = TemplateList("special").ca_bc_etf_table;
|
const EtfTemplate = TemplateList("special").ca_bc_etf_table;
|
||||||
@@ -63,14 +63,14 @@ export function ContractsFindModalContainer({
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (visible) {
|
if (open) {
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
}
|
}
|
||||||
}, [visible, form]);
|
}, [open, form]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
visible={visible}
|
open={open}
|
||||||
width="70%"
|
width="70%"
|
||||||
title={t("payments.labels.findermodal")}
|
title={t("payments.labels.findermodal")}
|
||||||
onCancel={() => toggleModalVisible()}
|
onCancel={() => toggleModalVisible()}
|
||||||
|
|||||||
@@ -10,7 +10,10 @@ export default function CABCpvrtCalculator({ disabled, form }) {
|
|||||||
|
|
||||||
const handleFinish = async (values) => {
|
const handleFinish = async (values) => {
|
||||||
logImEXEvent("job_ca_bc_pvrt_calculate");
|
logImEXEvent("job_ca_bc_pvrt_calculate");
|
||||||
form.setFieldsValue({ ca_bc_pvrt: ((values.rate||0) * (values.days||0)).toFixed(2) });
|
form.setFieldsValue({
|
||||||
|
ca_bc_pvrt: ((values.rate || 0) * (values.days || 0)).toFixed(2),
|
||||||
|
});
|
||||||
|
form.setFields([{ name: "ca_bc_pvrt", touched: true }]);
|
||||||
setVisibility(false);
|
setVisibility(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -35,7 +38,7 @@ export default function CABCpvrtCalculator({ disabled, form }) {
|
|||||||
<Popover
|
<Popover
|
||||||
destroyTooltipOnHide
|
destroyTooltipOnHide
|
||||||
content={popContent}
|
content={popContent}
|
||||||
visible={visibility}
|
open={visibility}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
<Button disabled={disabled} onClick={() => setVisibility(true)}>
|
<Button disabled={disabled} onClick={() => setVisibility(true)}>
|
||||||
|
|||||||
@@ -0,0 +1,374 @@
|
|||||||
|
import { DeleteFilled } from "@ant-design/icons";
|
||||||
|
import { useLazyQuery, useMutation } from "@apollo/client";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Col,
|
||||||
|
Form,
|
||||||
|
Input,
|
||||||
|
Row,
|
||||||
|
Space,
|
||||||
|
Spin,
|
||||||
|
Statistic,
|
||||||
|
notification,
|
||||||
|
} from "antd";
|
||||||
|
import axios from "axios";
|
||||||
|
import dayjs from "../../utils/day";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import {
|
||||||
|
INSERT_PAYMENT_RESPONSE,
|
||||||
|
QUERY_RO_AND_OWNER_BY_JOB_PKS,
|
||||||
|
} from "../../graphql/payment_response.queries";
|
||||||
|
import { INSERT_NEW_PAYMENT } from "../../graphql/payments.queries";
|
||||||
|
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||||
|
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
||||||
|
import { selectCardPayment } from "../../redux/modals/modals.selectors";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||||
|
import CurrencyFormItemComponent from "../form-items-formatted/currency-form-item.component";
|
||||||
|
import JobSearchSelectComponent from "../job-search-select/job-search-select.component";
|
||||||
|
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
cardPaymentModal: selectCardPayment,
|
||||||
|
bodyshop: selectBodyshop,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
insertAuditTrail: ({ jobid, operation }) =>
|
||||||
|
dispatch(insertAuditTrail({ jobid, operation })),
|
||||||
|
toggleModalVisible: () => dispatch(toggleModalVisible("cardPayment")),
|
||||||
|
});
|
||||||
|
|
||||||
|
const CardPaymentModalComponent = ({
|
||||||
|
bodyshop,
|
||||||
|
cardPaymentModal,
|
||||||
|
toggleModalVisible,
|
||||||
|
insertAuditTrail,
|
||||||
|
}) => {
|
||||||
|
const { context } = cardPaymentModal;
|
||||||
|
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [insertPayment] = useMutation(INSERT_NEW_PAYMENT);
|
||||||
|
const [insertPaymentResponse] = useMutation(INSERT_PAYMENT_RESPONSE);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const [, { data, refetch, queryLoading }] = useLazyQuery(
|
||||||
|
QUERY_RO_AND_OWNER_BY_JOB_PKS,
|
||||||
|
{
|
||||||
|
variables: { jobids: [context.jobid] },
|
||||||
|
skip: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("🚀 ~ file: card-payment-modal.component..jsx:61 ~ data:", data);
|
||||||
|
//Initialize the intellipay window.
|
||||||
|
const SetIntellipayCallbackFunctions = () => {
|
||||||
|
console.log("*** Set IntelliPay callback functions.");
|
||||||
|
window.intellipay.runOnClose(() => {
|
||||||
|
//window.intellipay.initialize();
|
||||||
|
});
|
||||||
|
|
||||||
|
window.intellipay.runOnApproval(async function (response) {
|
||||||
|
console.warn("*** Running On Approval Script ***");
|
||||||
|
form.setFieldValue("paymentResponse", response);
|
||||||
|
form.submit();
|
||||||
|
});
|
||||||
|
|
||||||
|
window.intellipay.runOnNonApproval(async function (response) {
|
||||||
|
// Mutate unsuccessful payment
|
||||||
|
|
||||||
|
const { payments } = form.getFieldsValue();
|
||||||
|
|
||||||
|
await insertPaymentResponse({
|
||||||
|
variables: {
|
||||||
|
paymentResponse: payments.map((payment) => ({
|
||||||
|
amount: payment.amount,
|
||||||
|
bodyshopid: bodyshop.id,
|
||||||
|
jobid: payment.jobid,
|
||||||
|
declinereason: response.declinereason,
|
||||||
|
ext_paymentid: response.paymentid.toString(),
|
||||||
|
successful: false,
|
||||||
|
response,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
payments.forEach((payment) =>
|
||||||
|
insertAuditTrail({
|
||||||
|
jobid: payment.jobid,
|
||||||
|
operation: AuditTrailMapping.failedpayment(),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFinish = async (values) => {
|
||||||
|
try {
|
||||||
|
await insertPayment({
|
||||||
|
variables: {
|
||||||
|
paymentInput: values.payments.map((payment) => ({
|
||||||
|
amount: payment.amount,
|
||||||
|
transactionid: (values.paymentResponse.paymentid || "").toString(),
|
||||||
|
payer: t("payments.labels.customer"),
|
||||||
|
type: values.paymentResponse.cardbrand,
|
||||||
|
jobid: payment.jobid,
|
||||||
|
date: dayjs(Date.now()),
|
||||||
|
payment_responses: {
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
amount: payment.amount,
|
||||||
|
bodyshopid: bodyshop.id,
|
||||||
|
|
||||||
|
jobid: payment.jobid,
|
||||||
|
declinereason: values.paymentResponse.declinereason,
|
||||||
|
ext_paymentid: values.paymentResponse.paymentid.toString(),
|
||||||
|
successful: true,
|
||||||
|
response: values.paymentResponse,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
refetchQueries: ["GET_JOB_BY_PK"],
|
||||||
|
});
|
||||||
|
toggleModalVisible();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
notification.open({
|
||||||
|
type: "error",
|
||||||
|
message: t("payments.errors.inserting", { error: error.message }),
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleIntelliPayCharge = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
//Validate
|
||||||
|
try {
|
||||||
|
await form.validateFields();
|
||||||
|
} catch (error) {
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.post("/intellipay/lightbox_credentials", {
|
||||||
|
bodyshop,
|
||||||
|
refresh: !!window.intellipay,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (window.intellipay) {
|
||||||
|
// eslint-disable-next-line no-eval
|
||||||
|
eval(response.data);
|
||||||
|
SetIntellipayCallbackFunctions();
|
||||||
|
window.intellipay.autoOpen();
|
||||||
|
} else {
|
||||||
|
var rg = document.createRange();
|
||||||
|
let node = rg.createContextualFragment(response.data);
|
||||||
|
document.documentElement.appendChild(node);
|
||||||
|
SetIntellipayCallbackFunctions();
|
||||||
|
window.intellipay.isAutoOpen = true;
|
||||||
|
window.intellipay.initialize();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
notification.open({
|
||||||
|
type: "error",
|
||||||
|
message: t("job_payments.notifications.error.openingip"),
|
||||||
|
});
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card title="Card Payment">
|
||||||
|
<Spin spinning={loading}>
|
||||||
|
<Form
|
||||||
|
onFinish={handleFinish}
|
||||||
|
form={form}
|
||||||
|
layout="vertical"
|
||||||
|
initialValues={{
|
||||||
|
payments: context.jobid ? [{ jobid: context.jobid }] : [],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Form.List name={["payments"]}>
|
||||||
|
{(fields, { add, remove, move }) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{fields.map((field, index) => (
|
||||||
|
<Form.Item key={field.key}>
|
||||||
|
<Row gutter={[16, 16]}>
|
||||||
|
<Col span={16}>
|
||||||
|
<Form.Item
|
||||||
|
key={`${index}jobid`}
|
||||||
|
label={t("jobs.fields.ro_number")}
|
||||||
|
name={[field.name, "jobid"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<JobSearchSelectComponent
|
||||||
|
notExported={false}
|
||||||
|
clm_no
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col span={6}>
|
||||||
|
<Form.Item
|
||||||
|
key={`${index}amount`}
|
||||||
|
label={t("payments.fields.amount")}
|
||||||
|
name={[field.name, "amount"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<CurrencyFormItemComponent />
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
<Col span={2}>
|
||||||
|
<DeleteFilled
|
||||||
|
style={{ margin: "1rem" }}
|
||||||
|
onClick={() => {
|
||||||
|
remove(field.name);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Form.Item>
|
||||||
|
))}
|
||||||
|
<Form.Item>
|
||||||
|
<Button
|
||||||
|
type="dashed"
|
||||||
|
onClick={() => {
|
||||||
|
add();
|
||||||
|
}}
|
||||||
|
style={{ width: "100%" }}
|
||||||
|
>
|
||||||
|
{t("general.actions.add")}
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Form.List>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
shouldUpdate={(prevValues, curValues) =>
|
||||||
|
prevValues.payments?.map((p) => p?.jobid).join() !==
|
||||||
|
curValues.payments?.map((p) => p?.jobid).join()
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{() => {
|
||||||
|
console.log("Updating the owner info section.");
|
||||||
|
//If all of the job ids have been fileld in, then query and update the IP field.
|
||||||
|
const { payments } = form.getFieldsValue();
|
||||||
|
if (
|
||||||
|
payments?.length > 0 &&
|
||||||
|
payments?.filter((p) => p?.jobid).length === payments?.length
|
||||||
|
) {
|
||||||
|
console.log("**Calling refetch.");
|
||||||
|
refetch({ jobids: payments.map((p) => p.jobid) });
|
||||||
|
}
|
||||||
|
console.log(
|
||||||
|
"Acc info",
|
||||||
|
data,
|
||||||
|
payments && data && data.jobs.length > 0
|
||||||
|
? data.jobs.map((j) => j.ro_number).join(", ")
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Input
|
||||||
|
className="ipayfield"
|
||||||
|
data-ipayname="account"
|
||||||
|
//type="hidden"
|
||||||
|
value={
|
||||||
|
payments && data && data.jobs.length > 0
|
||||||
|
? data.jobs.map((j) => j.ro_number).join(", ")
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
hidden
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
className="ipayfield"
|
||||||
|
data-ipayname="email"
|
||||||
|
// type="hidden"
|
||||||
|
value={
|
||||||
|
payments && data && data.jobs.length > 0
|
||||||
|
? data.jobs.filter((j) => j.ownr_ea)[0]?.ownr_ea
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
hidden
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
shouldUpdate={(prevValues, curValues) =>
|
||||||
|
prevValues.payments?.map((p) => p?.amount).join() !==
|
||||||
|
curValues.payments?.map((p) => p?.amount).join()
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{() => {
|
||||||
|
const { payments } = form.getFieldsValue();
|
||||||
|
const totalAmountToCharge = payments?.reduce((acc, val) => {
|
||||||
|
return acc + (val?.amount || 0);
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Space style={{ float: "right" }}>
|
||||||
|
<Statistic
|
||||||
|
title="Amount To Charge"
|
||||||
|
value={totalAmountToCharge}
|
||||||
|
precision={2}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
className="ipayfield"
|
||||||
|
data-ipayname="amount"
|
||||||
|
//type="hidden"
|
||||||
|
value={totalAmountToCharge?.toFixed(2)}
|
||||||
|
hidden
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
// data-ipayname="submit"
|
||||||
|
className="ipayfield"
|
||||||
|
loading={queryLoading || loading}
|
||||||
|
disabled={!(totalAmountToCharge > 0)}
|
||||||
|
onClick={handleIntelliPayCharge}
|
||||||
|
>
|
||||||
|
{t("job_payments.buttons.proceedtopayment")}
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
{/* Lightbox payment response when it is completed */}
|
||||||
|
<Form.Item name="paymentResponse" hidden>
|
||||||
|
<Input type="hidden" />
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Spin>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(CardPaymentModalComponent);
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { Button, Modal } from "antd";
|
||||||
|
import React from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
||||||
|
import { selectCardPayment } from "../../redux/modals/modals.selectors";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import CardPaymentModalComponent from "./card-payment-modal.component.";
|
||||||
|
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
cardPaymentModal: selectCardPayment,
|
||||||
|
bodyshop: selectBodyshop,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
toggleModalVisible: () => dispatch(toggleModalVisible("cardPayment")),
|
||||||
|
});
|
||||||
|
|
||||||
|
function CardPaymentModalContainer({
|
||||||
|
cardPaymentModal,
|
||||||
|
toggleModalVisible,
|
||||||
|
bodyshop,
|
||||||
|
}) {
|
||||||
|
const { open } = cardPaymentModal;
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
toggleModalVisible();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOK = () => {
|
||||||
|
toggleModalVisible();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
open={open}
|
||||||
|
onOk={handleOK}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
footer={[
|
||||||
|
<Button key="back" onClick={handleCancel}>
|
||||||
|
{t("job_payments.buttons.goback")}
|
||||||
|
</Button>,
|
||||||
|
]}
|
||||||
|
width="80%"
|
||||||
|
destroyOnClose
|
||||||
|
>
|
||||||
|
<CardPaymentModalComponent />
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(CardPaymentModalContainer);
|
||||||
@@ -4,20 +4,11 @@ import { Button, notification, Space } from "antd";
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
|
||||||
import { createStructuredSelector } from "reselect";
|
|
||||||
import { messaging, requestForToken } from "../../firebase/firebase.utils";
|
import { messaging, requestForToken } from "../../firebase/firebase.utils";
|
||||||
import { selectChatVisible } from "../../redux/messaging/messaging.selectors";
|
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
|
||||||
import FcmHandler from "../../utils/fcm-handler";
|
import FcmHandler from "../../utils/fcm-handler";
|
||||||
import ChatPopupComponent from "../chat-popup/chat-popup.component";
|
import ChatPopupComponent from "../chat-popup/chat-popup.component";
|
||||||
import "./chat-affix.styles.scss";
|
import "./chat-affix.styles.scss";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
|
||||||
bodyshop: selectBodyshop,
|
|
||||||
chatVisible: selectChatVisible,
|
|
||||||
});
|
|
||||||
|
|
||||||
export function ChatAffixContainer({ bodyshop, chatVisible }) {
|
export function ChatAffixContainer({ bodyshop, chatVisible }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const client = useApolloClient();
|
const client = useApolloClient();
|
||||||
@@ -28,7 +19,7 @@ export function ChatAffixContainer({ bodyshop, chatVisible }) {
|
|||||||
try {
|
try {
|
||||||
const r = await axios.post("/notifications/subscribe", {
|
const r = await axios.post("/notifications/subscribe", {
|
||||||
fcm_tokens: await getToken(messaging, {
|
fcm_tokens: await getToken(messaging, {
|
||||||
vapidKey: process.env.REACT_APP_FIREBASE_PUBLIC_VAPID_KEY,
|
vapidKey: import.meta.env.VITE_APP_FIREBASE_PUBLIC_VAPID_KEY,
|
||||||
}),
|
}),
|
||||||
type: "messaging",
|
type: "messaging",
|
||||||
imexshopid: bodyshop.imexshopid,
|
imexshopid: bodyshop.imexshopid,
|
||||||
@@ -36,35 +27,34 @@ export function ChatAffixContainer({ bodyshop, chatVisible }) {
|
|||||||
console.log("FCM Topic Subscription", r.data);
|
console.log("FCM Topic Subscription", r.data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(
|
console.log(
|
||||||
"Error attempting to subscribe to messaging topic: ",
|
"Error attempting to subscribe to messaging topic: ",
|
||||||
error
|
error
|
||||||
);
|
);
|
||||||
notification.open({
|
notification.open({
|
||||||
type: "warning",
|
type: "warning",
|
||||||
message: t("general.errors.fcm"),
|
message: t("general.errors.fcm"),
|
||||||
btn: (
|
btn: (
|
||||||
<Space>
|
<Space>
|
||||||
<Button
|
<Button
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await requestForToken();
|
await requestForToken();
|
||||||
|
SubscribeToTopic();
|
||||||
SubscribeToTopic();
|
}}
|
||||||
}}
|
>
|
||||||
>
|
{t("general.actions.tryagain")}
|
||||||
{t("general.actions.tryagain")}
|
</Button>
|
||||||
</Button>
|
<Button
|
||||||
<Button
|
onClick={() => {
|
||||||
onClick={() => {
|
const win = window.open(
|
||||||
const win = window.open(
|
"https://help.imex.online/en/article/enabling-notifications-o978xi/",
|
||||||
"https://help.imex.online/en/article/enabling-notifications-o978xi/",
|
"_blank"
|
||||||
"_blank"
|
);
|
||||||
);
|
win.focus();
|
||||||
win.focus();
|
}}
|
||||||
}}
|
>
|
||||||
>
|
{t("general.labels.help")}
|
||||||
{t("general.labels.help")}
|
</Button>
|
||||||
</Button>
|
</Space>
|
||||||
</Space>
|
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -81,16 +71,16 @@ export function ChatAffixContainer({ bodyshop, chatVisible }) {
|
|||||||
payload: (payload && payload.data && payload.data.data) || payload.data,
|
payload: (payload && payload.data && payload.data.data) || payload.data,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
let stopMessageListenr, channel;
|
let stopMessageListener, channel;
|
||||||
try {
|
try {
|
||||||
stopMessageListenr = onMessage(messaging, handleMessage);
|
stopMessageListener = onMessage(messaging, handleMessage);
|
||||||
channel = new BroadcastChannel("imex-sw-messages");
|
channel = new BroadcastChannel("imex-sw-messages");
|
||||||
channel.addEventListener("message", handleMessage);
|
channel.addEventListener("message", handleMessage);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("Unable to set event listeners.");
|
console.log("Unable to set event listeners.");
|
||||||
}
|
}
|
||||||
return () => {
|
return () => {
|
||||||
stopMessageListenr && stopMessageListenr();
|
stopMessageListener && stopMessageListener();
|
||||||
channel && channel.removeEventListener("message", handleMessage);
|
channel && channel.removeEventListener("message", handleMessage);
|
||||||
};
|
};
|
||||||
}, [client]);
|
}, [client]);
|
||||||
@@ -98,9 +88,10 @@ export function ChatAffixContainer({ bodyshop, chatVisible }) {
|
|||||||
if (!bodyshop || !bodyshop.messagingservicesid) return <></>;
|
if (!bodyshop || !bodyshop.messagingservicesid) return <></>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`chat-affix ${chatVisible ? "chat-affix-open" : ""}`}>
|
<div className={`chat-affix ${chatVisible ? "chat-affix-open" : ""}`}>
|
||||||
{bodyshop && bodyshop.messagingservicesid ? <ChatPopupComponent /> : null}
|
{bodyshop && bodyshop.messagingservicesid ? <ChatPopupComponent /> : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
export default connect(mapStateToProps, null)(ChatAffixContainer);
|
|
||||||
|
export default ChatAffixContainer;
|
||||||
@@ -1,77 +1,122 @@
|
|||||||
import { Badge, List, Tag } from "antd";
|
import {Badge, Card, List, Space, Tag} from "antd";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { connect } from "react-redux";
|
import {connect} from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import {AutoSizer, CellMeasurer, CellMeasurerCache, List as VirtualizedList,} from "react-virtualized";
|
||||||
import { setSelectedConversation } from "../../redux/messaging/messaging.actions";
|
import {createStructuredSelector} from "reselect";
|
||||||
import { selectSelectedConversation } from "../../redux/messaging/messaging.selectors";
|
import {setSelectedConversation} from "../../redux/messaging/messaging.actions";
|
||||||
import { TimeAgoFormatter } from "../../utils/DateFormatter";
|
import {selectSelectedConversation} from "../../redux/messaging/messaging.selectors";
|
||||||
|
import {TimeAgoFormatter} from "../../utils/DateFormatter";
|
||||||
import PhoneFormatter from "../../utils/PhoneFormatter";
|
import PhoneFormatter from "../../utils/PhoneFormatter";
|
||||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
import {OwnerNameDisplayFunction} from "../owner-name-display/owner-name-display.component";
|
||||||
|
import _ from "lodash";
|
||||||
import "./chat-conversation-list.styles.scss";
|
import "./chat-conversation-list.styles.scss";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
selectedConversation: selectSelectedConversation,
|
selectedConversation: selectSelectedConversation,
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
setSelectedConversation: (conversationId) =>
|
setSelectedConversation: (conversationId) =>
|
||||||
dispatch(setSelectedConversation(conversationId)),
|
dispatch(setSelectedConversation(conversationId)),
|
||||||
});
|
});
|
||||||
|
|
||||||
export function ChatConversationListComponent({
|
function ChatConversationListComponent({
|
||||||
conversationList,
|
conversationList,
|
||||||
selectedConversation,
|
selectedConversation,
|
||||||
setSelectedConversation,
|
setSelectedConversation,
|
||||||
}) {
|
loadMoreConversations,
|
||||||
return (
|
}) {
|
||||||
<div className="chat-list-container">
|
const cache = new CellMeasurerCache({
|
||||||
<List
|
fixedWidth: true,
|
||||||
bordered
|
defaultHeight: 60,
|
||||||
dataSource={conversationList}
|
});
|
||||||
renderItem={(item) => (
|
|
||||||
<List.Item
|
const rowRenderer = ({index, key, style, parent}) => {
|
||||||
key={item.id}
|
const item = conversationList[index];
|
||||||
onClick={() => setSelectedConversation(item.id)}
|
const cardContentRight =
|
||||||
className={`chat-list-item ${
|
<TimeAgoFormatter>{item.updated_at}</TimeAgoFormatter>;
|
||||||
item.id === selectedConversation
|
const cardContentLeft = item.job_conversations.length > 0
|
||||||
? "chat-list-selected-conversation"
|
? item.job_conversations.map((j, idx) => (
|
||||||
: null
|
<Tag key={idx}>{j.job.ro_number}</Tag>
|
||||||
}`}
|
))
|
||||||
>
|
: null;
|
||||||
<div sryle={{ display: "inline-block" }}>
|
|
||||||
{item.label && <div className="chat-name">{item.label}</div>}
|
const names = <>{_.uniq(item.job_conversations.map((j, idx) =>
|
||||||
{item.job_conversations.length > 0 ? (
|
OwnerNameDisplayFunction(j.job)
|
||||||
<div className="chat-name">
|
))}</>
|
||||||
{item.job_conversations.map((j, idx) => (
|
|
||||||
<div key={idx}>
|
const cardTitle = <>
|
||||||
<OwnerNameDisplay ownerObject={j.job} />
|
{item.label && <Tag color="blue">{item.label}</Tag>}
|
||||||
</div>
|
{item.job_conversations.length > 0 ? (
|
||||||
))}
|
<Space direction="vertical">
|
||||||
</div>
|
{names}
|
||||||
) : (
|
</Space>
|
||||||
<PhoneFormatter>{item.phone_num}</PhoneFormatter>
|
) : (
|
||||||
)}
|
<Space>
|
||||||
</div>
|
<PhoneFormatter>{item.phone_num}</PhoneFormatter>
|
||||||
<div sryle={{ display: "inline-block" }}>
|
</Space>
|
||||||
<div>
|
)}
|
||||||
{item.job_conversations.length > 0
|
</>
|
||||||
? item.job_conversations.map((j, idx) => (
|
const cardExtra = <Badge count={item.messages_aggregate.aggregate.count || 0}/>
|
||||||
<Tag key={idx} className="ro-number-tag">
|
|
||||||
{j.job.ro_number}
|
const getCardStyle = () =>
|
||||||
</Tag>
|
item.id === selectedConversation
|
||||||
))
|
? { backgroundColor: 'rgba(128, 128, 128, 0.2)' }
|
||||||
: null}
|
: { backgroundColor: index % 2 === 0 ? '#f0f2f5' : '#ffffff' };
|
||||||
</div>
|
|
||||||
<TimeAgoFormatter>{item.updated_at}</TimeAgoFormatter>
|
return (
|
||||||
</div>
|
<CellMeasurer
|
||||||
<Badge count={item.messages_aggregate.aggregate.count || 0} />
|
key={key}
|
||||||
</List.Item>
|
cache={cache}
|
||||||
)}
|
parent={parent}
|
||||||
/>
|
columnIndex={0}
|
||||||
</div>
|
rowIndex={index}
|
||||||
);
|
>
|
||||||
|
<List.Item
|
||||||
|
onClick={() => setSelectedConversation(item.id)}
|
||||||
|
style={style}
|
||||||
|
className={`chat-list-item
|
||||||
|
${
|
||||||
|
item.id === selectedConversation
|
||||||
|
? "chat-list-selected-conversation"
|
||||||
|
: null
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Card style={getCardStyle()} bordered={false} size="small" extra={cardExtra} title={cardTitle}>
|
||||||
|
<div style={{display: 'inline-block', width: '70%', textAlign: 'left'}}>
|
||||||
|
{cardContentLeft}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{display: 'inline-block', width: '30%', textAlign: 'right'}}>{cardContentRight}</div>
|
||||||
|
</Card>
|
||||||
|
</List.Item>
|
||||||
|
</CellMeasurer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="chat-list-container">
|
||||||
|
<AutoSizer>
|
||||||
|
{({height, width}) => (
|
||||||
|
<VirtualizedList
|
||||||
|
height={height}
|
||||||
|
width={width}
|
||||||
|
rowCount={conversationList.length}
|
||||||
|
rowHeight={cache.rowHeight}
|
||||||
|
rowRenderer={rowRenderer}
|
||||||
|
onScroll={({scrollTop, scrollHeight, clientHeight}) => {
|
||||||
|
if (scrollTop + clientHeight === scrollHeight) {
|
||||||
|
loadMoreConversations();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</AutoSizer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
mapStateToProps,
|
mapStateToProps,
|
||||||
mapDispatchToProps
|
mapDispatchToProps
|
||||||
)(ChatConversationListComponent);
|
)(ChatConversationListComponent);
|
||||||
|
|||||||
@@ -1,24 +1,14 @@
|
|||||||
.chat-list-selected-conversation {
|
|
||||||
background-color: rgba(128, 128, 128, 0.2);
|
|
||||||
}
|
|
||||||
.chat-list-container {
|
.chat-list-container {
|
||||||
flex: 1;
|
overflow: hidden;
|
||||||
overflow: auto;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
border: 1px solid gainsboro;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-list-item {
|
.chat-list-item {
|
||||||
display: flex;
|
.ant-card-head {
|
||||||
flex-direction: row;
|
border: none;
|
||||||
|
}
|
||||||
&:hover {
|
&:hover {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: #ff7a00;
|
color: #ff7a00;
|
||||||
}
|
}
|
||||||
.chat-name {
|
|
||||||
flex: 1;
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
.ro-number-tag {
|
|
||||||
align-self: baseline;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
|||||||
import ChatArchiveButton from "../chat-archive-button/chat-archive-button.component";
|
import ChatArchiveButton from "../chat-archive-button/chat-archive-button.component";
|
||||||
import ChatConversationTitleTags from "../chat-conversation-title-tags/chat-conversation-title-tags.component";
|
import ChatConversationTitleTags from "../chat-conversation-title-tags/chat-conversation-title-tags.component";
|
||||||
import ChatLabelComponent from "../chat-label/chat-label.component";
|
import ChatLabelComponent from "../chat-label/chat-label.component";
|
||||||
|
import ChatPrintButton from "../chat-print-button/chat-print-button.component";
|
||||||
import ChatTagRoContainer from "../chat-tag-ro/chat-tag-ro.container";
|
import ChatTagRoContainer from "../chat-tag-ro/chat-tag-ro.container";
|
||||||
|
|
||||||
export default function ChatConversationTitle({ conversation }) {
|
export default function ChatConversationTitle({ conversation }) {
|
||||||
@@ -13,6 +14,7 @@ export default function ChatConversationTitle({ conversation }) {
|
|||||||
{conversation && conversation.phone_num}
|
{conversation && conversation.phone_num}
|
||||||
</PhoneNumberFormatter>
|
</PhoneNumberFormatter>
|
||||||
<ChatLabelComponent conversation={conversation} />
|
<ChatLabelComponent conversation={conversation} />
|
||||||
|
<ChatPrintButton conversation={conversation} />
|
||||||
<ChatConversationTitleTags
|
<ChatConversationTitleTags
|
||||||
jobConversations={
|
jobConversations={
|
||||||
(conversation && conversation.job_conversations) || []
|
(conversation && conversation.job_conversations) || []
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ export function ChatConversationContainer({ bodyshop, selectedConversation }) {
|
|||||||
MARK_MESSAGES_AS_READ_BY_CONVERSATION,
|
MARK_MESSAGES_AS_READ_BY_CONVERSATION,
|
||||||
{
|
{
|
||||||
variables: { conversationId: selectedConversation },
|
variables: { conversationId: selectedConversation },
|
||||||
|
refetchQueries: ["UNREAD_CONVERSATION_COUNT"],
|
||||||
update(cache) {
|
update(cache) {
|
||||||
cache.modify({
|
cache.modify({
|
||||||
id: cache.identify({
|
id: cache.identify({
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export function ChatMediaSelector({
|
|||||||
conversation,
|
conversation,
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [visible, setVisible] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const { loading, error, data } = useQuery(GET_DOCUMENTS_BY_JOB, {
|
const { loading, error, data } = useQuery(GET_DOCUMENTS_BY_JOB, {
|
||||||
fetchPolicy: "network-only",
|
fetchPolicy: "network-only",
|
||||||
@@ -39,13 +39,13 @@ export function ChatMediaSelector({
|
|||||||
},
|
},
|
||||||
|
|
||||||
skip:
|
skip:
|
||||||
!visible ||
|
!open ||
|
||||||
!conversation.job_conversations ||
|
!conversation.job_conversations ||
|
||||||
conversation.job_conversations.length === 0,
|
conversation.job_conversations.length === 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleVisibleChange = (visible) => {
|
const handleVisibleChange = (change) => {
|
||||||
setVisible(visible);
|
setOpen(change);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -65,7 +65,7 @@ export function ChatMediaSelector({
|
|||||||
externalMediaState={[selectedMedia, setSelectedMedia]}
|
externalMediaState={[selectedMedia, setSelectedMedia]}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{bodyshop.uselocalmediaserver && visible && (
|
{bodyshop.uselocalmediaserver && open && (
|
||||||
<JobDocumentsLocalGalleryExternal
|
<JobDocumentsLocalGalleryExternal
|
||||||
externalMediaState={[selectedMedia, setSelectedMedia]}
|
externalMediaState={[selectedMedia, setSelectedMedia]}
|
||||||
jobId={
|
jobId={
|
||||||
@@ -88,8 +88,8 @@ export function ChatMediaSelector({
|
|||||||
}
|
}
|
||||||
title={t("messaging.labels.selectmedia")}
|
title={t("messaging.labels.selectmedia")}
|
||||||
trigger="click"
|
trigger="click"
|
||||||
visible={visible}
|
open={open}
|
||||||
onVisibleChange={handleVisibleChange}
|
onOpenChange={handleVisibleChange}
|
||||||
>
|
>
|
||||||
<Badge count={selectedMedia.filter((s) => s.isSelected).length}>
|
<Badge count={selectedMedia.filter((s) => s.isSelected).length}>
|
||||||
<PictureFilled style={{ margin: "0 .5rem" }} />
|
<PictureFilled style={{ margin: "0 .5rem" }} />
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import Icon from "@ant-design/icons";
|
import Icon from "@ant-design/icons";
|
||||||
import { Tooltip } from "antd";
|
import { Tooltip } from "antd";
|
||||||
import i18n from "i18next";
|
import i18n from "i18next";
|
||||||
import moment from "moment";
|
import dayjs from "../../utils/day";
|
||||||
import React, { useEffect, useRef } from "react";
|
import React, { useEffect, useRef } from "react";
|
||||||
import { MdDone, MdDoneAll } from "react-icons/md";
|
import { MdDone, MdDoneAll } from "react-icons/md";
|
||||||
import {
|
import {
|
||||||
@@ -52,7 +52,7 @@ export default function ChatMessageListComponent({ messages }) {
|
|||||||
<div style={{ fontSize: 10 }}>
|
<div style={{ fontSize: 10 }}>
|
||||||
{i18n.t("messaging.labels.sentby", {
|
{i18n.t("messaging.labels.sentby", {
|
||||||
by: messages[index].userid,
|
by: messages[index].userid,
|
||||||
time: moment(messages[index].created_at).format(
|
time: dayjs(messages[index].created_at).format(
|
||||||
"MM/DD/YYYY @ hh:mm a"
|
"MM/DD/YYYY @ hh:mm a"
|
||||||
),
|
),
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -8,15 +8,23 @@ import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
|||||||
|
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import { searchingForConversation } from "../../redux/messaging/messaging.selectors";
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
|
searchingForConversation: searchingForConversation,
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
openChatByPhone: (phone) => dispatch(openChatByPhone(phone)),
|
openChatByPhone: (phone) => dispatch(openChatByPhone(phone)),
|
||||||
});
|
});
|
||||||
|
|
||||||
export function ChatOpenButton({ bodyshop, phone, jobid, openChatByPhone }) {
|
export function ChatOpenButton({
|
||||||
|
bodyshop,
|
||||||
|
searchingForConversation,
|
||||||
|
phone,
|
||||||
|
jobid,
|
||||||
|
openChatByPhone,
|
||||||
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
if (!phone) return <></>;
|
if (!phone) return <></>;
|
||||||
|
|
||||||
@@ -29,7 +37,7 @@ export function ChatOpenButton({ bodyshop, phone, jobid, openChatByPhone }) {
|
|||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const p = parsePhoneNumber(phone, "CA");
|
const p = parsePhoneNumber(phone, "CA");
|
||||||
|
if (searchingForConversation) return; //This is to prevent finding the same thing twice.
|
||||||
if (p && p.isValid()) {
|
if (p && p.isValid()) {
|
||||||
openChatByPhone({ phone_num: p.formatInternational(), jobid: jobid });
|
openChatByPhone({ phone_num: p.formatInternational(), jobid: jobid });
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -4,13 +4,16 @@ import {
|
|||||||
ShrinkOutlined,
|
ShrinkOutlined,
|
||||||
SyncOutlined,
|
SyncOutlined,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import { useQuery } from "@apollo/client";
|
import { useLazyQuery, useQuery } from "@apollo/client";
|
||||||
import { Badge, Card, Col, Row, Space, Tag, Tooltip, Typography } from "antd";
|
import { Badge, Card, Col, Row, Space, Tag, Tooltip, Typography } from "antd";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useCallback, useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { CONVERSATION_LIST_QUERY } from "../../graphql/conversations.queries";
|
import {
|
||||||
|
CONVERSATION_LIST_QUERY,
|
||||||
|
UNREAD_CONVERSATION_COUNT,
|
||||||
|
} from "../../graphql/conversations.queries";
|
||||||
import { toggleChatVisible } from "../../redux/messaging/messaging.actions";
|
import { toggleChatVisible } from "../../redux/messaging/messaging.actions";
|
||||||
import {
|
import {
|
||||||
selectChatVisible,
|
selectChatVisible,
|
||||||
@@ -37,12 +40,21 @@ export function ChatPopupComponent({
|
|||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [pollInterval, setpollInterval] = useState(0);
|
const [pollInterval, setpollInterval] = useState(0);
|
||||||
const { loading, data, refetch, called } = useQuery(CONVERSATION_LIST_QUERY, {
|
|
||||||
|
const { data: unreadData } = useQuery(UNREAD_CONVERSATION_COUNT, {
|
||||||
fetchPolicy: "network-only",
|
fetchPolicy: "network-only",
|
||||||
nextFetchPolicy: "network-only",
|
nextFetchPolicy: "network-only",
|
||||||
...(pollInterval > 0 ? { pollInterval } : {}),
|
...(pollInterval > 0 ? { pollInterval } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [getConversations, { loading, data, refetch, fetchMore }] =
|
||||||
|
useLazyQuery(CONVERSATION_LIST_QUERY, {
|
||||||
|
fetchPolicy: "network-only",
|
||||||
|
nextFetchPolicy: "network-only",
|
||||||
|
skip: !chatVisible,
|
||||||
|
...(pollInterval > 0 ? { pollInterval } : {}),
|
||||||
|
});
|
||||||
|
|
||||||
const fcmToken = sessionStorage.getItem("fcmtoken");
|
const fcmToken = sessionStorage.getItem("fcmtoken");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -54,15 +66,24 @@ export function ChatPopupComponent({
|
|||||||
}, [fcmToken]);
|
}, [fcmToken]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (called && chatVisible) refetch();
|
if (chatVisible)
|
||||||
}, [chatVisible, called, refetch]);
|
getConversations({
|
||||||
|
variables: {
|
||||||
|
offset: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, [chatVisible, getConversations]);
|
||||||
|
|
||||||
const unreadCount = data
|
const loadMoreConversations = useCallback(() => {
|
||||||
? data.conversations.reduce(
|
if (data)
|
||||||
(acc, val) => val.messages_aggregate.aggregate.count + acc,
|
fetchMore({
|
||||||
0
|
variables: {
|
||||||
)
|
offset: data.conversations.length,
|
||||||
: 0;
|
},
|
||||||
|
});
|
||||||
|
}, [data, fetchMore]);
|
||||||
|
|
||||||
|
const unreadCount = unreadData?.messages_aggregate.aggregate.count || 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Badge count={unreadCount}>
|
<Badge count={unreadCount}>
|
||||||
@@ -97,6 +118,7 @@ export function ChatPopupComponent({
|
|||||||
) : (
|
) : (
|
||||||
<ChatConversationListComponent
|
<ChatConversationListComponent
|
||||||
conversationList={data ? data.conversations : []}
|
conversationList={data ? data.conversations : []}
|
||||||
|
loadMoreConversations={loadMoreConversations}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Col>
|
</Col>
|
||||||
@@ -110,7 +132,7 @@ export function ChatPopupComponent({
|
|||||||
onClick={() => toggleChatVisible()}
|
onClick={() => toggleChatVisible()}
|
||||||
style={{ cursor: "pointer" }}
|
style={{ cursor: "pointer" }}
|
||||||
>
|
>
|
||||||
<MessageOutlined />
|
<MessageOutlined className="chat-popup-info-icon" />
|
||||||
<strong>{t("messaging.labels.messaging")}</strong>
|
<strong>{t("messaging.labels.messaging")}</strong>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -13,6 +13,9 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.chat-popup-info-icon {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
@media only screen and (min-width: 992px) {
|
@media only screen and (min-width: 992px) {
|
||||||
.chat-popup {
|
.chat-popup {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { PlusCircleOutlined } from "@ant-design/icons";
|
import { PlusCircleOutlined } from "@ant-design/icons";
|
||||||
import { Dropdown, Menu } from "antd";
|
import { Dropdown } from "antd";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
@@ -16,19 +16,16 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export function ChatPresetsComponent({ bodyshop, setMessage, className }) {
|
export function ChatPresetsComponent({ bodyshop, setMessage, className }) {
|
||||||
const menu = (
|
|
||||||
<Menu>
|
const items = bodyshop.md_messaging_presets.map((i, idx) => ({
|
||||||
{bodyshop.md_messaging_presets.map((i, idx) => (
|
key: idx,
|
||||||
<Menu.Item onClick={() => setMessage(i.text)} key={idx}>
|
label: (i.label),
|
||||||
{i.label}
|
onClick: () => setMessage(i.text),
|
||||||
</Menu.Item>
|
}));
|
||||||
))}
|
|
||||||
</Menu>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
<Dropdown trigger={["click"]} overlay={menu}>
|
<Dropdown trigger={["click"]} menu={{items}}>
|
||||||
<PlusCircleOutlined />
|
<PlusCircleOutlined />
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { MailOutlined, PrinterOutlined } from "@ant-design/icons";
|
||||||
|
import { Space, Spin } from "antd";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { setEmailOptions } from "../../redux/email/email.actions";
|
||||||
|
import { GenerateDocument } from "../../utils/RenderTemplate";
|
||||||
|
import { TemplateList } from "../../utils/TemplateConstants";
|
||||||
|
|
||||||
|
const mapStateToProps = createStructuredSelector({});
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
setEmailOptions: (e) => dispatch(setEmailOptions(e)),
|
||||||
|
});
|
||||||
|
|
||||||
|
export function ChatPrintButton({ conversation }) {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const generateDocument = (type) => {
|
||||||
|
setLoading(true);
|
||||||
|
GenerateDocument(
|
||||||
|
{
|
||||||
|
name: TemplateList("messaging").conversation_list.key,
|
||||||
|
variables: { id: conversation.id },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subject: TemplateList("messaging").conversation_list.subject,
|
||||||
|
},
|
||||||
|
type,
|
||||||
|
conversation.id
|
||||||
|
).catch(e => {
|
||||||
|
console.warn('Something went wrong generating a document.');
|
||||||
|
});
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Space wrap>
|
||||||
|
<PrinterOutlined onClick={() => generateDocument('p')}/>
|
||||||
|
<MailOutlined onClick={() => generateDocument('e')}/>
|
||||||
|
{loading && <Spin />}
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(ChatPrintButton);
|
||||||
@@ -9,17 +9,17 @@ export default function ChatTagRoComponent({
|
|||||||
loading,
|
loading,
|
||||||
handleSearch,
|
handleSearch,
|
||||||
handleInsertTag,
|
handleInsertTag,
|
||||||
setVisible,
|
setOpen,
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Space flex>
|
<Space>
|
||||||
<div style={{ width: "15rem" }}>
|
<div style={{ width: "15rem" }}>
|
||||||
<Select
|
<Select
|
||||||
showSearch
|
showSearch
|
||||||
autoFocus
|
autoFocus
|
||||||
dropdownMatchSelectWidth
|
popupMatchSelectWidth
|
||||||
placeholder={t("general.labels.search")}
|
placeholder={t("general.labels.search")}
|
||||||
filterOption={false}
|
filterOption={false}
|
||||||
onSearch={handleSearch}
|
onSearch={handleSearch}
|
||||||
@@ -38,7 +38,7 @@ export default function ChatTagRoComponent({
|
|||||||
{loading ? (
|
{loading ? (
|
||||||
<LoadingOutlined />
|
<LoadingOutlined />
|
||||||
) : (
|
) : (
|
||||||
<CloseCircleOutlined onClick={() => setVisible(false)} />
|
<CloseCircleOutlined onClick={() => setOpen(false)} />
|
||||||
)}
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import ChatTagRo from "./chat-tag-ro.component";
|
|||||||
|
|
||||||
export default function ChatTagRoContainer({ conversation }) {
|
export default function ChatTagRoContainer({ conversation }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [visible, setVisible] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const [loadRo, { loading, data }] = useLazyQuery(SEARCH_FOR_JOBS);
|
const [loadRo, { loading, data }] = useLazyQuery(SEARCH_FOR_JOBS);
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ export default function ChatTagRoContainer({ conversation }) {
|
|||||||
const handleInsertTag = (value, option) => {
|
const handleInsertTag = (value, option) => {
|
||||||
logImEXEvent("messaging_add_job_tag");
|
logImEXEvent("messaging_add_job_tag");
|
||||||
insertTag({ variables: { jobId: option.key } });
|
insertTag({ variables: { jobId: option.key } });
|
||||||
setVisible(false);
|
setOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const existingJobTags =
|
const existingJobTags =
|
||||||
@@ -47,16 +47,16 @@ export default function ChatTagRoContainer({ conversation }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{visible ? (
|
{open ? (
|
||||||
<ChatTagRo
|
<ChatTagRo
|
||||||
loading={loading}
|
loading={loading}
|
||||||
roOptions={roOptions}
|
roOptions={roOptions}
|
||||||
handleSearch={handleSearch}
|
handleSearch={handleSearch}
|
||||||
handleInsertTag={handleInsertTag}
|
handleInsertTag={handleInsertTag}
|
||||||
setVisible={setVisible}
|
setOpen={setOpen}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Tag onClick={() => setVisible(true)}>
|
<Tag onClick={() => setOpen(true)}>
|
||||||
<PlusOutlined />
|
<PlusOutlined />
|
||||||
{t("messaging.actions.link")}
|
{t("messaging.actions.link")}
|
||||||
</Tag>
|
</Tag>
|
||||||
|
|||||||
@@ -35,6 +35,15 @@ export default function ContractsCarsComponent({
|
|||||||
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
||||||
render: (text, record) => <div>{t(record.status)}</div>,
|
render: (text, record) => <div>{t(record.status)}</div>,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: t("courtesycars.fields.readiness"),
|
||||||
|
dataIndex: "readiness",
|
||||||
|
key: "readiness",
|
||||||
|
sorter: (a, b) => alphaSort(a.readiness, b.readiness),
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "readiness" && state.sortedInfo.order,
|
||||||
|
render: (text, record) => t(record.readiness),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: t("courtesycars.fields.year"),
|
title: t("courtesycars.fields.year"),
|
||||||
dataIndex: "year",
|
dataIndex: "year",
|
||||||
@@ -59,6 +68,14 @@ export default function ContractsCarsComponent({
|
|||||||
sortOrder:
|
sortOrder:
|
||||||
state.sortedInfo.columnKey === "model" && state.sortedInfo.order,
|
state.sortedInfo.columnKey === "model" && state.sortedInfo.order,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: t("courtesycars.fields.color"),
|
||||||
|
dataIndex: "color",
|
||||||
|
key: "color",
|
||||||
|
sorter: (a, b) => alphaSort(a.color, b.color),
|
||||||
|
sortOrder:
|
||||||
|
state.sortedInfo.columnKey === "color" && state.sortedInfo.order,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: t("courtesycars.fields.plate"),
|
title: t("courtesycars.fields.plate"),
|
||||||
dataIndex: "plate",
|
dataIndex: "plate",
|
||||||
@@ -93,6 +110,9 @@ export default function ContractsCarsComponent({
|
|||||||
(cc.model || "")
|
(cc.model || "")
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.includes(state.search.toLowerCase()) ||
|
.includes(state.search.toLowerCase()) ||
|
||||||
|
(cc.color || "")
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(state.search.toLowerCase()) ||
|
||||||
(cc.plate || "").toLowerCase().includes(state.search.toLowerCase())
|
(cc.plate || "").toLowerCase().includes(state.search.toLowerCase())
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useQuery } from "@apollo/client";
|
import { useQuery } from "@apollo/client";
|
||||||
import moment from "moment";
|
import dayjs from "../../utils/day";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { QUERY_AVAILABLE_CC } from "../../graphql/courtesy-car.queries";
|
import { QUERY_AVAILABLE_CC } from "../../graphql/courtesy-car.queries";
|
||||||
import AlertComponent from "../alert/alert.component";
|
import AlertComponent from "../alert/alert.component";
|
||||||
@@ -7,7 +7,7 @@ import ContractCarsComponent from "./contract-cars.component";
|
|||||||
|
|
||||||
export default function ContractCarsContainer({ selectedCarState, form }) {
|
export default function ContractCarsContainer({ selectedCarState, form }) {
|
||||||
const { loading, error, data } = useQuery(QUERY_AVAILABLE_CC, {
|
const { loading, error, data } = useQuery(QUERY_AVAILABLE_CC, {
|
||||||
variables: { today: moment().format("YYYY-MM-DD") },
|
variables: { today: dayjs().format("YYYY-MM-DD") },
|
||||||
fetchPolicy: "network-only",
|
fetchPolicy: "network-only",
|
||||||
nextFetchPolicy: "network-only",
|
nextFetchPolicy: "network-only",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ import {
|
|||||||
Space,
|
Space,
|
||||||
} from "antd";
|
} from "antd";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import moment from "moment";
|
import dayjs from "../../utils/day";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { useHistory } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { INSERT_NEW_JOB } from "../../graphql/jobs.queries";
|
import { INSERT_NEW_JOB } from "../../graphql/jobs.queries";
|
||||||
import {
|
import {
|
||||||
@@ -38,17 +38,17 @@ export function ContractConvertToRo({
|
|||||||
disabled,
|
disabled,
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [visible, setVisible] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [insertJob] = useMutation(INSERT_NEW_JOB);
|
const [insertJob] = useMutation(INSERT_NEW_JOB);
|
||||||
const history = useHistory();
|
const history = useNavigate();
|
||||||
|
|
||||||
const handleFinish = async (values) => {
|
const handleFinish = async (values) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
const contractLength = moment(contract.actualreturn).diff(
|
const contractLength = dayjs(contract.actualreturn).diff(
|
||||||
moment(contract.start),
|
dayjs(contract.start),
|
||||||
"days"
|
"day"
|
||||||
);
|
);
|
||||||
const billingLines = [];
|
const billingLines = [];
|
||||||
if (contractLength > 0)
|
if (contractLength > 0)
|
||||||
@@ -306,7 +306,7 @@ export function ContractConvertToRo({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setVisible(false);
|
setOpen(false);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -380,7 +380,7 @@ export function ContractConvertToRo({
|
|||||||
<Button type="primary" htmlType="submit" loading={loading}>
|
<Button type="primary" htmlType="submit" loading={loading}>
|
||||||
{t("contracts.actions.convertoro")}
|
{t("contracts.actions.convertoro")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={() => setVisible(false)}>
|
<Button onClick={() => setOpen(false)}>
|
||||||
{t("general.actions.close")}
|
{t("general.actions.close")}
|
||||||
</Button>
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
@@ -390,9 +390,9 @@ export function ContractConvertToRo({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Popover content={popContent} visible={visible}>
|
<Popover content={popContent} open={open}>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => setVisible(true)}
|
onClick={() => setOpen(true)}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
disabled={!contract.dailyrate || !contract.actualreturn || disabled}
|
disabled={!contract.dailyrate || !contract.actualreturn || disabled}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { WarningFilled } from "@ant-design/icons";
|
import { WarningFilled } from "@ant-design/icons";
|
||||||
import { Form, Input, InputNumber, Space } from "antd";
|
import { Form, Input, InputNumber, Space } from "antd";
|
||||||
import moment from "moment";
|
import dayjs from "../../utils/day";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { DateFormatter } from "../../utils/DateFormatter";
|
import { DateFormatter } from "../../utils/DateFormatter";
|
||||||
import ContractLicenseDecodeButton from "../contract-license-decode-button/contract-license-decode-button.component";
|
//import ContractLicenseDecodeButton from "../contract-license-decode-button/contract-license-decode-button.component";
|
||||||
import ContractStatusSelector from "../contract-status-select/contract-status-select.component";
|
import ContractStatusSelector from "../contract-status-select/contract-status-select.component";
|
||||||
import ContractsRatesChangeButton from "../contracts-rates-change-button/contracts-rates-change-button.component";
|
import ContractsRatesChangeButton from "../contracts-rates-change-button/contracts-rates-change-button.component";
|
||||||
import CourtesyCarFuelSlider from "../courtesy-car-fuel-select/courtesy-car-fuel-select.component";
|
import CourtesyCarFuelSlider from "../courtesy-car-fuel-select/courtesy-car-fuel-select.component";
|
||||||
@@ -96,8 +96,8 @@ export default function ContractFormComponent({
|
|||||||
const dueForService =
|
const dueForService =
|
||||||
selectedCar &&
|
selectedCar &&
|
||||||
selectedCar.nextservicedate &&
|
selectedCar.nextservicedate &&
|
||||||
moment(selectedCar.nextservicedate).isBefore(
|
dayjs(selectedCar.nextservicedate).isBefore(
|
||||||
moment(form.getFieldValue("scheduledreturn"))
|
dayjs(form.getFieldValue("scheduledreturn"))
|
||||||
);
|
);
|
||||||
|
|
||||||
if (mileageOver || dueForService)
|
if (mileageOver || dueForService)
|
||||||
@@ -165,7 +165,9 @@ export default function ContractFormComponent({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<ContractLicenseDecodeButton form={form} />
|
{
|
||||||
|
//<ContractLicenseDecodeButton form={form} />
|
||||||
|
}
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
<LayoutFormRow header={t("contracts.labels.driverinformation")}>
|
<LayoutFormRow header={t("contracts.labels.driverinformation")}>
|
||||||
@@ -188,9 +190,9 @@ export default function ContractFormComponent({
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
{() => {
|
{() => {
|
||||||
const dlExpiresBeforeReturn = moment(
|
const dlExpiresBeforeReturn = dayjs(
|
||||||
form.getFieldValue("driver_dlexpiry")
|
form.getFieldValue("driver_dlexpiry")
|
||||||
).isBefore(moment(form.getFieldValue("scheduledreturn")));
|
).isBefore(dayjs(form.getFieldValue("scheduledreturn")));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { alphaSort } from "../../utils/sorters";
|
import { alphaSort } from "../../utils/sorters";
|
||||||
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
||||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||||
|
import {pageLimit} from "../../utils/config";
|
||||||
|
|
||||||
export default function ContractsJobsComponent({
|
export default function ContractsJobsComponent({
|
||||||
loading,
|
loading,
|
||||||
@@ -175,7 +176,7 @@ export default function ContractsJobsComponent({
|
|||||||
loading={loading}
|
loading={loading}
|
||||||
pagination={{
|
pagination={{
|
||||||
position: "top",
|
position: "top",
|
||||||
defaultPageSize: 10,
|
defaultPageSize: pageLimit,
|
||||||
defaultCurrent: defaultCurrent,
|
defaultCurrent: defaultCurrent,
|
||||||
}}
|
}}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ import { useQuery } from "@apollo/client";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { QUERY_ALL_ACTIVE_JOBS } from "../../graphql/jobs.queries";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import AlertComponent from "../alert/alert.component";
|
import AlertComponent from "../alert/alert.component";
|
||||||
import ContractJobsComponent from "./contract-jobs.component";
|
import ContractJobsComponent from "./contract-jobs.component";
|
||||||
import { QUERY_ALL_ACTIVE_JOBS } from "../../graphql/jobs.queries";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
//currentUser: selectCurrentUser
|
//currentUser: selectCurrentUser
|
||||||
@@ -15,6 +15,7 @@ export function ContractJobsContainer({ selectedJobState, bodyshop }) {
|
|||||||
const { loading, error, data } = useQuery(QUERY_ALL_ACTIVE_JOBS, {
|
const { loading, error, data } = useQuery(QUERY_ALL_ACTIVE_JOBS, {
|
||||||
variables: {
|
variables: {
|
||||||
statuses: bodyshop.md_ro_statuses.active_statuses || ["Open"],
|
statuses: bodyshop.md_ro_statuses.active_statuses || ["Open"],
|
||||||
|
isConverted: true,
|
||||||
},
|
},
|
||||||
fetchPolicy: "network-only",
|
fetchPolicy: "network-only",
|
||||||
nextFetchPolicy: "network-only",
|
nextFetchPolicy: "network-only",
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user