Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20752161d3 | ||
|
|
ac86b6ef61 | ||
|
|
dac8957904 | ||
|
|
ef686aec6a | ||
|
|
f31155e0b4 | ||
|
|
78e4857d1a | ||
|
|
c3144366d9 | ||
|
|
f00a7c9e50 | ||
|
|
803d25e9d1 | ||
|
|
ca1b87309f | ||
|
|
0b4548130d | ||
|
|
e8b3c7fed8 | ||
|
|
2ce43ec2c8 | ||
|
|
d5af1e8a17 | ||
|
|
fab7998e22 | ||
|
|
4a4db6851e | ||
|
|
870973e7f3 | ||
|
|
4146680c1e | ||
|
|
d9fcc69ca4 | ||
|
|
d1c405912b | ||
|
|
745aca1754 | ||
|
|
672eeea851 | ||
|
|
98efff65c9 | ||
|
|
fe21978538 | ||
|
|
ca4ccfd889 | ||
|
|
6cb2d69566 | ||
|
|
98b9ebb228 | ||
|
|
1c2f453496 | ||
|
|
b01d8125d9 | ||
|
|
85bc2fbbb0 | ||
|
|
e857724502 | ||
|
|
086d8ee5be | ||
|
|
04543e826a | ||
|
|
04f6e38778 | ||
|
|
1b5c85fd5f | ||
|
|
123425d7d1 | ||
|
|
a5b9d8178c | ||
|
|
aefd2ff933 | ||
|
|
fe93be8be7 | ||
|
|
369e3660fc | ||
|
|
378c0f057b | ||
|
|
4f3c034cec | ||
|
|
e7d3453744 | ||
|
|
47cc0ef580 | ||
|
|
04f71be6a2 | ||
|
|
07603f4bc2 | ||
|
|
ea16180551 | ||
|
|
75ad566d54 | ||
|
|
fda9fb534b | ||
|
|
cb3e31ebbf | ||
|
|
400525debb | ||
|
|
1cc017bb05 | ||
|
|
531dd11185 | ||
|
|
1e80754750 | ||
|
|
0d286938c7 | ||
|
|
360c13df25 | ||
|
|
71b9bd6367 | ||
|
|
4a5b35e147 | ||
|
|
7930a8f439 | ||
|
|
7204183542 | ||
|
|
5561673fdc | ||
|
|
0df44062d6 | ||
|
|
8983440763 | ||
|
|
5a28c0ed06 | ||
|
|
cbca22c308 | ||
|
|
b0e0b38e8f | ||
|
|
49c23fb52b | ||
|
|
da4b604710 | ||
|
|
82d518b3c9 | ||
|
|
dcbde2b661 | ||
|
|
27b0b426ad | ||
|
|
8788afaa8e | ||
|
|
428aa65b38 | ||
|
|
295d942427 | ||
|
|
c4e28e6e6e | ||
|
|
b19fb29b82 | ||
|
|
3d223ba343 | ||
|
|
98754c776f | ||
|
|
1550e3a3d0 | ||
|
|
3500883205 | ||
|
|
d888897b62 | ||
|
|
8f2433fbea | ||
|
|
b671dbac19 | ||
|
|
79cdbc9c8c | ||
|
|
e585b7c943 | ||
|
|
5336e2cb7c | ||
|
|
72401291f8 | ||
|
|
7b34314b9e | ||
|
|
b6f1cceeca | ||
|
|
c422cdac06 | ||
|
|
ab08d2615d | ||
|
|
2b0c7488d9 | ||
|
|
6927540741 | ||
|
|
1ceeee36ac | ||
|
|
c4779c14c1 | ||
|
|
e48869cb2f | ||
|
|
81d3969d1f | ||
|
|
5819103f57 | ||
|
|
6653b7ae5a | ||
|
|
b50c1e70ee | ||
|
|
365c2a217e | ||
|
|
b1b9b20e97 | ||
|
|
75c0cdb5f9 | ||
|
|
740ccca1f6 | ||
|
|
f3b79cb71f | ||
|
|
f95ea0b9e9 | ||
|
|
647a9ce5b4 | ||
|
|
3f3ca668f5 | ||
|
|
2fd6d1a37a | ||
|
|
307137f926 | ||
|
|
2394880dfc | ||
|
|
414cdcd4b5 | ||
|
|
1fa6b25890 | ||
|
|
ff2fd44b19 | ||
|
|
ad466fb066 | ||
|
|
0f179fed67 | ||
|
|
3ded6c35a1 | ||
|
|
64293db3a1 | ||
|
|
18a4c8523a | ||
|
|
f7b39409a8 | ||
|
|
6283524d2f | ||
|
|
216288fd09 | ||
|
|
3b1d8a48b7 | ||
|
|
ec68770ff0 | ||
|
|
f5d2027e3a | ||
|
|
8af0010f55 | ||
|
|
d7b7ad6285 | ||
|
|
fca45ced38 | ||
|
|
ac8ab70246 | ||
|
|
8373679f27 | ||
|
|
0a23aabbe4 | ||
|
|
4a85599703 | ||
|
|
3e9b84de72 | ||
|
|
4313d8d0e4 | ||
|
|
3088fdd163 | ||
|
|
8c86fe27ec | ||
|
|
01e09998e5 | ||
|
|
55fdb52bf3 | ||
|
|
08f46a393b | ||
|
|
9345da543a | ||
|
|
208891e10d | ||
|
|
db0fa61b2b | ||
|
|
221f098de6 | ||
|
|
9658761ede | ||
|
|
6aad832e37 | ||
|
|
e4b6be51d9 | ||
|
|
a3b2c797fb | ||
|
|
22d27abb76 | ||
|
|
45e0679ca7 | ||
|
|
2b2aa20deb | ||
|
|
8d6ed3d2b5 | ||
|
|
6064f66cdf | ||
|
|
0ba29eb799 | ||
|
|
d0dd571478 | ||
|
|
ddb63d81a7 | ||
|
|
f78451d3f9 | ||
|
|
28630e2d23 | ||
|
|
c5022eaee6 | ||
|
|
1b7f07a2c4 | ||
|
|
2484c67406 | ||
|
|
abfc3f32f2 | ||
|
|
5c06ff4242 | ||
|
|
1899acfffc | ||
|
|
796ff058c8 | ||
|
|
c88cb5357c | ||
|
|
a6a0325d4b | ||
|
|
b18d17c44d | ||
|
|
a73779e74b | ||
|
|
5b943c1965 | ||
|
|
f73844d730 | ||
|
|
aaadab540a | ||
|
|
83bc466df4 | ||
|
|
d85ed954f1 | ||
|
|
369deb6e26 | ||
|
|
295d1af910 | ||
|
|
080d7c08dc | ||
|
|
c877f9fc5d | ||
|
|
99c7a58d62 | ||
|
|
97642bf857 | ||
|
|
2f67ac7b0a | ||
|
|
23aecb45d9 | ||
|
|
92b507e621 | ||
|
|
1f6c2c7d38 | ||
|
|
c1be197346 | ||
|
|
38f8561d18 | ||
|
|
10e6ab58a5 | ||
|
|
24a95e8852 | ||
|
|
39fc2daa19 | ||
|
|
c38f59642f | ||
|
|
e9d0af7abb | ||
|
|
b39d845cd6 | ||
|
|
031673f925 | ||
|
|
e299891174 | ||
|
|
b55aff20d3 | ||
|
|
a9ccb1a3e5 | ||
|
|
f3d34b1d2c | ||
|
|
3f56aa0860 | ||
|
|
4eeacbaaf1 | ||
|
|
096a9e5736 | ||
|
|
4650660a26 | ||
|
|
9e6309644a | ||
|
|
14477a17ee | ||
|
|
7d3d618ff3 | ||
|
|
284cd277ea | ||
|
|
df61d752c8 | ||
|
|
9def956929 | ||
|
|
53aa5a26aa | ||
|
|
86cb5ccf78 | ||
|
|
c8e91e5205 | ||
|
|
87cfb643f9 | ||
|
|
2f7bd75608 | ||
|
|
930bac71a0 | ||
|
|
fcad7dc020 | ||
|
|
bada731d16 | ||
|
|
fd5e86edf2 | ||
|
|
1a886261ef | ||
|
|
41c3b4746d | ||
|
|
f52d013b06 | ||
|
|
0045603cb0 | ||
|
|
4235bc077d | ||
|
|
58b3cc6ba2 | ||
|
|
97f5d09306 | ||
|
|
fa98972b27 | ||
|
|
76848b7e09 | ||
|
|
c3faaa65b9 | ||
|
|
e819513650 | ||
|
|
dd0e2f0e58 | ||
|
|
fa08efeac6 | ||
|
|
f3ba02496c | ||
|
|
5cbbb00f66 | ||
|
|
7050df8b4c | ||
|
|
4fb27f4896 | ||
|
|
145aa5da97 | ||
|
|
1a455415f1 | ||
|
|
44ef1fe825 | ||
|
|
cb07e376d5 | ||
|
|
9b472a6b8b | ||
|
|
9840fe9412 | ||
|
|
d1a99d5211 | ||
|
|
5f7736882b | ||
|
|
d8095e077a | ||
|
|
c8fae00ce5 | ||
|
|
7c6a03a2d2 | ||
|
|
657ab13a04 | ||
|
|
40c21321dd | ||
|
|
476dbb555d | ||
|
|
19b4bfb9ef | ||
|
|
e9a85162e7 | ||
|
|
960c297995 | ||
|
|
50e525f448 | ||
|
|
c86f048498 | ||
|
|
39fd2f697f | ||
|
|
3f2cf3ddf4 | ||
|
|
5a53a70dcf | ||
|
|
66323f9347 | ||
|
|
c65f17a216 | ||
|
|
9bf93fb231 | ||
|
|
f418e02270 | ||
|
|
403254a5c6 | ||
|
|
c893d96ad3 | ||
|
|
3971118af8 | ||
|
|
4db0d93f41 | ||
|
|
affbfc5061 | ||
|
|
886ede2007 | ||
|
|
a3936c66aa | ||
|
|
d98453ffc2 | ||
|
|
7424912455 | ||
|
|
e095495081 | ||
|
|
eb4ce5544f | ||
|
|
8eef17834d | ||
|
|
40ed5e2353 | ||
|
|
2aa4c8c361 | ||
|
|
e5b120fa36 | ||
|
|
1e026f8ebd | ||
|
|
e5c7da0847 | ||
|
|
2e67002768 | ||
|
|
9c03649d48 | ||
|
|
df436b3b20 | ||
|
|
34acf4170e | ||
|
|
f9a13c880a | ||
|
|
df7cd45be6 | ||
|
|
b22d70c833 | ||
|
|
1ef8e8aa6a | ||
|
|
2aa58b7eb3 | ||
|
|
9fb61509c4 | ||
|
|
b99dc875ed | ||
|
|
7da31f00c3 | ||
|
|
894ccd1244 | ||
|
|
8d90e6e808 | ||
|
|
79771f725e | ||
|
|
786f56941a | ||
|
|
8fae910d46 | ||
|
|
8b5920bba3 | ||
|
|
03f1451e64 | ||
|
|
1d2a975b5f | ||
|
|
3db32979dc | ||
|
|
110431dae2 | ||
|
|
ade228f756 | ||
|
|
9b18a66f22 | ||
|
|
c88de90a24 | ||
|
|
737d66d97d | ||
|
|
b77b57d3fd | ||
|
|
240c7766c1 | ||
|
|
508b2e43d3 | ||
|
|
d6129e21e9 | ||
|
|
a11a2444f1 | ||
|
|
db1fb1c0e1 | ||
|
|
7f326b19aa | ||
|
|
2a1d7d2502 | ||
|
|
01edfce6e7 | ||
|
|
9176b8f99c | ||
|
|
0ff2e57424 | ||
|
|
567966866c | ||
|
|
e03f07a8ef | ||
|
|
9915a700bb | ||
|
|
f0a406a397 | ||
|
|
851ea24be4 | ||
|
|
50c4eff790 | ||
|
|
e69e6998c9 | ||
|
|
c87873cfb6 | ||
|
|
b09becf3eb | ||
|
|
0673242f41 | ||
|
|
1776931df5 | ||
|
|
124fbfd798 | ||
|
|
893e433805 | ||
|
|
a2c0921345 | ||
|
|
ad0a974c14 | ||
|
|
2d4a7d336c | ||
|
|
8d2df87de3 | ||
|
|
873e7e2068 | ||
|
|
3fcb1d0138 | ||
|
|
ce41f9a0ce | ||
|
|
34d8ed19bf | ||
|
|
ea9f34f0b3 | ||
|
|
4e9cbd23f2 | ||
|
|
bbd65c01e9 | ||
|
|
5dc30b7702 | ||
|
|
eb3b31bd59 | ||
|
|
57597dfef0 | ||
|
|
8cb7a610c7 | ||
|
|
2c464978b4 | ||
|
|
8d0bf6abe8 | ||
|
|
82c724a768 | ||
|
|
6146fe5f86 | ||
|
|
24ce6eae3b | ||
|
|
8db934a2c2 | ||
|
|
dc90d412ca | ||
|
|
b8415c8995 | ||
|
|
0d86f39ba3 | ||
|
|
0bab033673 | ||
|
|
72a0ce3f51 | ||
|
|
c3a14b82b3 | ||
|
|
b3252e276b | ||
|
|
10c63a66f6 | ||
|
|
f44205b90d | ||
|
|
cd087d410f | ||
|
|
c5812ba938 | ||
|
|
2e4c7f7690 | ||
|
|
a7389efb00 | ||
|
|
532062687c | ||
|
|
d3a603d115 | ||
|
|
26fece78c5 | ||
|
|
b8fb626591 | ||
|
|
732786067f | ||
|
|
45c0cd48c7 | ||
|
|
50d7982dd5 | ||
|
|
ff085b1284 | ||
|
|
c3564b1f8f | ||
|
|
32eba21e7a | ||
|
|
9952afcc38 | ||
|
|
b3feccab0d | ||
|
|
86ebd4903b | ||
|
|
0533bb9c30 | ||
|
|
185e90494c | ||
|
|
3f77b497ce | ||
|
|
87a8333cdf | ||
|
|
3d12261aa7 | ||
|
|
19c6cbbcd3 | ||
|
|
39c191db7c | ||
|
|
58c36266e0 | ||
|
|
0ebdfffdeb | ||
|
|
ed840f3048 | ||
|
|
1e13b509df | ||
|
|
d76a9a4628 | ||
|
|
f70e20498e | ||
|
|
1715b39629 | ||
|
|
3c63db8c9c | ||
|
|
7af78c95dd | ||
|
|
1c4a377641 | ||
|
|
bf56937585 | ||
|
|
e97442b0c8 | ||
|
|
d029ce5380 | ||
|
|
20049921ad | ||
|
|
05a0db75db | ||
|
|
a7b8100a18 | ||
|
|
112369b283 | ||
|
|
f9b915b728 | ||
|
|
8acdbc928e | ||
|
|
9665e14e6d | ||
|
|
b45e05cc9e | ||
|
|
116f4d94ea | ||
|
|
f18b5b49c5 | ||
|
|
09a4db7c03 | ||
|
|
2616a5776b | ||
|
|
36cfc4a867 | ||
|
|
631f118afb | ||
|
|
b43e3a61e6 | ||
|
|
04100adece | ||
|
|
a103f93925 | ||
|
|
8467340fb5 | ||
|
|
3a8e41de27 | ||
|
|
97c2f183ca | ||
|
|
f1ab35e71e | ||
|
|
240d892b94 | ||
|
|
364d96e7f6 | ||
|
|
03dfd58ac5 | ||
|
|
716023b534 | ||
|
|
67b3417ac9 | ||
|
|
f3667f6aaf | ||
|
|
7b35e71f3d | ||
|
|
51031878eb | ||
|
|
19d96230cd | ||
|
|
dbee7e2894 | ||
|
|
656241da54 | ||
|
|
2a3947ea1d | ||
|
|
9c1b4261dc | ||
|
|
7c37d8128e | ||
|
|
404785bf29 | ||
|
|
dc7fd34b07 | ||
|
|
86a2975b3c | ||
|
|
f824fc7c81 | ||
|
|
244cc30eb6 | ||
|
|
a833ed8060 | ||
|
|
1e869d2d2b | ||
|
|
64df5cfff3 | ||
|
|
d8b2a80e90 | ||
|
|
10061c061e | ||
|
|
6a4124fdf0 | ||
|
|
faaa915163 | ||
|
|
3fb911298d | ||
|
|
761f81d27c | ||
|
|
d4b7c8ae97 | ||
|
|
23679e58e1 | ||
|
|
f3e39f18ef | ||
|
|
55a4cda351 | ||
|
|
f0f270b44d | ||
|
|
529de2bd90 | ||
|
|
51dd83df09 | ||
|
|
9abe4ffd8e | ||
|
|
cdd92f6990 | ||
|
|
b08fa9bf60 | ||
|
|
f0bcc3c3e7 | ||
|
|
b9c45f24f5 | ||
|
|
d5f3967558 | ||
|
|
d3fccc9b87 | ||
|
|
7e6c03faf5 | ||
|
|
02b2a52280 | ||
|
|
6eb708b155 | ||
|
|
83e6444ddc | ||
|
|
168fe4d680 | ||
|
|
0a10c9a7c9 | ||
|
|
204c530712 | ||
|
|
5e55f0debf | ||
|
|
f8bd5a6c86 | ||
|
|
101e7a9a49 | ||
|
|
271e79d6a2 | ||
|
|
2ca8f8aff8 | ||
|
|
bc6891e746 | ||
|
|
26aaeccf11 | ||
|
|
91b512b693 | ||
|
|
88d7917a84 | ||
|
|
7ae0964dd1 | ||
|
|
edb0c305dd | ||
|
|
0321f35869 | ||
|
|
1870f8e90e | ||
|
|
6e112b904f | ||
|
|
dca8d2533e | ||
|
|
c57957b56d | ||
|
|
1f5ab87653 | ||
|
|
d5513e90bd | ||
|
|
00fcd17285 | ||
|
|
356ab4e7cb | ||
|
|
572cb55bf8 | ||
|
|
08b83469e3 | ||
|
|
6939150d2b | ||
|
|
cf9a4d8a26 | ||
|
|
a5f8e4fc58 | ||
|
|
775e325503 | ||
|
|
0c472ba22c | ||
|
|
4c0d15d6ef | ||
|
|
28cea81f61 | ||
|
|
d975dae88f | ||
|
|
30297584d5 | ||
|
|
1105f8e608 | ||
|
|
eeae097a54 | ||
|
|
8199e5a253 | ||
|
|
08c7f4b491 | ||
|
|
634744c22d | ||
|
|
45f4bdd1c5 | ||
|
|
c8f5c80d5b | ||
|
|
5556fafa94 | ||
|
|
20c21cb964 | ||
|
|
d2de889995 | ||
|
|
05cf3ef7fe | ||
|
|
6bd8a8fea8 | ||
|
|
4cba1a2fd2 | ||
|
|
86f0622c4a | ||
|
|
85ad7ca187 | ||
|
|
879279a382 | ||
|
|
74df84e449 | ||
|
|
2862ae3302 | ||
|
|
7d0dfe480c | ||
|
|
c47748ed4d | ||
|
|
396504138c | ||
|
|
6e067a160c | ||
|
|
66a56757d1 | ||
|
|
088ec5580a | ||
|
|
63b2693fe0 | ||
|
|
76a69d8f3b | ||
|
|
5cd0f55cb2 | ||
|
|
db640b04a6 | ||
|
|
0665bcf30a | ||
|
|
7e8ed42f46 | ||
|
|
8d7b1a5b9b | ||
|
|
c6829a7d5e | ||
|
|
bef8fbf183 | ||
|
|
fca95f09cc | ||
|
|
4d6cdf8823 | ||
|
|
e34f52dcc5 | ||
|
|
91cc4e6f3d | ||
|
|
429b7dcb0e | ||
|
|
5ffa681a80 | ||
|
|
91364db608 | ||
|
|
2433c68f27 | ||
|
|
6ea254d9ac | ||
|
|
a4d1df8324 | ||
|
|
ba15611903 | ||
|
|
eda6113792 | ||
|
|
4f1509c836 | ||
|
|
dfcf32ef3e | ||
|
|
4a95257ee2 | ||
|
|
ee1e13ef61 | ||
|
|
c9c099ffc5 | ||
|
|
a80f4fb3c9 | ||
|
|
941f3ac47f | ||
|
|
80d3200d57 | ||
|
|
55926e25d2 | ||
|
|
a31f3ebad7 | ||
|
|
dc9c32c6ee | ||
|
|
dac3aac2a1 | ||
|
|
40196e823c | ||
|
|
7708b7e938 | ||
|
|
8bbc5a9bc6 | ||
|
|
4dbdfb1b77 | ||
|
|
05c51edce3 | ||
|
|
379e8cd95e | ||
|
|
9fd00d4427 | ||
|
|
ca114cf124 | ||
|
|
75128784db | ||
|
|
722f0e277e | ||
|
|
7089998966 | ||
|
|
52211ddfa4 | ||
|
|
2b8b762d35 | ||
|
|
04c228cd36 | ||
|
|
915f8e7700 | ||
|
|
218cea00b5 | ||
|
|
f38f5ea670 | ||
|
|
a9c08c8a9f | ||
|
|
9406c4f718 | ||
|
|
791d8beb06 | ||
|
|
57a9f3aca0 | ||
|
|
ae8bf187c8 | ||
|
|
88f2ab1f8e | ||
|
|
76f5136073 | ||
|
|
0f84cd8949 | ||
|
|
adfdf7606f | ||
|
|
ce671d4294 | ||
|
|
2e7cf7a9df | ||
|
|
f699d93e44 | ||
|
|
531ef4d0b2 | ||
|
|
4c6dbb6690 | ||
|
|
0beb60d1b5 | ||
|
|
893c9d7c42 | ||
|
|
b7a22d94db | ||
|
|
3d854756d1 | ||
|
|
251e7058f1 | ||
|
|
368eb24217 | ||
|
|
f89ef54b82 | ||
|
|
b9d9f8772b | ||
|
|
f2ee03e850 | ||
|
|
92e59a0072 | ||
|
|
5394c0b520 | ||
|
|
e4a9c6f8c2 | ||
|
|
e559d037c3 | ||
|
|
c9909a0869 | ||
|
|
988fc1bbfd | ||
|
|
1831bbcd70 | ||
|
|
ced9bbdbd5 | ||
|
|
15d57d937b | ||
|
|
555979869d | ||
|
|
4e6477297e | ||
|
|
e9e06fc53b | ||
|
|
ecb2818589 | ||
|
|
c442689573 | ||
|
|
88919f2634 | ||
|
|
1a1bc41283 | ||
|
|
4188caed33 | ||
|
|
50557d8bc5 | ||
|
|
0f1ef0406e | ||
|
|
ffc742b460 | ||
|
|
0c04cc6ce6 | ||
|
|
4e6dfed838 | ||
|
|
fdc0524ad2 | ||
|
|
096d5962a3 | ||
|
|
9ea36f94a7 | ||
|
|
2b66df992f | ||
|
|
ff3c31975e | ||
|
|
beaa5eca69 | ||
|
|
b98040faac | ||
|
|
fbbff0df61 | ||
|
|
07d5f393c0 | ||
|
|
8220b15a29 | ||
|
|
7cf638b2be | ||
|
|
9852c87be7 | ||
|
|
30fb8f31e3 | ||
|
|
ec4d875547 | ||
|
|
1268f65516 | ||
|
|
6551b2537a | ||
|
|
43eee63df2 | ||
|
|
4caf9f598a | ||
|
|
107eaca8f0 | ||
|
|
cc11848346 | ||
|
|
b138ccf625 | ||
|
|
27ff7baa6d | ||
|
|
c8ecd34bd3 | ||
|
|
d4a4eb3248 | ||
|
|
f782733397 | ||
|
|
7720eddc47 | ||
|
|
71170a373a | ||
|
|
8f080080a7 | ||
|
|
71b2958c14 | ||
|
|
f69aafcbfa | ||
|
|
5f6a423697 | ||
|
|
5d245927c4 | ||
|
|
17c96fb849 | ||
|
|
b7c28cdf36 | ||
|
|
d05aa0d350 | ||
|
|
875dc086bf | ||
|
|
4719ebd52a | ||
|
|
c8a436a7df | ||
|
|
8d6f4dd64e | ||
|
|
63486927d6 | ||
|
|
c36a70f8d3 | ||
|
|
39472d6874 | ||
|
|
6207e99cc0 | ||
|
|
bdc163199d | ||
|
|
e7a04ede65 | ||
|
|
f6aee4148d | ||
|
|
6cb30a6597 | ||
|
|
34fcfc8f30 | ||
|
|
bc89b33468 | ||
|
|
af9af347e6 | ||
|
|
6f46279b52 | ||
|
|
8f93ed8b8e | ||
|
|
df91363ff4 | ||
|
|
e40e59714d | ||
|
|
a43d95afc3 | ||
|
|
8fa4b5ab22 | ||
|
|
68a030145f | ||
|
|
4f48657623 | ||
|
|
3fc41f4255 | ||
|
|
ff2f7f6586 | ||
|
|
93f33d73d9 | ||
|
|
402c176277 | ||
|
|
79b05b4a66 | ||
|
|
fb9e37e71c | ||
|
|
cd7a741d53 | ||
|
|
5fdc90b4f9 | ||
|
|
e775840abb | ||
|
|
d7df54dfa1 | ||
|
|
646d165d37 | ||
|
|
ba0a80cca4 | ||
|
|
9d59858542 | ||
|
|
aaff0a5697 | ||
|
|
1bb7cee238 | ||
|
|
d330e662d3 | ||
|
|
b530ff76c4 | ||
|
|
b20aa33a04 | ||
|
|
e59a166445 | ||
|
|
980e713735 | ||
|
|
7a5228bbab | ||
|
|
5a26e5c2ee | ||
|
|
a64da4cd85 | ||
|
|
c2dccf17ce | ||
|
|
181d6bf1e4 | ||
|
|
e80bcfc586 | ||
|
|
a07be3b5dc | ||
|
|
af2666c83d | ||
|
|
cad9929b56 | ||
|
|
3b79da527d | ||
|
|
3eaba55823 | ||
|
|
e2eddb6362 | ||
|
|
de5b8160a2 | ||
|
|
83df969751 | ||
|
|
d885570dd8 | ||
|
|
f3de883e39 | ||
|
|
6da22d23a8 | ||
|
|
49461a43bb | ||
|
|
d4414f99af | ||
|
|
52f37b0a37 | ||
|
|
ce949e4029 | ||
|
|
19d3465a13 | ||
|
|
a0e1425c74 | ||
|
|
a5d8379b18 | ||
|
|
9ac487ea5b | ||
|
|
9cc9a93c3d | ||
|
|
4f2b0313de | ||
|
|
806894b24b | ||
|
|
cbb737f96b | ||
|
|
5407f8156a | ||
|
|
a0c2fd09ae | ||
|
|
48b744c8ba | ||
|
|
812b946d85 | ||
|
|
27eca2b470 | ||
|
|
ab6c88bebd | ||
|
|
d547e0552a | ||
|
|
03fc0b84cb | ||
|
|
b0825634ee | ||
|
|
9675e051bd | ||
|
|
981654a2aa | ||
|
|
8c97ea0da0 | ||
|
|
6c80911c90 | ||
|
|
740b5ea719 | ||
|
|
76c958fb7a | ||
|
|
28c53fd115 | ||
|
|
6d9fc8b3df | ||
|
|
a044f4eceb | ||
|
|
1f35efcd51 | ||
|
|
c587abddae | ||
|
|
d7f4c9a329 | ||
|
|
8a3e6f5781 | ||
|
|
cfbebc4989 | ||
|
|
99ec772d64 | ||
|
|
1f60b0bbcb | ||
|
|
c8fb9a5b09 | ||
|
|
d6f117c7bf | ||
|
|
a765668354 | ||
|
|
da053658e8 | ||
|
|
cb5bcd72e4 | ||
|
|
d4d53623ef | ||
|
|
8a31b008d2 | ||
|
|
fa4541b59f | ||
|
|
aae6b32828 | ||
|
|
7158d6735b | ||
|
|
f04fd16c9e | ||
|
|
f4b896d5e6 | ||
|
|
d2f814e6f8 | ||
|
|
87cec67e60 | ||
|
|
041c46f38e | ||
|
|
54a7628f75 | ||
|
|
7cbbc16390 | ||
|
|
14532bf6db | ||
|
|
7bfe18e718 | ||
|
|
07903a07a8 | ||
|
|
14fe3b2b98 | ||
|
|
fa2756ff3f | ||
|
|
9d4841b4dc | ||
|
|
4b74b08825 | ||
|
|
6457cfc958 | ||
|
|
30081c893b | ||
|
|
e69890a47e | ||
|
|
9d31162eec | ||
|
|
0031cc7418 | ||
|
|
57132892b5 | ||
|
|
be084d1570 | ||
|
|
03d29a9ebd | ||
|
|
e9a837b534 | ||
|
|
36c601f9fa | ||
|
|
af41234484 | ||
|
|
bcaf03a770 | ||
|
|
9429b009ce | ||
|
|
3395785cbc | ||
|
|
97c048c7af | ||
|
|
726eb2ffe6 | ||
|
|
a9f22d3a10 | ||
|
|
5a6437e371 | ||
|
|
4ffc98b371 | ||
|
|
16c73d4f91 | ||
|
|
96aff5230f | ||
|
|
b298571be8 | ||
|
|
7b7b10c9a5 | ||
|
|
56beed5ad5 | ||
|
|
8c8ef0ea53 | ||
|
|
90377d98c0 | ||
|
|
d167e474d8 | ||
|
|
0cfacfe057 | ||
|
|
fbda5231d0 | ||
|
|
bd4d333d59 | ||
|
|
07878207de | ||
|
|
b2352877b9 | ||
|
|
669ba9e8cb | ||
|
|
5fa8a76219 | ||
|
|
476dcf051c | ||
|
|
db96dbba6a | ||
|
|
e9c6509970 | ||
|
|
b26c7dfa8f | ||
|
|
3f251761d6 | ||
|
|
bc9d489ee8 | ||
|
|
271563ba9f | ||
|
|
82aba20e83 | ||
|
|
12f60c9be5 | ||
|
|
1c98a354c7 | ||
|
|
9ca7aee859 | ||
|
|
937b306128 | ||
|
|
69a2ab0ce2 | ||
|
|
b720711217 | ||
|
|
bf56348c62 | ||
|
|
f993817cb6 | ||
|
|
1c2b4794f3 | ||
|
|
523d210503 | ||
|
|
39da8bd636 | ||
|
|
fbea98bbe6 | ||
|
|
5d89f724f4 | ||
|
|
0313b6f3ab | ||
|
|
fe92111dc9 | ||
|
|
9a03bfb9e7 | ||
|
|
882db6acc9 | ||
|
|
5596468b03 | ||
|
|
eb66de1220 | ||
|
|
f97896f6a6 | ||
|
|
cfb47f880e | ||
|
|
eba87776a0 | ||
|
|
dda0e6d676 | ||
|
|
8d514644c1 | ||
|
|
0870a34391 | ||
|
|
38118986a3 | ||
|
|
1b4e5247ad | ||
|
|
3ef2eeef93 | ||
|
|
4d5682e79b | ||
|
|
24100d203f | ||
|
|
86946a2c21 | ||
|
|
54f3e4cc6f | ||
|
|
d9360b2d8c | ||
|
|
abb47118c0 | ||
|
|
9e4e55bb8d | ||
|
|
498f720507 | ||
|
|
b9bc9cb75e | ||
|
|
3b65917422 | ||
|
|
862dd78cd5 | ||
|
|
d0bc48d119 | ||
|
|
37462ba282 | ||
|
|
99e7f5862a | ||
|
|
048e529bb5 | ||
|
|
5b8b06f3a1 | ||
|
|
3c7d025802 | ||
|
|
4f8b46da14 | ||
|
|
0b386a882e | ||
|
|
6329d58a67 | ||
|
|
0444e17848 | ||
|
|
c3d039fe45 | ||
|
|
1d5d713854 | ||
|
|
4336c15ca4 | ||
|
|
aac644d15c | ||
|
|
ab3dde17c7 | ||
|
|
924ec33d73 | ||
|
|
d798e02126 | ||
|
|
af0d780ac0 | ||
|
|
45c2c863ef | ||
|
|
bc08aeb5b7 | ||
|
|
c32067890c | ||
|
|
83a180e3b8 | ||
|
|
3372e16409 | ||
|
|
a04fad9c1f | ||
|
|
7500797dd0 | ||
|
|
9427f4141f | ||
|
|
1440244c4d | ||
|
|
47c8b82cfa | ||
|
|
540f23fb41 | ||
|
|
323c9f2d4f | ||
|
|
1d648b525f | ||
|
|
11c2771451 | ||
|
|
1733756b4a | ||
|
|
edf1933800 | ||
|
|
38d3165d14 | ||
|
|
5766b307c1 | ||
|
|
6497703d3b | ||
|
|
60e48580ae | ||
|
|
08b52f3618 | ||
|
|
55001cc434 | ||
|
|
b84b864396 | ||
|
|
34835b8cdf | ||
|
|
df8bdbf5b3 | ||
|
|
e775a832cb | ||
|
|
b9c4bada23 | ||
|
|
97f35f7ee6 | ||
|
|
0f90a06028 | ||
|
|
db4f2e7696 | ||
|
|
69837b0515 | ||
|
|
9811c49288 | ||
|
|
d709c99641 | ||
|
|
a92666082f | ||
|
|
cc05b28a42 | ||
|
|
a2c9953096 | ||
|
|
75ef6934b4 | ||
|
|
aa4d3c039b | ||
|
|
a8a89dc9bd | ||
|
|
a0305a745a | ||
|
|
290ff54638 | ||
|
|
dc4decffe4 | ||
|
|
f06b702360 | ||
|
|
e2eca66db0 | ||
|
|
d20913023e | ||
|
|
f4466dc325 | ||
|
|
00e6ea5b3b | ||
|
|
50b0a3ea17 | ||
|
|
0072e3e5ba | ||
|
|
6f104c4ee6 | ||
|
|
c721580516 | ||
|
|
2238f1fafc | ||
|
|
00bc5a00c1 | ||
|
|
1fe1381fe2 | ||
|
|
6bc0470c69 | ||
|
|
a9ab25e9c2 | ||
|
|
fd5bc22a70 | ||
|
|
28fe1fafd6 | ||
|
|
6695138244 | ||
|
|
3fb19cbe96 | ||
|
|
3f8a17c3ea | ||
|
|
9fa97f27f0 | ||
|
|
0e98312d55 | ||
|
|
06057a1d35 | ||
|
|
4373fddaa8 | ||
|
|
c45511d9a5 | ||
|
|
7d2b2494e5 | ||
|
|
870a25dad1 | ||
|
|
d74dc2f4ca | ||
|
|
f8c9c535a0 | ||
|
|
d22322be86 | ||
|
|
e4e5d5c6d7 | ||
|
|
7e45f23b6c | ||
|
|
719f61f612 | ||
|
|
908edda14b | ||
|
|
74792a9c21 | ||
|
|
eb345b5ca2 | ||
|
|
23c2b4b204 | ||
|
|
095e616e4c | ||
|
|
89f4626fce | ||
|
|
c759d673f0 | ||
|
|
8d1be554d4 | ||
|
|
e931d06ed4 | ||
|
|
76b90f9bc0 | ||
|
|
b4e5fedb51 | ||
|
|
6a229aabaf | ||
|
|
841c7df2f3 | ||
|
|
dd85ce7cfe | ||
|
|
4c7d771bb3 | ||
|
|
c55bdba948 | ||
|
|
4f2dce0a03 | ||
|
|
450a992b26 | ||
|
|
4284b2402b | ||
|
|
50c88f82f4 | ||
|
|
7dff3dc658 | ||
|
|
adb3864ea9 | ||
|
|
d6330060dc | ||
|
|
2dcb35a34c | ||
|
|
624ba95792 | ||
|
|
ae24a80b95 | ||
|
|
e7f1b1cc32 | ||
|
|
e71940ecdc | ||
|
|
65a800701c | ||
|
|
36dc1fb3e9 | ||
|
|
99305b4185 | ||
|
|
bd2e8be186 | ||
|
|
dc4c6ceff5 | ||
|
|
10083ac807 | ||
|
|
11023c6400 | ||
|
|
6d31314df6 | ||
|
|
d5367194ab | ||
|
|
cf179b2f9c | ||
|
|
de82188663 | ||
|
|
4aefe775f9 | ||
|
|
8c06c98917 | ||
|
|
ee9f778725 | ||
|
|
ff0883c8e6 | ||
|
|
d4e0f05cef | ||
|
|
dfcb4fe286 | ||
|
|
d546d5d0a7 | ||
|
|
375dc86f0a | ||
|
|
d2f187f6c0 | ||
|
|
5393ee9ba9 | ||
|
|
d4d2948455 | ||
|
|
37617c04ad | ||
|
|
10e0153018 | ||
|
|
42cbe94af8 | ||
|
|
8f95e53d9d | ||
|
|
0dae21dcf1 | ||
|
|
912efeb5fd | ||
|
|
3d9307f96d | ||
|
|
873f4b080a | ||
|
|
e337a79e2e | ||
|
|
980ce62bf6 | ||
|
|
dccf757f56 | ||
|
|
c6e93cd3c8 | ||
|
|
4a6b88328e | ||
|
|
779a84cf0b | ||
|
|
aff409ba63 | ||
|
|
275167c0c4 | ||
|
|
19b2af7f57 | ||
|
|
779d5df264 | ||
|
|
5050b9ee97 | ||
|
|
23da4be6b1 | ||
|
|
299648b8e0 | ||
|
|
86fa0c87ae | ||
|
|
77cd52ca04 | ||
|
|
bed2a77232 | ||
|
|
9619eec40f | ||
|
|
886161d65f | ||
|
|
d5d646e6e0 | ||
|
|
494103163c | ||
|
|
72e9e4848b | ||
|
|
1181ba6f90 | ||
|
|
b8d6ec5fe0 | ||
|
|
7335d7df0a | ||
|
|
44754095fb | ||
|
|
0f515bae01 | ||
|
|
76a11f6ee6 | ||
|
|
5fb28feb65 | ||
|
|
e5f1524b10 | ||
|
|
8a875691fb | ||
|
|
0b2710b97d | ||
|
|
57552a32b0 | ||
|
|
572ec3658e | ||
|
|
6d5c791f61 | ||
|
|
168e479c0c | ||
|
|
927e1b7ef9 | ||
|
|
76dccf804e | ||
|
|
4262e0e6eb | ||
|
|
7d487420aa | ||
|
|
c422b11e3a |
@@ -232,6 +232,24 @@
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "1024mb",
|
||||
"name": "1024mb",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/9301204?v=4",
|
||||
"profile": "https://github.com/1024mb",
|
||||
"contributions": [
|
||||
"translation"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "ehowe",
|
||||
"name": "Eugene Howe",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/756984?v=4",
|
||||
"profile": "https://github.com/ehowe",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -3,24 +3,22 @@
|
||||
# Note that any data in staging will get cleared from time to time and you may have to sign up again and again
|
||||
|
||||
# Set it to the URL form where the app home page will be accessed
|
||||
PUBLIC_URL=http://localhost:3000
|
||||
PUBLIC_URL=https://localhost:3001
|
||||
# Set it to true if you want to generate the Sourcemap for debugging
|
||||
GENERATE_SOURCEMAP=false
|
||||
# Set it to the URL from where APIs will be accessible, for local development it should be localhost:3000/api/app (use your local port number instead)
|
||||
REACT_APP_SERVERURL=http://localhost:8080/app
|
||||
# A 12 character long random app identifier. The value of this should be same as APP_ID which is a variable used by backend API.
|
||||
# REACT_APP_SERVERURL=http://localhost:8080/app
|
||||
# (DEPRECATED) This should not be changed if provided; it should be 'opensign'.
|
||||
REACT_APP_APPID=opensign
|
||||
|
||||
|
||||
# Backend ExpressJS config ****************************************************************************************************************************************************************************************
|
||||
# A 12 character long random app identifier. The value of this should be same as REACT_APP_APPID which is a variable used by Frontend React App.
|
||||
APP_ID=opensign
|
||||
# Name of the app. It will be visible in the verification emails sent out.
|
||||
appName=open_sign_server
|
||||
# A 12 character long random secret key that allows access to all the data. It is used in Parse dashboard config to view all the data in the database.
|
||||
MASTER_KEY=XnAadwKxxByMr
|
||||
# Mongodb URI to connect to
|
||||
MONGODB_URI=mongodb://mongo:27017/OpenSignDB
|
||||
MONGODB_URI=mongodb://mongo-container:27017/OpenSignDB
|
||||
# Path on which APIs should be mounted. Do not change this. This variable shall be removed & value hardcoded in the source code in coming versions.
|
||||
PARSE_MOUNT=/app
|
||||
# Set it to the URL from where APIs will be accessible to the NodeJS functions, for local development it should be localhost:3000/api/app (use your local port number instead)
|
||||
@@ -46,7 +44,7 @@ USE_LOCAL=true
|
||||
MAILGUN_API_KEY=
|
||||
MAILGUN_DOMAIN=mail.yourdomain.com
|
||||
MAILGUN_SENDER=postmaster@mail.yourdomain.com
|
||||
SMTP_ENABLE=
|
||||
SMTP_ENABLE=true
|
||||
SMTP_HOST=smtp.yourhost.com
|
||||
SMTP_PORT=443
|
||||
SMTP_USER_EMAIL=mailer@yourdomain.com
|
||||
@@ -111,3 +109,6 @@ CRUxFgQUDYlgGVxSxuOknhQc256x3++7BDwwMTAhMAkGBSsOAwIaBQAEFFjASdYl
|
||||
|
||||
# Provide Pass pharse of above PFX or p12 document
|
||||
PASS_PHRASE=opensign
|
||||
|
||||
# (DEPRECATED) This should not be changed if provided; it should be 'opensign'.
|
||||
APP_ID=opensign
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
name: ci
|
||||
|
||||
# 👇 add this block
|
||||
permissions:
|
||||
contents: read # allow checkout & metadata-action to read repo
|
||||
id-token: write # needed by docker/metadata-action v4
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
- 'staging'
|
||||
|
||||
jobs:
|
||||
@@ -14,34 +21,46 @@ jobs:
|
||||
dockerfile: apps/OpenSign/Dockerhubfile
|
||||
- image: opensign/opensignserver
|
||||
dockerfile: apps/OpenSignServer/Dockerhubfile
|
||||
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
-
|
||||
name: Login to Docker Hub
|
||||
|
||||
# 👇 Enable emulation so we can build for arm64 on amd64 runners
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
# 👇 Enable Buildx for multi-arch builds
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
-
|
||||
name: Extract metadata (tags, labels) for Docker
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: ${{ matrix.image }}
|
||||
-
|
||||
name: Debug - List files opensign
|
||||
|
||||
- name: Debug - List files opensign
|
||||
run: ls -R apps/OpenSign/ # Adjust the path as needed
|
||||
-
|
||||
name: Debug - List files opensignserver
|
||||
|
||||
- name: Debug - List files opensignserver
|
||||
run: ls -R apps/OpenSignServer/ # Adjust the path as needed
|
||||
-
|
||||
name: Build and push
|
||||
|
||||
- name: Build and push (multi-arch)
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
file: ${{ matrix.dockerfile }}
|
||||
push: true
|
||||
# 👇 Build for both AMD64 & ARM64
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
@@ -77,6 +77,6 @@ Engage with the community on our [Gitter Channel](https://gitter.im/OpenSignLabs
|
||||
|
||||
## License
|
||||
|
||||
By contributing, you agree that your contributions will be licensed under its [MIT License](LICENSE).
|
||||
By contributing, you agree that your contributions will be licensed under its [AGPL-V3 License](LICENSE).
|
||||
|
||||
---
|
||||
---
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{$HOST_URL} {
|
||||
reverse_proxy client:3000
|
||||
handle_path /api/* {
|
||||
reverse_proxy server:8080
|
||||
rewrite * {uri}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,9 @@
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
* All content that resides under the "apps/OpenSignServer/cloud/customRoute" directory of this repository, if that directory exists, is licensed under the license defined in "apps/OpenSignServer/cloud/customRoute".
|
||||
* All third party components incorporated into the OpenSign Software are licensed under the original license provided by the owner of the applicable component.
|
||||
* Content outside of the above mentioned directories or restrictions above is available under the "GNU AFFERO GENERAL PUBLIC LICENSE" as defined below -
|
||||
|
||||
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
build:
|
||||
@echo "Building with HOST_URL=${HOST_URL}"
|
||||
cp .env.local_dev .env
|
||||
cd apps/OpenSign && cp ../../.env.local_dev .env && npm install && npm run build
|
||||
docker compose up --build --force-recreate
|
||||
HOST_URL=${HOST_URL} docker compose up --build --force-recreate
|
||||
|
||||
run:
|
||||
@echo "Building with HOST_URL=${HOST_URL}"
|
||||
cp .env.local_dev .env
|
||||
docker compose up -d
|
||||
docker compose up -d
|
||||
|
||||
@@ -73,9 +73,31 @@ Welcome to OpenSign, the premier open source docusign alternative - document e-s
|
||||
|
||||
---
|
||||
|
||||
### Installation
|
||||
### Deploy
|
||||
|
||||
Please refer to the [Installation Guide](INSTALLATION.md) for detailed instructions on how to install OpenSign on your system.
|
||||
Note: The default MongoDB instance used in deployment is not persistant and will be cleared on every restart. To retain your data, configure and supply your own MongoDB connection URL.
|
||||
|
||||
#### DigitalOcean
|
||||
[](https://cloud.digitalocean.com/apps/new?repo=https://github.com/OpenSignLabs/Deploy-OpenSign-to-Digital-Ocean/tree/main&refcode=30db1c901ab0)
|
||||
|
||||
#### Docker
|
||||
The simplest way to install OpenSign on your own server is using official docker images by running the following command -
|
||||
|
||||
**Command for linux/MacOS**
|
||||
```
|
||||
export HOST_URL=https://opensign.yourdomain.com && curl --remote-name-all https://raw.githubusercontent.com/OpenSignLabs/OpenSign/main/docker-compose.yml https://raw.githubusercontent.com/OpenSignLabs/OpenSign/main/Caddyfile https://raw.githubusercontent.com/OpenSignLabs/OpenSign/main/.env.local_dev && mv .env.local_dev .env.prod && docker compose up --force-recreate
|
||||
```
|
||||
**Command for Windows (Powershell)**
|
||||
```
|
||||
$env:HOST_URL="https://opensign.yourdomain.com"; Invoke-WebRequest -Uri https://raw.githubusercontent.com/OpenSignLabs/OpenSign/main/docker-compose.yml -OutFile docker-compose.yml; Invoke-WebRequest -Uri https://raw.githubusercontent.com/OpenSignLabs/OpenSign/main/Caddyfile -OutFile Caddyfile; Invoke-WebRequest -Uri https://raw.githubusercontent.com/OpenSignLabs/OpenSign/main/.env.local_dev -OutFile .env.local_dev; Rename-Item -Path .env.local_dev -NewName .env.prod; docker compose up --force-recreate
|
||||
```
|
||||
**Command for Windows (CMD/Terminal)**
|
||||
```
|
||||
set HOST_URL=https://opensign.yourdomain.com && curl -O https://raw.githubusercontent.com/OpenSignLabs/OpenSign/main/docker-compose.yml && curl -O https://raw.githubusercontent.com/OpenSignLabs/OpenSign/main/Caddyfile && curl -O https://raw.githubusercontent.com/OpenSignLabs/OpenSign/main/.env.local_dev && rename .env.local_dev .env.prod && docker compose up --force-recreate
|
||||
```
|
||||
Make sure that you have `Docker` and `git` installed before you run this command -
|
||||
|
||||
Please refer to the [Installation Guide](https://docs.opensignlabs.com/docs/self-host/docker/run-locally/) for detailed instructions on how to install OpenSign on your system.
|
||||
|
||||
---
|
||||
|
||||
@@ -141,7 +163,9 @@ We would like to thank all our contributors and users for their support and feed
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/VikramNagwal"><img src="https://avatars.githubusercontent.com/u/123088024?v=4?s=100" width="100px;" alt="Vikram"/><br /><sub><b>Vikram</b></sub></a><br /><a href="#code-VikramNagwal" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ugoconsonni"><img src="https://avatars.githubusercontent.com/u/13661702?v=4?s=100" width="100px;" alt="ugoconsonni"/><br /><sub><b>ugoconsonni</b></sub></a><br /><a href="#code-ugoconsonni" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/daniel-mutwiri"><img src="https://avatars.githubusercontent.com/u/8936960?v=4?s=100" width="100px;" alt="Daniel Mutwiri"/><br /><sub><b>Daniel Mutwiri</b></sub></a><br /><a href="#code-daniel-mutwiri" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Zathiel"><img src="https://avatars.githubusercontent.com/u/26553418?v=4?s=100" width="100px;" alt="Zathiel"/><br /><sub><b>Zathiel</b></sub></a><br /><a href="#code-Zathiel" title="Code">💻</a><a href="#security-Zathiel" title="Security">🛡️</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Zathiel"><img src="https://avatars.githubusercontent.com/u/26553418?v=4?s=100" width="100px;" alt="Zathiel"/><br /><sub><b>Zathiel</b></sub></a><br /><a href="#code-Zathiel" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/1024mb"><img src="https://avatars.githubusercontent.com/u/9301204?v=4?s=100" width="100px;" alt="1024mb"/><br /><sub><b>1024mb</b></sub></a><br /><a href="#translation-1024mb" title="Translation">🌍</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ehowe"><img src="https://avatars.githubusercontent.com/u/756984?v=4?s=100" width="100px;" alt="Eugene Howe"/><br /><sub><b>Eugene Howe</b></sub></a><br /><a href="#code-ehowe" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -151,3 +175,5 @@ We would like to thank all our contributors and users for their support and feed
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||
|
||||
This project is tested with BrowserStack.
|
||||
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
# Use an official Node runtime as the base image
|
||||
FROM node:18
|
||||
|
||||
# Set the working directory inside the container
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
# Copy package.json and package-lock.json first to leverage Docker cache
|
||||
COPY ./package*.json ./
|
||||
|
||||
# Install application dependencies
|
||||
RUN npm install
|
||||
|
||||
# Copy the current directory contents into the container
|
||||
COPY ./ .
|
||||
COPY ./.husky .
|
||||
|
||||
# Make port 3000 available to the world outside this container
|
||||
EXPOSE 3000
|
||||
|
||||
# Define environment variables if needed
|
||||
# ENV NODE_ENV production
|
||||
|
||||
# Run the application
|
||||
ENTRYPOINT npm run start
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Use an official Node runtime as the base image
|
||||
FROM node:18
|
||||
FROM node:22.14.0
|
||||
|
||||
# Set the working directory inside the container
|
||||
WORKDIR /usr/src/app
|
||||
@@ -13,15 +13,24 @@ RUN npm install
|
||||
# Copy the current directory contents into the container
|
||||
COPY apps/OpenSign/ .
|
||||
COPY apps/OpenSign/.husky .
|
||||
COPY apps/OpenSign/entrypoint.sh .
|
||||
|
||||
# make the entrypoint.sh file executable
|
||||
RUN chmod +x entrypoint.sh
|
||||
|
||||
# Define environment variables if needed
|
||||
ENV NODE_ENV=production
|
||||
ENV GENERATE_SOURCEMAP=false
|
||||
# build
|
||||
RUN npm run build
|
||||
|
||||
# Inject env.js loader into index.html
|
||||
RUN sed -i '/<head>/a\<script src="/env.js"></script>' build/index.html
|
||||
|
||||
# Make port 3000 available to the world outside this container
|
||||
EXPOSE 3000
|
||||
|
||||
# Define environment variables if needed
|
||||
# ENV NODE_ENV production
|
||||
ENTRYPOINT ["./entrypoint.sh"]
|
||||
|
||||
# Run the application
|
||||
CMD ["npm", "start"]
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
# Tailwind Dark Mode Usage Guide
|
||||
|
||||
## Overview
|
||||
This guide shows how to use the new Tailwind utilities for better dark mode visibility in OpenSign.
|
||||
|
||||
## Button Styling
|
||||
|
||||
### VS Code-style Disabled Buttons
|
||||
```jsx
|
||||
// Option 1: Direct VS Code styling
|
||||
<button className="op-btn op-btn-primary op-btn-vscode-disabled" disabled>
|
||||
Disabled Button
|
||||
</button>
|
||||
|
||||
// Option 2: Themed disabled styling
|
||||
<button className="op-btn btn-themed-disabled">
|
||||
Themed Button
|
||||
</button>
|
||||
|
||||
// Option 3: Conditional styling
|
||||
<button
|
||||
className={`op-btn op-btn-primary ${isDisabled ? 'op-btn-vscode-disabled' : ''}`}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
Dynamic Button
|
||||
</button>
|
||||
```
|
||||
|
||||
## Icon Styling
|
||||
|
||||
### Theme-aware Icons
|
||||
```jsx
|
||||
// Better visibility in dark mode
|
||||
<i className="fa-light fa-folder icon-improved"></i>
|
||||
|
||||
// Muted but still visible
|
||||
<i className="fa-light fa-plus icon-muted"></i>
|
||||
|
||||
// Disabled state
|
||||
<i className="fa-light fa-trash icon-disabled"></i>
|
||||
```
|
||||
|
||||
### CSS Variable Approach
|
||||
```jsx
|
||||
// Using CSS variables
|
||||
<i className="fa-light fa-search icon-themed"></i>
|
||||
<i className="fa-light fa-settings icon-themed-muted"></i>
|
||||
|
||||
// Inline styles with CSS variables
|
||||
<i
|
||||
className="fa-light fa-plus"
|
||||
style={{ color: 'var(--icon-color)' }}
|
||||
/>
|
||||
```
|
||||
|
||||
### Legacy JavaScript Function (Still Supported)
|
||||
```jsx
|
||||
// Existing approach - still works
|
||||
<i
|
||||
className="fa-light fa-plus"
|
||||
style={{ color: getThemeIconColor() }}
|
||||
/>
|
||||
```
|
||||
|
||||
## Text Styling
|
||||
|
||||
### Improved Gray Text
|
||||
```jsx
|
||||
// These automatically improve in dark mode
|
||||
<span className="text-gray-500">More visible in dark mode</span>
|
||||
<span className="text-gray-400">Muted but readable</span>
|
||||
<span className="text-gray-600">Clear text</span>
|
||||
```
|
||||
|
||||
## Complete Examples
|
||||
|
||||
### Toolbar with Better Visibility
|
||||
```jsx
|
||||
const Toolbar = () => (
|
||||
<div className="flex space-x-2 p-2">
|
||||
<button className="p-2 hover:bg-gray-200 rounded">
|
||||
<i className="fa-light fa-plus icon-improved"></i>
|
||||
</button>
|
||||
<button className="p-2 hover:bg-gray-200 rounded" disabled>
|
||||
<i className="fa-light fa-trash icon-disabled"></i>
|
||||
</button>
|
||||
<button className="p-2 hover:bg-gray-200 rounded">
|
||||
<i className="fa-light fa-edit icon-improved"></i>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
```
|
||||
|
||||
### Form with Disabled States
|
||||
```jsx
|
||||
const Form = ({ isSubmitting }) => (
|
||||
<form>
|
||||
<input className="op-input" />
|
||||
<button
|
||||
className={`op-btn op-btn-primary ${isSubmitting ? 'op-btn-vscode-disabled' : ''}`}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? 'Submitting...' : 'Submit'}
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
```
|
||||
|
||||
## React-Tour and Tooltip Dark Mode Support
|
||||
|
||||
### React-Tour Modals
|
||||
The react-tour modals now automatically support dark mode with VS Code-inspired styling:
|
||||
|
||||
```jsx
|
||||
// These components automatically get dark mode styling
|
||||
<Tour
|
||||
onRequestClose={closeTour}
|
||||
steps={tourConfig}
|
||||
isOpen={isOpen}
|
||||
rounded={5}
|
||||
/>
|
||||
```
|
||||
|
||||
### ReactTooltip Components
|
||||
All ReactTooltip instances now support dark mode:
|
||||
|
||||
```jsx
|
||||
// Automatically styled for dark mode
|
||||
<ReactTooltip id="my-tooltip" className="z-[999]">
|
||||
<div className="max-w-[200px]">
|
||||
<p>Tooltip content</p>
|
||||
</div>
|
||||
</ReactTooltip>
|
||||
```
|
||||
|
||||
### HoverCard Balloon UI
|
||||
The balloon tooltips in OpenSign Drive now properly support dark mode:
|
||||
|
||||
```jsx
|
||||
// These automatically get dark styling in dark mode
|
||||
<HoverCard>
|
||||
<HoverCardContent>
|
||||
Document information
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
```
|
||||
|
||||
## Dark Mode Features Added
|
||||
|
||||
### 1. **React-Tour Modal Styling**
|
||||
- Background: `#1F2937` (VS Code modal background)
|
||||
- Text: `#E5E7EB` (soft white for readability)
|
||||
- Borders: `#374151` (subtle borders)
|
||||
- Buttons: VS Code-style primary/secondary buttons
|
||||
|
||||
### 2. **ReactTooltip Styling**
|
||||
- Background: `#1F2937` with proper contrast
|
||||
- Border: `#374151` for definition
|
||||
- Box shadow: Enhanced for dark backgrounds
|
||||
- Text: `#E5E7EB` for optimal readability
|
||||
|
||||
### 3. **HoverCard Balloon UI**
|
||||
- Background: `#1F2937` (matches VS Code)
|
||||
- Text: `#E5E7EB` for readability
|
||||
- Arrow: Automatically matches background color
|
||||
- Enhanced shadows for dark backgrounds
|
||||
|
||||
### 4. **React-Datepicker Support**
|
||||
- Calendar background: `#1F2937`
|
||||
- Selected dates: VS Code blue (`#007ACC`)
|
||||
- Hover states: Proper contrast ratios
|
||||
- Navigation arrows: Themed appropriately
|
||||
|
||||
## CSS Classes Reference
|
||||
|
||||
| Class | Purpose | Dark Mode Color |
|
||||
|-------|---------|----------------|
|
||||
| `icon-improved` | Better icon visibility | `#CCCCCC` |
|
||||
| `icon-muted` | Muted but visible icons | `#999999` |
|
||||
| `icon-disabled` | Disabled icon state | `#858585` |
|
||||
| `op-btn-vscode-disabled` | VS Code disabled button | Background: `#3C3C3C` |
|
||||
| `btn-themed-disabled` | Themed disabled button | Uses CSS variables |
|
||||
| `icon-themed` | Variable-based icon color | `var(--icon-color)` |
|
||||
| `.reactour__helper` | `#1F2937` background |
|
||||
| `.react-tooltip` | `#1F2937` background |
|
||||
| `.HoverCardContent` | `#1F2937` background |
|
||||
| `.react-datepicker` | `#1F2937` background |
|
||||
|
||||
## Migration Guide
|
||||
|
||||
### From JavaScript Function to Tailwind
|
||||
```jsx
|
||||
// Before
|
||||
<i style={{ color: getThemeIconColor() }} className="fa-light fa-plus" />
|
||||
|
||||
// After
|
||||
<i className="fa-light fa-plus icon-improved" />
|
||||
```
|
||||
|
||||
### From Hardcoded Colors to Theme-aware
|
||||
```jsx
|
||||
// Before
|
||||
<i className="fa-light fa-plus text-gray-500" />
|
||||
|
||||
// After (automatic improvement)
|
||||
<i className="fa-light fa-plus text-gray-500" />
|
||||
// OR explicitly
|
||||
<i className="fa-light fa-plus icon-improved" />
|
||||
```
|
||||
|
||||
## Migration Notes
|
||||
|
||||
All existing tooltip and tour components will automatically inherit the new dark mode styling when the theme is set to `opensigndark`. No code changes required for existing implementations.
|
||||
@@ -0,0 +1,29 @@
|
||||
#!/bin/sh
|
||||
|
||||
ENV_FILE=./build/env.js
|
||||
DOTENV_FILE=./.env.prod # ✅ use .env.prod
|
||||
|
||||
echo "Generating runtime env file at $ENV_FILE..."
|
||||
|
||||
echo "window.RUNTIME_ENV = {" > $ENV_FILE
|
||||
|
||||
# List of keys to include
|
||||
RUNTIME_KEYS="REACT_APP_SERVERURL"
|
||||
|
||||
for key in $RUNTIME_KEYS; do
|
||||
# First check docker env (-e), fallback to .env file
|
||||
value=$(printenv "$key")
|
||||
|
||||
if [ -z "$value" ] && [ -f "$DOTENV_FILE" ]; then
|
||||
# fallback: read from .env
|
||||
value=$(grep "^$key=" "$DOTENV_FILE" | cut -d '=' -f2- | tr -d '\r\n' | sed 's/"/\\"/g')
|
||||
else
|
||||
value=$(echo "$value" | sed 's/"/\\"/g')
|
||||
fi
|
||||
|
||||
echo " $key: \"$value\"," >> $ENV_FILE
|
||||
done
|
||||
|
||||
echo "};" >> $ENV_FILE
|
||||
|
||||
exec "$@"
|
||||
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* Tailwind Dark Mode Usage Examples for OpenSign
|
||||
*
|
||||
* This file demonstrates how to use the new Tailwind utilities
|
||||
* for better dark mode visibility of buttons and icons.
|
||||
*/
|
||||
|
||||
// Example 1: VS Code-style disabled buttons
|
||||
const DisabledButtonExamples = () => {
|
||||
return (
|
||||
<div>
|
||||
{/* Option A: Using the VS Code disabled style */}
|
||||
<button className="op-btn op-btn-primary op-btn-vscode-disabled" disabled>
|
||||
VS Code Style Disabled Button
|
||||
</button>
|
||||
|
||||
{/* Option B: Using themed disabled style */}
|
||||
<button className="op-btn btn-themed-disabled">
|
||||
Themed Disabled Button
|
||||
</button>
|
||||
|
||||
{/* Option C: Conditional styling */}
|
||||
<button
|
||||
className={`op-btn op-btn-primary ${
|
||||
isDisabled ? "op-btn-vscode-disabled" : ""
|
||||
}`}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
Conditional Button
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Example 2: Icon visibility improvements
|
||||
const IconExamples = () => {
|
||||
return (
|
||||
<div>
|
||||
{/* Theme-aware icons with better visibility */}
|
||||
<i className="fa-light fa-folder icon-improved"></i>
|
||||
<i className="fa-light fa-plus icon-muted"></i>
|
||||
<i className="fa-light fa-trash icon-disabled"></i>
|
||||
|
||||
{/* Using CSS variables */}
|
||||
<i className="fa-light fa-search icon-themed"></i>
|
||||
<i className="fa-light fa-settings icon-themed-muted"></i>
|
||||
|
||||
{/* Gray text that automatically improves in dark mode */}
|
||||
<span className="text-gray-500">
|
||||
This text is now more visible in dark mode
|
||||
</span>
|
||||
<span className="text-gray-400">Muted but still readable</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Example 3: Using CSS variables in inline styles
|
||||
const InlineStyleExamples = () => {
|
||||
return (
|
||||
<div>
|
||||
{/* Using CSS variables directly */}
|
||||
<i className="fa-light fa-plus" style={{ color: "var(--icon-color)" }} />
|
||||
|
||||
{/* Using the existing JavaScript function */}
|
||||
<i className="fa-light fa-minus" style={{ color: getThemeIconColor() }} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Example 4: Toolbar with improved icons
|
||||
const ToolbarExample = () => {
|
||||
return (
|
||||
<div className="flex space-x-2 p-2">
|
||||
<button className="p-2 hover:bg-gray-200 rounded">
|
||||
<i className="fa-light fa-plus icon-improved"></i>
|
||||
</button>
|
||||
<button className="p-2 hover:bg-gray-200 rounded" disabled>
|
||||
<i className="fa-light fa-trash icon-disabled"></i>
|
||||
</button>
|
||||
<button className="p-2 hover:bg-gray-200 rounded">
|
||||
<i className="fa-light fa-edit icon-improved"></i>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export {
|
||||
DisabledButtonExamples,
|
||||
IconExamples,
|
||||
InlineStyleExamples,
|
||||
ToolbarExample
|
||||
};
|
||||
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* Tailwind Dark Mode Usage Examples for OpenSign
|
||||
*
|
||||
* This file demonstrates how to use the new Tailwind utilities
|
||||
* for better dark mode visibility of buttons and icons.
|
||||
*/
|
||||
|
||||
// Example 1: VS Code-style disabled buttons
|
||||
const DisabledButtonExamples = () => {
|
||||
return (
|
||||
<div>
|
||||
{/* Option A: Using the VS Code disabled style */}
|
||||
<button className="op-btn op-btn-primary op-btn-vscode-disabled" disabled>
|
||||
VS Code Style Disabled Button
|
||||
</button>
|
||||
|
||||
{/* Option B: Using themed disabled style */}
|
||||
<button className="op-btn btn-themed-disabled">
|
||||
Themed Disabled Button
|
||||
</button>
|
||||
|
||||
{/* Option C: Conditional styling */}
|
||||
<button
|
||||
className={`op-btn op-btn-primary ${isDisabled ? 'op-btn-vscode-disabled' : ''}`}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
Conditional Button
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Example 2: Icon visibility improvements
|
||||
const IconExamples = () => {
|
||||
return (
|
||||
<div>
|
||||
{/* Theme-aware icons with better visibility */}
|
||||
<i className="fa-light fa-folder icon-improved"></i>
|
||||
<i className="fa-light fa-plus icon-muted"></i>
|
||||
<i className="fa-light fa-trash icon-disabled"></i>
|
||||
|
||||
{/* Using CSS variables */}
|
||||
<i className="fa-light fa-search icon-themed"></i>
|
||||
<i className="fa-light fa-settings icon-themed-muted"></i>
|
||||
|
||||
{/* Gray text that automatically improves in dark mode */}
|
||||
<span className="text-gray-500">This text is now more visible in dark mode</span>
|
||||
<span className="text-gray-400">Muted but still readable</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Example 3: Using CSS variables in inline styles
|
||||
const InlineStyleExamples = () => {
|
||||
return (
|
||||
<div>
|
||||
{/* Using CSS variables directly */}
|
||||
<i
|
||||
className="fa-light fa-plus"
|
||||
style={{ color: 'var(--icon-color)' }}
|
||||
/>
|
||||
|
||||
{/* Using the existing JavaScript function */}
|
||||
<i
|
||||
className="fa-light fa-minus"
|
||||
style={{ color: getThemeIconColor() }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Example 4: Toolbar with improved icons
|
||||
const ToolbarExample = () => {
|
||||
return (
|
||||
<div className="flex space-x-2 p-2">
|
||||
<button className="p-2 hover:bg-gray-200 rounded">
|
||||
<i className="fa-light fa-plus icon-improved"></i>
|
||||
</button>
|
||||
<button className="p-2 hover:bg-gray-200 rounded" disabled>
|
||||
<i className="fa-light fa-trash icon-disabled"></i>
|
||||
</button>
|
||||
<button className="p-2 hover:bg-gray-200 rounded">
|
||||
<i className="fa-light fa-edit icon-improved"></i>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export {
|
||||
DisabledButtonExamples,
|
||||
IconExamples,
|
||||
InlineStyleExamples,
|
||||
ToolbarExample
|
||||
};
|
||||
@@ -1,20 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="opensigncss">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta name="description" content="The fastest way to sign PDFs & request signatures from others" />
|
||||
<!-- <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> -->
|
||||
<!-- <link rel="apple-touch-icon" href="/logo192.png" /> -->
|
||||
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.0.0/dist/css/bootstrap.min.css"
|
||||
integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous" />
|
||||
<link rel="stylesheet" href="https://cdn.opensignlabs.com/fonts.css" />
|
||||
<title>OpenSign™</title>
|
||||
<link rel="stylesheet" href="https://cdn.opensignlabs.com/fonts.css" />
|
||||
<title>...Loading</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<div id="root" style="touch-action:pan-x pan-y;"></div>
|
||||
<script type="module" src="/src/index.jsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,66 +1,71 @@
|
||||
{
|
||||
"name": "open_sign",
|
||||
"version": "0.1.0",
|
||||
"version": "2.30.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@formkit/auto-animate": "^0.8.2",
|
||||
"@lottiefiles/dotlottie-react": "^0.7.2",
|
||||
"@radix-ui/themes": "^3.1.1",
|
||||
"@react-pdf/renderer": "^3.4.4",
|
||||
"@reduxjs/toolkit": "^2.2.6",
|
||||
"axios": "^1.7.2",
|
||||
"@formkit/auto-animate": "^0.9.0",
|
||||
"@imgly/background-removal": "^1.7.0",
|
||||
"@lottiefiles/dotlottie-react": "^0.16.2",
|
||||
"@pdf-lib/fontkit": "^1.1.1",
|
||||
"@radix-ui/themes": "^3.2.1",
|
||||
"@reduxjs/toolkit": "^2.10.1",
|
||||
"axios": "^1.13.2",
|
||||
"date-fns-tz": "^3.2.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"i18next": "^25.6.3",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"i18next-http-backend": "^3.0.2",
|
||||
"jszip": "^3.10.1",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"moment": "^2.30.1",
|
||||
"nth-check": "^2.1.1",
|
||||
"parse": "^5.3.0",
|
||||
"parse": "^7.0.2",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"pkijs": "^3.3.3",
|
||||
"print-js": "^1.6.0",
|
||||
"radix-ui": "^1.0.1",
|
||||
"react": "^18.2.0",
|
||||
"react-bootstrap": "^2.10.4",
|
||||
"react-confetti": "^6.1.0",
|
||||
"react-cookie": "^7.1.4",
|
||||
"react-datepicker": "^7.2.0",
|
||||
"prismjs": "^1.30.0",
|
||||
"quill-html-edit-button": "^3.0.0",
|
||||
"radix-ui": "^1.4.3",
|
||||
"react": "^18.3.1",
|
||||
"react-bootstrap": "^2.10.10",
|
||||
"react-confetti": "^6.4.0",
|
||||
"react-datepicker": "^8.9.0",
|
||||
"react-dnd": "^16.0.1",
|
||||
"react-dnd-html5-backend": "^16.0.1",
|
||||
"react-dnd-multi-backend": "^8.0.3",
|
||||
"react-dnd-touch-backend": "^16.0.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-dnd-multi-backend": "^9.0.0",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-gtm-module": "^2.0.11",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-konva": "^18.2.10",
|
||||
"react-pdf": "^9.1.0",
|
||||
"react-quill": "^2.0.0",
|
||||
"react-redux": "^9.1.2",
|
||||
"react-rnd": "^10.4.11",
|
||||
"react-router-dom": "^6.25.1",
|
||||
"react-scripts": "5.0.1",
|
||||
"react-i18next": "^15.7.4",
|
||||
"react-konva": "^18.2.12",
|
||||
"react-pdf": "^9.2.1",
|
||||
"react-quill-new": "^3.6.0",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-rnd": "^10.5.2",
|
||||
"react-router": "^7.9.6",
|
||||
"react-scrollbars-custom": "^4.1.1",
|
||||
"react-select": "^5.8.0",
|
||||
"react-signature-canvas": "^1.0.6",
|
||||
"react-tooltip": "^5.27.1",
|
||||
"react-web-share": "^2.0.2",
|
||||
"react-select": "^5.10.2",
|
||||
"react-signature-canvas": "^1.1.0-alpha.2",
|
||||
"react-timezone-select": "^3.2.8",
|
||||
"react-tooltip": "^5.30.0",
|
||||
"reactour": "^1.19.4",
|
||||
"redux": "^5.0.1",
|
||||
"redux-thunk": "^3.1.0",
|
||||
"regex-parser": "^2.3.0",
|
||||
"serve": "^14.2.3",
|
||||
"styled-components": "^4.4.1",
|
||||
"web-vitals": "^4.2.2",
|
||||
"ws": "^8.18.0"
|
||||
"regex-parser": "^2.3.1",
|
||||
"serve": "^14.2.5",
|
||||
"styled-components": "^5.3.11",
|
||||
"web-vitals": "^5.1.0",
|
||||
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz"
|
||||
},
|
||||
"scripts": {
|
||||
"build-webpack": "webpack --config webpack.config.js",
|
||||
"build": "npm run version && npm run build-webpack && react-scripts build",
|
||||
"build-watch": "webpack --config webpack.config.js --watch",
|
||||
"start-dev": "concurrently \"npm run build-watch\" \"react-scripts start\"",
|
||||
"build": "npm run version && NODE_OPTIONS=\"--max-old-space-size=8192\" vite build",
|
||||
"start-dev": "vite",
|
||||
"dev": "vite",
|
||||
"preview": "vite preview",
|
||||
"start": "serve -s build",
|
||||
"version": "curl -s https://api.github.com/repos/opensignlabs/opensign/releases/latest | grep '\"tag_name\":' | awk -F '\"' '{print $4}' > ./public/version.txt",
|
||||
"version-win": "powershell -Command \"Invoke-RestMethod -Uri 'https://api.github.com/repos/opensignlabs/opensign/releases/latest' | Select-Object -ExpandProperty tag_name | Out-File -FilePath ./public/version.txt\"",
|
||||
"build-win": "npm run version-win && npm run build-webpack && react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject",
|
||||
"build-win": "npm run version-win && vite build",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"release": "standard-version",
|
||||
"commit": "cz"
|
||||
},
|
||||
@@ -88,33 +93,38 @@
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.24.6",
|
||||
"@babel/preset-env": "^7.24.6",
|
||||
"@babel/preset-react": "^7.24.6",
|
||||
"@babel/runtime-corejs2": "^7.24.7",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"babel-loader": "^9.1.3",
|
||||
"clean-webpack-plugin": "^4.0.0",
|
||||
"commitizen": "^4.3.0",
|
||||
"concurrently": "^8.2.2",
|
||||
"@babel/core": "^7.28.3",
|
||||
"@babel/preset-env": "^7.28.3",
|
||||
"@babel/preset-react": "^7.28.5",
|
||||
"@babel/runtime-corejs2": "^7.28.3",
|
||||
"@testing-library/jest-dom": "^6.8.0",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/react": "^19.2.6",
|
||||
"@vitejs/plugin-react": "^4.7.0",
|
||||
"@vitest/ui": "^3.2.4",
|
||||
"autoprefixer": "^10.4.22",
|
||||
"babel-loader": "^10.0.0",
|
||||
"commitizen": "^4.3.1",
|
||||
"concurrently": "^9.2.1",
|
||||
"css-loader": "^7.1.2",
|
||||
"daisyui": "^4.12.10",
|
||||
"dotenv": "^16.4.5",
|
||||
"dotenv-webpack": "^8.1.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-react": "^7.34.3",
|
||||
"lint-staged": "^15.2.7",
|
||||
"mini-css-extract-plugin": "^2.9.0",
|
||||
"postcss": "^8.4.39",
|
||||
"prettier": "^3.3.3",
|
||||
"pretty-quick": "^4.0.0",
|
||||
"tailwindcss": "^3.4.6",
|
||||
"terser-webpack-plugin": "^5.3.10",
|
||||
"webpack-cli": "^5.1.4"
|
||||
"daisyui": "^4.12.24",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-plugin-prettier": "^5.5.4",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"jsdom": "^27.2.0",
|
||||
"lint-staged": "^16.2.7",
|
||||
"postcss": "^8.5.6",
|
||||
"prettier": "^3.6.2",
|
||||
"pretty-quick": "^4.2.2",
|
||||
"rollup-plugin-node-polyfills": "^0.2.1",
|
||||
"tailwindcss": "^3.4.18",
|
||||
"vite": "^7.2.4",
|
||||
"vite-plugin-svgr": "^4.5.0",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"overrides": {
|
||||
"nth-check": "$nth-check",
|
||||
"ws": "$ws"
|
||||
"engines": {
|
||||
"node": "18 || 20 || 22"
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 11 KiB |
@@ -0,0 +1,4 @@
|
||||
Name,Email,Phone,Company,JobTitle
|
||||
John Doe,john@example.com,1234567890,abc firm,dev
|
||||
Jane Smith,jane@example.com,9876543210,xyz firm,manager
|
||||
Foo Bar,foo@example.com,5555555555,xyz firm,director
|
||||
|
|
After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 86 KiB |
@@ -0,0 +1 @@
|
||||
import "@testing-library/jest-dom";
|
||||
@@ -1,212 +0,0 @@
|
||||
import React, { useState, useEffect, lazy } from "react";
|
||||
import { Routes, Route, BrowserRouter } from "react-router-dom";
|
||||
import { pdfjs } from "react-pdf";
|
||||
import Login from "./pages/Login";
|
||||
import Form from "./pages/Form";
|
||||
import Report from "./pages/Report";
|
||||
import Dashboard from "./pages/Dashboard";
|
||||
import HomeLayout from "./layout/HomeLayout";
|
||||
import PageNotFound from "./pages/PageNotFound";
|
||||
import ValidateRoute from "./primitives/ValidateRoute";
|
||||
import Validate from "./primitives/Validate";
|
||||
import TemplatePlaceholder from "./pages/TemplatePlaceholder";
|
||||
import SignYourSelf from "./pages/SignyourselfPdf";
|
||||
import DraftDocument from "./components/pdf/DraftDocument";
|
||||
import PlaceHolderSign from "./pages/PlaceHolderSign";
|
||||
import PdfRequestFiles from "./pages/PdfRequestFiles";
|
||||
import LazyPage from "./primitives/LazyPage";
|
||||
import { isEnableSubscription } from "./constant/const";
|
||||
import SSOVerify from "./pages/SSOVerify";
|
||||
import Loader from "./primitives/Loader";
|
||||
import TeamList from "./pages/TeamList";
|
||||
import UserList from "./pages/UserList";
|
||||
const DebugPdf = lazy(() => import("./pages/DebugPdf"));
|
||||
const ForgetPassword = lazy(() => import("./pages/ForgetPassword"));
|
||||
const GuestLogin = lazy(() => import("./pages/GuestLogin"));
|
||||
const Pgsignup = lazy(() => import("./pages/Pgsignup"));
|
||||
const Subscriptions = lazy(() => import("./pages/PlanSubscriptions"));
|
||||
const ChangePassword = lazy(() => import("./pages/ChangePassword"));
|
||||
const UserProfile = lazy(() => import("./pages/UserProfile"));
|
||||
const Signup = lazy(() => import("./pages/Signup"));
|
||||
const Opensigndrive = lazy(() => import("./pages/Opensigndrive"));
|
||||
const ManageSign = lazy(() => import("./pages/Managesign"));
|
||||
const GenerateToken = lazy(() => import("./pages/GenerateToken"));
|
||||
const Webhook = lazy(() => import("./pages/Webhook"));
|
||||
const AddAdmin = lazy(() => import("./pages/AddAdmin"));
|
||||
const UpdateExistUserAdmin = lazy(() => import("./pages/UpdateExistUserAdmin"));
|
||||
|
||||
pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/legacy/build/pdf.worker.min.mjs`;
|
||||
const AppLoader = () => {
|
||||
return (
|
||||
<div className="flex justify-center items-center h-[100vh]">
|
||||
<Loader />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
function App() {
|
||||
const [isloading, setIsLoading] = useState(true);
|
||||
useEffect(() => {
|
||||
handleCredentials();
|
||||
}, []);
|
||||
|
||||
const handleCredentials = () => {
|
||||
const appId = process.env.REACT_APP_APPID
|
||||
? process.env.REACT_APP_APPID
|
||||
: "opensign";
|
||||
const baseurl = process.env.REACT_APP_SERVERURL
|
||||
? process.env.REACT_APP_SERVERURL
|
||||
: window.location.origin + "/api/app";
|
||||
try {
|
||||
localStorage.setItem("baseUrl", `${baseurl}/`);
|
||||
localStorage.setItem("parseAppId", appId);
|
||||
setIsLoading(false);
|
||||
} catch (error) {
|
||||
console.log("err ", error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-base-200">
|
||||
{isloading ? (
|
||||
<AppLoader />
|
||||
) : (
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route element={<ValidateRoute />}>
|
||||
<Route exact path="/" element={<Login />} />
|
||||
{isEnableSubscription && (
|
||||
<Route path="/signup" element={<LazyPage Page={Signup} />} />
|
||||
)}
|
||||
{!isEnableSubscription && (
|
||||
<>
|
||||
<Route
|
||||
path="/addadmin"
|
||||
element={<LazyPage Page={AddAdmin} />}
|
||||
/>
|
||||
<Route
|
||||
path="/upgrade-2.1"
|
||||
element={<LazyPage Page={UpdateExistUserAdmin} />}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Route>
|
||||
<Route element={<Validate />}>
|
||||
<Route
|
||||
path="/load/template/:templateId"
|
||||
element={<TemplatePlaceholder />}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/load/placeholdersign/:docId"
|
||||
element={<PlaceHolderSign />}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/load/recipientSignPdf/:docId/:contactBookId"
|
||||
element={<PdfRequestFiles />}
|
||||
/>
|
||||
</Route>
|
||||
<Route
|
||||
path="/loadmf/signmicroapp/login/:id/:userMail/:contactBookId/:serverUrl"
|
||||
element={<LazyPage Page={GuestLogin} />}
|
||||
/>
|
||||
<Route
|
||||
path="/login/:id/:userMail/:contactBookId/:serverUrl"
|
||||
element={<LazyPage Page={GuestLogin} />}
|
||||
/>
|
||||
<Route
|
||||
path="/login/:base64url"
|
||||
element={<LazyPage Page={GuestLogin} />}
|
||||
/>
|
||||
<Route path="/debugpdf" element={<LazyPage Page={DebugPdf} />} />
|
||||
<Route
|
||||
path="/forgetpassword"
|
||||
element={<LazyPage Page={ForgetPassword} />}
|
||||
/>
|
||||
{isEnableSubscription && (
|
||||
<>
|
||||
<Route
|
||||
path="/pgsignup"
|
||||
element={<LazyPage Page={Pgsignup} />}
|
||||
/>
|
||||
<Route
|
||||
path="/subscription"
|
||||
element={<LazyPage Page={Subscriptions} />}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Route element={<HomeLayout />}>
|
||||
<Route
|
||||
path="/changepassword"
|
||||
element={<LazyPage Page={ChangePassword} />}
|
||||
/>
|
||||
<Route path="/form/:id" element={<Form />} />
|
||||
<Route path="/report/:id" element={<Report />} />
|
||||
<Route path="/dashboard/:id" element={<Dashboard />} />
|
||||
<Route
|
||||
path="/profile"
|
||||
element={<LazyPage Page={UserProfile} />}
|
||||
/>
|
||||
<Route
|
||||
path="/opensigndrive"
|
||||
element={<LazyPage Page={Opensigndrive} />}
|
||||
/>
|
||||
<Route
|
||||
path="/managesign"
|
||||
element={<LazyPage Page={ManageSign} />}
|
||||
/>
|
||||
<Route
|
||||
path="/template/:templateId"
|
||||
element={<TemplatePlaceholder />}
|
||||
/>
|
||||
{/* signyouself route with no rowlevel data using docId from url */}
|
||||
<Route path="/signaturePdf/:docId" element={<SignYourSelf />} />
|
||||
{/* draft document route to handle and navigate route page accordiing to document status */}
|
||||
<Route path="/draftDocument" element={<DraftDocument />} />
|
||||
{/* recipient placeholder set route with no rowlevel data using docId from url*/}
|
||||
<Route
|
||||
path="/placeHolderSign/:docId"
|
||||
element={<PlaceHolderSign />}
|
||||
/>
|
||||
{/* for user signature (need your sign route) with row level data */}
|
||||
<Route path="/pdfRequestFiles" element={<PdfRequestFiles />} />
|
||||
{/* for user signature (need your sign route) with no row level data */}
|
||||
<Route
|
||||
path="/pdfRequestFiles/:docId"
|
||||
element={<PdfRequestFiles />}
|
||||
/>
|
||||
|
||||
{/* recipient signature route with no rowlevel data using docId from url */}
|
||||
<Route
|
||||
path="/recipientSignPdf/:docId/:contactBookId"
|
||||
element={<PdfRequestFiles />}
|
||||
/>
|
||||
<Route
|
||||
path="/recipientSignPdf/:docId"
|
||||
element={<PdfRequestFiles />}
|
||||
/>
|
||||
{isEnableSubscription && (
|
||||
<>
|
||||
<Route path="/teams" element={<TeamList />} />
|
||||
<Route
|
||||
path="/generatetoken"
|
||||
element={<LazyPage Page={GenerateToken} />}
|
||||
/>
|
||||
<Route
|
||||
path="/webhook"
|
||||
element={<LazyPage Page={Webhook} />}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Route path="/users" element={<UserList />} />
|
||||
</Route>
|
||||
<Route path="/sso" element={<SSOVerify />} />
|
||||
<Route path="*" element={<PageNotFound />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,148 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { lazyWithRetry, hideUpgradeProgress } from "./utils";
|
||||
import { Routes, Route, BrowserRouter } from "react-router";
|
||||
import { pdfjs } from "react-pdf";
|
||||
import Form from "./pages/Form";
|
||||
import Report from "./pages/Report";
|
||||
import Dashboard from "./pages/Dashboard";
|
||||
import HomeLayout from "./layout/HomeLayout";
|
||||
import PageNotFound from "./pages/PageNotFound";
|
||||
import ValidateRoute from "./primitives/ValidateRoute";
|
||||
import Validate from "./primitives/Validate";
|
||||
import TemplatePlaceholder from "./pages/TemplatePlaceholder";
|
||||
import SignYourSelf from "./pages/SignyourselfPdf";
|
||||
import DraftDocument from "./components/pdf/DraftDocument";
|
||||
import PlaceHolderSign from "./pages/PlaceHolderSign";
|
||||
import PdfRequestFiles from "./pages/PdfRequestFiles";
|
||||
import Lazy from "./primitives/LazyPage";
|
||||
import Loader from "./primitives/Loader";
|
||||
import UserList from "./pages/UserList";
|
||||
import { serverUrl_fn } from "./constant/appinfo";
|
||||
import DocSuccessPage from "./pages/DocSuccessPage";
|
||||
import DragProvider from "./components/DragProivder";
|
||||
import Title from "./components/Title";
|
||||
const DebugPdf = lazyWithRetry(() => import("./pages/DebugPdf"));
|
||||
const ForgetPassword = lazyWithRetry(() => import("./pages/ForgetPassword"));
|
||||
const GuestLogin = lazyWithRetry(() => import("./pages/GuestLogin"));
|
||||
const ChangePassword = lazyWithRetry(() => import("./pages/ChangePassword"));
|
||||
const UserProfile = lazyWithRetry(() => import("./pages/UserProfile"));
|
||||
const Opensigndrive = lazyWithRetry(() => import("./pages/Opensigndrive"));
|
||||
const ManageSign = lazyWithRetry(() => import("./pages/Managesign"));
|
||||
const AddAdmin = lazyWithRetry(() => import("./pages/AddAdmin"));
|
||||
const UpdateExistUserAdmin = lazyWithRetry(
|
||||
() => import("./pages/UpdateExistUserAdmin")
|
||||
);
|
||||
const Preferences = lazyWithRetry(() => import("./pages/Preferences"));
|
||||
const Login = lazyWithRetry(() => import("./pages/Login"));
|
||||
const VerifyDocument = lazyWithRetry(() => import("./pages/VerifyDocument"));
|
||||
pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/legacy/build/pdf.worker.min.mjs`;
|
||||
const AppLoader = () => {
|
||||
return (
|
||||
<div className="flex justify-center items-center h-[100vh]">
|
||||
<Loader />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
function App() {
|
||||
const [isloading, setIsLoading] = useState(true);
|
||||
useEffect(() => {
|
||||
// initialize creds
|
||||
const id = process.env.REACT_APP_APPID ?? "opensign";
|
||||
localStorage.setItem("parseAppId", id);
|
||||
localStorage.setItem("baseUrl", `${serverUrl_fn()}/`);
|
||||
hideUpgradeProgress();
|
||||
localStorage.removeItem("showUpgradeProgress");
|
||||
setIsLoading(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="bg-base-200">
|
||||
{isloading ? (
|
||||
<AppLoader />
|
||||
) : (
|
||||
<BrowserRouter>
|
||||
<Title />
|
||||
<Routes>
|
||||
<Route element={<ValidateRoute />}>
|
||||
<Route exact path="/" element={<Lazy Page={Login} />} />
|
||||
<Route path="/addadmin" element={<Lazy Page={AddAdmin} />} />
|
||||
<Route
|
||||
path="/upgrade-2.1"
|
||||
element={<Lazy Page={UpdateExistUserAdmin} />}
|
||||
/>
|
||||
</Route>
|
||||
<Route element={<Validate />}>
|
||||
<Route
|
||||
exact
|
||||
path="/load/recipientSignPdf/:docId/:contactBookId"
|
||||
element={<DragProvider Page={PdfRequestFiles} />}
|
||||
/>
|
||||
</Route>
|
||||
<Route
|
||||
path="/login/:base64url"
|
||||
element={<Lazy Page={GuestLogin} />}
|
||||
/>
|
||||
<Route path="/debugpdf" element={<Lazy Page={DebugPdf} />} />
|
||||
<Route
|
||||
path="/forgetpassword"
|
||||
element={<Lazy Page={ForgetPassword} />}
|
||||
/>
|
||||
<Route element={<HomeLayout />}>
|
||||
<Route path="/users" element={<UserList />} />
|
||||
<Route
|
||||
path="/changepassword"
|
||||
element={<Lazy Page={ChangePassword} />}
|
||||
/>
|
||||
<Route path="/form/:id" element={<Form />} />
|
||||
<Route path="/report/:id" element={<Report />} />
|
||||
<Route path="/dashboard/:id" element={<Dashboard />} />
|
||||
<Route path="/profile" element={<Lazy Page={UserProfile} />} />
|
||||
<Route path="/drive" element={<Lazy Page={Opensigndrive} />} />
|
||||
<Route path="/managesign" element={<Lazy Page={ManageSign} />} />
|
||||
<Route
|
||||
path="/template/:templateId"
|
||||
element={<DragProvider Page={TemplatePlaceholder} />}
|
||||
/>
|
||||
{/* signyouself route with no rowlevel data using docId from url */}
|
||||
<Route
|
||||
path="/signaturePdf/:docId"
|
||||
element={<DragProvider Page={SignYourSelf} />}
|
||||
/>
|
||||
{/* draft document route to handle and navigate route page according to document status */}
|
||||
<Route
|
||||
path="/draftDocument"
|
||||
element={<DragProvider Page={DraftDocument} />}
|
||||
/>
|
||||
{/* recipient placeholder set route with no rowlevel data using docId from url*/}
|
||||
<Route
|
||||
path="/placeHolderSign/:docId"
|
||||
element={<DragProvider Page={PlaceHolderSign} />}
|
||||
/>
|
||||
{/* recipient signature route with no rowlevel data using docId from url */}
|
||||
<Route
|
||||
path="/recipientSignPdf/:docId/:contactBookId"
|
||||
element={<DragProvider Page={PdfRequestFiles} />}
|
||||
/>
|
||||
<Route
|
||||
path="/recipientSignPdf/:docId"
|
||||
element={<DragProvider Page={PdfRequestFiles} />}
|
||||
/>
|
||||
<Route
|
||||
path="/verify-document"
|
||||
element={<Lazy Page={VerifyDocument} />}
|
||||
/>
|
||||
<Route
|
||||
path="/preferences"
|
||||
element={<Lazy Page={Preferences} />}
|
||||
/>
|
||||
</Route>
|
||||
<Route path="/success" element={<DocSuccessPage />} />
|
||||
<Route path="*" element={<PageNotFound />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
Before Width: | Height: | Size: 805 B |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 23 KiB |
@@ -1,283 +0,0 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import Parse from "parse";
|
||||
import Loader from "../primitives/Loader";
|
||||
|
||||
const AddSigner = (props) => {
|
||||
const [name, setName] = useState("");
|
||||
const [phone, setPhone] = useState("");
|
||||
const [email, setEmail] = useState("");
|
||||
const [addYourself, setAddYourself] = useState(false);
|
||||
const [isLoader, setIsLoader] = useState(false);
|
||||
const [isUserExist, setIsUserExist] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
checkUserExist();
|
||||
}, []);
|
||||
// Load user details from localStorage when the component mounts
|
||||
useEffect(() => {
|
||||
const savedUserDetails = JSON.parse(
|
||||
localStorage.getItem("UserInformation")
|
||||
);
|
||||
if (savedUserDetails && addYourself) {
|
||||
setName(savedUserDetails.name);
|
||||
setPhone(savedUserDetails?.phone || "");
|
||||
setEmail(savedUserDetails.email);
|
||||
}
|
||||
}, [addYourself]);
|
||||
|
||||
const checkUserExist = async () => {
|
||||
const user = Parse.User.current();
|
||||
try {
|
||||
const query = new Parse.Query("contracts_Contactbook");
|
||||
query.equalTo("CreatedBy", user);
|
||||
query.notEqualTo("IsDeleted", true);
|
||||
query.equalTo("Email", user.getEmail());
|
||||
const res = await query.first();
|
||||
// console.log(res);
|
||||
if (!res) {
|
||||
setIsUserExist(true);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("err", err);
|
||||
}
|
||||
};
|
||||
// Define a function to handle form submission
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsLoader(true);
|
||||
if (localStorage.getItem("TenantId")) {
|
||||
try {
|
||||
const user = Parse.User.current();
|
||||
const query = new Parse.Query("contracts_Contactbook");
|
||||
query.equalTo("CreatedBy", user);
|
||||
query.notEqualTo("IsDeleted", true);
|
||||
query.equalTo("Email", email);
|
||||
const res = await query.first();
|
||||
// console.log(res);
|
||||
if (!res) {
|
||||
const contactQuery = new Parse.Object("contracts_Contactbook");
|
||||
contactQuery.set("Name", name);
|
||||
if (phone) {
|
||||
contactQuery.set("Phone", phone);
|
||||
}
|
||||
contactQuery.set("Email", email);
|
||||
contactQuery.set("UserRole", "contracts_Guest");
|
||||
|
||||
contactQuery.set("TenantId", {
|
||||
__type: "Pointer",
|
||||
className: "partners_Tenant",
|
||||
objectId: localStorage.getItem("TenantId")
|
||||
});
|
||||
|
||||
try {
|
||||
const _users = Parse.Object.extend("User");
|
||||
const _user = new _users();
|
||||
_user.set("name", name);
|
||||
_user.set("username", email);
|
||||
_user.set("email", email);
|
||||
_user.set("password", email);
|
||||
if (phone) {
|
||||
_user.set("phone", phone);
|
||||
}
|
||||
const user = await _user.save();
|
||||
if (user) {
|
||||
const currentUser = Parse.User.current();
|
||||
contactQuery.set(
|
||||
"CreatedBy",
|
||||
Parse.User.createWithoutData(currentUser.id)
|
||||
);
|
||||
|
||||
contactQuery.set("UserId", user);
|
||||
const acl = new Parse.ACL();
|
||||
acl.setPublicReadAccess(true);
|
||||
acl.setPublicWriteAccess(true);
|
||||
acl.setReadAccess(currentUser.id, true);
|
||||
acl.setWriteAccess(currentUser.id, true);
|
||||
|
||||
contactQuery.setACL(acl);
|
||||
|
||||
const res = await contactQuery.save();
|
||||
|
||||
const parseData = JSON.parse(JSON.stringify(res));
|
||||
if (props.details) {
|
||||
props.details({
|
||||
value: parseData[props.valueKey],
|
||||
label: parseData[props.displayKey]
|
||||
});
|
||||
}
|
||||
if (props.closePopup) {
|
||||
props.closePopup();
|
||||
}
|
||||
if (props.handleUserData) {
|
||||
props.handleUserData(parseData);
|
||||
}
|
||||
|
||||
setIsLoader(false);
|
||||
// Reset the form fields
|
||||
setAddYourself(false);
|
||||
setName("");
|
||||
setPhone("");
|
||||
setEmail("");
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("err ", err);
|
||||
if (err.code === 202) {
|
||||
const params = { email: email };
|
||||
const userRes = await Parse.Cloud.run("getUserId", params);
|
||||
const currentUser = Parse.User.current();
|
||||
contactQuery.set(
|
||||
"CreatedBy",
|
||||
Parse.User.createWithoutData(currentUser.id)
|
||||
);
|
||||
|
||||
contactQuery.set("UserId", {
|
||||
__type: "Pointer",
|
||||
className: "_User",
|
||||
objectId: userRes.id
|
||||
});
|
||||
const acl = new Parse.ACL();
|
||||
acl.setPublicReadAccess(true);
|
||||
acl.setPublicWriteAccess(true);
|
||||
acl.setReadAccess(currentUser.id, true);
|
||||
acl.setWriteAccess(currentUser.id, true);
|
||||
|
||||
contactQuery.setACL(acl);
|
||||
const res = await contactQuery.save();
|
||||
|
||||
const parseData = JSON.parse(JSON.stringify(res));
|
||||
if (props.details) {
|
||||
props.details({
|
||||
value: parseData[props.valueKey],
|
||||
label: parseData[props.displayKey]
|
||||
});
|
||||
}
|
||||
if (props.closePopup) {
|
||||
props.closePopup();
|
||||
}
|
||||
if (props.handleUserData) {
|
||||
props.handleUserData(parseData);
|
||||
}
|
||||
setIsLoader(false);
|
||||
// Reset the form fields
|
||||
setAddYourself(false);
|
||||
setName("");
|
||||
setPhone("");
|
||||
setEmail("");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
alert("Contact already exist!");
|
||||
setIsLoader(false);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("err in fetch contact", err);
|
||||
setIsLoader(false);
|
||||
alert("something went wrong, please try again later");
|
||||
}
|
||||
} else {
|
||||
setIsLoader(false);
|
||||
alert("something went wrong, please try again later");
|
||||
}
|
||||
};
|
||||
|
||||
// Define a function to handle the "add yourself" checkbox
|
||||
const handleAddYourselfChange = () => {
|
||||
if (addYourself) {
|
||||
setAddYourself(false);
|
||||
setName("");
|
||||
setPhone("");
|
||||
setEmail("");
|
||||
} else {
|
||||
setAddYourself(true);
|
||||
}
|
||||
};
|
||||
const handleReset = () => {
|
||||
setAddYourself(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full p-[20px]">
|
||||
{isLoader && (
|
||||
<div className="fixed inset-0 flex justify-center items-center bg-black bg-opacity-30">
|
||||
<Loader />
|
||||
</div>
|
||||
)}
|
||||
<div className="w-full mx-auto p-2 text-base-content">
|
||||
{isUserExist && (
|
||||
<div className="mb-3 flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="addYourself"
|
||||
checked={addYourself}
|
||||
onChange={handleAddYourselfChange}
|
||||
className="op-checkbox op-checkbox-sm"
|
||||
/>
|
||||
<label htmlFor="addYourself" className="ml-2 mb-0">
|
||||
Add Yourself
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="mb-3">
|
||||
<label htmlFor="name" className="block text-xs font-semibold">
|
||||
Name
|
||||
<span className="text-[red] text-[13px]"> *</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
required
|
||||
disabled={addYourself}
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label htmlFor="email" className="block text-xs font-semibold">
|
||||
Email
|
||||
<span className="text-[red] text-[13px]"> *</span>
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value?.toLowerCase())}
|
||||
required
|
||||
disabled={addYourself}
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label htmlFor="phone" className="block text-xs font-semibold">
|
||||
Phone
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="phone"
|
||||
value={phone}
|
||||
onChange={(e) => setPhone(e.target.value)}
|
||||
disabled={addYourself}
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-4 flex gap-x-2 justify-start">
|
||||
<button type="submit" className="op-btn op-btn-primary">
|
||||
Submit
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleReset()}
|
||||
className="op-btn op-btn-secondary"
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddSigner;
|
||||
@@ -1,194 +0,0 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import Parse from "parse";
|
||||
import Title from "./Title";
|
||||
import Loader from "../primitives/Loader";
|
||||
|
||||
const AddTeam = (props) => {
|
||||
const [formdata, setFormdata] = useState({
|
||||
name: "",
|
||||
team: ""
|
||||
});
|
||||
const [isLoader, setIsLoader] = useState(false);
|
||||
const [teamList, setTeamList] = useState([]);
|
||||
useEffect(() => {
|
||||
getTeamList();
|
||||
}, []);
|
||||
|
||||
const getTeamList = async () => {
|
||||
setIsLoader(true);
|
||||
try {
|
||||
const teams = await Parse.Cloud.run("getteams");
|
||||
const teamRes = JSON.parse(JSON.stringify(teams));
|
||||
if (teamRes.length > 0) {
|
||||
const _teamRes = JSON.parse(JSON.stringify(teamRes));
|
||||
const allUsersteam = _teamRes.find((x) => x.Name === "All Users");
|
||||
if (allUsersteam) {
|
||||
setFormdata({ team: allUsersteam.objectId });
|
||||
}
|
||||
setTeamList(_teamRes);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("Err in fetch top level teamList", err);
|
||||
} finally {
|
||||
setIsLoader(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDropdown = (e) => {
|
||||
setFormdata((prev) => ({ ...prev, [e.target.name]: e.target.value }));
|
||||
};
|
||||
// Define a function to handle form submission
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
// Extracting values except for the 'name' key
|
||||
let updatedAncestors = [];
|
||||
if (formdata.team) {
|
||||
const Ancestors = teamList.find(
|
||||
(x) => x.objectId === formdata.team
|
||||
)?.Ancestors;
|
||||
if (Ancestors && Ancestors.length > 0) {
|
||||
updatedAncestors = Ancestors.map((x) => ({
|
||||
__type: "Pointer",
|
||||
className: "contracts_Teams",
|
||||
objectId: x.objectId
|
||||
}));
|
||||
} else {
|
||||
const AllUser = teamList.find((x) => x.objectId === "All Users");
|
||||
updatedAncestors = [
|
||||
AllUser,
|
||||
{
|
||||
__type: "Pointer",
|
||||
className: "contracts_Teams",
|
||||
objectId: formdata.team
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
try {
|
||||
setIsLoader(true);
|
||||
let data = { Name: formdata.name };
|
||||
if (updatedAncestors.length > 0) {
|
||||
const ParentId = updatedAncestors[updatedAncestors.length - 1];
|
||||
data["ParentId"] = ParentId?.objectId;
|
||||
data["Ancestors"] = updatedAncestors;
|
||||
}
|
||||
const newTeamRes = await Parse.Cloud.run("addteam", data);
|
||||
// console.log("teamRes ", newTeamRes);
|
||||
if (updatedAncestors.length > 0) {
|
||||
const ParentId = teamList.find((x) => x.objectId === formdata.team);
|
||||
props.handleTeamInfo({
|
||||
objectId: newTeamRes.id,
|
||||
Name: formdata.name,
|
||||
ParentId: ParentId,
|
||||
Ancestors: updatedAncestors,
|
||||
IsActive: true
|
||||
});
|
||||
} else {
|
||||
props.handleTeamInfo({
|
||||
objectId: newTeamRes.id,
|
||||
Name: formdata.name,
|
||||
ParentId: "",
|
||||
Ancestors: "",
|
||||
IsActive: true
|
||||
});
|
||||
}
|
||||
if (props.closePopup) {
|
||||
props.closePopup();
|
||||
}
|
||||
setFormdata({ name: "", team: { name: "", objectId: "" } });
|
||||
props.setIsAlert({ type: "success", msg: "Team created successfully." });
|
||||
} catch (err) {
|
||||
console.log("err in save team", err);
|
||||
if (err.code === 137) {
|
||||
props.setIsAlert({ type: "danger", msg: "Teams already exists." });
|
||||
} else if (err.code === 102) {
|
||||
props.setIsAlert({ type: "warning", msg: "Provide team name." });
|
||||
} else {
|
||||
props.setIsAlert({ type: "danger", msg: "Something went wrong." });
|
||||
}
|
||||
} finally {
|
||||
setTimeout(() => props.setIsAlert({ type: "success", msg: "" }), 1500);
|
||||
setIsLoader(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Define a function to handle the "add yourself" checkbox
|
||||
const handleReset = () => {
|
||||
setFormdata({
|
||||
name: "",
|
||||
team: { name: "", objectId: "" }
|
||||
});
|
||||
};
|
||||
const handleChange = (e) => {
|
||||
setFormdata((prev) => ({ ...prev, [e.target.name]: e.target.value }));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="shadow-md rounded-box my-[1px] p-3 bg-[#ffffff]">
|
||||
<Title title="Add Team" />
|
||||
{isLoader && (
|
||||
<div className="fixed inset-0 flex justify-center items-center bg-black bg-opacity-30 z-50 rounded-box">
|
||||
<Loader />
|
||||
</div>
|
||||
)}
|
||||
<div className="w-full mx-auto">
|
||||
<form onSubmit={handleSubmit}>
|
||||
{/* <h1 className="text-[20px] font-semibold mb-4">Add User</h1> */}
|
||||
<div className="mb-3">
|
||||
<label
|
||||
htmlFor="name"
|
||||
className="block text-xs text-gray-700 font-semibold"
|
||||
>
|
||||
Name
|
||||
<span className="text-[red] text-[13px]"> *</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
value={formdata.name}
|
||||
onChange={(e) => handleChange(e)}
|
||||
required
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label
|
||||
htmlFor="phone"
|
||||
className="block text-xs text-gray-700 font-semibold"
|
||||
>
|
||||
Parent Team
|
||||
</label>
|
||||
<select
|
||||
value={formdata.team}
|
||||
onChange={(e) => handleDropdown(e)}
|
||||
name="team"
|
||||
className="op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
>
|
||||
{teamList.length > 0 &&
|
||||
teamList.map((x) => (
|
||||
<option key={x.objectId} value={x.objectId}>
|
||||
{x.Name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex items-center mt-3 gap-2 text-white">
|
||||
<button type="submit" className="op-btn op-btn-primary">
|
||||
Submit
|
||||
</button>
|
||||
<div
|
||||
type="button"
|
||||
onClick={() => handleReset()}
|
||||
className="op-btn op-btn-secondary"
|
||||
>
|
||||
Reset
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddTeam;
|
||||
@@ -1,406 +0,0 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import Parse from "parse";
|
||||
import Title from "./Title";
|
||||
import Loader from "../primitives/Loader";
|
||||
import { copytoData } from "../constant/Utils";
|
||||
import { isEnableSubscription } from "../constant/const";
|
||||
function generatePassword(length) {
|
||||
const characters =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
let result = "";
|
||||
const charactersLength = characters.length;
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += characters.charAt(Math.floor(Math.random() * charactersLength));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const AddUser = (props) => {
|
||||
const [formdata, setFormdata] = useState({
|
||||
name: "",
|
||||
phone: "",
|
||||
email: "",
|
||||
team: "",
|
||||
password: "",
|
||||
role: ""
|
||||
});
|
||||
const [isLoader, setIsLoader] = useState(false);
|
||||
const [teamList, setTeamList] = useState([]);
|
||||
const role = ["OrgAdmin", "Editor", "User"];
|
||||
|
||||
useEffect(() => {
|
||||
getTeamList();
|
||||
}, []);
|
||||
|
||||
const getTeamList = async () => {
|
||||
setFormdata((prev) => ({ ...prev, password: generatePassword(12) }));
|
||||
const extUser = JSON.parse(localStorage.getItem("Extand_Class"))?.[0];
|
||||
const team = new Parse.Query("contracts_Teams");
|
||||
team.equalTo("OrganizationId", {
|
||||
__type: "Pointer",
|
||||
className: "contracts_Organizations",
|
||||
objectId: extUser.OrganizationId.objectId
|
||||
});
|
||||
team.notEqualTo("IsActive", false);
|
||||
const teamRes = await team.find();
|
||||
if (teamRes.length > 0) {
|
||||
const _teamRes = JSON.parse(JSON.stringify(teamRes));
|
||||
setTeamList(_teamRes);
|
||||
if (!isEnableSubscription) {
|
||||
const allUserId =
|
||||
_teamRes.find((x) => x.Name === "All Users")?.objectId || "";
|
||||
setFormdata((prev) => ({ ...prev, team: allUserId }));
|
||||
}
|
||||
}
|
||||
};
|
||||
const checkUserExist = async () => {
|
||||
try {
|
||||
const res = await Parse.Cloud.run("getUserDetails", {
|
||||
email: formdata.email
|
||||
});
|
||||
if (res) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("err", err);
|
||||
}
|
||||
};
|
||||
// Define a function to handle form submission
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const localUser = JSON.parse(localStorage.getItem("Extand_Class"))?.[0];
|
||||
setIsLoader(true);
|
||||
const res = await checkUserExist();
|
||||
if (res) {
|
||||
props.setIsAlert({ type: "danger", msg: "User already exist." });
|
||||
setIsLoader(false);
|
||||
setTimeout(() => {
|
||||
props.setIsAlert({ type: "success", msg: "" });
|
||||
}, 1000);
|
||||
} else {
|
||||
try {
|
||||
const extUser = new Parse.Object("contracts_Users");
|
||||
extUser.set("Name", formdata.name);
|
||||
if (formdata.phone) {
|
||||
extUser.set("Phone", formdata.phone);
|
||||
}
|
||||
extUser.set("Email", formdata.email);
|
||||
extUser.set("UserRole", `contracts_${formdata.role}`);
|
||||
if (formdata?.team) {
|
||||
extUser.set("TeamIds", [
|
||||
{
|
||||
__type: "Pointer",
|
||||
className: "contracts_Teams",
|
||||
objectId: formdata.team
|
||||
}
|
||||
]);
|
||||
}
|
||||
if (localUser && localUser.OrganizationId) {
|
||||
extUser.set("OrganizationId", {
|
||||
__type: "Pointer",
|
||||
className: "contracts_Organizations",
|
||||
objectId: localUser.OrganizationId.objectId
|
||||
});
|
||||
}
|
||||
if (localUser && localUser.Company) {
|
||||
extUser.set("Company", localUser.Company);
|
||||
}
|
||||
|
||||
if (localStorage.getItem("TenantId")) {
|
||||
extUser.set("TenantId", {
|
||||
__type: "Pointer",
|
||||
className: "partners_Tenant",
|
||||
objectId: localStorage.getItem("TenantId")
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const _users = Parse.Object.extend("User");
|
||||
const _user = new _users();
|
||||
_user.set("name", formdata.name);
|
||||
_user.set("username", formdata.email);
|
||||
_user.set("email", formdata.email);
|
||||
_user.set("password", formdata.password);
|
||||
if (formdata.phone) {
|
||||
_user.set("phone", formdata.phone);
|
||||
}
|
||||
|
||||
const user = await _user.save();
|
||||
if (user) {
|
||||
const currentUser = Parse.User.current();
|
||||
extUser.set(
|
||||
"CreatedBy",
|
||||
Parse.User.createWithoutData(currentUser.id)
|
||||
);
|
||||
|
||||
extUser.set("UserId", user);
|
||||
const acl = new Parse.ACL();
|
||||
acl.setPublicReadAccess(true);
|
||||
acl.setPublicWriteAccess(true);
|
||||
acl.setReadAccess(currentUser.id, true);
|
||||
acl.setWriteAccess(currentUser.id, true);
|
||||
|
||||
extUser.setACL(acl);
|
||||
|
||||
const res = await extUser.save();
|
||||
|
||||
const parseData = JSON.parse(JSON.stringify(res));
|
||||
|
||||
if (props.closePopup) {
|
||||
props.closePopup();
|
||||
}
|
||||
if (props.handleUserData) {
|
||||
if (formdata?.team) {
|
||||
const team = teamList.find((x) => x.objectId === formdata.team);
|
||||
parseData.TeamIds = parseData.TeamIds.map((y) =>
|
||||
y.objectId === team.objectId ? team : y
|
||||
);
|
||||
}
|
||||
props.handleUserData(parseData);
|
||||
}
|
||||
|
||||
setIsLoader(false);
|
||||
setFormdata({
|
||||
name: "",
|
||||
email: "",
|
||||
phone: "",
|
||||
team: "",
|
||||
role: ""
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("err ", err);
|
||||
if (err.code === 202) {
|
||||
const params = { email: formdata.email };
|
||||
const userRes = await Parse.Cloud.run("getUserId", params);
|
||||
const currentUser = Parse.User.current();
|
||||
extUser.set(
|
||||
"CreatedBy",
|
||||
Parse.User.createWithoutData(currentUser.id)
|
||||
);
|
||||
|
||||
extUser.set("UserId", {
|
||||
__type: "Pointer",
|
||||
className: "_User",
|
||||
objectId: userRes.id
|
||||
});
|
||||
const acl = new Parse.ACL();
|
||||
acl.setPublicReadAccess(true);
|
||||
acl.setPublicWriteAccess(true);
|
||||
acl.setReadAccess(currentUser.id, true);
|
||||
acl.setWriteAccess(currentUser.id, true);
|
||||
|
||||
extUser.setACL(acl);
|
||||
const res = await extUser.save();
|
||||
|
||||
const parseData = JSON.parse(JSON.stringify(res));
|
||||
if (props.closePopup) {
|
||||
props.closePopup();
|
||||
}
|
||||
if (props.handleUserData) {
|
||||
if (formdata?.team) {
|
||||
const team = teamList.find((x) => x.objectId === formdata.team);
|
||||
parseData.TeamIds = parseData.TeamIds.map((y) =>
|
||||
y.objectId === team.objectId ? team : y
|
||||
);
|
||||
}
|
||||
props.handleUserData(parseData);
|
||||
}
|
||||
setIsLoader(false);
|
||||
setFormdata({
|
||||
name: "",
|
||||
email: "",
|
||||
phone: "",
|
||||
team: "",
|
||||
role: ""
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("err", err);
|
||||
setIsLoader(false);
|
||||
props.setIsAlert({ type: "danger", msg: "something went wrong." });
|
||||
} finally {
|
||||
setTimeout(() => props.setIsAlert({ type: "success", msg: "" }), 1500);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Define a function to handle the "add yourself" checkbox
|
||||
const handleReset = () => {
|
||||
setFormdata({
|
||||
name: "",
|
||||
email: "",
|
||||
phone: "",
|
||||
team: "",
|
||||
role: ""
|
||||
});
|
||||
if (props.closePopup) {
|
||||
props.closePopup();
|
||||
}
|
||||
};
|
||||
const handleChange = (e) => {
|
||||
setFormdata((prev) => ({ ...prev, [e.target.name]: e.target.value }));
|
||||
};
|
||||
|
||||
const copytoclipboard = (text) => {
|
||||
copytoData(text);
|
||||
props.setIsAlert({ type: "success", msg: "Copied" });
|
||||
setTimeout(() => props.setIsAlert({ type: "success", msg: "" }), 1500); // Reset copied state after 1.5 seconds
|
||||
};
|
||||
return (
|
||||
<div className="shadow-md rounded-box my-[1px] p-3 bg-[#ffffff]">
|
||||
<Title title={"Add User"} />
|
||||
{isLoader && (
|
||||
<div className="fixed inset-0 flex justify-center items-center bg-black bg-opacity-30 z-50 rounded-box">
|
||||
<Loader />
|
||||
</div>
|
||||
)}
|
||||
<div className="w-full mx-auto">
|
||||
<form onSubmit={handleSubmit}>
|
||||
{/* <h1 className="text-[20px] font-semibold mb-4">Add User</h1> */}
|
||||
<div className="mb-3">
|
||||
<label
|
||||
htmlFor="name"
|
||||
className="block text-xs text-gray-700 font-semibold"
|
||||
>
|
||||
Name
|
||||
<span className="text-[red] text-[13px]"> *</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
value={formdata.name}
|
||||
onChange={(e) => handleChange(e)}
|
||||
required
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label
|
||||
htmlFor="email"
|
||||
className="block text-xs text-gray-700 font-semibold"
|
||||
>
|
||||
Email
|
||||
<span className="text-[red] text-[13px]"> *</span>
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
value={formdata.email}
|
||||
onChange={(e) => handleChange(e)}
|
||||
required
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label
|
||||
htmlFor="email"
|
||||
className="block text-xs text-gray-700 font-semibold"
|
||||
>
|
||||
Password
|
||||
</label>
|
||||
<div className="flex justify-between items-center op-input op-input-bordered op-input-sm text-base-content w-full h-full text-[13px]">
|
||||
<div className="break-all">{formdata?.password}</div>
|
||||
<i
|
||||
onClick={() => copytoclipboard(formdata?.password)}
|
||||
className="fa-light fa-copy rounded-full hover:bg-base-300 p-[8px] cursor-pointer "
|
||||
></i>
|
||||
</div>
|
||||
<div className="text-[12px] ml-2 mb-0 text-[red] select-none">
|
||||
Password will only be generated once; make sure to copy it.
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label
|
||||
htmlFor="phone"
|
||||
className="block text-xs text-gray-700 font-semibold"
|
||||
>
|
||||
Phone
|
||||
{/* <span className="text-[red] text-[13px]"> *</span> */}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="phone"
|
||||
placeholder="optional"
|
||||
value={formdata.phone}
|
||||
onChange={(e) => handleChange(e)}
|
||||
// required
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
/>
|
||||
</div>
|
||||
{isEnableSubscription && (
|
||||
<div className="mb-3">
|
||||
<label
|
||||
htmlFor="phone"
|
||||
className="block text-xs text-gray-700 font-semibold"
|
||||
>
|
||||
Team<span className="text-[red] text-[13px]"> *</span>
|
||||
</label>
|
||||
<select
|
||||
value={formdata.team}
|
||||
onChange={(e) => handleChange(e)}
|
||||
name="team"
|
||||
className="op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
required
|
||||
>
|
||||
<option defaultValue={""} value={""}>
|
||||
select
|
||||
</option>
|
||||
{teamList.length > 0 &&
|
||||
teamList.map((x) => (
|
||||
<option key={x.objectId} value={x.objectId}>
|
||||
{x.Name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
<div className="mb-3">
|
||||
<label
|
||||
htmlFor="phone"
|
||||
className="block text-xs text-gray-700 font-semibold"
|
||||
>
|
||||
Role<span className="text-[red] text-[13px]"> *</span>
|
||||
</label>
|
||||
<select
|
||||
value={formdata.role}
|
||||
onChange={(e) => handleChange(e)}
|
||||
name="role"
|
||||
className="op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
required
|
||||
>
|
||||
<option defaultValue={""} value={""}>
|
||||
select
|
||||
</option>
|
||||
{role.length > 0 &&
|
||||
role.map((x) => (
|
||||
<option key={x} value={x}>
|
||||
{x}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex items-center mt-3 gap-2 text-white">
|
||||
<button type="submit" className="op-btn op-btn-primary">
|
||||
Submit
|
||||
</button>
|
||||
<div
|
||||
type="button"
|
||||
onClick={() => handleReset()}
|
||||
className="op-btn op-btn-secondary"
|
||||
>
|
||||
Cancel
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddUser;
|
||||
@@ -0,0 +1,287 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import Parse from "parse";
|
||||
import Loader from "../primitives/Loader";
|
||||
import {
|
||||
copytoData,
|
||||
usertimezone
|
||||
} from "../constant/Utils";
|
||||
import {
|
||||
emailRegex,
|
||||
} from "../constant/const";
|
||||
import {
|
||||
useTranslation
|
||||
} from "react-i18next";
|
||||
function generatePassword(length) {
|
||||
const characters =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
let result = "";
|
||||
const charactersLength = characters.length;
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += characters.charAt(Math.floor(Math.random() * charactersLength));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const AddUser = (props) => {
|
||||
const { t } = useTranslation();
|
||||
const [formdata, setFormdata] = useState({
|
||||
name: "",
|
||||
phone: "",
|
||||
email: "",
|
||||
team: "",
|
||||
password: "",
|
||||
role: ""
|
||||
});
|
||||
const [isFormLoader, setIsFormLoader] = useState(false);
|
||||
const [teamList, setTeamList] = useState([]);
|
||||
const role = ["OrgAdmin", "Editor", "User"];
|
||||
useEffect(() => {
|
||||
getTeamList();
|
||||
// eslint-disable-next-line
|
||||
}, []);
|
||||
|
||||
const getTeamList = async () => {
|
||||
setFormdata((prev) => ({ ...prev, password: generatePassword(12) }));
|
||||
const teamRes = await Parse.Cloud.run("getteams", { active: true });
|
||||
if (teamRes.length > 0) {
|
||||
const _teamRes = JSON.parse(JSON.stringify(teamRes));
|
||||
setTeamList(_teamRes);
|
||||
const allUserId =
|
||||
_teamRes.find((x) => x.Name === "All Users")?.objectId || "";
|
||||
setFormdata((prev) => ({ ...prev, team: allUserId }));
|
||||
}
|
||||
};
|
||||
const checkUserExist = async () => {
|
||||
try {
|
||||
const res = await Parse.Cloud.run("getUserDetails", {
|
||||
email: formdata.email
|
||||
});
|
||||
if (res) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("err", err);
|
||||
}
|
||||
};
|
||||
// Define a function to handle form submission
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!emailRegex.test(formdata.email)) {
|
||||
alert(t("valid-email-alert"));
|
||||
} else {
|
||||
const localUser = JSON.parse(localStorage.getItem("Extand_Class"))?.[0];
|
||||
setIsFormLoader(true);
|
||||
const res = await checkUserExist();
|
||||
if (res) {
|
||||
props.showAlert("danger", t("user-already-exist"));
|
||||
setIsFormLoader(false);
|
||||
} else {
|
||||
if (localStorage.getItem("TenantId")) {
|
||||
const timezone = usertimezone;
|
||||
try {
|
||||
const params = {
|
||||
name: formdata.name,
|
||||
email: formdata.email,
|
||||
phone: formdata.phone,
|
||||
password: formdata.password,
|
||||
role: formdata.role,
|
||||
team: formdata.team,
|
||||
timezone: timezone,
|
||||
tenantId: localStorage.getItem("TenantId"),
|
||||
organization: {
|
||||
objectId: localUser?.OrganizationId?.objectId,
|
||||
company: localUser?.Company
|
||||
},
|
||||
};
|
||||
const res = await Parse.Cloud.run("adduser", params);
|
||||
const parseData = JSON.parse(JSON.stringify(res));
|
||||
if (props.closePopup) {
|
||||
props.closePopup();
|
||||
}
|
||||
if (props.handleUserData) {
|
||||
if (formdata?.team) {
|
||||
const team = teamList.find((x) => x.objectId === formdata.team);
|
||||
parseData.TeamIds = parseData.TeamIds.map((y) =>
|
||||
y.objectId === team.objectId ? team : y
|
||||
);
|
||||
}
|
||||
props.handleUserData(parseData);
|
||||
}
|
||||
setIsFormLoader(false);
|
||||
setFormdata({
|
||||
name: "",
|
||||
email: "",
|
||||
phone: "",
|
||||
team: "",
|
||||
role: ""
|
||||
});
|
||||
props.showAlert("success", t("user-created-successfully"));
|
||||
} catch (err) {
|
||||
console.log("err", err);
|
||||
setIsFormLoader(false);
|
||||
props.showAlert("danger", t("something-went-wrong-mssg"));
|
||||
}
|
||||
} else {
|
||||
props.showAlert("danger", t("something-went-wrong-mssg"));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Define a function to handle the "add yourself" checkbox
|
||||
const handleReset = () => {
|
||||
setFormdata({ name: "", email: "", phone: "", team: "", role: "" });
|
||||
if (props.closePopup) {
|
||||
props.closePopup();
|
||||
}
|
||||
};
|
||||
const handleChange = (event) => {
|
||||
let { name, value } = event.target;
|
||||
if (name === "email") {
|
||||
value = value?.toLowerCase()?.replace(/\s/g, "");
|
||||
}
|
||||
setFormdata((prev) => ({ ...prev, [name]: value }));
|
||||
};
|
||||
|
||||
const copytoclipboard = (text) => {
|
||||
copytoData(text);
|
||||
props.showAlert("success", t("copied"));
|
||||
};
|
||||
return (
|
||||
<div className="shadow-md rounded-box my-[1px] p-3 bg-base-100 relative">
|
||||
{isFormLoader && (
|
||||
<div className="absolute w-full h-full inset-0 flex justify-center items-center bg-base-content/30 z-50">
|
||||
<Loader />
|
||||
</div>
|
||||
)}
|
||||
<div className="w-full mx-auto">
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="mb-3">
|
||||
<label
|
||||
htmlFor="name"
|
||||
className="block text-xs font-semibold"
|
||||
>
|
||||
{t("name")}
|
||||
<span className="text-[red] text-[13px]"> *</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
value={formdata.name}
|
||||
onChange={(e) => handleChange(e)}
|
||||
onInvalid={(e) =>
|
||||
e.target.setCustomValidity(t("input-required"))
|
||||
}
|
||||
onInput={(e) => e.target.setCustomValidity("")}
|
||||
required
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
placeholder={t("enter-name")}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label
|
||||
htmlFor="email"
|
||||
className="block text-xs font-semibold"
|
||||
>
|
||||
{t("email")}
|
||||
<span className="text-[red] text-[13px]"> *</span>
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
value={formdata.email}
|
||||
onChange={(e) => handleChange(e)}
|
||||
required
|
||||
onInvalid={(e) =>
|
||||
e.target.setCustomValidity(t("input-required"))
|
||||
}
|
||||
onInput={(e) => e.target.setCustomValidity("")}
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
placeholder={t("enter-email")}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label className="block text-xs font-semibold">
|
||||
{t("password")}
|
||||
</label>
|
||||
<div className="flex justify-between items-center op-input op-input-bordered op-input-sm text-base-content w-full h-full text-[13px]">
|
||||
<div className="break-all">{formdata?.password}</div>
|
||||
<i
|
||||
onClick={() => copytoclipboard(formdata?.password)}
|
||||
className="fa-light fa-copy rounded-full hover:bg-base-300 p-[8px] cursor-pointer "
|
||||
></i>
|
||||
</div>
|
||||
<div className="text-[12px] ml-2 mb-0 text-[red] select-none">
|
||||
{t("password-generateed")}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label
|
||||
htmlFor="phone"
|
||||
className="block text-xs font-semibold"
|
||||
>
|
||||
{t("phone")}
|
||||
{/* <span className="text-[red] text-[13px]"> *</span> */}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="phone"
|
||||
placeholder={t("phone-optional")}
|
||||
value={formdata.phone}
|
||||
onChange={(e) => handleChange(e)}
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label
|
||||
htmlFor="phone"
|
||||
className="block text-xs font-semibold"
|
||||
>
|
||||
{t("Role")}
|
||||
<span className="text-[red] text-[13px]"> *</span>
|
||||
</label>
|
||||
<select
|
||||
value={formdata.role}
|
||||
onChange={(e) => handleChange(e)}
|
||||
name="role"
|
||||
className="op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
onInvalid={(e) =>
|
||||
e.target.setCustomValidity(t("input-required"))
|
||||
}
|
||||
onInput={(e) => e.target.setCustomValidity("")}
|
||||
required
|
||||
>
|
||||
<option defaultValue={""} value={""}>
|
||||
{t("Select")}
|
||||
</option>
|
||||
{role.length > 0 &&
|
||||
role.map((x) => (
|
||||
<option key={x} value={x}>
|
||||
{x}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex items-center mt-3 gap-2 text-white">
|
||||
<button type="submit" className="op-btn op-btn-primary">
|
||||
{t("submit")}
|
||||
</button>
|
||||
<div
|
||||
type="button"
|
||||
onClick={() => handleReset()}
|
||||
className="op-btn op-btn-secondary"
|
||||
>
|
||||
{t("cancel")}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddUser;
|
||||
@@ -1,283 +0,0 @@
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import axios from "axios";
|
||||
import SuggestionInput from "./shared/fields/SuggestionInput";
|
||||
import Loader from "../primitives/Loader";
|
||||
|
||||
const BulkSendUi = (props) => {
|
||||
const [forms, setForms] = useState([]);
|
||||
const [formId, setFormId] = useState(2);
|
||||
const formRef = useRef(null);
|
||||
const [scrollOnNextUpdate, setScrollOnNextUpdate] = useState(false);
|
||||
const [isSubmit, setIsSubmit] = useState(false);
|
||||
const [allowedForm, setAllowedForm] = useState(0);
|
||||
const [isSignatureExist, setIsSignatureExist] = useState();
|
||||
const allowedSigners = 50;
|
||||
useEffect(() => {
|
||||
signatureExist();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
//function to check atleast one signature field exist
|
||||
const signatureExist = () => {
|
||||
const getPlaceholder = props.item?.Placeholders;
|
||||
const checkIsSignatureExistt = getPlaceholder?.every((placeholderObj) =>
|
||||
placeholderObj?.placeHolder?.some((holder) =>
|
||||
holder?.pos?.some((posItem) => posItem?.type === "signature")
|
||||
)
|
||||
);
|
||||
setIsSignatureExist(checkIsSignatureExistt);
|
||||
};
|
||||
useEffect(() => {
|
||||
if (scrollOnNextUpdate && formRef.current) {
|
||||
formRef.current.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "end",
|
||||
inline: "nearest"
|
||||
});
|
||||
setScrollOnNextUpdate(false);
|
||||
}
|
||||
}, [forms, scrollOnNextUpdate]);
|
||||
|
||||
useEffect(() => {
|
||||
(() => {
|
||||
if (props?.Placeholders?.length > 0) {
|
||||
let users = [];
|
||||
props?.Placeholders?.forEach((element) => {
|
||||
if (!element.signerObjId) {
|
||||
users = [
|
||||
...users,
|
||||
{
|
||||
fieldId: element.Id,
|
||||
email: "",
|
||||
label: element.Role,
|
||||
signer: {}
|
||||
}
|
||||
];
|
||||
}
|
||||
});
|
||||
setForms((prevForms) => [...prevForms, { Id: 1, fields: users }]);
|
||||
const totalForms = Math.floor(allowedSigners / users?.length);
|
||||
setAllowedForm(totalForms);
|
||||
}
|
||||
})();
|
||||
// eslint-disable-next-line
|
||||
}, []);
|
||||
const handleInputChange = (index, signer, fieldIndex) => {
|
||||
const newForms = [...forms];
|
||||
newForms[index].fields[fieldIndex].email = signer?.Email
|
||||
? signer?.Email
|
||||
: signer || "";
|
||||
newForms[index].fields[fieldIndex].signer = signer?.objectId ? signer : "";
|
||||
setForms(newForms);
|
||||
};
|
||||
|
||||
const handleAddForm = (e) => {
|
||||
e.preventDefault();
|
||||
// Check if the quick send limit has been reached
|
||||
if (forms?.length < allowedForm) {
|
||||
if (props?.Placeholders.length > 0) {
|
||||
let newForm = [];
|
||||
props?.Placeholders?.forEach((element) => {
|
||||
if (!element.signerObjId) {
|
||||
newForm = [
|
||||
...newForm,
|
||||
{
|
||||
fieldId: element.Id,
|
||||
email: "",
|
||||
label: element.Role,
|
||||
signer: {}
|
||||
}
|
||||
];
|
||||
}
|
||||
});
|
||||
setForms([...forms, { Id: formId, fields: newForm }]);
|
||||
}
|
||||
setFormId(formId + 1);
|
||||
setScrollOnNextUpdate(true);
|
||||
} else {
|
||||
// If the limit has been reached, throw an error with the appropriate message
|
||||
alert("Quick send reached limit.");
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveForm = (index) => {
|
||||
const updatedForms = forms.filter((_, i) => i !== index);
|
||||
setForms(updatedForms);
|
||||
};
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsSubmit(true);
|
||||
|
||||
// Create a copy of Placeholders array from props.item
|
||||
let Placeholders = [...props.item.Placeholders];
|
||||
// Initialize an empty array to store updated documents
|
||||
let Documents = [];
|
||||
// Loop through each form
|
||||
forms.forEach((form) => {
|
||||
//checking if user enter email which already exist as a signer then add user in a signers array
|
||||
let existSigner = [];
|
||||
form.fields.map((data) => {
|
||||
if (data.signer) {
|
||||
existSigner.push(data.signer);
|
||||
}
|
||||
});
|
||||
// Map through the copied Placeholders array to update email values
|
||||
const updatedPlaceholders = Placeholders.map((placeholder) => {
|
||||
// Find the field in the current form that matches the placeholder Id
|
||||
const field = form.fields.find(
|
||||
(element) => parseInt(element.fieldId) === placeholder.Id
|
||||
);
|
||||
// If a matching field is found, update the email value in the placeholder
|
||||
const signer = field?.signer?.objectId ? field.signer : "";
|
||||
if (field) {
|
||||
if (signer) {
|
||||
return {
|
||||
...placeholder,
|
||||
signerObjId: field?.signer?.objectId || "",
|
||||
signerPtr: signer
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
...placeholder,
|
||||
email: field.email,
|
||||
signerObjId: field?.signer?.objectId || "",
|
||||
signerPtr: signer
|
||||
};
|
||||
}
|
||||
}
|
||||
// If no matching field is found, keep the placeholder as is
|
||||
return placeholder;
|
||||
});
|
||||
|
||||
// Push a new document object with updated Placeholders into the Documents array
|
||||
if (existSigner?.length > 0) {
|
||||
Documents.push({
|
||||
...props.item,
|
||||
Placeholders: updatedPlaceholders,
|
||||
Signers: props.item.Signers
|
||||
? [...props.item.Signers, ...existSigner]
|
||||
: [...existSigner]
|
||||
});
|
||||
} else {
|
||||
Documents.push({
|
||||
...props.item,
|
||||
Placeholders: updatedPlaceholders
|
||||
});
|
||||
}
|
||||
});
|
||||
//console.log("Documents ", Documents);
|
||||
await batchQuery(Documents);
|
||||
};
|
||||
|
||||
const batchQuery = async (Documents) => {
|
||||
const serverUrl = localStorage.getItem("baseUrl");
|
||||
const functionsUrl = `${serverUrl}functions/batchdocuments`;
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
|
||||
sessionToken: localStorage.getItem("accesstoken")
|
||||
};
|
||||
const params = {
|
||||
Documents: JSON.stringify(Documents)
|
||||
};
|
||||
try {
|
||||
const res = await axios.post(functionsUrl, params, { headers: headers });
|
||||
// console.log("res ", res);
|
||||
if (res.data && res.data.result) {
|
||||
props.handleClose("success", Documents?.length);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("Err ", err);
|
||||
props.handleClose("error", 0);
|
||||
} finally {
|
||||
setIsSubmit(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{isSubmit && (
|
||||
<div className="absolute z-[999] h-full w-full flex justify-center items-center bg-black bg-opacity-30">
|
||||
<Loader />
|
||||
</div>
|
||||
)}
|
||||
{props.Placeholders?.length > 0 ? (
|
||||
isSignatureExist ? (
|
||||
<>
|
||||
{props.Placeholders?.some((x) => !x.signerObjId) ? (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="min-h-max max-h-[250px] overflow-y-auto">
|
||||
{forms?.map((form, index) => (
|
||||
<div
|
||||
key={form.Id}
|
||||
className="p-3 op-card border-[1px] border-gray-400 mt-3 mx-4 mb-4 bg-base-200 text-base-content grid grid-cols-1 md:grid-cols-2 gap-2 relative"
|
||||
>
|
||||
{form?.fields?.map((field, fieldIndex) => (
|
||||
<div className="flex flex-col" key={field.fieldId}>
|
||||
<label>{field.label}</label>
|
||||
<SuggestionInput
|
||||
required
|
||||
type="email"
|
||||
value={field.value}
|
||||
index={fieldIndex}
|
||||
onChange={(signer) =>
|
||||
handleInputChange(index, signer, fieldIndex)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{forms?.length > 1 && (
|
||||
<button
|
||||
onClick={() => handleRemoveForm(index)}
|
||||
className="absolute right-3 top-1 text-[red] border-[1px] border-[red] rounded-lg w-[1.7rem] h-[1.7rem]"
|
||||
>
|
||||
<i className="fa-light fa-trash"></i>
|
||||
</button>
|
||||
)}
|
||||
<div ref={formRef}></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-col mx-4 mb-4 gap-3">
|
||||
<button
|
||||
onClick={handleAddForm}
|
||||
className="op-btn op-btn-primary focus:outline-none"
|
||||
>
|
||||
<i className="fa-light fa-plus"></i> <span>Add new</span>
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="op-btn op-btn-secondary focus:outline-none"
|
||||
>
|
||||
<i className="fa-light fa-paper-plane"></i>{" "}
|
||||
<span>Send</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
<div className="text-black p-3 bg-white w-full text-sm md:text-base flex justify-center items-center">
|
||||
All roles in this document are currently linked to contacts. To
|
||||
'quick send' copies of this template to multiple
|
||||
signers, please ensure that at least one role is not linked to
|
||||
any contact.
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="text-black p-3 bg-white w-full text-sm md:text-base flex justify-center items-center">
|
||||
Please ensure there's at least one signature widget added for
|
||||
all recipients.
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className="text-black p-3 bg-white w-full text-sm md:text-base flex justify-center items-center">
|
||||
Please add at least one role to this template in order to 'quick
|
||||
send' copies of it to multiple signers.
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default BulkSendUi;
|
||||
@@ -0,0 +1,316 @@
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import axios from "axios";
|
||||
import SuggestionInput from "./shared/fields/SuggestionInput";
|
||||
import Loader from "../primitives/Loader";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
emailRegex,
|
||||
} from "../constant/const";
|
||||
const BulkSendUi = (props) => {
|
||||
const { t } = useTranslation();
|
||||
const [forms, setForms] = useState([]);
|
||||
const formRef = useRef(null);
|
||||
const [scrollOnNextUpdate, setScrollOnNextUpdate] = useState(false);
|
||||
const [isSubmit, setIsSubmit] = useState(false);
|
||||
const [isSignatureExist, setIsSignatureExist] = useState();
|
||||
const [isDisableBulkSend, setIsDisableBulkSend] = useState(false);
|
||||
const [isLoader, setIsLoader] = useState(false);
|
||||
const [signers, setSigners] = useState([]);
|
||||
const [emails, setEmails] = useState([]);
|
||||
const [isPrefillExist, setIsPrefillExist] = useState(false);
|
||||
useEffect(() => {
|
||||
signatureExist();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
//function to check at least one signature field exist
|
||||
const signatureExist = async () => {
|
||||
const isPrefill = props?.Placeholders.some((x) => x?.Role === "prefill");
|
||||
if (isPrefill) {
|
||||
setIsPrefillExist(isPrefill);
|
||||
}
|
||||
setIsDisableBulkSend(false);
|
||||
const getPlaceholder = props?.Placeholders;
|
||||
const removePrefill = getPlaceholder.filter((x) => x?.Role !== "prefill");
|
||||
const checkIsSignatureExistt = removePrefill?.every((placeholderObj) =>
|
||||
placeholderObj?.placeHolder?.some((holder) =>
|
||||
holder?.pos?.some((posItem) => posItem?.type === "signature")
|
||||
)
|
||||
);
|
||||
setIsSignatureExist(checkIsSignatureExistt);
|
||||
setIsLoader(false);
|
||||
};
|
||||
useEffect(() => {
|
||||
if (scrollOnNextUpdate && formRef.current) {
|
||||
formRef.current.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "end",
|
||||
inline: "nearest"
|
||||
});
|
||||
setScrollOnNextUpdate(false);
|
||||
}
|
||||
}, [forms, scrollOnNextUpdate]);
|
||||
|
||||
useEffect(() => {
|
||||
(() => {
|
||||
if (props?.Placeholders?.length > 0) {
|
||||
let users = [];
|
||||
let emails = [];
|
||||
props?.Placeholders?.forEach((element) => {
|
||||
const signerEmail = element?.email || element?.signerPtr?.Email;
|
||||
|
||||
// only add when there's a non-empty signerEmail
|
||||
if (signerEmail) {
|
||||
emails = [...emails, signerEmail];
|
||||
}
|
||||
if (!element.signerObjId) {
|
||||
users = [
|
||||
...users,
|
||||
{
|
||||
fieldId: element.Id,
|
||||
email: "",
|
||||
label: element.Role,
|
||||
signer: {}
|
||||
}
|
||||
];
|
||||
}
|
||||
});
|
||||
setEmails(emails);
|
||||
setForms((prevForms) => [...prevForms, { Id: 1, fields: users }]);
|
||||
const signer = props.item?.Signers?.filter((x) => x?.objectId);
|
||||
setSigners(signer);
|
||||
}
|
||||
})();
|
||||
// eslint-disable-next-line
|
||||
}, []);
|
||||
const handleInputChange = (index, signer, fieldIndex) => {
|
||||
const newForms = [...forms];
|
||||
newForms[index].fields[fieldIndex].email = signer?.Email
|
||||
? signer?.Email
|
||||
: signer || "";
|
||||
newForms[index].fields[fieldIndex].signer = signer?.objectId ? signer : "";
|
||||
setForms(newForms);
|
||||
};
|
||||
|
||||
|
||||
const handleRemoveForm = (index) => {
|
||||
const updatedForms = forms.filter((_, i) => i !== index);
|
||||
setForms(updatedForms);
|
||||
};
|
||||
|
||||
function validateEmails(data) {
|
||||
for (const item of data) {
|
||||
let email = "";
|
||||
for (const field of item.fields) {
|
||||
if (!emailRegex.test(field.email)) {
|
||||
alert(t("invalid-email-found", { email: field.email }));
|
||||
return false;
|
||||
} else if (email === field.email || emails?.includes(field.email)) {
|
||||
alert(t("duplicate-email-found", { email: field.email }));
|
||||
return false;
|
||||
} else {
|
||||
email = field.email;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsSubmit(true);
|
||||
if (validateEmails(forms)) {
|
||||
// Create a copy of Placeholders array from props.item
|
||||
let Placeholders = [...props.Placeholders];
|
||||
// Initialize an empty array to store updated documents
|
||||
let Documents = [];
|
||||
// Loop through each form
|
||||
forms.forEach((form) => {
|
||||
//checking if user enter email which already exist as a signer then add user in a signers array
|
||||
let existSigner = [];
|
||||
form.fields.map((data) => {
|
||||
if (data.signer) {
|
||||
existSigner.push(data.signer);
|
||||
}
|
||||
});
|
||||
// Map through the copied Placeholders array to update email values
|
||||
const updatedPlaceholders = Placeholders.map((placeholder) => {
|
||||
// Find the field in the current form that matches the placeholder Id
|
||||
const field = form.fields.find(
|
||||
(element) => parseInt(element.fieldId) === placeholder.Id
|
||||
);
|
||||
// If a matching field is found, update the email value in the placeholder
|
||||
const signer = field?.signer?.objectId ? field.signer : "";
|
||||
if (field) {
|
||||
if (signer) {
|
||||
return {
|
||||
...placeholder,
|
||||
signerObjId: field?.signer?.objectId || "",
|
||||
signerPtr: signer
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
...placeholder,
|
||||
email: field.email,
|
||||
signerObjId: field?.signer?.objectId || "",
|
||||
signerPtr: signer
|
||||
};
|
||||
}
|
||||
}
|
||||
// If no matching field is found, keep the placeholder as is
|
||||
return placeholder;
|
||||
});
|
||||
|
||||
// Push a new document object with updated Placeholders into the Documents array
|
||||
if (existSigner?.length > 0) {
|
||||
Documents.push({
|
||||
...props.item,
|
||||
Placeholders: updatedPlaceholders,
|
||||
Signers: signers ? [...signers, ...existSigner] : [...existSigner]
|
||||
});
|
||||
} else {
|
||||
Documents.push({
|
||||
...props.item,
|
||||
Placeholders: updatedPlaceholders,
|
||||
SignatureType: props.signatureType,
|
||||
Signers: signers
|
||||
});
|
||||
}
|
||||
});
|
||||
await batchQuery(Documents);
|
||||
} else {
|
||||
setIsSubmit(false);
|
||||
}
|
||||
};
|
||||
|
||||
const batchQuery = async (Documents) => {
|
||||
const token =
|
||||
{ "X-Parse-Session-Token": localStorage.getItem("accesstoken") };
|
||||
const functionsUrl = `${localStorage.getItem(
|
||||
"baseUrl"
|
||||
)}functions/batchdocuments`;
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
|
||||
...token,
|
||||
};
|
||||
const params = { Documents: JSON.stringify(Documents) };
|
||||
try {
|
||||
const res = await axios.post(functionsUrl, params, { headers: headers });
|
||||
// console.log("res ", res);
|
||||
if (res.data && res.data.result) {
|
||||
props.handleClose("success", Documents?.length);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("Err ", err);
|
||||
props.handleClose("error", 0);
|
||||
} finally {
|
||||
setIsSubmit(false);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<>
|
||||
{isLoader ? (
|
||||
<div className="w-full h-[100px] flex justify-center items-center z-[999]">
|
||||
<Loader />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{isSubmit && (
|
||||
<div className="absolute z-[999] h-full w-full flex justify-center items-center bg-black bg-opacity-30">
|
||||
<Loader />
|
||||
</div>
|
||||
)}
|
||||
{isPrefillExist ? (
|
||||
<div className="text-black p-3 bg-white w-full text-sm md:text-base flex justify-center items-center">
|
||||
{t("prefill-bulk-error")}
|
||||
</div>
|
||||
) : !isDisableBulkSend ? (
|
||||
<>
|
||||
{props.Placeholders?.length > 0 ? (
|
||||
isSignatureExist ? (
|
||||
<>
|
||||
{props.Placeholders?.some((x) => !x.signerObjId) ? (
|
||||
<>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="min-h-max max-h-[250px] overflow-y-auto">
|
||||
{forms?.map((form, index) => (
|
||||
<div
|
||||
key={form.Id}
|
||||
className="p-3 op-card border-[1px] border-gray-400 mt-3 mx-4 mb-4 bg-base-200 text-base-content grid grid-cols-1 md:grid-cols-2 gap-2 relative"
|
||||
>
|
||||
{form?.fields?.map((field, fieldIndex) => (
|
||||
<div
|
||||
className="flex flex-col"
|
||||
key={field.fieldId}
|
||||
>
|
||||
<label className="block text-xs font-semibold">
|
||||
{field.label}
|
||||
</label>
|
||||
<SuggestionInput
|
||||
required
|
||||
type="email"
|
||||
value={field.value}
|
||||
index={fieldIndex}
|
||||
onChange={(signer) =>
|
||||
handleInputChange(
|
||||
index,
|
||||
signer,
|
||||
fieldIndex
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{forms?.length > 1 && (
|
||||
<button
|
||||
onClick={() => handleRemoveForm(index)}
|
||||
className="absolute right-3 top-1 text-[red] border-[1px] border-[red] rounded-lg w-[1.7rem] h-[1.7rem]"
|
||||
>
|
||||
<i className="fa-light fa-trash"></i>
|
||||
</button>
|
||||
)}
|
||||
<div ref={formRef}></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-col mx-4 mb-4 gap-3">
|
||||
<button
|
||||
type="submit"
|
||||
className="op-btn op-btn-accent focus:outline-none"
|
||||
>
|
||||
<i className="fa-light fa-paper-plane"></i>{" "}
|
||||
<span>{t("send")}</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
) : (
|
||||
<div className="text-black p-3 bg-white w-full text-sm md:text-base flex justify-center items-center">
|
||||
{t("quick-send-alert-1")}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="text-black p-3 bg-white w-full text-sm md:text-base flex justify-center items-center">
|
||||
{t("quick-send-alert-2")}
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className="text-black p-3 bg-white w-full text-sm md:text-base flex justify-center items-center">
|
||||
{t("quick-send-alert-3")}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default BulkSendUi;
|
||||
@@ -0,0 +1,63 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import ModalUi from "../primitives/ModalUi";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const ColumnSelector = ({
|
||||
isOpen,
|
||||
allColumns = [],
|
||||
visibleColumns = [],
|
||||
columnLabels = {},
|
||||
defaultColumns = [],
|
||||
onApply,
|
||||
onClose
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [selected, setSelected] = useState(visibleColumns);
|
||||
const [names, setNames] = useState(columnLabels);
|
||||
|
||||
useEffect(() => {
|
||||
setSelected(visibleColumns);
|
||||
setNames(columnLabels);
|
||||
}, [visibleColumns, columnLabels]);
|
||||
|
||||
const handleChange = (col) => {
|
||||
setSelected((prev) =>
|
||||
prev.includes(col) ? prev.filter((c) => c !== col) : [...prev, col]
|
||||
);
|
||||
};
|
||||
|
||||
const handleApply = () => {
|
||||
onApply && onApply(selected, names);
|
||||
onClose && onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalUi isOpen={isOpen} title={t("select-columns")} handleClose={onClose}>
|
||||
<div className="p-[20px] flex flex-col gap-2">
|
||||
{allColumns.map((col, i) => (
|
||||
<div key={col} className="flex justify-between items-center gap-2">
|
||||
<span className="flex justify-center items-center h-full">
|
||||
<input
|
||||
id={col + "_" + i}
|
||||
type="checkbox"
|
||||
checked={selected.includes(col)}
|
||||
onChange={() => handleChange(col)}
|
||||
className="mb-0 cursor-pointer"
|
||||
/>
|
||||
<span className="whitespace-nowrap ml-1">
|
||||
{t(`report-heading.${col}`, { defaultValue: col })}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
<div className="flex justify-start mt-2">
|
||||
<button onClick={handleApply} className="op-btn op-btn-primary">
|
||||
{t("apply")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ModalUi>
|
||||
);
|
||||
};
|
||||
|
||||
export default ColumnSelector;
|
||||
@@ -0,0 +1,27 @@
|
||||
import { HTML5Backend } from "react-dnd-html5-backend";
|
||||
import {
|
||||
DndProvider,
|
||||
MouseTransition
|
||||
} from "react-dnd-multi-backend";
|
||||
import LazyPage from "../primitives/LazyPage";
|
||||
import { GuidelinesProvider } from "../context/GuidelinesContext";
|
||||
|
||||
const HTML5toTouch = {
|
||||
backends: [
|
||||
{
|
||||
id: "html5",
|
||||
backend: HTML5Backend,
|
||||
transition: MouseTransition
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
export default function DragProvider({ Page, lazy = false }) {
|
||||
return (
|
||||
<DndProvider options={HTML5toTouch}>
|
||||
<GuidelinesProvider>
|
||||
{lazy ? <LazyPage Page={Page} /> : <Page />}
|
||||
</GuidelinesProvider>
|
||||
</DndProvider>
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,10 @@ import React, { useEffect, useState } from "react";
|
||||
import Package from "../../package.json";
|
||||
import axios from "axios";
|
||||
import { openInNewTab } from "../constant/Utils";
|
||||
import { useTranslation } from "react-i18next";
|
||||
const Footer = () => {
|
||||
const appName = "OpenSign™";
|
||||
const { t } = useTranslation();
|
||||
const [showButton, setShowButton] = useState(false);
|
||||
const [version, setVersion] = useState("");
|
||||
useEffect(() => {
|
||||
@@ -37,7 +40,6 @@ const Footer = () => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
const appName = "OpenSign™";
|
||||
const openUrl = () => {
|
||||
openInNewTab(
|
||||
"https://github.com/OpenSignLabs/OpenSign/releases/tag/" + version
|
||||
@@ -48,9 +50,10 @@ const Footer = () => {
|
||||
<footer className="op-footer op-footer-center py-3 bg-base-300 text-base-content text-center text-[13px]">
|
||||
<aside>
|
||||
<p>
|
||||
All Rights Reserved © {new Date().getFullYear()}
|
||||
{t("all-right")} © {new Date().getFullYear()}
|
||||
<span onClick={openUrl} className="hover:underline cursor-pointer">
|
||||
{appName} ( version: {version ? version : `${Package.version} `})
|
||||
{appName} ( {t("version")}:{" "}
|
||||
{version ? version : `${Package.version} `})
|
||||
</span>
|
||||
</p>
|
||||
</aside>
|
||||
@@ -1,216 +0,0 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import dp from "../assets/images/dp.png";
|
||||
import FullScreenButton from "./FullScreenButton";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import Parse from "parse";
|
||||
import { useWindowSize } from "../hook/useWindowSize";
|
||||
import { checkIsSubscribed, getAppLogo, openInNewTab } from "../constant/Utils";
|
||||
import { isEnableSubscription, isStaging } from "../constant/const";
|
||||
|
||||
const Header = ({ showSidebar }) => {
|
||||
const navigate = useNavigate();
|
||||
const { width } = useWindowSize();
|
||||
let username = localStorage.getItem("username");
|
||||
const image = localStorage.getItem("profileImg") || dp;
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isSubscribe, setIsSubscribe] = useState(true);
|
||||
const [isPro, setIsPro] = useState(false);
|
||||
const [applogo, setAppLogo] = useState(
|
||||
localStorage.getItem("appLogo") || " "
|
||||
);
|
||||
|
||||
const toggleDropdown = () => {
|
||||
setIsOpen(!isOpen);
|
||||
};
|
||||
useEffect(() => {
|
||||
checkSubscription();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
async function checkSubscription() {
|
||||
if (isEnableSubscription) {
|
||||
const getIsSubscribe = await checkIsSubscribed();
|
||||
if (getIsSubscribe) {
|
||||
const applogo = await getAppLogo();
|
||||
if (applogo?.logo) {
|
||||
setAppLogo(applogo?.logo);
|
||||
} else {
|
||||
setAppLogo(localStorage.getItem("appLogo") || "");
|
||||
}
|
||||
}
|
||||
setIsPro(getIsSubscribe);
|
||||
setIsSubscribe(getIsSubscribe);
|
||||
}
|
||||
}
|
||||
|
||||
const closeDropdown = () => {
|
||||
setIsOpen(false);
|
||||
if (Parse?.User?.current()) {
|
||||
try {
|
||||
Parse.User.logOut();
|
||||
} catch (err) {
|
||||
console.log("Err", err);
|
||||
}
|
||||
}
|
||||
let appdata = localStorage.getItem("userSettings");
|
||||
let applogo = localStorage.getItem("appLogo");
|
||||
let defaultmenuid = localStorage.getItem("defaultmenuid");
|
||||
let PageLanding = localStorage.getItem("PageLanding");
|
||||
let baseUrl = localStorage.getItem("baseUrl");
|
||||
let appid = localStorage.getItem("parseAppId");
|
||||
|
||||
localStorage.clear();
|
||||
|
||||
localStorage.setItem("appLogo", applogo);
|
||||
localStorage.setItem("defaultmenuid", defaultmenuid);
|
||||
localStorage.setItem("PageLanding", PageLanding);
|
||||
localStorage.setItem("userSettings", appdata);
|
||||
localStorage.setItem("baseUrl", baseUrl);
|
||||
localStorage.setItem("parseAppId", appid);
|
||||
|
||||
navigate("/");
|
||||
};
|
||||
|
||||
//handle to close profile drop down menu onclick screen
|
||||
useEffect(() => {
|
||||
const closeMenuOnOutsideClick = (e) => {
|
||||
if (isOpen && !e.target.closest("#profile-menu")) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("click", closeMenuOnOutsideClick);
|
||||
|
||||
return () => {
|
||||
// Cleanup the event listener when the component unmounts
|
||||
document.removeEventListener("click", closeMenuOnOutsideClick);
|
||||
};
|
||||
}, [isOpen]);
|
||||
const handleConsoleRedirect = () => {
|
||||
if (isStaging) {
|
||||
window.open(" https://staging-console.opensignlabs.com/");
|
||||
} else {
|
||||
window.open("https://console.opensignlabs.com/");
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className="op-navbar bg-base-100 shadow">
|
||||
<div className="flex-none">
|
||||
<button
|
||||
className="op-btn op-btn-square op-btn-ghost focus:outline-none hover:bg-transparent op-btn-sm no-animation"
|
||||
onClick={showSidebar}
|
||||
>
|
||||
<i className="fa-light fa-bars text-xl text-base-content"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex-1 ml-2">
|
||||
<div className="h-[25px] md:h-[40px] w-auto overflow-hidden">
|
||||
<img
|
||||
className="object-contain h-full w-auto"
|
||||
src={applogo}
|
||||
alt="img"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div id="profile-menu" className="flex-none gap-2">
|
||||
{!isSubscribe && (
|
||||
<div>
|
||||
<button
|
||||
className="text-xs md:text-sm shadow op-btn op-btn-outline op-btn-sm md:op-btn-md op-btn-accent"
|
||||
onClick={() => navigate("/subscription")}
|
||||
>
|
||||
Upgrade Now
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{isPro && (
|
||||
<div className="w-[35px] h-[35px] bg-white rounded-full ring-[1px] ring-offset-2 ring-[#002862] text-[#002862] overflow-hidden font-semibold flex items-center justify-center">
|
||||
PRO
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<FullScreenButton />
|
||||
</div>
|
||||
{width >= 768 && (
|
||||
<div
|
||||
onClick={toggleDropdown}
|
||||
className="cursor-pointer w-[35px] h-[35px] rounded-full ring-[1px] ring-offset-2 ring-gray-400 overflow-hidden"
|
||||
>
|
||||
<img
|
||||
className="w-[35px] h-[35px] object-contain"
|
||||
src={image}
|
||||
alt="img"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{width >= 768 && (
|
||||
<div
|
||||
onClick={toggleDropdown}
|
||||
className="cursor-pointer text-base-content text-sm"
|
||||
>
|
||||
{username && username}
|
||||
</div>
|
||||
)}
|
||||
<div className="op-dropdown op-dropdown-end" id="profile-menu">
|
||||
<div
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
className="op-btn op-btn-ghost op-btn-xs w-[10px] h-[20px] hover:bg-transparent"
|
||||
>
|
||||
<i
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
onClick={toggleDropdown}
|
||||
className="fa-light fa-angle-down text-base-content"
|
||||
></i>
|
||||
</div>
|
||||
<ul
|
||||
tabIndex={0}
|
||||
className="mt-3 z-[1] p-2 shadow op-menu op-menu-sm op-dropdown-content text-base-content bg-base-100 rounded-box w-52"
|
||||
>
|
||||
<li onClick={() => openInNewTab("https://docs.opensignlabs.com")}>
|
||||
<span>
|
||||
<i className="fa-light fa-book"></i> Docs
|
||||
</span>
|
||||
</li>
|
||||
<li
|
||||
onClick={() => {
|
||||
setIsOpen(false);
|
||||
navigate("/profile");
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
<i className="fa-light fa-user"></i> Profile
|
||||
</span>
|
||||
</li>
|
||||
<li
|
||||
onClick={() => {
|
||||
setIsOpen(false);
|
||||
navigate("/changepassword");
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
<i className="fa-light fa-lock"></i> Change Password
|
||||
</span>
|
||||
</li>
|
||||
<li
|
||||
onClick={() => {
|
||||
handleConsoleRedirect();
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
<i className="fa-light fa-id-card"></i> Console
|
||||
</span>
|
||||
</li>
|
||||
<li onClick={closeDropdown}>
|
||||
<span>
|
||||
<i className="fa-light fa-arrow-right-from-bracket"></i> Log Out
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
@@ -0,0 +1,274 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import dp from "../assets/images/dp.png";
|
||||
import FullScreenButton from "./FullScreenButton";
|
||||
import ThemeToggle from "./ThemeToggle";
|
||||
import { useNavigate } from "react-router";
|
||||
import Parse from "parse";
|
||||
import { useWindowSize } from "../hook/useWindowSize";
|
||||
import {
|
||||
getAppLogo,
|
||||
openInNewTab,
|
||||
saveLanguageInLocal
|
||||
} from "../constant/Utils";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { appInfo } from "../constant/appinfo";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { toggleSidebar } from "../redux/reducers/sidebarReducer";
|
||||
import { sessionStatus } from "../redux/reducers/userReducer";
|
||||
|
||||
const Header = ({ isConsole, setIsLoggingOut }) => {
|
||||
const { t, i18n } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const { width } = useWindowSize();
|
||||
const dispatch = useDispatch();
|
||||
const username = localStorage.getItem("username") || "";
|
||||
const image = localStorage.getItem("profileImg") || dp;
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [applogo, setAppLogo] = useState("");
|
||||
const [isDarkTheme, setIsDarkTheme] = useState();
|
||||
|
||||
const toggleDropdown = () => {
|
||||
setIsOpen(!isOpen);
|
||||
closeSidebar();
|
||||
};
|
||||
const closeSidebar = () => {
|
||||
if (width && width <= 768) {
|
||||
dispatch(toggleSidebar(false));
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
initializeHead();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
closeSidebar();
|
||||
}, [width]);
|
||||
|
||||
const showSidebar = () => {
|
||||
dispatch(toggleSidebar());
|
||||
};
|
||||
|
||||
|
||||
async function initializeHead() {
|
||||
const applogo = await getAppLogo();
|
||||
if (applogo?.logo) {
|
||||
setAppLogo(applogo?.logo);
|
||||
} else {
|
||||
const logo = localStorage.getItem("appLogo") || appInfo.applogo;
|
||||
setAppLogo(logo);
|
||||
}
|
||||
}
|
||||
|
||||
const handleLogout = async () => {
|
||||
setIsOpen(false);
|
||||
setIsLoggingOut(true);
|
||||
try {
|
||||
await Parse.User.logOut();
|
||||
} catch (err) {
|
||||
console.log("Err while logging out", err);
|
||||
} finally {
|
||||
dispatch(sessionStatus(true));
|
||||
}
|
||||
let appdata = localStorage.getItem("userSettings");
|
||||
let applogo = localStorage.getItem("appLogo");
|
||||
let defaultmenuid = localStorage.getItem("defaultmenuid");
|
||||
let PageLanding = localStorage.getItem("PageLanding");
|
||||
let baseUrl = localStorage.getItem("baseUrl");
|
||||
let appid = localStorage.getItem("parseAppId");
|
||||
let favicon = localStorage.getItem("favicon");
|
||||
|
||||
localStorage.clear();
|
||||
saveLanguageInLocal(i18n);
|
||||
localStorage.setItem("appLogo", applogo);
|
||||
localStorage.setItem("defaultmenuid", defaultmenuid);
|
||||
localStorage.setItem("PageLanding", PageLanding);
|
||||
localStorage.setItem("userSettings", appdata);
|
||||
localStorage.setItem("baseUrl", baseUrl);
|
||||
localStorage.setItem("parseAppId", appid);
|
||||
localStorage.setItem("favicon", favicon);
|
||||
setIsLoggingOut(false);
|
||||
navigate("/");
|
||||
};
|
||||
|
||||
//handle to close profile drop down menu onclick screen
|
||||
useEffect(() => {
|
||||
const closeMenuOnOutsideClick = (e) => {
|
||||
if (isOpen && !e.target.closest("#profile-menu")) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("click", closeMenuOnOutsideClick);
|
||||
|
||||
return () => {
|
||||
// Cleanup the event listener when the component unmounts
|
||||
document.removeEventListener("click", closeMenuOnOutsideClick);
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const updateThemeStatus = () => {
|
||||
const isDarkTheme =
|
||||
document.documentElement.getAttribute("data-theme") === "opensigndark";
|
||||
setIsDarkTheme(isDarkTheme);
|
||||
};
|
||||
updateThemeStatus();
|
||||
|
||||
const observer = new MutationObserver(() => {
|
||||
updateThemeStatus();
|
||||
});
|
||||
|
||||
observer.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ["data-theme"]
|
||||
});
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="op-navbar bg-base-100 shadow touch-none">
|
||||
<div className="flex-none">
|
||||
<button
|
||||
className="op-btn op-btn-square op-btn-ghost focus:outline-none hover:bg-transparent op-btn-sm no-animation"
|
||||
onClick={showSidebar}
|
||||
>
|
||||
<i className="fa-light fa-bars text-xl text-base-content"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex-1 ml-2">
|
||||
<div
|
||||
onClick={() => navigate("/dashboard/35KBoSgoAK")}
|
||||
className="h-[25px] md:h-[40px] w-auto overflow-hidden cursor-pointer"
|
||||
>
|
||||
{applogo && (
|
||||
<img
|
||||
className="object-contain h-full w-auto"
|
||||
src={
|
||||
isDarkTheme
|
||||
? "/static/js/assets/images/logo-dark.png"
|
||||
: applogo
|
||||
}
|
||||
alt="logo"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div id="profile-menu" className="flex-none gap-2">
|
||||
<div>
|
||||
<FullScreenButton />
|
||||
</div>
|
||||
{width >= 768 && (
|
||||
<div
|
||||
onClick={toggleDropdown}
|
||||
className="cursor-pointer w-[35px] h-[35px] rounded-full ring-[1px] ring-offset-2 ring-gray-400 overflow-hidden"
|
||||
>
|
||||
<img
|
||||
className="w-[35px] h-[35px] object-contain"
|
||||
src={image}
|
||||
alt="img"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{width >= 768 && (
|
||||
<div
|
||||
onClick={toggleDropdown}
|
||||
role="button"
|
||||
tabIndex="0"
|
||||
className="cursor-pointer text-base-content text-sm"
|
||||
>
|
||||
{username && username}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className="op-dropdown op-dropdown-open op-dropdown-end"
|
||||
id="profile-menu"
|
||||
>
|
||||
<div
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
onClick={toggleDropdown}
|
||||
className="op-btn op-btn-ghost op-btn-xs w-[10px] h-[20px] hover:bg-transparent"
|
||||
>
|
||||
<i className="fa-light fa-angle-down text-base-content"></i>
|
||||
</div>
|
||||
<ul
|
||||
tabIndex={0}
|
||||
className={`mt-3 z-[1] p-2 shadow op-dropdown-open op-menu op-menu-sm op-dropdown-content text-base-content bg-base-100 rounded-box w-56 ${
|
||||
isOpen ? "" : "hidden"
|
||||
}`}
|
||||
>
|
||||
{!isConsole && (
|
||||
<>
|
||||
<li
|
||||
onClick={() =>
|
||||
openInNewTab("https://docs.opensignlabs.com")
|
||||
}
|
||||
>
|
||||
<span>
|
||||
<i className="fa-light fa-book"></i> {t("docs")}
|
||||
</span>
|
||||
</li>
|
||||
<li
|
||||
onClick={() => {
|
||||
setIsOpen(false);
|
||||
navigate("/profile");
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
<i className="fa-light fa-user"></i> {t("profile")}
|
||||
</span>
|
||||
</li>
|
||||
<li
|
||||
onClick={() => {
|
||||
setIsOpen(false);
|
||||
navigate("/changepassword");
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
<i className="fa-light fa-lock"></i>{" "}
|
||||
{t("change-password")}
|
||||
</span>
|
||||
</li>
|
||||
<li
|
||||
onClick={() => {
|
||||
setIsOpen(false);
|
||||
navigate("/verify-document");
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
<i className="fa-light fa-check-square"></i>{" "}
|
||||
{t("verify-document")}
|
||||
</span>
|
||||
</li>
|
||||
<li>
|
||||
<span>
|
||||
<i className="fa-light fa-moon"></i>
|
||||
{t("dark-mode")}
|
||||
<span className="text-[10px] font-semibold bg-base-300 text-base-content px-1 rounded-md">
|
||||
BETA
|
||||
</span>
|
||||
<ThemeToggle />
|
||||
</span>
|
||||
</li>
|
||||
</>
|
||||
)}
|
||||
<li onClick={handleLogout}>
|
||||
<span>
|
||||
<i className="fa-light fa-arrow-right-from-bracket"></i>{" "}
|
||||
{t("log-out")}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
@@ -1,208 +0,0 @@
|
||||
import React, { useState } from "react";
|
||||
import Parse from "parse";
|
||||
import Loader from "../primitives/Loader";
|
||||
// import FacebookLogin from "react-facebook-login";
|
||||
|
||||
const LoginFacebook = ({
|
||||
FBCred,
|
||||
thirdpartyLoginfn,
|
||||
thirdpartyLoader,
|
||||
setThirdpartyLoader
|
||||
}) => {
|
||||
const [isModal, setIsModal] = useState(false);
|
||||
const [userDetails, setUserDetails] = useState({
|
||||
Phone: "",
|
||||
Company: "",
|
||||
Name: "",
|
||||
Email: ""
|
||||
});
|
||||
const [fBDetails, setFBDetails] = useState({
|
||||
Id: "",
|
||||
AccessToken: "",
|
||||
Name: "",
|
||||
Email: ""
|
||||
});
|
||||
const responseFacebook = async (response) => {
|
||||
// console.log("response ", response);
|
||||
if (response.userID) {
|
||||
setThirdpartyLoader(true);
|
||||
const details = {
|
||||
Email: response.email,
|
||||
Name: response.name
|
||||
};
|
||||
setUserDetails({ ...userDetails, ...details });
|
||||
const fDetails = {
|
||||
Id: response.userID,
|
||||
AccessToken: response.accessToken,
|
||||
Name: response.name,
|
||||
Email: response.email
|
||||
};
|
||||
setFBDetails({ ...fBDetails, ...fDetails });
|
||||
const res = await checkExtUser(fDetails);
|
||||
}
|
||||
};
|
||||
|
||||
const checkExtUser = async (details) => {
|
||||
// const extUser = new Parse.Query("contracts_Users");
|
||||
// extUser.equalTo("Email", details.Email);
|
||||
// const extRes = await extUser.first();
|
||||
const params = { email: details.Email };
|
||||
const extRes = await Parse.Cloud.run("getUserDetails", params);
|
||||
// console.log("extRes ", extRes);
|
||||
if (extRes) {
|
||||
const params = { ...details, Phone: extRes?.get("Phone") || "" };
|
||||
const payload = await Parse.Cloud.run("facebooksign", params);
|
||||
// console.log("payload ", payload);
|
||||
if (payload && payload.sessiontoken) {
|
||||
const billingDate =
|
||||
extRes.get("Next_billing_date") && extRes.get("Next_billing_date");
|
||||
// console.log("billingDate expired", billingDate > new Date());
|
||||
const LocalUserDetails = {
|
||||
name: details.Name,
|
||||
email: details.Email,
|
||||
phone: extRes?.get("Phone") || "",
|
||||
company: extRes.get("Company")
|
||||
};
|
||||
localStorage.setItem("userDetails", JSON.stringify(LocalUserDetails));
|
||||
thirdpartyLoginfn(payload.sessiontoken, billingDate);
|
||||
}
|
||||
return { msg: "exist" };
|
||||
} else {
|
||||
setIsModal(true);
|
||||
setThirdpartyLoader(false);
|
||||
return { msg: "notexist" };
|
||||
}
|
||||
};
|
||||
const handleSubmitbtn = async () => {
|
||||
if (userDetails.Phone && userDetails.Company) {
|
||||
setThirdpartyLoader(true);
|
||||
const params = { ...fBDetails, Phone: userDetails?.Phone || "" };
|
||||
const payload = await Parse.Cloud.run("facebooksign", params);
|
||||
// console.log("payload ", payload);
|
||||
|
||||
if (payload && payload.sessiontoken) {
|
||||
const params = {
|
||||
userDetails: {
|
||||
name: userDetails.Name,
|
||||
email: userDetails.Email,
|
||||
// "passsword":userDetails.Phone,
|
||||
phone: userDetails?.Phone || "",
|
||||
role: "contracts_User",
|
||||
company: userDetails.Company
|
||||
}
|
||||
};
|
||||
const userSignUp = await Parse.Cloud.run("usersignup", params);
|
||||
// console.log("userSignUp ", userSignUp);
|
||||
if (userSignUp && userSignUp.sessionToken) {
|
||||
const LocalUserDetails = {
|
||||
name: userDetails.Name,
|
||||
email: userDetails.Email,
|
||||
phone: userDetails?.Phone || "",
|
||||
company: userDetails.Company
|
||||
};
|
||||
localStorage.setItem("userDetails", JSON.stringify(LocalUserDetails));
|
||||
thirdpartyLoginfn(userSignUp.sessionToken);
|
||||
} else {
|
||||
alert(userSignUp.message);
|
||||
}
|
||||
} else {
|
||||
alert("Internal server error !");
|
||||
}
|
||||
} else {
|
||||
alert("Please fill required details!");
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className="relative">
|
||||
{thirdpartyLoader && (
|
||||
<div className="fixed flex justify-center items-center inset-0 bg-black bg-opacity-25 z-20 ">
|
||||
<Loader />
|
||||
</div>
|
||||
)}
|
||||
{/* <FacebookLogin
|
||||
appId={FBCred}
|
||||
fields="name,email,picture"
|
||||
callback={responseFacebook}
|
||||
cssClass="btn btn-info btn-md btn-fb"
|
||||
icon="fa fa-facebook-f"
|
||||
/> */}
|
||||
{isModal && (
|
||||
<div
|
||||
className="modal fade show block z-[1]"
|
||||
id="exampleModal"
|
||||
tabIndex="-1"
|
||||
role="dialog"
|
||||
>
|
||||
<div className="modal-dialog" role="document">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<h5 className="modal-title">Sign up form</h5>
|
||||
<span>
|
||||
<span></span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<form>
|
||||
<div className="form-group">
|
||||
<label htmlFor="Phone" className="col-form-label">
|
||||
Phone <span className="text-[red] text-[13px]">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="tel"
|
||||
className="form-control"
|
||||
id="Phone"
|
||||
value={userDetails.Phone}
|
||||
onChange={(e) =>
|
||||
setUserDetails({
|
||||
...userDetails,
|
||||
Phone: e.target.value
|
||||
})
|
||||
}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="Company" className="col-form-label">
|
||||
Company <span className="text-[red] text-[13px]">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
id="Company"
|
||||
value={userDetails.Company}
|
||||
onChange={(e) =>
|
||||
setUserDetails({
|
||||
...userDetails,
|
||||
Company: e.target.value
|
||||
})
|
||||
}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleSubmitbtn()}
|
||||
className="btn btn-info mr-[10px]"
|
||||
>
|
||||
Sign up
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary w-[90px]"
|
||||
onClick={() => setIsModal(false)}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginFacebook;
|
||||
@@ -1,277 +0,0 @@
|
||||
import React, { useState, useRef } from "react";
|
||||
import Parse from "parse";
|
||||
import { jwtDecode } from "jwt-decode";
|
||||
import { useScript } from "../hook/useScript";
|
||||
import ModalUi from "../primitives/ModalUi";
|
||||
import Loader from "../primitives/Loader";
|
||||
|
||||
/*
|
||||
* `GoogleSignInBtn`as it's name indicates it render google sign in button
|
||||
* and in this `useScript` in which we have created for generate google sign button
|
||||
* and when user click on sign in with google it will be verify on server side
|
||||
* and then generate token
|
||||
*/
|
||||
const GoogleSignInBtn = ({
|
||||
GoogleCred,
|
||||
thirdpartyLoginfn,
|
||||
thirdpartyLoader,
|
||||
setThirdpartyLoader
|
||||
}) => {
|
||||
const [isModal, setIsModal] = useState(false);
|
||||
const googleBtn = useRef();
|
||||
const [userDetails, setUserDetails] = useState({
|
||||
Phone: "",
|
||||
Company: "",
|
||||
Name: "",
|
||||
Email: "",
|
||||
Destination: ""
|
||||
});
|
||||
const [googleDetails, setGoogleDetails] = useState({
|
||||
Id: "",
|
||||
TokenId: "",
|
||||
Gmail: "",
|
||||
Name: ""
|
||||
});
|
||||
useScript("https://accounts.google.com/gsi/client", () => {
|
||||
window.google.accounts.id.initialize({
|
||||
client_id: GoogleCred,
|
||||
callback: responseGoogle,
|
||||
auto_select: false
|
||||
});
|
||||
window.google.accounts.id.renderButton(googleBtn.current, {
|
||||
theme: "outline",
|
||||
size: "large",
|
||||
width: "187px"
|
||||
});
|
||||
});
|
||||
const clearStorage = async () => {
|
||||
if (Parse.User.current()) {
|
||||
await Parse.User.logOut();
|
||||
}
|
||||
let baseUrl = localStorage.getItem("baseUrl");
|
||||
let appid = localStorage.getItem("parseAppId");
|
||||
let applogo = localStorage.getItem("appLogo");
|
||||
let defaultmenuid = localStorage.getItem("defaultmenuid");
|
||||
let PageLanding = localStorage.getItem("PageLanding");
|
||||
let userSettings = localStorage.getItem("userSettings");
|
||||
|
||||
localStorage.clear();
|
||||
|
||||
localStorage.setItem("baseUrl", baseUrl);
|
||||
localStorage.setItem("parseAppId", appid);
|
||||
localStorage.setItem("appLogo", applogo);
|
||||
localStorage.setItem("defaultmenuid", defaultmenuid);
|
||||
localStorage.setItem("PageLanding", PageLanding);
|
||||
localStorage.setItem("userSettings", userSettings);
|
||||
localStorage.setItem("baseUrl", baseUrl);
|
||||
localStorage.setItem("parseAppId", appid);
|
||||
};
|
||||
const responseGoogle = async (response) => {
|
||||
clearStorage();
|
||||
setThirdpartyLoader(true);
|
||||
// console.log("response ", response);
|
||||
if (response.credential) {
|
||||
const data = jwtDecode(response.credential);
|
||||
// console.log("data ", data);
|
||||
if (data.sub && data.email) {
|
||||
const details = {
|
||||
Email: data.email,
|
||||
Name: data.name
|
||||
};
|
||||
setUserDetails({ ...userDetails, ...details });
|
||||
const Gdetails = {
|
||||
Id: data.sub,
|
||||
TokenId: response.credential,
|
||||
Gmail: data.email,
|
||||
Name: data.name
|
||||
};
|
||||
setGoogleDetails({ ...googleDetails, ...Gdetails });
|
||||
await checkExtUser(Gdetails);
|
||||
}
|
||||
}
|
||||
};
|
||||
const checkExtUser = async (details) => {
|
||||
// const extUser = new Parse.Query("contracts_Users");
|
||||
// extUser.equalTo("Email", details.Gmail);
|
||||
// const extRes = await extUser.first();
|
||||
const params = { email: details.Gmail };
|
||||
const extRes = await Parse.Cloud.run("getUserDetails", params);
|
||||
// console.log("extRes ", extRes);
|
||||
if (extRes) {
|
||||
const params = { ...details, Phone: extRes?.get("Phone") || "" };
|
||||
const payload = await Parse.Cloud.run("googlesign", params);
|
||||
// console.log("payload ", payload);
|
||||
if (payload && payload.sessiontoken) {
|
||||
// setThirdpartyLoader(true);
|
||||
// const billingDate =
|
||||
// extRes.get("Next_billing_date") && extRes.get("Next_billing_date");
|
||||
// console.log("billingDate expired", billingDate > new Date());
|
||||
const LocalUserDetails = {
|
||||
name: details.Name,
|
||||
email: details.Gmail,
|
||||
phone: extRes?.get("Phone") || "",
|
||||
company: extRes.get("Company")
|
||||
};
|
||||
localStorage.setItem("userDetails", JSON.stringify(LocalUserDetails));
|
||||
thirdpartyLoginfn(payload.sessiontoken);
|
||||
}
|
||||
return { msg: "exist" };
|
||||
} else {
|
||||
setIsModal(true);
|
||||
setThirdpartyLoader(false);
|
||||
return { msg: "notexist" };
|
||||
}
|
||||
};
|
||||
const handleSubmitbtn = async () => {
|
||||
if (userDetails.Phone && userDetails.Company) {
|
||||
setThirdpartyLoader(true);
|
||||
// e.preventDefault()
|
||||
// console.log("handelSubmit", userDetails);
|
||||
const params = { ...googleDetails, Phone: userDetails?.Phone };
|
||||
const payload = await Parse.Cloud.run("googlesign", params);
|
||||
|
||||
// console.log("payload ", payload);
|
||||
if (payload && payload.sessiontoken) {
|
||||
const params = {
|
||||
userDetails: {
|
||||
name: userDetails.Name,
|
||||
email: userDetails.Email,
|
||||
// "passsword":userDetails.Phone,
|
||||
phone: userDetails?.Phone || "",
|
||||
role: "contracts_User",
|
||||
company: userDetails.Company,
|
||||
jobTitle: userDetails.Destination
|
||||
}
|
||||
};
|
||||
const userSignUp = await Parse.Cloud.run("usersignup", params);
|
||||
// console.log("userSignUp ", userSignUp);
|
||||
if (userSignUp && userSignUp.sessionToken) {
|
||||
const LocalUserDetails = {
|
||||
name: userDetails.Name,
|
||||
email: userDetails.Email,
|
||||
phone: userDetails?.Phone || "",
|
||||
company: userDetails.Company
|
||||
// jobTitle: userDetails.JobTitle
|
||||
};
|
||||
localStorage.setItem("userDetails", JSON.stringify(LocalUserDetails));
|
||||
thirdpartyLoginfn(userSignUp.sessionToken);
|
||||
} else {
|
||||
alert(userSignUp.message);
|
||||
}
|
||||
} else if (
|
||||
payload &&
|
||||
payload.message.replace(/ /g, "_") === "Internal_server_err"
|
||||
) {
|
||||
alert("Internal server error !");
|
||||
}
|
||||
} else {
|
||||
alert("Please fill required details!");
|
||||
}
|
||||
};
|
||||
const handleCloseModal = () => {
|
||||
setIsModal(false);
|
||||
Parse.User.logOut();
|
||||
|
||||
let appdata = localStorage.getItem("userSettings");
|
||||
let applogo = localStorage.getItem("appLogo");
|
||||
let defaultmenuid = localStorage.getItem("defaultmenuid");
|
||||
let PageLanding = localStorage.getItem("PageLanding");
|
||||
let baseUrl = localStorage.getItem("baseUrl");
|
||||
let appid = localStorage.getItem("parseAppId");
|
||||
|
||||
localStorage.clear();
|
||||
|
||||
localStorage.setItem("appLogo", applogo);
|
||||
localStorage.setItem("defaultmenuid", defaultmenuid);
|
||||
localStorage.setItem("PageLanding", PageLanding);
|
||||
localStorage.setItem("userSettings", appdata);
|
||||
localStorage.setItem("baseUrl", baseUrl);
|
||||
localStorage.setItem("parseAppId", appid);
|
||||
};
|
||||
return (
|
||||
<div className="relative">
|
||||
{thirdpartyLoader && (
|
||||
<div className="fixed flex justify-center items-center inset-0 bg-black bg-opacity-25 z-20 ">
|
||||
<Loader />
|
||||
</div>
|
||||
)}
|
||||
<div ref={googleBtn} className="text-sm"></div>
|
||||
<ModalUi showClose={false} isOpen={isModal} title="Sign up form">
|
||||
<form className="px-4 py-3 text-base-content">
|
||||
<div className="mb-3">
|
||||
<label htmlFor="Phone" className="block text-xs font-semibold">
|
||||
Phone <span className="text-[13px] text-[red]">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="tel"
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
id="Phone"
|
||||
value={userDetails.Phone}
|
||||
onChange={(e) =>
|
||||
setUserDetails({
|
||||
...userDetails,
|
||||
Phone: e.target.value
|
||||
})
|
||||
}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label htmlFor="Company" className="block text-xs font-semibold">
|
||||
Company <span className="text-[13px] text-[red]">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
id="Company"
|
||||
value={userDetails.Company}
|
||||
onChange={(e) =>
|
||||
setUserDetails({
|
||||
...userDetails,
|
||||
Company: e.target.value
|
||||
})
|
||||
}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-3">
|
||||
<label htmlFor="JobTitle" className="block text-xs font-semibold">
|
||||
Job Title <span className="text-[13px] text-[red]">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
id="JobTitle"
|
||||
value={userDetails.Destination}
|
||||
onChange={(e) =>
|
||||
setUserDetails({
|
||||
...userDetails,
|
||||
Destination: e.target.value
|
||||
})
|
||||
}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-row gap-2">
|
||||
<button
|
||||
type="button"
|
||||
className="op-btn op-btn-primary"
|
||||
onClick={() => handleSubmitbtn()}
|
||||
>
|
||||
Sign up
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="op-btn op-btn-ghost"
|
||||
onClick={handleCloseModal}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</ModalUi>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GoogleSignInBtn;
|
||||
@@ -1,7 +1,9 @@
|
||||
import React from "react";
|
||||
import { Document, Page } from "react-pdf";
|
||||
import { Stage, Layer, Rect, Text } from "react-konva";
|
||||
import { useTranslation } from "react-i18next";
|
||||
const RenderDebugPdf = (props) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div>
|
||||
<div className="sticky top-0 p-[10px] z-10 bg-white border-[1px] border-[gray] my-[5px]">
|
||||
@@ -12,10 +14,9 @@ const RenderDebugPdf = (props) => {
|
||||
onMouseMove={props.handleMouseMoveDiv}
|
||||
>
|
||||
<Document
|
||||
onLoadError={() => {
|
||||
props.setPdfLoadFail(false);
|
||||
}}
|
||||
loading={"Loading Document.."}
|
||||
onLoadError={() => props.setPdfLoadFail(false)}
|
||||
loading={t("loading-doc")}
|
||||
error={<p className="mx-2">{t("failed-to-load-refresh-page")}</p>}
|
||||
onLoadSuccess={props.pageDetails}
|
||||
ref={props.pdfRef}
|
||||
file={props.pdfUrl}
|
||||
@@ -0,0 +1,39 @@
|
||||
import React from "react";
|
||||
import ModalUi from "../primitives/ModalUi";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
function RotateAlert(props) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<ModalUi
|
||||
isOpen={props.showRotateAlert}
|
||||
title={t("Rotation-alert")}
|
||||
handleClose={() => props.setShowRotateAlert({ status: false, degree: 0 })}
|
||||
>
|
||||
<div className="p-[20px] h-full">
|
||||
<p>{t("rotate-alert-mssg")}</p>
|
||||
<div className="h-[1px] w-full my-[15px] bg-[#9f9f9f]"></div>
|
||||
<div className="flex gap-1">
|
||||
<button
|
||||
onClick={() => props.handleRemoveWidgets()}
|
||||
type="button"
|
||||
className="op-btn op-btn-primary"
|
||||
>
|
||||
{t("yes")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
props.setShowRotateAlert({ status: false, degree: 0 })
|
||||
}
|
||||
type="button"
|
||||
className="op-btn op-btn-ghost text-base-content shadow-md"
|
||||
>
|
||||
{t("no")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ModalUi>
|
||||
);
|
||||
}
|
||||
|
||||
export default RotateAlert;
|
||||
@@ -0,0 +1,54 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { NavLink } from "react-router";
|
||||
|
||||
const SocialMedia = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<NavLink
|
||||
to="https://github.com/opensignlabs/opensign"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<i aria-hidden="true" className="fa-brands fa-github"></i>
|
||||
<span className="fa-sr-only">
|
||||
OpenSign's {t("social-media.github")}
|
||||
</span>
|
||||
</NavLink>
|
||||
<NavLink
|
||||
to="https://www.linkedin.com/company/opensign%E2%84%A2/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<i aria-hidden="true" className="fa-brands fa-linkedin"></i>
|
||||
<span className="fa-sr-only">
|
||||
OpenSign's {t("social-media.linked-in")}
|
||||
</span>
|
||||
</NavLink>
|
||||
<NavLink
|
||||
to="https://www.twitter.com/opensignlabs"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<i aria-hidden="true" className="fa-brands fa-square-x-twitter"></i>
|
||||
<span className="fa-sr-only">
|
||||
OpenSign's {t("social-media.twitter")}
|
||||
</span>
|
||||
</NavLink>
|
||||
<NavLink
|
||||
to="https://discord.com/invite/xe9TDuyAyj"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<i aria-hidden="true" className="fa-brands fa-discord"></i>
|
||||
<span className="fa-sr-only">
|
||||
OpenSign's {t("social-media.discord")}
|
||||
</span>
|
||||
</NavLink>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default SocialMedia;
|
||||
@@ -0,0 +1,41 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const ThemeToggle = () => {
|
||||
const [isDark, setIsDark] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const storedTheme = localStorage.getItem("theme");
|
||||
if (storedTheme === "dark") {
|
||||
setIsDark(true);
|
||||
document.documentElement.setAttribute("data-theme", "opensigndark");
|
||||
} else {
|
||||
document.documentElement.setAttribute("data-theme", "opensigncss");
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleChange = () => {
|
||||
const newTheme = !isDark;
|
||||
setIsDark(newTheme);
|
||||
if (newTheme) {
|
||||
document.documentElement.setAttribute("data-theme", "opensigndark");
|
||||
localStorage.setItem("theme", "dark");
|
||||
} else {
|
||||
document.documentElement.setAttribute("data-theme", "opensigncss");
|
||||
localStorage.setItem("theme", "light");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
id="dark-mode-toggle"
|
||||
type="checkbox"
|
||||
className="op-toggle checked:[--tglbg:#3368ff] transition-all checked:bg-white"
|
||||
checked={isDark}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThemeToggle;
|
||||
@@ -1,19 +0,0 @@
|
||||
import React from "react";
|
||||
import { Helmet } from "react-helmet";
|
||||
|
||||
function Title({ title, drive }) {
|
||||
return (
|
||||
<Helmet>
|
||||
<title>{drive ? title : `${title} - OpenSign™`}</title>
|
||||
<meta name="description" content={`${title} - OpenSign™`} />
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
href={localStorage.getItem("fev_Icon")}
|
||||
sizes="40x40"
|
||||
/>
|
||||
</Helmet>
|
||||
);
|
||||
}
|
||||
|
||||
export default Title;
|
||||
@@ -0,0 +1,82 @@
|
||||
import { Helmet } from "react-helmet";
|
||||
import { useLocation, matchPath } from "react-router";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMemo } from "react";
|
||||
import { useManifestUrl } from "../hook/useManifestUrl";
|
||||
|
||||
const TITLE_MAP = {
|
||||
"/": "login",
|
||||
// Homelayout
|
||||
"/dashboard/35KBoSgoAK": "Dashboard",
|
||||
"/form/sHAnZphf69": "Sign Yourself",
|
||||
"/form/8mZzFxbG1z": "Request Signatures",
|
||||
"/form/template": "New Template",
|
||||
"/report/6TeaPr321t": "Templates",
|
||||
"/report/4Hhwbp482K": "Need your sign",
|
||||
"/report/1MwEuxLEkF": "In Progress",
|
||||
"/report/kQUoW4hUXz": "Completed",
|
||||
"/report/ByHuevtCFY": "Drafts",
|
||||
"/report/UPr2Fm5WY3": "Declined",
|
||||
"/report/zNqBHXHsYH": "Expired",
|
||||
"/report/contacts": "Contactbook",
|
||||
"/drive": "Drive",
|
||||
"/managesign": "My Signature",
|
||||
"/preferences": "Preferences",
|
||||
"/users": "Users",
|
||||
"/profile": "profile",
|
||||
"/changepassword": "change-password",
|
||||
"/verify-document": "verify-document",
|
||||
|
||||
"/signaturePdf/:docId": "Sign Yourself",
|
||||
"/placeHolderSign/:docId": "Request Signatures",
|
||||
"/template/:templateId": "New Template",
|
||||
"/recipientSignPdf/:docId": "Request Signatures",
|
||||
"/recipientSignPdf/:docId/:contactBookId": "Request Signatures",
|
||||
"/load/recipientSignPdf/:docId/:contactBookId": "Request Signatures",
|
||||
|
||||
// alone
|
||||
"/debugpdf": "Debug Pdf",
|
||||
"/forgetpassword": "forgot-password",
|
||||
"/success": "success",
|
||||
"/addadmin": "add-admin",
|
||||
"/upgrade-2.1": "add-admin",
|
||||
"/draftDocument": "New Document",
|
||||
"/login/:base64url": "Request Signatures",
|
||||
|
||||
};
|
||||
|
||||
function resolveTitle(pathname, override) {
|
||||
if (override) return override;
|
||||
for (let [pattern, label] of Object.entries(TITLE_MAP)) {
|
||||
if (matchPath({ path: pattern, end: true }, pathname)) {
|
||||
return label;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
export default function Title() {
|
||||
const { pathname, state } = useLocation();
|
||||
const { t } = useTranslation();
|
||||
const appName =
|
||||
"OpenSign™";
|
||||
const logo = useMemo(() => localStorage.getItem("favicon"), []);
|
||||
const prefix = useMemo(
|
||||
() => resolveTitle(pathname, state?.title),
|
||||
[pathname, state?.title]
|
||||
);
|
||||
const title = useMemo(
|
||||
() => (prefix ? `${t(prefix)} - ${appName}` : appName),
|
||||
[t, prefix, appName]
|
||||
);
|
||||
const manifestUrl = useManifestUrl(appName, logo);
|
||||
|
||||
return (
|
||||
<Helmet>
|
||||
<title>{title}</title>
|
||||
<meta name="description" content={title} />
|
||||
{logo && <link rel="icon" type="image/png" href={logo} />}
|
||||
<link rel="manifest" href={manifestUrl} />
|
||||
</Helmet>
|
||||
);
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
import React from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { openInNewTab } from "../../constant/Utils";
|
||||
|
||||
const DashboardButton = (props) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
function openReport() {
|
||||
if (props.Data && props.Data.Redirect_type) {
|
||||
const Redirect_type = props.Data.Redirect_type;
|
||||
const id = props.Data.Redirect_id;
|
||||
if (Redirect_type === "Form") {
|
||||
navigate(`/form/${id}`);
|
||||
} else if (Redirect_type === "Report") {
|
||||
navigate(`/report/${id}`);
|
||||
} else if (Redirect_type === "Url") {
|
||||
openInNewTab(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div
|
||||
onClick={() => openReport()}
|
||||
className={`${
|
||||
props.Data && props.Data.Redirect_type
|
||||
? "cursor-pointer"
|
||||
: "cursor-default"
|
||||
} w-full shadow-md px-3 py-2 op-card bg-base-100`}
|
||||
>
|
||||
<div className="flex flex-row items-center">
|
||||
<span className="rounded-full bg-base-content bg-opacity-20 w-[60px] h-[60px] self-start flex justify-center items-center">
|
||||
<i
|
||||
className={`${
|
||||
props.Icon ? props.Icon : "fa-light fa-info"
|
||||
} text-[25px] lg:text-[30px]`}
|
||||
></i>
|
||||
</span>
|
||||
<div className=" text-lg ml-3 text-base-content">{props.Label}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardButton;
|
||||
@@ -0,0 +1,60 @@
|
||||
import React from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { openInNewTab } from "../../constant/Utils";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const DashboardButton = (props) => {
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
|
||||
function openReport() {
|
||||
if (props.Data && props.Data.Redirect_type) {
|
||||
const Redirect_type = props.Data.Redirect_type;
|
||||
const id = props.Data.Redirect_id;
|
||||
if (Redirect_type === "Form") {
|
||||
navigate(`/form/${id}`);
|
||||
} else if (Redirect_type === "Report") {
|
||||
navigate(`/report/${id}`);
|
||||
} else if (Redirect_type === "Url") {
|
||||
openInNewTab(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div
|
||||
onClick={() => openReport()}
|
||||
className={`${
|
||||
props.Data && props.Data.Redirect_type
|
||||
? "cursor-pointer"
|
||||
: "cursor-default"
|
||||
} w-full shadow-md px-3 py-2 op-card bg-base-100`}
|
||||
>
|
||||
<div className="flex flex-row items-center text-base-content">
|
||||
<div className="flex flex-row items-center">
|
||||
<span className="rounded-full bg-base-content bg-opacity-20 w-[60px] h-[60px] self-start flex justify-center items-center">
|
||||
<i
|
||||
className={`${
|
||||
props.Icon ? props.Icon : "fa-light fa-info"
|
||||
} text-[25px] lg:text-[30px]`}
|
||||
></i>
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-lg ml-3">
|
||||
{t(`sidebar.${props.Label}`)}
|
||||
{props.Label === "Sign yourself" && (
|
||||
<div className="text-gray-500 text-xs mt-1">
|
||||
{t("signyour-self-button")}
|
||||
</div>
|
||||
)}
|
||||
{props.Label === "Request signatures" && (
|
||||
<div className="text-gray-500 text-xs mt-1">
|
||||
{t("requestsign-button")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardButton;
|
||||
@@ -1,12 +1,14 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import axios from "axios";
|
||||
import Parse from "parse";
|
||||
import getReplacedHashQuery from "../../constant/getReplacedHashQuery";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useNavigate } from "react-router";
|
||||
import Tooltip from "../../primitives/Tooltip";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const DashboardCard = (props) => {
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
const [parseBaseUrl] = useState(localStorage.getItem("baseUrl"));
|
||||
const [parseAppId] = useState(localStorage.getItem("parseAppId"));
|
||||
const [response, setresponse] = useState("");
|
||||
@@ -23,17 +25,12 @@ const DashboardCard = (props) => {
|
||||
sessionToken: localStorage.getItem("accesstoken")
|
||||
};
|
||||
let body = {};
|
||||
let currentUser;
|
||||
let currentUser1 = Parse.User.current();
|
||||
currentUser = currentUser1.id;
|
||||
let res;
|
||||
if (localStorage.getItem("Extand_Class")) {
|
||||
let data = JSON.parse(localStorage.getItem("Extand_Class"));
|
||||
res = data[0];
|
||||
} else {
|
||||
res = await Parse.Cloud.run("getUserDetails", {
|
||||
email: currentUser.get("email")
|
||||
});
|
||||
res = await Parse.Cloud.run("getUserDetails");
|
||||
if (res) res = res.toJSON();
|
||||
}
|
||||
if (res) {
|
||||
@@ -94,9 +91,7 @@ const DashboardCard = (props) => {
|
||||
let data = JSON.parse(localStorage.getItem("Extand_Class"));
|
||||
resr = data[0];
|
||||
} else {
|
||||
resr = await Parse.Cloud.run("getUserDetails", {
|
||||
email: currentUser.get("email")
|
||||
});
|
||||
resr = await Parse.Cloud.run("getUserDetails");
|
||||
if (resr) resr = resr.toJSON();
|
||||
}
|
||||
|
||||
@@ -138,7 +133,7 @@ const DashboardCard = (props) => {
|
||||
skip: 0,
|
||||
limit: 200
|
||||
};
|
||||
const url = `${parseBaseUrl}/functions/getReport`;
|
||||
const url = `${parseBaseUrl}functions/getReport`;
|
||||
await axios
|
||||
.post(url, params, {
|
||||
headers: {
|
||||
@@ -153,7 +148,7 @@ const DashboardCard = (props) => {
|
||||
);
|
||||
let arr = [];
|
||||
for (const obj of listData) {
|
||||
const isSigner = obj.Signers.some(
|
||||
const isSigner = obj.Signers?.some(
|
||||
(item) => item.UserId.objectId === currentUser.id
|
||||
);
|
||||
if (isSigner) {
|
||||
@@ -173,25 +168,21 @@ const DashboardCard = (props) => {
|
||||
}
|
||||
}
|
||||
setresponse(arr.length);
|
||||
setLoading(false);
|
||||
});
|
||||
} else {
|
||||
await axios.get(url, { headers: headers }).then((res) => {
|
||||
if (res.data.results.length > 0) {
|
||||
setLoading(false);
|
||||
if (props.Data.key !== "count") {
|
||||
setresponse(res.data.results[0][props.Data.key]);
|
||||
} else {
|
||||
setresponse(res.data[props.Data.key]);
|
||||
}
|
||||
if (res?.data?.[props.Data.key]) {
|
||||
setresponse(parseInt(res.data[props.Data.key]));
|
||||
} else if (res?.data?.results?.length > 0) {
|
||||
setresponse(res.data.results.length);
|
||||
} else {
|
||||
setresponse(0);
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Problem", e);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
@@ -218,15 +209,12 @@ const DashboardCard = (props) => {
|
||||
|
||||
if (restr.includes("#")) {
|
||||
try {
|
||||
const currentUser = Parse.User.current();
|
||||
let res;
|
||||
if (localStorage.getItem("Extand_Class")) {
|
||||
let data = JSON.parse(localStorage.getItem("Extand_Class"));
|
||||
res = data[0];
|
||||
} else {
|
||||
let resr = await Parse.Cloud.run("getUserDetails", {
|
||||
email: currentUser.get("email")
|
||||
});
|
||||
let resr = await Parse.Cloud.run("getUserDetails");
|
||||
if (res) res = resr.toJSON();
|
||||
}
|
||||
|
||||
@@ -321,6 +309,7 @@ const DashboardCard = (props) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={() => openReport()}
|
||||
@@ -338,8 +327,11 @@ const DashboardCard = (props) => {
|
||||
} text-[25px] lg:text-[30px]`}
|
||||
></i>
|
||||
</span>
|
||||
|
||||
<div className="font-medium">
|
||||
<div className="text-base lg:text-lg"> {props.Label}</div>
|
||||
<div className="text-base lg:text-lg">
|
||||
{t(`dashboard-card.${props.Label}`)}
|
||||
</div>
|
||||
<div className="text-2xl font-light">
|
||||
{loading ? <div className="loader-01"></div> : setFormat(response)}
|
||||
</div>
|
||||
@@ -349,7 +341,7 @@ const DashboardCard = (props) => {
|
||||
<Tooltip
|
||||
id={props.Label}
|
||||
iconColor={"white"}
|
||||
message={props?.Data?.tourMessage}
|
||||
message={t(`tour-mssg.${props.Label}`)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,11 +1,12 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import Parse from "parse";
|
||||
import ReportTable from "../../primitives/GetReportDisplay";
|
||||
import DocumentsReport from "../../reports/document/DocumentsReport";
|
||||
import reportJson from "../../json/ReportJson";
|
||||
import axios from "axios";
|
||||
import Loader from "../../primitives/Loader";
|
||||
import { useTranslation } from "react-i18next";
|
||||
function DashboardReport(props) {
|
||||
// console.log("props ", props)
|
||||
const { t } = useTranslation();
|
||||
const [List, setList] = useState([]);
|
||||
const [isLoader, setIsLoader] = useState(true);
|
||||
const [reportName, setReportName] = useState("");
|
||||
@@ -15,10 +16,16 @@ function DashboardReport(props) {
|
||||
const [isMoreDocs, setIsMoreDocs] = useState(true);
|
||||
const abortController = new AbortController();
|
||||
const docPerPage = 5;
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [mobileSearchOpen, setMobileSearchOpen] = useState(false);
|
||||
const [isSearchResult, setIsSearchResult] = useState(false);
|
||||
const debounceTimer = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
setReportName("");
|
||||
getReportData(props.Record.reportId);
|
||||
setSearchTerm("");
|
||||
setMobileSearchOpen(false);
|
||||
getReportData(props.Record.reportId, 0, 20, "");
|
||||
|
||||
// Function returned from useEffect is called on unmount
|
||||
return () => {
|
||||
@@ -34,12 +41,69 @@ function DashboardReport(props) {
|
||||
// below useEffect call when isNextRecord state is true and fetch next record
|
||||
useEffect(() => {
|
||||
if (isNextRecord) {
|
||||
getReportData(props.Record.reportId, List.length, 200);
|
||||
getReportData(props.Record.reportId, List.length, 20, searchTerm);
|
||||
}
|
||||
// eslint-disable-next-line
|
||||
}, [isNextRecord]);
|
||||
|
||||
const getReportData = async (id, skipUserRecord = 0, limit = 200) => {
|
||||
const handleSearchChange = async (e) => {
|
||||
const term = e.target.value.toLowerCase();
|
||||
setSearchTerm(term);
|
||||
if (debounceTimer.current) {
|
||||
clearTimeout(debounceTimer.current);
|
||||
}
|
||||
debounceTimer.current = setTimeout(async () => {
|
||||
try {
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
|
||||
sessiontoken: localStorage.getItem("accesstoken")
|
||||
};
|
||||
const url = `${localStorage.getItem("baseUrl")}functions/getReport`;
|
||||
const res = await axios.post(
|
||||
url,
|
||||
{
|
||||
reportId: props.Record.reportId,
|
||||
searchTerm: term,
|
||||
skip: 0,
|
||||
limit: docPerPage
|
||||
},
|
||||
{ headers }
|
||||
);
|
||||
const data = res.data?.result || [];
|
||||
if (!data.error) {
|
||||
setList(data);
|
||||
setIsMoreDocs(data.length >= docPerPage);
|
||||
setIsNextRecord(false);
|
||||
setIsSearchResult(true);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Search error:", err);
|
||||
}
|
||||
}, 300);
|
||||
setIsSearchResult(false);
|
||||
};
|
||||
|
||||
const handleSearchPaste = (e) => {
|
||||
setTimeout(() => {
|
||||
handleSearchChange({ target: { value: e.target.value } });
|
||||
}, 0);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (debounceTimer.current) {
|
||||
clearTimeout(debounceTimer.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const getReportData = async (
|
||||
id,
|
||||
skipUserRecord = 0,
|
||||
limit = 20,
|
||||
term = searchTerm
|
||||
) => {
|
||||
setIsLoader(true);
|
||||
const json = reportJson(id);
|
||||
if (json) {
|
||||
@@ -54,8 +118,13 @@ function DashboardReport(props) {
|
||||
sessiontoken: localStorage.getItem("accesstoken")
|
||||
};
|
||||
try {
|
||||
const params = { reportId: id, skip: skipUserRecord, limit: limit };
|
||||
const url = `${localStorage.getItem("baseUrl")}/functions/getReport`;
|
||||
const skipRecord = id === "5Go51Q7T8r" ? 0 : skipUserRecord;
|
||||
const limitRecord = id === "5Go51Q7T8r" ? 200 : limit;
|
||||
const params = { reportId: id, skip: skipRecord, limit: limitRecord };
|
||||
if (term) {
|
||||
params.searchTerm = term;
|
||||
}
|
||||
const url = `${localStorage.getItem("baseUrl")}functions/getReport`;
|
||||
const res = await axios.post(url, params, {
|
||||
headers: headers,
|
||||
signal: abortController.signal // is used to cancel fetch query
|
||||
@@ -64,7 +133,7 @@ function DashboardReport(props) {
|
||||
const listData = res.data?.result.filter((x) => x.Signers.length > 0);
|
||||
let arr = [];
|
||||
for (const obj of listData) {
|
||||
const isSigner = obj.Signers.some(
|
||||
const isSigner = obj.Signers?.some(
|
||||
(item) => item.UserId.objectId === currentUser
|
||||
);
|
||||
if (isSigner) {
|
||||
@@ -92,7 +161,7 @@ function DashboardReport(props) {
|
||||
prevRecord.length > 0 ? [...prevRecord, ...arr] : arr
|
||||
);
|
||||
} else {
|
||||
if (res.data.result.length === docPerPage) {
|
||||
if (res.data.result.length >= docPerPage) {
|
||||
setIsMoreDocs(true);
|
||||
} else {
|
||||
setIsMoreDocs(false);
|
||||
@@ -128,7 +197,7 @@ function DashboardReport(props) {
|
||||
) : (
|
||||
<>
|
||||
{reportName ? (
|
||||
<ReportTable
|
||||
<DocumentsReport
|
||||
ReportName={reportName}
|
||||
List={List}
|
||||
setList={setList}
|
||||
@@ -137,11 +206,17 @@ function DashboardReport(props) {
|
||||
setIsNextRecord={setIsNextRecord}
|
||||
isMoreDocs={isMoreDocs}
|
||||
docPerPage={docPerPage}
|
||||
mobileSearchOpen={mobileSearchOpen}
|
||||
setMobileSearchOpen={setMobileSearchOpen}
|
||||
searchTerm={searchTerm}
|
||||
handleSearchChange={handleSearchChange}
|
||||
handleSearchPaste={handleSearchPaste}
|
||||
isSearchResult={isSearchResult}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-[100px] w-full bg-white rounded">
|
||||
<div className="text-center">
|
||||
<p className="text-xl text-black">Report Not Found</p>
|
||||
<div className="flex items-center justify-center h-[100px] w-full bg-white rounded-box">
|
||||
<div className="text-center text-xl text-base-content">
|
||||
{t("report-not-found")}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -1,7 +1,9 @@
|
||||
import React, { Suspense, lazy } from "react";
|
||||
const DashboardButton = lazy(() => import("./DashboardButton"));
|
||||
const DashboardCard = lazy(() => import("./DashboardCard"));
|
||||
const DashboardReport = lazy(() => import("./DashboardReport"));
|
||||
import { useState, useEffect, Suspense } from "react";
|
||||
import { lazyWithRetry } from "../../utils";
|
||||
import { useTranslation } from "react-i18next";
|
||||
const DashboardButton = lazyWithRetry(() => import("./DashboardButton"));
|
||||
const DashboardCard = lazyWithRetry(() => import("./DashboardCard"));
|
||||
const DashboardReport = lazyWithRetry(() => import("./DashboardReport"));
|
||||
const buttonList = [
|
||||
{
|
||||
label: "Sign yourself",
|
||||
@@ -10,13 +12,15 @@ const buttonList = [
|
||||
icon: "fa-light fa-pen-nib"
|
||||
},
|
||||
{
|
||||
label: "Request signature",
|
||||
label: "Request signatures",
|
||||
redirectId: "8mZzFxbG1z",
|
||||
redirectType: "Form",
|
||||
icon: "fa-light fa-paper-plane"
|
||||
}
|
||||
];
|
||||
const GetDashboard = (props) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const Button = ({ label, redirectId, redirectType, icon }) => (
|
||||
<DashboardButton
|
||||
Icon={icon}
|
||||
@@ -37,7 +41,7 @@ const GetDashboard = (props) => {
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="h-[150px] w-full flex justify-center items-center">
|
||||
loading...
|
||||
{t("loading")}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
@@ -56,7 +60,9 @@ const GetDashboard = (props) => {
|
||||
<div data-tut={col.widget.data.tourSection}>
|
||||
<Suspense fallback={<div>please wait</div>}>
|
||||
<div className="mb-3 md:mb-0">
|
||||
<DashboardReport Record={col.widget} />
|
||||
<DashboardReport
|
||||
Record={col.widget}
|
||||
/>
|
||||
</div>
|
||||
</Suspense>
|
||||
</div>
|
||||
@@ -90,7 +96,9 @@ const GetDashboard = (props) => {
|
||||
return (
|
||||
<Suspense fallback={<div>please wait</div>}>
|
||||
<div className="mb-3 md:mb-0">
|
||||
<DashboardReport Record={col.widget} />
|
||||
<DashboardReport
|
||||
Record={col.widget}
|
||||
/>
|
||||
</div>
|
||||
</Suspense>
|
||||
);
|
||||
@@ -1,21 +1,24 @@
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import "../../styles/opensigndrive.css";
|
||||
import axios from "axios";
|
||||
import * as ContextMenu from "@radix-ui/react-context-menu";
|
||||
import { saveAs } from "file-saver";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { ContextMenu } from "radix-ui";
|
||||
import { useNavigate } from "react-router";
|
||||
import Table from "react-bootstrap/Table";
|
||||
import * as HoverCard from "@radix-ui/react-hover-card";
|
||||
import { HoverCard } from "radix-ui";
|
||||
import ModalUi from "../../primitives/ModalUi";
|
||||
import FolderModal from "../shared/fields/FolderModal";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { handleDownloadPdf, isMobile } from "../../constant/Utils";
|
||||
import Parse from "parse";
|
||||
|
||||
function DriveBody(props) {
|
||||
const { t } = useTranslation();
|
||||
const [rename, setRename] = useState("");
|
||||
const [renameValue, setRenameValue] = useState("");
|
||||
const inputRef = useRef(null);
|
||||
const [isOpenMoveModal, setIsOpenMoveModal] = useState(false);
|
||||
const [selectDoc, setSelectDoc] = useState();
|
||||
const [isDeleteDoc, setIsDeleteDoc] = useState(false);
|
||||
const [isDeleteDoc, setIsDeleteDoc] = useState({});
|
||||
const contextMenu = [
|
||||
{ type: "Download", icon: "fa-light fa-arrow-down" },
|
||||
{ type: "Rename", icon: "fa-light fa-font" },
|
||||
@@ -42,7 +45,7 @@ function DriveBody(props) {
|
||||
props.setFolderName((prev) => [...prev, folderData]);
|
||||
props.setIsLoading({
|
||||
isLoad: true,
|
||||
message: "This might take some time"
|
||||
message: t("loading-mssg")
|
||||
});
|
||||
props.setDocId(data.objectId);
|
||||
props.setPdfData([]);
|
||||
@@ -68,11 +71,9 @@ function DriveBody(props) {
|
||||
});
|
||||
props.setPdfData(updatedData);
|
||||
props.sortingData(null, null, updatedData);
|
||||
await axios
|
||||
.put(
|
||||
`${localStorage.getItem(
|
||||
"baseUrl"
|
||||
)}classes/contracts_Document/${docId}`,
|
||||
try {
|
||||
await axios.put(
|
||||
`${localStorage.getItem("baseUrl")}classes/contracts_Document/${docId}`,
|
||||
updateName,
|
||||
{
|
||||
headers: {
|
||||
@@ -81,33 +82,30 @@ function DriveBody(props) {
|
||||
"X-Parse-Session-Token": localStorage.getItem("accesstoken")
|
||||
}
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
// const res = result.data;
|
||||
// console.log("res", res);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log("Err ", err);
|
||||
props.setIsAlert({
|
||||
isShow: true,
|
||||
alertMessage: "something went wrong"
|
||||
});
|
||||
);
|
||||
} catch (err) {
|
||||
console.log("Err ", err);
|
||||
props.setIsAlert({
|
||||
isShow: true,
|
||||
alertMessage: t("something-went-wrong-mssg")
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//function for navigate user to microapp-signature component
|
||||
const checkPdfStatus = async (data) => {
|
||||
const signerExist = data.Signers && data.Signers;
|
||||
const isDecline = data.IsDeclined && data.IsDeclined;
|
||||
const isPlaceholder = data.Placeholders && data.Placeholders;
|
||||
const signedUrl = data.SignedUrl;
|
||||
const signerExist = data?.Signers;
|
||||
const isDecline = data?.IsDeclined;
|
||||
const isPlaceholder = data?.Placeholders;
|
||||
const signedUrl = data?.SignedUrl;
|
||||
const isSignYourself = data?.IsSignyourself;
|
||||
//checking if document has completed and request signature flow
|
||||
if (data?.IsCompleted && signerExist?.length > 0) {
|
||||
navigate(`/recipientSignPdf/${data.objectId}`);
|
||||
}
|
||||
//checking if document has completed and signyour-self flow
|
||||
else if (!signerExist && !isPlaceholder) {
|
||||
else if ((!signerExist && !isPlaceholder) || isSignYourself) {
|
||||
navigate(`/signaturePdf/${data.objectId}`);
|
||||
}
|
||||
//checking if document has declined by someone
|
||||
@@ -134,12 +132,10 @@ function DriveBody(props) {
|
||||
}
|
||||
};
|
||||
|
||||
const handleMenuItemClick = (selectType, data) => {
|
||||
const handleMenuItemClick = async (selectType, data, deleteType) => {
|
||||
switch (selectType) {
|
||||
case "Download": {
|
||||
const pdfName = data && data.Name;
|
||||
const pdfUrl = data && data.SignedUrl ? data.SignedUrl : data.URL;
|
||||
saveAs(pdfUrl, `${sanitizeFileName(pdfName)}_signed_by_OpenSign™.pdf`);
|
||||
await handleDownloadPdf([data]);
|
||||
break;
|
||||
}
|
||||
case "Rename": {
|
||||
@@ -148,7 +144,7 @@ function DriveBody(props) {
|
||||
break;
|
||||
}
|
||||
case "Delete": {
|
||||
setIsDeleteDoc(true);
|
||||
setIsDeleteDoc({ status: true, deleteType });
|
||||
setSelectDoc(data);
|
||||
break;
|
||||
}
|
||||
@@ -162,7 +158,7 @@ function DriveBody(props) {
|
||||
};
|
||||
//function for delete document
|
||||
const handleDeleteDocument = async (docData) => {
|
||||
setIsDeleteDoc(false);
|
||||
setIsDeleteDoc({});
|
||||
const docId = docData.objectId;
|
||||
const data = {
|
||||
IsArchive: true
|
||||
@@ -191,7 +187,7 @@ function DriveBody(props) {
|
||||
console.log("Err ", err);
|
||||
props.setIsAlert({
|
||||
isShow: true,
|
||||
alertMessage: "something went wrong"
|
||||
alertMessage: t("something-went-wrong-mssg")
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -243,7 +239,6 @@ function DriveBody(props) {
|
||||
)
|
||||
|
||||
.then((Listdata) => {
|
||||
// console.log("Listdata ", Listdata);
|
||||
const res = Listdata.data;
|
||||
if (res) {
|
||||
const updatedData = props.pdfData.filter(
|
||||
@@ -258,22 +253,60 @@ function DriveBody(props) {
|
||||
|
||||
setIsOpenMoveModal(false);
|
||||
} else {
|
||||
alert("folder already exist!");
|
||||
alert(t("folder-already-exist!"));
|
||||
setIsOpenMoveModal(false);
|
||||
}
|
||||
};
|
||||
|
||||
const sanitizeFileName = (pdfName) => {
|
||||
// Replace spaces with underscore
|
||||
return pdfName.replace(/ /g, "_");
|
||||
};
|
||||
|
||||
const handleEnterPress = (e, data) => {
|
||||
if (e.key === "Enter") {
|
||||
handledRenameDoc(data);
|
||||
}
|
||||
};
|
||||
|
||||
const checkFolderEmpty = async (docData) => {
|
||||
let isEmptyFolder = true;
|
||||
const query = new Parse.Query("contracts_Document");
|
||||
query.equalTo("Folder", {
|
||||
__type: "Pointer",
|
||||
className: "contracts_Document",
|
||||
objectId: docData.objectId
|
||||
});
|
||||
query.notEqualTo("IsArchive", true);
|
||||
const res = await query.find();
|
||||
const jsonRes = JSON.parse(JSON.stringify(res));
|
||||
if (jsonRes && jsonRes.length > 0) {
|
||||
isEmptyFolder = false;
|
||||
return isEmptyFolder;
|
||||
} else {
|
||||
return isEmptyFolder;
|
||||
}
|
||||
};
|
||||
const handleDeleteFolder = async (docData) => {
|
||||
setIsDeleteDoc({});
|
||||
const isEmptyFolder = await checkFolderEmpty(docData);
|
||||
if (isEmptyFolder) {
|
||||
const docId = docData?.objectId;
|
||||
try {
|
||||
const updateQuery = new Parse.Query("contracts_Document");
|
||||
const updateObj = await updateQuery.get(docId);
|
||||
updateObj.set("IsArchive", true);
|
||||
const res = await updateObj.save();
|
||||
if (res) {
|
||||
const updatedData = props.pdfData.filter((x) => x.objectId !== docId);
|
||||
props.setPdfData(updatedData);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("Err ", err);
|
||||
props.setIsAlert({
|
||||
isShow: true,
|
||||
alertMessage: t("something-went-wrong-mssg")
|
||||
});
|
||||
}
|
||||
} else {
|
||||
alert(t("delete-folder-alert-1"));
|
||||
}
|
||||
};
|
||||
|
||||
//component to handle type of document and render according to type
|
||||
const handleFolderData = (data, ind, listType) => {
|
||||
let createddate, status, isDecline, signerExist, isComplete;
|
||||
@@ -307,15 +340,18 @@ function DriveBody(props) {
|
||||
}
|
||||
|
||||
const signersName = () => {
|
||||
const getSignersName = signerExist.map((data) => data.Name);
|
||||
const signerName = getSignersName.join(", ");
|
||||
const getSignersName =
|
||||
signerExist?.length > 0 && signerExist?.map((data) => data?.Name || "");
|
||||
const signerName =
|
||||
getSignersName?.length > 0 ? getSignersName?.join(", ") : "";
|
||||
|
||||
return (
|
||||
<span className="text-[12px] font-medium w-[90%] break-words">
|
||||
{signerName}
|
||||
{signerName && signerName}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
return listType === "table" ? (
|
||||
data.Type === "Folder" ? (
|
||||
<tr onClick={() => handleOnclikFolder(data)}>
|
||||
@@ -323,14 +359,14 @@ function DriveBody(props) {
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 512 512"
|
||||
className="w-[26px] h-[26px] fill-current op-text-secondary"
|
||||
className="w-[26px] h-[26px] fill-current"
|
||||
>
|
||||
<path d="M64 480H448c35.3 0 64-28.7 64-64V160c0-35.3-28.7-64-64-64H288c-10.1 0-19.6-4.7-25.6-12.8L243.2 57.6C231.1 41.5 212.1 32 192 32H64C28.7 32 0 60.7 0 96V416c0 35.3 28.7 64 64 64z" />
|
||||
</svg>
|
||||
<span className="text-[12px] font-medium">{data.Name}</span>
|
||||
</td>
|
||||
<td>_</td>
|
||||
<td>Folder</td>
|
||||
<td>{t("folder")}</td>
|
||||
<td>_</td>
|
||||
<td>_</td>
|
||||
</tr>
|
||||
@@ -347,41 +383,44 @@ function DriveBody(props) {
|
||||
<span className="text-[12px] font-medium">{data.Name}</span>
|
||||
</td>
|
||||
<td>{createddate}</td>
|
||||
<td>Pdf</td>
|
||||
<td>{status}</td>
|
||||
<td>{t("pdf")}</td>
|
||||
<td>{t(`drive-document-status.${status}`)}</td>
|
||||
<td>
|
||||
<i
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleMenuItemClick("Download", data);
|
||||
}}
|
||||
className="fa-light fa-download mr-[8px] op-text-primary"
|
||||
className="fa-light fa-download mr-[8px] op-text-primary cursor-pointer"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
) : listType === "list" && data.Type === "Folder" ? (
|
||||
<div key={ind} className="relative w-[100px] h-[100px] mx-2 my-3">
|
||||
<div className="relative w-[100px] h-[100px] mx-2 my-3">
|
||||
<ContextMenu.Root>
|
||||
<ContextMenu.Trigger className="flex flex-col justify-center items-center">
|
||||
<ContextMenu.Trigger className="flex flex-col justify-center items-center select-none-cls">
|
||||
{/* folder */}
|
||||
<div
|
||||
data-tut={props.dataTutSeventh}
|
||||
onClick={() => handleOnclikFolder(data)}
|
||||
onClick={() => {
|
||||
if (!rename) {
|
||||
handleOnclikFolder(data);
|
||||
}
|
||||
}}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 512 512"
|
||||
className="w-[100px] h-[100px] fill-current op-text-secondary"
|
||||
className="w-[100px] h-[100px] fill-current"
|
||||
>
|
||||
<path d="M64 480H448c35.3 0 64-28.7 64-64V160c0-35.3-28.7-64-64-64H288c-10.1 0-19.6-4.7-25.6-12.8L243.2 57.6C231.1 41.5 212.1 32 192 32H64C28.7 32 0 60.7 0 96V416c0 35.3 28.7 64 64 64z" />
|
||||
</svg>
|
||||
{rename === data.objectId ? (
|
||||
<input
|
||||
onFocus={() => {
|
||||
inputRef.current.setSelectionRange(0, 0);
|
||||
const input = inputRef.current;
|
||||
if (input) {
|
||||
input.select();
|
||||
@@ -393,16 +432,14 @@ function DriveBody(props) {
|
||||
onKeyDown={(e) => handleEnterPress(e, data)}
|
||||
ref={inputRef}
|
||||
defaultValue={renameValue}
|
||||
// value={renameValue}
|
||||
onChange={(e) => setRenameValue(e.target.value)}
|
||||
className="w-[100px] border-[1.5px] border-black rounded-sm text-[10px]"
|
||||
className="op-input op-input-bordered op-input-xs w-[100px] focus:outline-none hover:border-base-content text-[10px]"
|
||||
/>
|
||||
) : (
|
||||
<span className="fileName">{data.Name}</span>
|
||||
<span className="fileName select-none-cls">{data.Name}</span>
|
||||
)}
|
||||
</div>
|
||||
</ContextMenu.Trigger>
|
||||
|
||||
<ContextMenu.Portal>
|
||||
<ContextMenu.Content
|
||||
className="ContextMenuContent"
|
||||
@@ -414,23 +451,41 @@ function DriveBody(props) {
|
||||
className="ContextMenuItem"
|
||||
>
|
||||
<i className="fa-light fa-font mr-[8px]"></i>
|
||||
<span>Rename</span>
|
||||
<span>{t(`context-menu.Rename`)}</span>
|
||||
</ContextMenu.Item>
|
||||
<ContextMenu.Item
|
||||
onClick={() => handleMenuItemClick("Delete", data, data.Type)}
|
||||
className="ContextMenuItem"
|
||||
>
|
||||
<i className="fa-light fa-trash mr-[8px]"></i>
|
||||
<span>{t(`context-menu.Delete`)}</span>
|
||||
</ContextMenu.Item>
|
||||
</ContextMenu.Content>
|
||||
</ContextMenu.Portal>
|
||||
</ContextMenu.Root>
|
||||
</div>
|
||||
) : (
|
||||
<HoverCard.Root openDelay={0} closeDelay={100}>
|
||||
<HoverCard.Root
|
||||
open={rename || isMobile ? false : undefined}
|
||||
openDelay={0}
|
||||
closeDelay={100}
|
||||
>
|
||||
<HoverCard.Trigger asChild>
|
||||
<div>
|
||||
<ContextMenu.Root>
|
||||
<div className="relative w-[100px] h-[100px] mx-2 my-3">
|
||||
<ContextMenu.Trigger className="flex flex-col justify-center items-center">
|
||||
<ContextMenu.Trigger
|
||||
asChild
|
||||
className="flex flex-col justify-center items-center select-none-cls"
|
||||
>
|
||||
{/* pdf */}
|
||||
<div
|
||||
data-tut={props.dataTutSixth}
|
||||
onClick={() => checkPdfStatus(data)}
|
||||
onClick={() => {
|
||||
if (!rename) {
|
||||
checkPdfStatus(data);
|
||||
}
|
||||
}}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<svg
|
||||
@@ -445,22 +500,22 @@ function DriveBody(props) {
|
||||
autoFocus={true}
|
||||
type="text"
|
||||
onFocus={() => {
|
||||
inputRef.current.setSelectionRange(0, 0);
|
||||
const input = inputRef.current;
|
||||
if (input) {
|
||||
input.select();
|
||||
}
|
||||
}}
|
||||
onBlur={() => handledRenameDoc(data)}
|
||||
onKeyDown={(e) => handleEnterPress(e, data)}
|
||||
onKeyDown={(e) => handleEnterPress(e, data, data.Type)}
|
||||
ref={inputRef}
|
||||
defaultValue={renameValue}
|
||||
// value={renameValue}
|
||||
onChange={(e) => setRenameValue(e.target.value)}
|
||||
className="w-[100px] border-[1.5px] border-black rounded-sm text-[10px]"
|
||||
className="op-input op-input-bordered op-input-xs w-[100px] focus:outline-none hover:border-base-content text-[10px]"
|
||||
/>
|
||||
) : (
|
||||
<span className="fileName">{data.Name}</span>
|
||||
<span className="fileName select-none-cls">
|
||||
{data.Name}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</ContextMenu.Trigger>
|
||||
@@ -488,7 +543,6 @@ function DriveBody(props) {
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ContextMenu.Portal>
|
||||
<ContextMenu.Content
|
||||
className="ContextMenuContent"
|
||||
@@ -503,7 +557,9 @@ function DriveBody(props) {
|
||||
className="ContextMenuItem"
|
||||
>
|
||||
<i className={menu.icon}></i>
|
||||
<span className="ml-[8px]">{menu.type}</span>
|
||||
<span className="ml-[8px]">
|
||||
{t(`context-menu.${menu.type}`)}
|
||||
</span>
|
||||
</ContextMenu.Item>
|
||||
);
|
||||
})}
|
||||
@@ -514,22 +570,31 @@ function DriveBody(props) {
|
||||
</HoverCard.Trigger>
|
||||
<HoverCard.Portal>
|
||||
<HoverCard.Content className="HoverCardContent" sideOffset={5}>
|
||||
<strong className="text-[13px]">Title: </strong>
|
||||
<strong className="text-[13px]">
|
||||
{t("report-heading.Title")}:{" "}
|
||||
</strong>
|
||||
<span className="text-[12px] font-medium mb-0"> {data.Name}</span>
|
||||
<br />
|
||||
<strong className="text-[13px]">Status: </strong>
|
||||
<span className="text-[12px] font-medium"> {status}</span>
|
||||
<strong className="text-[13px]">
|
||||
{t("report-heading.Status")}:{" "}
|
||||
</strong>
|
||||
<span className="text-[12px] font-medium">
|
||||
{t(`drive-document-status.${status}`)}
|
||||
</span>
|
||||
<br />
|
||||
<strong className="text-[13px]">Created Date: </strong>
|
||||
<strong className="text-[13px]">
|
||||
{t("report-heading.created-date")}:{" "}
|
||||
</strong>
|
||||
<span className="text-[12px] font-medium">{createddate}</span>
|
||||
<br />
|
||||
{signerExist && (
|
||||
<>
|
||||
<strong className="text-[13px]">Signers: </strong>
|
||||
<strong className="text-[13px]">
|
||||
{t("report-heading.Signers")}:{" "}
|
||||
</strong>
|
||||
{signersName()}
|
||||
</>
|
||||
)}
|
||||
|
||||
<HoverCard.Arrow className="HoverCardArrow" />
|
||||
</HoverCard.Content>
|
||||
</HoverCard.Portal>
|
||||
@@ -545,29 +610,29 @@ function DriveBody(props) {
|
||||
<Table striped bordered hover>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Created Date</th>
|
||||
<th>Type</th>
|
||||
<th>Status</th>
|
||||
<th>Action</th>
|
||||
<th>{t("report-heading.Name")}</th>
|
||||
<th>{t("report-heading.created-date")}</th>
|
||||
<th>{t("report-heading.Type")}</th>
|
||||
<th>{t("report-heading.Status")}</th>
|
||||
<th>{t("action")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{props.pdfData.map((data, ind) => {
|
||||
return (
|
||||
<React.Fragment key={ind}>
|
||||
{handleFolderData(data, ind, "table")}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
{props?.pdfData?.map((data, ind) => (
|
||||
<React.Fragment key={ind}>
|
||||
{handleFolderData(data, ind, "table")}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-row flex-wrap items-center mt-1 pb-[20px] mx-[5px]">
|
||||
{props.pdfData.map((data, ind) => {
|
||||
return <div key={ind}>{handleFolderData(data, ind, "list")}</div>;
|
||||
})}
|
||||
{props?.pdfData?.map((data, ind) => (
|
||||
<React.Fragment key={ind}>
|
||||
{handleFolderData(data, ind, "list")}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -581,26 +646,37 @@ function DriveBody(props) {
|
||||
/>
|
||||
)}
|
||||
<ModalUi
|
||||
isOpen={isDeleteDoc}
|
||||
title={"Delete Document"}
|
||||
handleClose={() => setIsDeleteDoc(false)}
|
||||
isOpen={isDeleteDoc.status}
|
||||
title={t("delete-document")}
|
||||
handleClose={() => setIsDeleteDoc({})}
|
||||
>
|
||||
<div className="h-full p-[20px] text-base-content">
|
||||
<p>Are you sure you want to delete this document?</p>
|
||||
{isDeleteDoc.deleteType ? (
|
||||
<p>{t("delete-folder-alert")}</p>
|
||||
) : (
|
||||
<p>{t("delete-document-alert")}</p>
|
||||
)}
|
||||
|
||||
<div className="h-[1px] w-full bg-[#9f9f9f] my-[15px]"></div>
|
||||
<button
|
||||
onClick={() => handleDeleteDocument(selectDoc)}
|
||||
onClick={() => {
|
||||
if (isDeleteDoc.deleteType) {
|
||||
handleDeleteFolder(selectDoc);
|
||||
} else {
|
||||
handleDeleteDocument(selectDoc);
|
||||
}
|
||||
}}
|
||||
type="button"
|
||||
className="op-btn op-btn-primary mr-2"
|
||||
>
|
||||
Yes
|
||||
{t("yes")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setIsDeleteDoc(false)}
|
||||
onClick={() => setIsDeleteDoc({})}
|
||||
type="button"
|
||||
className="op-btn op-btn-neutral"
|
||||
>
|
||||
No
|
||||
{t("no")}
|
||||
</button>
|
||||
</div>
|
||||
</ModalUi>
|
||||
@@ -1,39 +1,41 @@
|
||||
import React from "react";
|
||||
import ModalUi from "../../primitives/ModalUi";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const AddRoleModal = (props) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<ModalUi
|
||||
title={"Add Role"}
|
||||
title={t("add-role")}
|
||||
isOpen={props.isModalRole}
|
||||
handleClose={props.handleCloseRoleModal}
|
||||
>
|
||||
<div className="h-full py-[10px] px-[20px]">
|
||||
<div className="text-base-content h-full py-[10px] px-[20px]">
|
||||
<form className="flex flex-col" onSubmit={props.handleAddRole}>
|
||||
<input
|
||||
value={props.roleName}
|
||||
onChange={(e) => props.setRoleName(e.target.value)}
|
||||
placeholder={
|
||||
props.signersdata.length > 0
|
||||
? "User " + (props.signersdata.length + 1)
|
||||
: "User 1"
|
||||
? "Role " + (props.signersdata.length + 1)
|
||||
: "Role 1"
|
||||
}
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs mt-1"
|
||||
/>
|
||||
<p className="text-[gray] text-[11px] mt-[5px] mb-[10px] ml-[10px]">
|
||||
e.g: Customer, Hr, Director, Manager, Student, etc...
|
||||
{t("role-ex")}..
|
||||
</p>
|
||||
<div>
|
||||
<div className="h-[1px] w-full bg-[#9f9f9f] mb-[10px]"></div>
|
||||
<button type="submit" className="op-btn op-btn-primary">
|
||||
Add
|
||||
{t("add")}
|
||||
</button>
|
||||
<button
|
||||
onClick={props.handleCloseRoleModal}
|
||||
type="button"
|
||||
className="op-btn op-btn-ghost ml-2"
|
||||
className="op-btn op-btn-ghost text-base-content ml-2"
|
||||
>
|
||||
Close
|
||||
{t("close")}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -0,0 +1,133 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import ModalUi from "../../primitives/ModalUi";
|
||||
|
||||
function AgreementContent(props) {
|
||||
const { t } = useTranslation();
|
||||
const appName =
|
||||
"OpenSign™";
|
||||
const h2Style = "text-base-content font-medium text-lg";
|
||||
const ulStyle = "list-disc px-4 py-3";
|
||||
const handleOnclick = () => {
|
||||
props.setIsAgree(true);
|
||||
props.setIsShowAgreeTerms(false);
|
||||
props.showFirstWidget();
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<ModalUi
|
||||
isOpen={true}
|
||||
title={t("term-cond-title")}
|
||||
handleClose={() => props.setIsShowAgreeTerms(false)}
|
||||
>
|
||||
<div className="h-[100%] p-[20px]">
|
||||
<h2 className={h2Style}>{t("term-cond-h")}</h2>
|
||||
<span className="mt-2">
|
||||
{t("term-cond-p1", { appName: appName })}
|
||||
</span>
|
||||
<hr className="bg-[#9f9f9f] w-full my-[15px]" />
|
||||
<h2 className={h2Style}>{t("term-cond-h1")}</h2>
|
||||
<span className="mt-2">
|
||||
{t("term-cond-p2", { appName: appName })}
|
||||
</span>
|
||||
<ul className={ulStyle}>
|
||||
<li>{t("term-cond-p3", { appName: appName })}</li>
|
||||
<li>{t("term-cond-p4")}</li>
|
||||
</ul>
|
||||
<hr className="bg-[#9f9f9f] w-full my-[15px]" />
|
||||
<h2 className={h2Style}>{t("term-cond-h2")}</h2>
|
||||
<span className="mt-2">{t("term-cond-p5")}</span>
|
||||
<ul className={ulStyle}>
|
||||
<li>{t("term-cond-p6", { appName: appName })}</li>
|
||||
<li>{t("term-cond-p7", { appName: appName })}</li>
|
||||
<li>{t("term-cond-p8")}</li>
|
||||
</ul>
|
||||
<hr className="bg-[#9f9f9f] w-full my-[15px]" />
|
||||
<h2 className={h2Style}>{t("term-cond-h3")}</h2>
|
||||
<span className="mt-2">{t("term-cond-p9")}</span>
|
||||
<ul className={ulStyle}>
|
||||
<li>{t("term-cond-p10")}</li>
|
||||
<li>{t("term-cond-p11")}</li>
|
||||
</ul>
|
||||
<hr className="bg-[#9f9f9f] w-full my-[15px]" />
|
||||
<h2 className={h2Style}>{t("term-cond-h4")}</h2>
|
||||
<span className="mt-2">
|
||||
{t("term-cond-p12", { appName: appName })}
|
||||
</span>
|
||||
<ul className={ulStyle}>
|
||||
<li>{t("term-cond-p13")}</li>
|
||||
<li>{t("term-cond-p14")}</li>
|
||||
<li>{t("term-cond-p15")}</li>
|
||||
</ul>
|
||||
<span>{t("term-cond-p16", { appName: appName })}</span>
|
||||
<hr className="bg-[#9f9f9f] w-full my-[15px]" />
|
||||
<h2 className={h2Style}>{t("term-cond-h5")}</h2>
|
||||
<p>{t("term-cond-p17", { appName: appName })}</p>
|
||||
<p className="mt-2">{t("term-cond-p18")}</p>
|
||||
<hr className="bg-[#9f9f9f] w-full my-[15px]" />
|
||||
<h2 className={h2Style}>{t("term-cond-h6")}</h2>
|
||||
<span className="mt-2">{t("term-cond-p19")}</span>
|
||||
<ul className={ulStyle}>
|
||||
<li>{t("term-cond-p20")}</li>
|
||||
<li>{t("term-cond-p21")}</li>
|
||||
<li>{t("term-cond-p22", { appName: appName })}</li>
|
||||
</ul>
|
||||
<hr className="bg-[#9f9f9f] w-full my-[15px]" />
|
||||
<h2 className={h2Style}>{t("term-cond-h7")}</h2>
|
||||
<span className="mt-2">
|
||||
{t("term-cond-p23", { appName: appName })}
|
||||
</span>
|
||||
<ul className={ulStyle}>
|
||||
<li>{t("term-cond-p24")}</li>
|
||||
<li>{t("term-cond-p25")}</li>
|
||||
</ul>
|
||||
<hr className="bg-[#9f9f9f] w-full my-[15px]" />
|
||||
<h2 className={h2Style}>{t("term-cond-h8")}</h2>
|
||||
<span className="mt-2">
|
||||
{t("term-cond-p26", { appName: appName })}
|
||||
</span>
|
||||
<hr className="bg-[#9f9f9f] w-full my-[15px]" />
|
||||
<h2 className={h2Style}>{t("term-cond-h9")}</h2>
|
||||
<span className="mt-2">
|
||||
{t("term-cond-p27", { appName: appName })}
|
||||
</span>
|
||||
<hr className="bg-[#9f9f9f] w-full my-[15px]" />
|
||||
<span className="mt-2 font-medium">
|
||||
{t("term-cond-p28", { appName: appName })}
|
||||
</span>
|
||||
<hr className="bg-[#9f9f9f] w-full my-[15px]" />
|
||||
<span className="mt-2">
|
||||
{t("term-cond-p29", { appName: appName })}
|
||||
</span>
|
||||
<a
|
||||
href="www.opensignlabs.com"
|
||||
target="_blank"
|
||||
className="text-blue-700 cursor-pointer"
|
||||
>
|
||||
www.opensignlabs.com
|
||||
</a>
|
||||
|
||||
<span>{t("term-cond-p30")}</span>
|
||||
<span className="font-medium"> support@opensignlabs.com </span>
|
||||
<hr className="bg-[#9f9f9f] w-full my-[15px]" />
|
||||
<div className="mt-6 flex justify-start gap-2">
|
||||
<button
|
||||
onClick={() => handleOnclick()}
|
||||
className="op-btn op-btn-primary"
|
||||
>
|
||||
{t("agrre-button")}
|
||||
</button>
|
||||
<button
|
||||
className="op-btn op-btn-ghost text-base-content"
|
||||
onClick={() => props.setIsShowAgreeTerms(false)}
|
||||
>
|
||||
{t("close")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ModalUi>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AgreementContent;
|
||||
@@ -0,0 +1,54 @@
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import AgreementContent from "./AgreementContent";
|
||||
|
||||
function AgreementSign(props) {
|
||||
const { t } = useTranslation();
|
||||
const [isShowAgreeTerms, setIsShowAgreeTerms] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="op-modal op-modal-open absolute z-[448]">
|
||||
<div className="w-[95%] md:w-[60%] lg:w-[40%] op-modal-box overflow-y-auto hide-scrollbar text-sm p-4">
|
||||
<div className="flex flex-row items-center">
|
||||
<div className="text-[11px] md:text-base text-base-content">
|
||||
<span>{t("agree-p1")}</span>
|
||||
<span
|
||||
className="font-bold text-blue-600 cursor-pointer"
|
||||
onClick={() => {
|
||||
setIsShowAgreeTerms(true);
|
||||
}}
|
||||
>
|
||||
{t("agree-p2")}
|
||||
</span>
|
||||
<span> {t("agree-p3")}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex mt-3">
|
||||
<button
|
||||
onClick={() => {
|
||||
props.setIsAgree(true);
|
||||
props.showFirstWidget();
|
||||
}}
|
||||
className="op-btn op-btn-primary op-btn-sm w-full md:w-auto"
|
||||
>
|
||||
{t("agrre-button")}
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-2 text-base-content">
|
||||
<span className="text-[11px]">{t("agreement-note")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{isShowAgreeTerms && (
|
||||
<AgreementContent
|
||||
setIsAgree={props.setIsAgree}
|
||||
setIsShowAgreeTerms={setIsShowAgreeTerms}
|
||||
showFirstWidget={props.showFirstWidget}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default AgreementSign;
|
||||
@@ -1,13 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
function BorderResize({ right, top }) {
|
||||
return (
|
||||
<div
|
||||
className={`${right ? `-right-[12px]` : "-right-[1px]"} ${
|
||||
top ? `-bottom-[11px]` : "-bottom-[1px]"
|
||||
} absolute inline-block w-[14px] h-[14px] hover:cursor-sw-resize border-r-[3px] border-b-[3px] border-[#188ae2]`}
|
||||
></div>
|
||||
);
|
||||
}
|
||||
|
||||
export default BorderResize;
|
||||
@@ -0,0 +1,23 @@
|
||||
import React from "react";
|
||||
|
||||
function BorderResize(props) {
|
||||
const getHeight = () => {
|
||||
const height = props.posHeight(props.pos, props.isSignYourself);
|
||||
if (height > 14) {
|
||||
return "14px";
|
||||
} else {
|
||||
return `${height}px`;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{ width: getHeight() || "14px", height: getHeight() || "14px" }}
|
||||
className={`${props.right ? `-right-[12px]` : "-right-[2px]"} ${
|
||||
props.top ? `-bottom-[12px]` : "-bottom-[2px]"
|
||||
} absolute inline-block hover:cursor-sw-resize border-r-[3px] border-b-[3px] border-[#188ae2]`}
|
||||
></div>
|
||||
);
|
||||
}
|
||||
|
||||
export default BorderResize;
|
||||
@@ -0,0 +1,104 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import ModalUi from "../../primitives/ModalUi";
|
||||
import { fontsizeArr, fontColorArr } from "../../constant/Utils";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function CellsSettingModal({
|
||||
isOpen,
|
||||
handleClose,
|
||||
defaultData,
|
||||
handleSave
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [name, setName] = useState("");
|
||||
const [cellCount, setCellCount] = useState(5);
|
||||
const [fontSize, setFontSize] = useState(12);
|
||||
const [fontColor, setFontColor] = useState("black");
|
||||
|
||||
useEffect(() => {
|
||||
if (defaultData) {
|
||||
setName(defaultData.options?.name || "Cells");
|
||||
setCellCount(defaultData.options?.cellCount || 5);
|
||||
setFontSize(defaultData.options?.fontSize || 12);
|
||||
setFontColor(defaultData.options?.fontColor || "black");
|
||||
}
|
||||
}, [defaultData]);
|
||||
|
||||
const onSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
handleSave &&
|
||||
handleSave({
|
||||
name,
|
||||
cellCount: parseInt(cellCount, 10),
|
||||
fontSize,
|
||||
fontColor
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalUi isOpen={isOpen} handleClose={handleClose} title={t("widget-info")}>
|
||||
<form onSubmit={onSubmit} className="p-[20px] text-base-content flex flex-col gap-3">
|
||||
<div>
|
||||
<label htmlFor="name" className="text-[13px]">
|
||||
{t("name")} <span className="text-[red]">*</span>
|
||||
</label>
|
||||
<input
|
||||
required
|
||||
name="name"
|
||||
className="op-input op-input-bordered op-input-sm w-full text-xs focus:outline-none hover:border-base-content"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="cellCount" className="text-[13px]">
|
||||
{t("cell-count")}
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
name="cellCount"
|
||||
className="op-input op-input-bordered op-input-sm w-full text-xs focus:outline-none hover:border-base-content"
|
||||
value={cellCount}
|
||||
onChange={(e) => setCellCount(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col md:flex-row md:items-center gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="whitespace-nowrap">{t("font-size")}: </span>
|
||||
<select
|
||||
className="ml-[7px] w-[60%] op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content text-xs"
|
||||
value={fontSize}
|
||||
onChange={(e) => setFontSize(parseInt(e.target.value))}
|
||||
>
|
||||
{fontsizeArr.map((size, ind) => (
|
||||
<option className="text-[13px]" value={size} key={ind}>
|
||||
{size}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<span>{t("color")}: </span>
|
||||
<select
|
||||
className="ml-[33px] md:ml-4 w-[65%] md:w-full op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content text-xs"
|
||||
value={fontColor}
|
||||
onChange={(e) => setFontColor(e.target.value)}
|
||||
>
|
||||
{fontColorArr.map((color, ind) => (
|
||||
<option value={color} key={ind}>
|
||||
{t(`color-type.${color}`)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<span className="w-5 h-[19px] ml-1" style={{ background: fontColor }} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-[1px] w-full bg-[#b7b3b3] my-[16px]"></div>
|
||||
<button type="submit" className="op-btn op-btn-primary">
|
||||
{t("save")}
|
||||
</button>
|
||||
</form>
|
||||
</ModalUi>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
|
||||
const Cell = ({
|
||||
isEnabled,
|
||||
count,
|
||||
h,
|
||||
value,
|
||||
editable,
|
||||
onChange,
|
||||
onKeyDown,
|
||||
onBlur,
|
||||
inputRef,
|
||||
index,
|
||||
fontSize,
|
||||
fontColor,
|
||||
hint
|
||||
}) => (
|
||||
<div
|
||||
className={`${isEnabled ? "bg-white border-gray-400" : "select-none-cls pointer-events-none border-gray-500"} flex items-center justify-center border-[1px]`}
|
||||
style={{ flex: `0 0 ${100 / count}%`, height: h }}
|
||||
>
|
||||
<input
|
||||
disabled={!isEnabled}
|
||||
maxLength={1}
|
||||
value={value}
|
||||
readOnly={!editable}
|
||||
ref={inputRef}
|
||||
onChange={editable ? (e) => onChange && onChange(e, index) : undefined}
|
||||
onKeyDown={editable ? (e) => onKeyDown && onKeyDown(e, index) : undefined}
|
||||
// trigger validation when leaving a cell
|
||||
onBlur={editable ? (e) => onBlur && onBlur(e, index) : undefined}
|
||||
className={`${isEnabled ? "placeholder-gray-300" : "placeholder-gray-500"} w-full text-center focus:outline-none bg-transparent`}
|
||||
placeholder={hint}
|
||||
style={{ fontFamily: "Arial, sans-serif", fontSize, color: fontColor }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default function CellsWidget({
|
||||
isEnabled,
|
||||
count = 8,
|
||||
height = 40,
|
||||
value = "",
|
||||
editable = false,
|
||||
onChange,
|
||||
onKeyDown,
|
||||
onBlur,
|
||||
onCellCountChange,
|
||||
inputRefs,
|
||||
resizable = false,
|
||||
fontSize = "12px",
|
||||
fontColor = "black",
|
||||
hint = ""
|
||||
}) {
|
||||
const [cellCount, setCellCount] = useState(count);
|
||||
|
||||
// keep internal state in sync with prop updates
|
||||
useEffect(() => setCellCount(count), [count]);
|
||||
|
||||
const startX = useRef(0);
|
||||
const startCount = useRef(cellCount);
|
||||
|
||||
const capture = (downEv, onMove, onUp = () => {}) => {
|
||||
const id = downEv.pointerId;
|
||||
const move = (ev) => id === ev.pointerId && onMove(ev);
|
||||
const up = (ev) => {
|
||||
if (id !== ev.pointerId) return;
|
||||
onUp(ev);
|
||||
downEv.target.releasePointerCapture(id);
|
||||
window.removeEventListener("pointermove", move);
|
||||
window.removeEventListener("pointerup", up);
|
||||
};
|
||||
window.addEventListener("pointermove", move);
|
||||
window.addEventListener("pointerup", up);
|
||||
downEv.target.setPointerCapture(id);
|
||||
};
|
||||
|
||||
const onTopHandlePointerDown = (ev) => {
|
||||
// Prevent triggering the widget drag logic
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
startX.current = ev.clientX;
|
||||
startCount.current = cellCount;
|
||||
capture(ev, (moveEv) => {
|
||||
const dx = moveEv.clientX - startX.current;
|
||||
const delta = Math.floor(-dx / 5);
|
||||
let newCount = Math.max(1, startCount.current + delta);
|
||||
if (newCount !== cellCount) {
|
||||
setCellCount(newCount);
|
||||
onCellCountChange?.(newCount);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const cells = Array.from({ length: cellCount }).map((_, i) => value[i] || "");
|
||||
const hints = Array.from({ length: cellCount }).map((_, i) => hint[i] || "");
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative flex w-full h-full overflow-visible"
|
||||
style={{ height }}
|
||||
>
|
||||
{resizable && (
|
||||
<div
|
||||
className="cell-size-handle absolute left-1/2 -translate-x-1/2 -bottom-4 rotate-180 cursor-ew-resize touch-none"
|
||||
onPointerDown={onTopHandlePointerDown}
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4 text-blue-600"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="m9.69 18.933.003.001C9.89 19.02 10 19 10 19s.11.02.308-.066l.002-.001.006-.003.018-.008a5.741 5.741 0 0 0 .281-.14c.186-.096.446-.24.757-.433.62-.384 1.445-.966 2.274-1.765C15.302 14.988 17 12.493 17 9A7 7 0 1 0 3 9c0 3.492 1.698 5.988 3.355 7.584a13.731 13.731 0 0 0 2.273 1.765 11.842 11.842 0 0 0 .976.544l.062.029.018.008.006.003ZM10 11.25a2.25 2.25 0 1 0 0-4.5 2.25 2.25 0 0 0 0 4.5Z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
{cells.map((val, i) => (
|
||||
<Cell
|
||||
key={i}
|
||||
isEnabled={isEnabled}
|
||||
count={cellCount}
|
||||
h={height}
|
||||
value={val}
|
||||
editable={editable}
|
||||
onChange={onChange}
|
||||
onKeyDown={onKeyDown}
|
||||
onBlur={onBlur}
|
||||
inputRef={inputRefs ? (el) => (inputRefs.current[i] = el) : undefined}
|
||||
index={i}
|
||||
fontSize={fontSize}
|
||||
fontColor={fontColor}
|
||||
hint={hints[i]}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,276 +0,0 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import opensignLogo from "../../assets/images/logo.png";
|
||||
import {
|
||||
Page,
|
||||
Text,
|
||||
View,
|
||||
Document,
|
||||
StyleSheet,
|
||||
Image
|
||||
} from "@react-pdf/renderer";
|
||||
|
||||
function Certificate({ pdfData }) {
|
||||
const [isMultiSigners, setIsMultiSigners] = useState();
|
||||
const [multiSigner, setMultiSigners] = useState([]);
|
||||
const [isLoad, setIsLoad] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
handleSignerData();
|
||||
// eslint-disable-next-line
|
||||
}, []);
|
||||
|
||||
const handleSignerData = () => {
|
||||
const checkSigners = pdfData.filter((data) => data.Signers);
|
||||
if (checkSigners && checkSigners.length > 0) {
|
||||
setIsMultiSigners(true);
|
||||
|
||||
const checkSignSigners =
|
||||
pdfData[0].AuditTrail &&
|
||||
pdfData[0].AuditTrail.length > 0 &&
|
||||
pdfData[0].AuditTrail.filter((data) => data.Activity === "Signed");
|
||||
|
||||
setMultiSigners(checkSignSigners);
|
||||
} else {
|
||||
setIsMultiSigners(false);
|
||||
}
|
||||
setIsLoad(true);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
page: {
|
||||
borderRadius: "5px",
|
||||
padding: "10px",
|
||||
backgroundColor: "white"
|
||||
},
|
||||
section1: {
|
||||
border: "1px solid rgb(177, 174, 174)",
|
||||
padding: "20px"
|
||||
},
|
||||
textStyle: {
|
||||
fontWeight: "bold",
|
||||
fontSize: "11px",
|
||||
marginBottom: "10px"
|
||||
},
|
||||
textStyle2: {
|
||||
fontWeight: "600",
|
||||
fontSize: "11px",
|
||||
marginBottom: "10px",
|
||||
color: "gray"
|
||||
},
|
||||
image: {
|
||||
width: "71px",
|
||||
height: "17px"
|
||||
}
|
||||
});
|
||||
|
||||
const generatedDate = () => {
|
||||
const newDate = new Date();
|
||||
const utcTime = newDate.toUTCString();
|
||||
|
||||
return (
|
||||
<Text
|
||||
style={{
|
||||
color: "gray",
|
||||
fontSize: "10px"
|
||||
}}
|
||||
>
|
||||
Generated On {utcTime}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
const changeCompletedDate = () => {
|
||||
const completedOn = pdfData[0].updatedAt;
|
||||
const newDate = new Date(completedOn);
|
||||
const utcTime = newDate.toUTCString();
|
||||
|
||||
return <Text style={styles.textStyle2}>{utcTime}</Text>;
|
||||
};
|
||||
|
||||
const signerName = (data) => {
|
||||
const getSignerName = pdfData[0].Signers.filter(
|
||||
(sign) => sign.objectId === data.UserPtr.objectId
|
||||
);
|
||||
|
||||
return (
|
||||
getSignerName[0] &&
|
||||
getSignerName.length > 0 && (
|
||||
<>
|
||||
<Text style={styles.textStyle}>
|
||||
Name :
|
||||
<Text style={styles.textStyle2}>{getSignerName[0].Name}</Text>
|
||||
</Text>
|
||||
<Text style={styles.textStyle}>
|
||||
Email :
|
||||
<Text style={styles.textStyle2}>{getSignerName[0].Email}</Text>
|
||||
</Text>
|
||||
</>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
isLoad && (
|
||||
<Document>
|
||||
{/** Page defines a single page of content. */}
|
||||
<Page size="A4" style={styles.page}>
|
||||
<View style={styles.section1}>
|
||||
<View
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginBottom: "30px"
|
||||
}}
|
||||
>
|
||||
<Image src={opensignLogo} style={styles.image} />
|
||||
{generatedDate()}
|
||||
</View>
|
||||
|
||||
<View style={{ justifyContent: "center" }}>
|
||||
<Text
|
||||
style={{
|
||||
textAlign: "center",
|
||||
fontSize: "20px",
|
||||
fontWeight: "bold",
|
||||
color: "#31bceb",
|
||||
marginBottom: "10px"
|
||||
}}
|
||||
>
|
||||
{" "}
|
||||
Certificate of Completion
|
||||
</Text>
|
||||
<View style={{ border: "1px solid #bdbbbb" }}></View>
|
||||
<View>
|
||||
<View>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: "16px",
|
||||
fontWeight: "bold",
|
||||
color: "#31bceb",
|
||||
margin: "10px 0px 10px 0px"
|
||||
}}
|
||||
>
|
||||
Summary
|
||||
</Text>
|
||||
</View>
|
||||
<View style={{ display: "flex", flexDirection: "column" }}>
|
||||
<Text style={styles.textStyle}>
|
||||
Document ID :
|
||||
<Text style={styles.textStyle2}>{pdfData[0].objectId}</Text>
|
||||
</Text>
|
||||
<Text style={styles.textStyle}>
|
||||
Document Name :
|
||||
<Text style={styles.textStyle2}>{pdfData[0].Name}</Text>
|
||||
</Text>
|
||||
<Text style={styles.textStyle}>
|
||||
Organization :
|
||||
<Text style={styles.textStyle2}>
|
||||
{pdfData[0].ExtUserPtr.Company}
|
||||
</Text>
|
||||
</Text>
|
||||
<Text style={styles.textStyle}>
|
||||
Completed on : {changeCompletedDate()}
|
||||
</Text>
|
||||
{multiSigner && multiSigner.length > 0 && (
|
||||
<Text style={styles.textStyle}>
|
||||
Signers :
|
||||
<Text style={styles.textStyle2}>
|
||||
{multiSigner.length}
|
||||
</Text>
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
{isMultiSigners ? (
|
||||
<View style={{ display: "flex", flexDirection: "column" }}>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: "16px",
|
||||
fontWeight: "bold",
|
||||
color: "#31bceb",
|
||||
margin: "10px 0px 10px 0px"
|
||||
}}
|
||||
>
|
||||
Recipients
|
||||
</Text>
|
||||
|
||||
<View>
|
||||
{multiSigner &&
|
||||
multiSigner.map((data, ind) => {
|
||||
return (
|
||||
<View
|
||||
key={ind}
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
border: "0.4px solid #bdbbbb",
|
||||
marginBottom: "10px"
|
||||
}}
|
||||
></View>
|
||||
{signerName(data)}
|
||||
|
||||
<Text style={styles.textStyle}>
|
||||
Accessed from :
|
||||
<Text style={styles.textStyle2}>
|
||||
{data.ipAddress}
|
||||
</Text>
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
</View>
|
||||
) : (
|
||||
<View style={{ display: "flex", flexDirection: "column" }}>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: "16px",
|
||||
fontWeight: "bold",
|
||||
color: "#31bceb",
|
||||
margin: "10px 0px 10px 0px"
|
||||
}}
|
||||
>
|
||||
Recipients
|
||||
</Text>
|
||||
<Text style={styles.textStyle}>
|
||||
Signers : <Text style={styles.textStyle2}>1</Text>
|
||||
</Text>
|
||||
<Text style={styles.textStyle}>
|
||||
Name :
|
||||
<Text style={styles.textStyle2}>
|
||||
{pdfData[0].ExtUserPtr.Name}
|
||||
</Text>
|
||||
</Text>
|
||||
<Text style={styles.textStyle}>
|
||||
Email :
|
||||
<Text style={styles.textStyle2}>
|
||||
{pdfData[0].ExtUserPtr.Email}
|
||||
</Text>
|
||||
</Text>
|
||||
<Text style={styles.textStyle}>
|
||||
Accessed from :
|
||||
<Text style={styles.textStyle2}>
|
||||
{pdfData[0].AuditTrail &&
|
||||
pdfData[0].AuditTrail[0].ipAddress}
|
||||
</Text>
|
||||
</Text>
|
||||
|
||||
<Text style={styles.textStyle}>
|
||||
Signed on : {changeCompletedDate()}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</Page>
|
||||
</Document>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default Certificate;
|
||||
@@ -0,0 +1,160 @@
|
||||
import { useRef, useState } from "react";
|
||||
import ModalUi from "../../primitives/ModalUi";
|
||||
import { EmailBody } from "./EmailBody";
|
||||
import {
|
||||
contractDocument,
|
||||
defaultMailBody,
|
||||
defaultMailSubject,
|
||||
sendEmailToSigners
|
||||
} from "../../constant/Utils";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Loader from "../../primitives/Loader";
|
||||
import { useNavigate } from "react-router";
|
||||
|
||||
function CustomizeMail(props) {
|
||||
const { t } = useTranslation();
|
||||
const editorRef = useRef();
|
||||
const navigate = useNavigate();
|
||||
const copyUrlRef = useRef(null);
|
||||
const [isCustomize, setIsCustomize] = useState(false);
|
||||
const [isLoader, setIsLoader] = useState(false);
|
||||
|
||||
const handleCloseSendmailModal = () => {
|
||||
if (props?.handleClose) {
|
||||
props?.handleClose();
|
||||
return;
|
||||
}
|
||||
props?.setIsMailModal(false);
|
||||
navigate("/report/1MwEuxLEkF");
|
||||
};
|
||||
const handleOnchangeRequest = () => {
|
||||
if (editorRef.current) {
|
||||
const html = editorRef.current.editor.root.innerHTML;
|
||||
props?.setCustomizeMail((prev) => ({
|
||||
...prev,
|
||||
body: html
|
||||
}));
|
||||
}
|
||||
};
|
||||
const handleEmailSendToSigners = async () => {
|
||||
setIsLoader(true);
|
||||
const documentData = await contractDocument(props?.documentId);
|
||||
if (documentData && documentData?.length > 0) {
|
||||
props?.setDocumentDetails(documentData[0]);
|
||||
if (
|
||||
documentData?.[0]?.SendinOrder &&
|
||||
documentData?.[0]?.SendinOrder === true
|
||||
) {
|
||||
const ownerEmail = documentData[0].ExtUserPtr.Email;
|
||||
const ownerDetails = documentData[0].Signers.find(
|
||||
(x) => x.Email === ownerEmail
|
||||
);
|
||||
props?.setCurrUserId(ownerDetails?.objectId);
|
||||
}
|
||||
//function is used to send email to signers for sign the document
|
||||
const mailRes = await sendEmailToSigners(
|
||||
documentData,
|
||||
props?.signerList,
|
||||
props?.customizeMail,
|
||||
props?.defaultMail,
|
||||
isCustomize,
|
||||
);
|
||||
props?.setIsMailModal(false);
|
||||
props?.setIsSend(true);
|
||||
setIsLoader(false);
|
||||
if (mailRes?.status === "success") {
|
||||
props?.setMailStatus("success");
|
||||
} else if (mailRes?.status === "quota-reached") {
|
||||
props?.setMailStatus("quotareached");
|
||||
} else if (mailRes?.status === "daily-quota-reached") {
|
||||
props?.setMailStatus("dailyquotareached");
|
||||
} else {
|
||||
props?.setMailStatus("failed");
|
||||
}
|
||||
// setMailStatus(mail_status);
|
||||
} else {
|
||||
alert("something-went-wrong-mssg");
|
||||
}
|
||||
};
|
||||
return (
|
||||
<>
|
||||
{isLoader ? (
|
||||
<div className="absolute w-full h-full flex justify-center items-center bg-black/30 rounded-box z-30">
|
||||
<Loader />
|
||||
</div>
|
||||
) : (
|
||||
<ModalUi
|
||||
isOpen={props?.isMailModal}
|
||||
title={t("send-mail")}
|
||||
handleClose={() => handleCloseSendmailModal()}
|
||||
>
|
||||
<div className="max-h-96 overflow-y-scroll scroll-hide p-[20px] text-base-content">
|
||||
{!isCustomize && <span>{t("placeholder-alert-3")}</span>}
|
||||
{
|
||||
isCustomize && (
|
||||
<>
|
||||
<EmailBody
|
||||
editorRef={editorRef}
|
||||
requestBody={props?.customizeMail.body}
|
||||
requestSubject={props?.customizeMail.subject}
|
||||
handleOnchangeRequest={handleOnchangeRequest}
|
||||
setCustomizeMail={props?.setCustomizeMail}
|
||||
/>
|
||||
<div
|
||||
className="flex justify-end items-center gap-1 mt-2 op-link op-link-primary"
|
||||
onClick={() => {
|
||||
props?.setCustomizeMail({
|
||||
subject: defaultMailSubject,
|
||||
body: defaultMailBody
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span>{t("reset-to-default")}</span>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
<div className="flex flex-row items-center gap-2 md:gap-6 mt-2">
|
||||
<div className="flex flex-row gap-2">
|
||||
<button
|
||||
onClick={() => handleEmailSendToSigners()}
|
||||
className="op-btn op-btn-primary font-[500] text-sm shadow"
|
||||
>
|
||||
{t("send")}
|
||||
</button>
|
||||
{isCustomize && (
|
||||
<button
|
||||
onClick={() => setIsCustomize(false)}
|
||||
className="op-btn op-btn-ghost font-[500] text-sm"
|
||||
>
|
||||
{t("close")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{
|
||||
!isCustomize && (
|
||||
<span
|
||||
className="op-link op-link-accent text-sm"
|
||||
onClick={() => setIsCustomize(!isCustomize)}
|
||||
>
|
||||
{t("cutomize-email")}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center items-center mt-3">
|
||||
<span className="h-[1px] w-[20%] bg-[#ccc]"></span>
|
||||
<span className="ml-[5px] mr-[5px]">{t("or")}</span>
|
||||
<span className="h-[1px] w-[20%] bg-[#ccc]"></span>
|
||||
</div>
|
||||
<div className="my-3">{props?.handleShareList()}</div>
|
||||
<p id="copyUrl" ref={copyUrlRef} className="hidden"></p>
|
||||
</div>
|
||||
</ModalUi>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default CustomizeMail;
|
||||
@@ -1,44 +0,0 @@
|
||||
import React from "react";
|
||||
function DefaultSignature({ defaultSignImg, xyPostion, setDefaultSignAlert }) {
|
||||
const confirmToaddDefaultSign = () => {
|
||||
if (xyPostion.length > 0) {
|
||||
setDefaultSignAlert({
|
||||
isShow: true,
|
||||
alertMessage:
|
||||
"Are you sure you want to auto sign at requested all locations?"
|
||||
});
|
||||
} else {
|
||||
setDefaultSignAlert({
|
||||
isShow: true,
|
||||
alertMessage: "please select position!"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div data-tut="reactourThird">
|
||||
<div className="mx-2 pr-2 pt-2 pb-1 text-[15px] text-base-content font-semibold border-b-[1px] border-base-300">
|
||||
Signature
|
||||
</div>
|
||||
<div className="flex flex-col items-center mt-[10px] font-semibold">
|
||||
<p className="text-base-content">Your Signature</p>
|
||||
<div className="op-card shadow-md h-[111px] w-[90%] p-2">
|
||||
<img
|
||||
alt="signature"
|
||||
className="w-full h-full object-contain"
|
||||
src={defaultSignImg}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="op-btn op-btn-primary op-btn-sm mt-[10px]"
|
||||
onClick={() => confirmToaddDefaultSign()}
|
||||
>
|
||||
Auto Sign All
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DefaultSignature;
|
||||
@@ -0,0 +1,118 @@
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSelector } from "react-redux";
|
||||
function DefaultSignature(props) {
|
||||
const { t } = useTranslation();
|
||||
const defaultSignImg = useSelector((state) => state.widget.defaultSignImg);
|
||||
const myInitial = useSelector((state) => state.widget.myInitial);
|
||||
const tabName = ["my-signature", "my-initials"];
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
const confirmToaddDefaultSign = (type) => {
|
||||
if (props?.xyPosition.length > 0) {
|
||||
//check signature or initial widgets exist or not for auto signing
|
||||
const getCurrentSignerXY = props?.xyPosition.filter(
|
||||
(data) => data.Id === props.uniqueId
|
||||
);
|
||||
const checkIsSignInitialExist = getCurrentSignerXY?.every(
|
||||
(placeholderObj) =>
|
||||
placeholderObj?.placeHolder?.some((placeholder) =>
|
||||
placeholder?.pos?.some((posItem) => posItem?.type === type)
|
||||
)
|
||||
);
|
||||
if (checkIsSignInitialExist) {
|
||||
props?.setDefaultSignAlert({
|
||||
isShow: true,
|
||||
alertMessage: t("default-sign-alert", { widgetsType: type }),
|
||||
type: type
|
||||
});
|
||||
} else {
|
||||
props?.setDefaultSignAlert({
|
||||
isShow: true,
|
||||
alertMessage: t("defaultSign-alert", { widgetsType: type })
|
||||
});
|
||||
}
|
||||
} else {
|
||||
props?.setDefaultSignAlert({
|
||||
isShow: true,
|
||||
alertMessage: t("please-select-position!")
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div data-tut="reactourThird">
|
||||
<div className="mx-2 pr-2 pt-2 pb-1 text-[15px] text-base-content font-semibold border-b-[1px] border-base-300">
|
||||
<p className="text-base-content">{t("signature")}</p>
|
||||
</div>
|
||||
<div className="flex justify-center items-center mt-2">
|
||||
<div role="tablist" className="op-tabs op-tabs-bordered">
|
||||
{tabName.map((tabData, ind) => (
|
||||
<div
|
||||
onClick={() => setActiveTab(ind)}
|
||||
key={ind}
|
||||
role="tab"
|
||||
className={`${
|
||||
activeTab === ind ? "op-tab-active" : ""
|
||||
} op-tab flex items-center pb-10 md:pb-0`}
|
||||
>
|
||||
<span className="ml-1 text-[7px] font-medium md:font-normal md:text-[12px]">
|
||||
{t(`${tabData}`)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-center mt-[10px] font-semibold relative">
|
||||
<div className="op-card shadow-md h-[111px] w-[90%] p-2">
|
||||
{activeTab === 0 ? (
|
||||
<img
|
||||
alt="signature"
|
||||
className="w-full h-full object-contain"
|
||||
src={defaultSignImg}
|
||||
/>
|
||||
) : (
|
||||
activeTab === 1 &&
|
||||
(myInitial ? (
|
||||
<img
|
||||
alt="signature"
|
||||
className="w-full h-full object-contain"
|
||||
src={myInitial}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex justify-center items-center h-full">
|
||||
<span>{t("initial-alert")}</span>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="op-btn op-btn-primary op-btn-sm mt-[10px]"
|
||||
onClick={() =>
|
||||
confirmToaddDefaultSign(
|
||||
activeTab === 0 ? "signature" : activeTab === 1 && "initials"
|
||||
)
|
||||
}
|
||||
disabled={
|
||||
activeTab === 0 && !props?.isDefault
|
||||
? true
|
||||
: activeTab === 1 && !myInitial
|
||||
? true
|
||||
: false
|
||||
}
|
||||
>
|
||||
{t("auto-sign-all")}
|
||||
</button>
|
||||
{!props.isDefault && (
|
||||
<div className="absolute bg-black/70 text-white w-full h-full flex items-center justify-center text-[11px] cursor-default">
|
||||
<span className="-rotate-45">
|
||||
Document creator has disabled this option
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DefaultSignature;
|
||||
@@ -2,17 +2,19 @@ import React, { useEffect, useState } from "react";
|
||||
import LoaderWithMsg from "../../primitives/LoaderWithMsg";
|
||||
import { contractDocument } from "../../constant/Utils";
|
||||
import HandleError from "../../primitives/HandleError";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import { useLocation, useNavigate } from "react-router";
|
||||
import { useTranslation } from "react-i18next";
|
||||
function useQuery() {
|
||||
return new URLSearchParams(useLocation().search);
|
||||
}
|
||||
function DraftDocument() {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const query = useQuery();
|
||||
const docId = query.get("docId");
|
||||
const [isLoading, setIsLoading] = useState({
|
||||
isLoader: true,
|
||||
message: "This might take some time"
|
||||
message: t("loading-mssg")
|
||||
});
|
||||
useEffect(() => {
|
||||
getDocumentDetails();
|
||||
@@ -28,15 +30,19 @@ function DraftDocument() {
|
||||
documentData === "Error: Something went wrong!" ||
|
||||
(documentData.result && documentData.result.error)
|
||||
) {
|
||||
setIsLoading({
|
||||
isLoader: false,
|
||||
message: "Error: Something went wrong!"
|
||||
});
|
||||
if (documentData?.result?.error?.includes("deleted")) {
|
||||
setIsLoading({
|
||||
isLoader: false,
|
||||
message: t("document-deleted")
|
||||
});
|
||||
} else {
|
||||
setIsLoading({
|
||||
isLoader: false,
|
||||
message: t("something-went-wrong-mssg")
|
||||
});
|
||||
}
|
||||
} else {
|
||||
setIsLoading({
|
||||
isLoader: false,
|
||||
message: "No data found!"
|
||||
});
|
||||
setIsLoading({ isLoader: false, message: t("no-data") });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -52,7 +58,7 @@ function DraftDocument() {
|
||||
navigate(`/recipientSignPdf/${data.objectId}`);
|
||||
}
|
||||
//checking if document has completed and signyour-self flow
|
||||
else if (!signerExist && !isPlaceholder) {
|
||||
else if ((!signerExist && !isPlaceholder) || data?.IsSignyourself) {
|
||||
navigate(`/signaturePdf/${data.objectId}`);
|
||||
}
|
||||
//checking if document has declined by someone
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import { getWidgetType, widgets } from "../../constant/Utils";
|
||||
import { widgets } from "../../constant/Utils";
|
||||
import getWidgetType from "./getWidgetType";
|
||||
|
||||
function DragElement(item) {
|
||||
const getWidgets = widgets;
|
||||
@@ -0,0 +1,76 @@
|
||||
import { useDragLayer } from "react-dnd";
|
||||
import { defaultWidthHeight } from "../../constant/Utils";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
function DragGuideLinesLayer(props) {
|
||||
const { showGuidelines } = props;
|
||||
const prevDragState = useRef({ isDragging: false, x: 0, y: 0 });
|
||||
const { isDraggingWidgets, offset, itemType } = useDragLayer((monitor) => ({
|
||||
isDraggingWidgets: monitor.isDragging(),
|
||||
offset: monitor.getClientOffset(),
|
||||
itemType: monitor.getItem()
|
||||
}));
|
||||
useEffect(() => {
|
||||
if (!offset) return;
|
||||
//getting container by id
|
||||
const container = document.getElementById("container");
|
||||
if (!container) return;
|
||||
if (itemType?.text) {
|
||||
// Calculate the position of the dragged element relative to the container
|
||||
// Get the container's bounding rectangle (position and size in the viewport)
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
|
||||
// Compute the X and Y coordinates of the dragged item inside the container
|
||||
// by subtracting the container's top-left offset from the current drag offset
|
||||
const x = offset.x - containerRect.left;
|
||||
const y = offset.y - containerRect.top;
|
||||
|
||||
// Check if there’s any change in the dragging state or the dragged element’s position
|
||||
// Compare the current drag state with the previously stored one
|
||||
const hasStateChanged =
|
||||
prevDragState.current.isDragging !== isDraggingWidgets || // Drag started or stopped
|
||||
prevDragState.current.x !== x || // X position changed
|
||||
prevDragState.current.y !== y; // Y position changed
|
||||
|
||||
if (hasStateChanged) {
|
||||
// Update the previous drag state with the latest values
|
||||
prevDragState.current = { isDragging: isDraggingWidgets, x, y };
|
||||
|
||||
if (isDraggingWidgets) {
|
||||
// Adjust the current drag coordinates relative to the initial widgets button position
|
||||
const getXPosition = props?.signBtnPosition?.[0]
|
||||
? x - props.signBtnPosition[0].xPos
|
||||
: x;
|
||||
const getYPosition = props?.signBtnPosition?.[0]
|
||||
? y - props.signBtnPosition[0].yPos
|
||||
: y;
|
||||
|
||||
// Get the default width and height of the dragged checkbox/radio widget type
|
||||
const el = document.getElementById("checkbox&radio-preview");
|
||||
let rect;
|
||||
if (el) {
|
||||
rect = el.getBoundingClientRect();
|
||||
}
|
||||
const widget = {
|
||||
type: itemType?.text,
|
||||
width: defaultWidthHeight(itemType?.text).width,
|
||||
height: defaultWidthHeight(itemType?.text).height
|
||||
};
|
||||
|
||||
const getWidth =
|
||||
rect?.width || props?.posWidth(widget, props?.isSignYourself);
|
||||
const getHeight =
|
||||
rect?.height || props?.posHeight(widget, props?.isSignYourself);
|
||||
|
||||
// Display the alignment guidelines based on the current drag position and widget size
|
||||
showGuidelines(true, getXPosition, getYPosition, getWidth, getHeight);
|
||||
} else {
|
||||
// Hide guidelines when dragging stops
|
||||
showGuidelines(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [isDraggingWidgets, offset, itemType, showGuidelines]);
|
||||
}
|
||||
|
||||
export default DragGuideLinesLayer;
|
||||
@@ -1,381 +0,0 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { isEnableSubscription } from "../../constant/const";
|
||||
import ModalUi from "../../primitives/ModalUi";
|
||||
import { radioButtonWidget } from "../../constant/Utils";
|
||||
import Upgrade from "../../primitives/Upgrade";
|
||||
function DropdownWidgetOption(props) {
|
||||
const [dropdownOptionList, setDropdownOptionList] = useState([
|
||||
"option-1",
|
||||
"option-2"
|
||||
]);
|
||||
const [minCount, setMinCount] = useState(0);
|
||||
const [maxCount, setMaxCount] = useState(0);
|
||||
const [dropdownName, setDropdownName] = useState(props.type);
|
||||
const [isReadOnly, setIsReadOnly] = useState(false);
|
||||
const [isHideLabel, setIsHideLabel] = useState(false);
|
||||
const [status, setStatus] = useState("required");
|
||||
const [defaultValue, setDefaultValue] = useState("");
|
||||
const statusArr = ["required", "optional"];
|
||||
const [defaultCheckbox, setDefaultCheckbox] = useState([]);
|
||||
|
||||
const resetState = () => {
|
||||
setDropdownOptionList(["option-1", "option-2"]);
|
||||
setDropdownName(props.type);
|
||||
setIsReadOnly(false);
|
||||
setIsHideLabel(false);
|
||||
setMinCount(0);
|
||||
setMaxCount(0);
|
||||
setDefaultCheckbox([]);
|
||||
setDefaultValue("");
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
props.currWidgetsDetails?.options?.name &&
|
||||
props.currWidgetsDetails?.options?.values
|
||||
) {
|
||||
setDropdownName(props.currWidgetsDetails?.options?.name);
|
||||
setDropdownOptionList(props.currWidgetsDetails?.options?.values);
|
||||
setMinCount(
|
||||
props.currWidgetsDetails?.options?.validation?.minRequiredCount
|
||||
);
|
||||
setMaxCount(
|
||||
props.currWidgetsDetails?.options?.validation?.maxRequiredCount
|
||||
);
|
||||
setIsReadOnly(props.currWidgetsDetails?.options?.isReadOnly);
|
||||
setIsHideLabel(props.currWidgetsDetails?.options?.isHideLabel);
|
||||
setStatus(props.currWidgetsDetails?.options?.status || "required");
|
||||
setDefaultValue(props.currWidgetsDetails?.options?.defaultValue || "");
|
||||
setDefaultCheckbox(props.currWidgetsDetails?.options?.defaultValue || []);
|
||||
} else {
|
||||
setStatus("required");
|
||||
resetState();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [props.currWidgetsDetails]);
|
||||
const handleInputChange = (index, value) => {
|
||||
setDropdownOptionList((prevInputs) => {
|
||||
const newInputs = [...prevInputs];
|
||||
newInputs[index] = value;
|
||||
return newInputs;
|
||||
});
|
||||
};
|
||||
|
||||
//function add add checkbox option and add width of checkbox
|
||||
const handleAddInput = () => {
|
||||
const deleteOption = false;
|
||||
const addOption = true;
|
||||
setDropdownOptionList((prevInputs) => [...prevInputs, ""]);
|
||||
props.handleSaveWidgetsOptions(
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
addOption,
|
||||
deleteOption
|
||||
);
|
||||
};
|
||||
|
||||
//function add add checkbox option and delete width of checkbox
|
||||
const handleDeleteInput = (ind) => {
|
||||
const deleteOption = true;
|
||||
const addOption = false;
|
||||
const getUpdatedOptions = dropdownOptionList.filter(
|
||||
(data, index) => index !== ind
|
||||
);
|
||||
setDropdownOptionList(getUpdatedOptions);
|
||||
props.handleSaveWidgetsOptions(
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
addOption,
|
||||
deleteOption
|
||||
);
|
||||
};
|
||||
|
||||
const handleSaveOption = () => {
|
||||
const defaultData =
|
||||
defaultCheckbox && defaultCheckbox.length > 0
|
||||
? defaultCheckbox
|
||||
: defaultValue;
|
||||
|
||||
props.handleSaveWidgetsOptions(
|
||||
dropdownName,
|
||||
dropdownOptionList,
|
||||
minCount,
|
||||
maxCount,
|
||||
isReadOnly,
|
||||
null,
|
||||
null,
|
||||
status,
|
||||
defaultData,
|
||||
isHideLabel
|
||||
);
|
||||
// props.setShowDropdown(false);
|
||||
setDropdownOptionList(["option-1", "option-2"]);
|
||||
setDropdownName(props.type);
|
||||
// props.setCurrWidgetsDetails({});
|
||||
setIsReadOnly(false);
|
||||
setIsHideLabel(false);
|
||||
setMinCount(0);
|
||||
setMaxCount(0);
|
||||
setDefaultCheckbox([]);
|
||||
setDefaultValue("");
|
||||
};
|
||||
|
||||
const handleSetMinMax = (e) => {
|
||||
const minValue = e.target.value;
|
||||
if (minValue > dropdownOptionList.length) {
|
||||
return "";
|
||||
} else {
|
||||
return minValue;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDefaultCheck = (index) => {
|
||||
const getDefaultCheck = defaultCheckbox.includes(index);
|
||||
if (getDefaultCheck) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalUi isOpen={props.showDropdown} title={props.title} showClose={false}>
|
||||
<div className="h-full p-[15px] text-base-content">
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleSaveOption();
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<label className="text-[13px] font-semibold">
|
||||
Name<span className="text-[red] text-[13px]"> *</span>
|
||||
</label>
|
||||
<input
|
||||
required
|
||||
defaultValue={dropdownName}
|
||||
value={dropdownName}
|
||||
onChange={(e) => setDropdownName(e.target.value)}
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
/>
|
||||
|
||||
<label className="text-[13px] font-semibold mt-[5px]">
|
||||
Options
|
||||
</label>
|
||||
<div className="flex flex-col">
|
||||
{dropdownOptionList.map((option, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex flex-row mb-[5px] items-center"
|
||||
>
|
||||
{props.type === "checkbox" && !props.isSignYourself && (
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={handleDefaultCheck(index)}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
const getDefaultCheck =
|
||||
defaultCheckbox?.includes(index);
|
||||
if (!getDefaultCheck) {
|
||||
setDefaultCheckbox((prev) => [...prev, index]);
|
||||
}
|
||||
} else {
|
||||
const removeOption = defaultCheckbox.filter(
|
||||
(data) => data !== index
|
||||
);
|
||||
setDefaultCheckbox(removeOption);
|
||||
}
|
||||
}}
|
||||
className="op-checkbox focus:outline-none hover:border-base-content mr-[5px]"
|
||||
/>
|
||||
)}
|
||||
<input
|
||||
required
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
type="text"
|
||||
value={option}
|
||||
onChange={(e) => handleInputChange(index, e.target.value)}
|
||||
/>
|
||||
|
||||
<i
|
||||
className="fa-light fa-rectangle-xmark text-[25px] ml-[10px] text-accent cursor-pointer"
|
||||
onClick={() => handleDeleteInput(index)}
|
||||
></i>
|
||||
</div>
|
||||
))}
|
||||
<i
|
||||
onClick={handleAddInput}
|
||||
className="fa-light fa-square-plus text-[25px] ml-[10px] op-text-primary cursor-pointer"
|
||||
></i>
|
||||
{isEnableSubscription && (
|
||||
<div>
|
||||
{props.type === "checkbox" && !props.isSignYourself && (
|
||||
<>
|
||||
<label
|
||||
className={`${
|
||||
!props.isSubscribe ? "text-[gray]" : ""
|
||||
} text-[13px] font-semibold`}
|
||||
>
|
||||
Minimun check
|
||||
</label>
|
||||
{!props.isSubscribe && isEnableSubscription && (
|
||||
<Upgrade />
|
||||
)}
|
||||
<input
|
||||
required
|
||||
defaultValue={0}
|
||||
value={minCount}
|
||||
disabled={props.isSubscribe ? false : true}
|
||||
onChange={(e) => {
|
||||
const count = handleSetMinMax(e);
|
||||
setMinCount(count);
|
||||
}}
|
||||
className={`${
|
||||
props.isSubscribe ? "" : "pointer-events-none"
|
||||
} op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs`}
|
||||
/>
|
||||
<label
|
||||
className={`${
|
||||
!props.isSubscribe ? "text-[gray]" : ""
|
||||
} text-[13px] font-semibold`}
|
||||
>
|
||||
Maximum check
|
||||
</label>
|
||||
<input
|
||||
required
|
||||
defaultValue={0}
|
||||
value={maxCount}
|
||||
disabled={props.isSubscribe ? false : true}
|
||||
onChange={(e) => {
|
||||
const count = handleSetMinMax(e);
|
||||
setMaxCount(count);
|
||||
}}
|
||||
className={`${
|
||||
props.isSubscribe ? "" : "pointer-events-none"
|
||||
} op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs`}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{["dropdown", radioButtonWidget].includes(props.type) && (
|
||||
<>
|
||||
<label className="text-[13px] font-semibold mt-[5px]">
|
||||
Default value
|
||||
</label>
|
||||
<select
|
||||
onChange={(e) => setDefaultValue(e.target.value)}
|
||||
className="op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
name="defaultvalue"
|
||||
value={defaultValue}
|
||||
placeholder="select default value"
|
||||
>
|
||||
<option value="" disabled hidden className="text-[13px]">
|
||||
Select...
|
||||
</option>
|
||||
{dropdownOptionList.map((data, ind) => {
|
||||
return (
|
||||
<option className="text-[13px]" key={ind} value={data}>
|
||||
{data}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</>
|
||||
)}
|
||||
{props.type !== "checkbox" && props.type !== radioButtonWidget && (
|
||||
<>
|
||||
<div className="flex flex-row gap-[10px] mt-[0.5rem]">
|
||||
{statusArr.map((data, ind) => {
|
||||
return (
|
||||
<div
|
||||
key={ind}
|
||||
className="flex flex-row gap-[5px] items-center"
|
||||
>
|
||||
<input
|
||||
className="op-radio op-radio-xs my-1"
|
||||
type="radio"
|
||||
name="status"
|
||||
onChange={() => setStatus(data.toLowerCase())}
|
||||
checked={status.toLowerCase() === data.toLowerCase()}
|
||||
/>
|
||||
<div className="text-[13px] font-500">{data}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{["checkbox", radioButtonWidget].includes(props.type) && (
|
||||
<div className="flex flex-row gap-5 my-2 items-center text-center">
|
||||
{!props.isSignYourself && (
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
id="isreadonly"
|
||||
type="checkbox"
|
||||
checked={isReadOnly}
|
||||
className="op-checkbox op-checkbox-sm"
|
||||
onChange={(e) => setIsReadOnly(e.target.checked)}
|
||||
/>
|
||||
<label className="ml-1 mb-0" htmlFor="isreadonly">
|
||||
Is read only
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
id="ishidelabel"
|
||||
type="checkbox"
|
||||
checked={isHideLabel}
|
||||
className="op-checkbox op-checkbox-sm"
|
||||
onChange={(e) => setIsHideLabel(e.target.checked)}
|
||||
/>
|
||||
|
||||
<label className="ml-1 mb-0" htmlFor="ishidelabel">
|
||||
Hide labels
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={`${
|
||||
props.type === "checkbox" && !props.isSignYourself
|
||||
? "mb-[15px]"
|
||||
: "my-[15px]"
|
||||
} w-full h-[1px] bg-[#9f9f9f]`}
|
||||
></div>
|
||||
|
||||
<button
|
||||
disabled={dropdownOptionList.length === 0 && true}
|
||||
type="submit"
|
||||
className="op-btn op-btn-primary"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
{props.currWidgetsDetails?.options?.values?.length > 0 && (
|
||||
<button
|
||||
type="submit"
|
||||
className="op-btn op-btn-ghost ml-1"
|
||||
onClick={() => {
|
||||
props.handleClose && props.handleClose();
|
||||
resetState();
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
</ModalUi>
|
||||
);
|
||||
}
|
||||
|
||||
export default DropdownWidgetOption;
|
||||
@@ -0,0 +1,446 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import ModalUi from "../../primitives/ModalUi";
|
||||
import { radioButtonWidget } from "../../constant/Utils";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { fontColorArr, fontsizeArr } from "../../constant/Utils";
|
||||
|
||||
function DropdownWidgetOption(props) {
|
||||
const { t } = useTranslation();
|
||||
const [dropdownOptionList, setDropdownOptionList] = useState([
|
||||
"Option-1",
|
||||
"Option-2"
|
||||
]);
|
||||
const [minCount, setMinCount] = useState(0);
|
||||
const [maxCount, setMaxCount] = useState(0);
|
||||
const [dropdownName, setDropdownName] = useState();
|
||||
const [isReadOnly, setIsReadOnly] = useState(false);
|
||||
const [isHideLabel, setIsHideLabel] = useState(false);
|
||||
const [status, setStatus] = useState("required");
|
||||
const [defaultValue, setDefaultValue] = useState("");
|
||||
const [defaultCheckbox, setDefaultCheckbox] = useState([]);
|
||||
const [layout, setLayout] = useState("vertical");
|
||||
const statusArr = ["required", "optional"];
|
||||
const layoutArr = ["vertical", "horizontal"];
|
||||
const isPrefillExist = props?.roleName === "prefill";
|
||||
|
||||
const resetState = () => {
|
||||
setDropdownOptionList(["Option-1", "Option-2"]);
|
||||
setDropdownName(props.currWidgetsDetails?.options?.name || props.type);
|
||||
setIsReadOnly(false);
|
||||
setIsHideLabel(false);
|
||||
setMinCount(0);
|
||||
setMaxCount(0);
|
||||
setDefaultCheckbox([]);
|
||||
setDefaultValue("");
|
||||
setLayout("vertical");
|
||||
};
|
||||
useEffect(() => {
|
||||
if (
|
||||
props.currWidgetsDetails?.options?.name &&
|
||||
props.currWidgetsDetails?.options?.values?.length > 0
|
||||
) {
|
||||
setDropdownName(props.currWidgetsDetails?.options?.name);
|
||||
setDropdownOptionList(props.currWidgetsDetails?.options?.values);
|
||||
setMinCount(
|
||||
props.currWidgetsDetails?.options?.validation?.minRequiredCount
|
||||
);
|
||||
setMaxCount(
|
||||
props.currWidgetsDetails?.options?.validation?.maxRequiredCount
|
||||
);
|
||||
setIsReadOnly(props.currWidgetsDetails?.options?.isReadOnly);
|
||||
setIsHideLabel(props.currWidgetsDetails?.options?.isHideLabel);
|
||||
setStatus(props.currWidgetsDetails?.options?.status || "required");
|
||||
setDefaultValue(props.currWidgetsDetails?.options?.defaultValue || "");
|
||||
setDefaultCheckbox(props.currWidgetsDetails?.options?.defaultValue || []);
|
||||
setLayout(props.currWidgetsDetails?.options?.layout || "vertical");
|
||||
} else {
|
||||
setStatus("required");
|
||||
resetState();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [props.currWidgetsDetails]);
|
||||
const handleInputChange = (index, value) => {
|
||||
setDropdownOptionList((prevInputs) => {
|
||||
const newInputs = [...prevInputs];
|
||||
newInputs[index] = value;
|
||||
return newInputs;
|
||||
});
|
||||
};
|
||||
|
||||
//function add add checkbox option and add width of checkbox
|
||||
const handleAddInput = () => {
|
||||
const deleteOption = false;
|
||||
const addOption = true;
|
||||
setDropdownOptionList((prevInputs) => [...prevInputs, ""]);
|
||||
props.handleSaveWidgetsOptions(
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
addOption,
|
||||
deleteOption
|
||||
);
|
||||
};
|
||||
|
||||
//function add add checkbox option and delete width of checkbox
|
||||
const handleDeleteInput = (ind) => {
|
||||
const deleteOption = true;
|
||||
const addOption = false;
|
||||
const getUpdatedOptions = dropdownOptionList.filter(
|
||||
(_, index) => index !== ind
|
||||
);
|
||||
setDropdownOptionList(getUpdatedOptions);
|
||||
props.handleSaveWidgetsOptions(
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
addOption,
|
||||
deleteOption
|
||||
);
|
||||
};
|
||||
|
||||
const handleSaveOption = () => {
|
||||
if (["checkbox", radioButtonWidget, "dropdown"].includes(props.type)) {
|
||||
const allUnique =
|
||||
new Set(dropdownOptionList).size === dropdownOptionList.length;
|
||||
if (!allUnique) {
|
||||
alert("Please remove duplicate option");
|
||||
return;
|
||||
}
|
||||
}
|
||||
const defaultData =
|
||||
defaultCheckbox && defaultCheckbox.length > 0
|
||||
? defaultCheckbox
|
||||
: defaultValue;
|
||||
|
||||
const isDropdownOrRadio =
|
||||
props?.type === "dropdown" || props?.type === radioButtonWidget;
|
||||
const readOnlyWithoutValue =
|
||||
isReadOnly && !defaultValue && status !== "optional";
|
||||
const isCheckbox = props?.type === "checkbox";
|
||||
const WidgetLayout = ["checkbox", radioButtonWidget].includes(props.type)
|
||||
? layout
|
||||
: null;
|
||||
|
||||
// If it’s a dropdown and it’s read-only without a value (nor marked optional), stop here.
|
||||
if (isDropdownOrRadio && readOnlyWithoutValue) {
|
||||
alert(t("readonly-error", { widgetName: props?.type }));
|
||||
return;
|
||||
} else if (
|
||||
isCheckbox &&
|
||||
isReadOnly &&
|
||||
minCount > 0 &&
|
||||
defaultCheckbox?.length === 0
|
||||
) {
|
||||
alert(t("readonly-error", { widgetName: props?.type }));
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise (either not a dropdown, or a valid dropdown), do the save + reset exactly once.
|
||||
props.handleSaveWidgetsOptions(
|
||||
dropdownName,
|
||||
dropdownOptionList,
|
||||
minCount,
|
||||
maxCount,
|
||||
isReadOnly,
|
||||
null,
|
||||
null,
|
||||
status,
|
||||
defaultData,
|
||||
isHideLabel,
|
||||
WidgetLayout
|
||||
);
|
||||
resetState();
|
||||
};
|
||||
|
||||
|
||||
const handleDefaultCheck = (index) => {
|
||||
const getDefaultCheck = defaultCheckbox.includes(index);
|
||||
if (getDefaultCheck) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
return (
|
||||
<ModalUi isOpen={props.showDropdown} title={props.title} showClose={false}>
|
||||
<div className="h-full p-[15px] text-base-content">
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleSaveOption();
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<label htmlFor="title" className="text-[13px] font-semibold">
|
||||
{t("name")}
|
||||
<span className="text-[red] text-[13px]"> *</span>
|
||||
</label>
|
||||
<input
|
||||
id="title"
|
||||
onInvalid={(e) => e.target.setCustomValidity(t("input-required"))}
|
||||
onInput={(e) => e.target.setCustomValidity("")}
|
||||
value={dropdownName}
|
||||
onChange={(e) => setDropdownName(e.target.value)}
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
required
|
||||
/>
|
||||
|
||||
<label className="text-[13px] font-semibold mt-[5px]">
|
||||
{t("options")}
|
||||
</label>
|
||||
<div className="flex flex-col">
|
||||
{dropdownOptionList?.map((option, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex flex-row mb-[5px] items-center"
|
||||
>
|
||||
{props.type === "checkbox" && props.isShowAdvanceFeature && (
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={handleDefaultCheck(index)}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
const getDefaultCheck =
|
||||
defaultCheckbox?.includes(index);
|
||||
if (!getDefaultCheck) {
|
||||
setDefaultCheckbox((prev) => [...prev, index]);
|
||||
}
|
||||
} else {
|
||||
const removeOption = defaultCheckbox.filter(
|
||||
(data) => data !== index
|
||||
);
|
||||
setDefaultCheckbox(removeOption);
|
||||
}
|
||||
}}
|
||||
className="op-checkbox focus:outline-none hover:border-base-content mr-[5px]"
|
||||
/>
|
||||
)}
|
||||
<input
|
||||
onInvalid={(e) =>
|
||||
e.target.setCustomValidity(t("input-required"))
|
||||
}
|
||||
onInput={(e) => e.target.setCustomValidity("")}
|
||||
required
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
type="text"
|
||||
value={option}
|
||||
onChange={(e) => handleInputChange(index, e.target.value)}
|
||||
/>
|
||||
|
||||
<i
|
||||
className="fa-light fa-rectangle-xmark text-[25px] ml-[10px] text-accent cursor-pointer"
|
||||
onClick={() => handleDeleteInput(index)}
|
||||
></i>
|
||||
</div>
|
||||
))}
|
||||
<div>
|
||||
<i
|
||||
className="fa-light fa-square-plus text-[25px] ml-[10px] op-text-primary cursor-pointer"
|
||||
aria-label="Add option"
|
||||
onClick={handleAddInput}
|
||||
></i>
|
||||
</div>
|
||||
</div>
|
||||
{["dropdown", radioButtonWidget].includes(props.type) &&
|
||||
!isPrefillExist && (
|
||||
<>
|
||||
<label className="text-[13px] font-semibold mt-[5px]">
|
||||
{t("default-value")}
|
||||
</label>
|
||||
<select
|
||||
value={defaultValue}
|
||||
onChange={(e) => setDefaultValue(e.target.value)}
|
||||
className="op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
name="defaultvalue"
|
||||
>
|
||||
<option value="" disabled hidden className="text-[13px]">
|
||||
{t("select")}...
|
||||
</option>
|
||||
{dropdownOptionList.map((data, ind) => {
|
||||
return (
|
||||
<option className="text-[13px]" key={ind} value={data}>
|
||||
{data}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</>
|
||||
)}
|
||||
{props.type !== "checkbox" && !isPrefillExist && (
|
||||
<div className="flex flex-row gap-[10px] mt-[0.5rem]">
|
||||
{statusArr.map((data, ind) => (
|
||||
<div
|
||||
key={ind}
|
||||
className="flex flex-row gap-[5px] items-center"
|
||||
>
|
||||
<input
|
||||
className="op-radio op-radio-xs my-1"
|
||||
type="radio"
|
||||
name="status"
|
||||
onChange={() => setStatus(data.toLowerCase())}
|
||||
checked={status.toLowerCase() === data.toLowerCase()}
|
||||
/>
|
||||
<div className="text-[13px] font-500 capitalize">
|
||||
{data}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center mt-3 mb-3">
|
||||
<span>{t("font-size")} :</span>
|
||||
<select
|
||||
className="ml-[7px] op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content text-xs"
|
||||
value={
|
||||
props.fontSize ||
|
||||
props.currWidgetsDetails?.options?.fontSize ||
|
||||
12
|
||||
}
|
||||
onChange={(e) => props.setFontSize(parseInt(e.target.value))}
|
||||
>
|
||||
{fontsizeArr.map((size, ind) => {
|
||||
return (
|
||||
<option className="text-[13px]" value={size} key={ind}>
|
||||
{size}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
<div className="flex flex-row gap-1 items-center ml-4">
|
||||
<span className="capitalize">{t("color")} : </span>
|
||||
<select
|
||||
value={
|
||||
props.fontColor ||
|
||||
props.currWidgetsDetails?.options?.fontColor ||
|
||||
"black"
|
||||
}
|
||||
onChange={(e) => props.setFontColor(e.target.value)}
|
||||
className="ml-[7px] op-select op-select-bordered op-select-sm focus:outline-none hover:border-base-content text-xs"
|
||||
>
|
||||
{fontColorArr.map((color, ind) => {
|
||||
return (
|
||||
<option value={color} key={ind}>
|
||||
{t(`color-type.${color}`)}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
<span
|
||||
style={{
|
||||
background:
|
||||
props.fontColor ||
|
||||
props.currWidgetsDetails?.options?.fontColor ||
|
||||
"black"
|
||||
}}
|
||||
className="w-5 h-[19px] ml-1"
|
||||
></span>
|
||||
</div>
|
||||
</div>
|
||||
{["checkbox", radioButtonWidget, "dropdown"].includes(
|
||||
props.type
|
||||
) && (
|
||||
<div className="flex flex-row gap-5 my-2 items-center text-center">
|
||||
{props.isShowAdvanceFeature && !isPrefillExist && (
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
id="isreadonly"
|
||||
type="checkbox"
|
||||
checked={isReadOnly}
|
||||
className="op-checkbox op-checkbox-sm"
|
||||
onChange={(e) => setIsReadOnly(e.target.checked)}
|
||||
/>
|
||||
<label
|
||||
className="ml-2 mb-0 capitalize"
|
||||
htmlFor="isreadonly"
|
||||
>
|
||||
{t("read-only")}
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
{props.type !== "dropdown" && (
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
id="ishidelabel"
|
||||
type="checkbox"
|
||||
checked={isHideLabel}
|
||||
className="op-checkbox op-checkbox-sm"
|
||||
onChange={(e) => setIsHideLabel(e.target.checked)}
|
||||
/>
|
||||
|
||||
<label
|
||||
className="ml-2 mb-0 capitalize"
|
||||
htmlFor="ishidelabel"
|
||||
>
|
||||
{t("hide-labels")}
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{["checkbox", radioButtonWidget].includes(props.type) && (
|
||||
<>
|
||||
<div className="text-[13px] font-semibold mt-[5px] capitalize">
|
||||
{t("layout")}
|
||||
</div>
|
||||
<div
|
||||
className={`${props.type === "checkbox" ? "mb-[10px]" : ""} flex flex-row gap-[10px] mt-[0.5rem]`}
|
||||
>
|
||||
{layoutArr.map((data, ind) => (
|
||||
<div
|
||||
key={ind}
|
||||
className="flex flex-row gap-[5px] items-center"
|
||||
>
|
||||
<input
|
||||
className="op-radio op-radio-xs my-1"
|
||||
type="radio"
|
||||
name="layout"
|
||||
checked={layout.toLowerCase() === data.toLowerCase()}
|
||||
onChange={() => setLayout(data.toLowerCase())}
|
||||
/>
|
||||
<label className="text-[13px] font-500 mb-0">
|
||||
{t(data)}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={`${
|
||||
props.type === "checkbox" && props.isShowAdvanceFeature
|
||||
? "mb-[15px]"
|
||||
: "my-[15px]"
|
||||
} w-full h-[1px] bg-[#9f9f9f]`}
|
||||
></div>
|
||||
|
||||
<button
|
||||
disabled={dropdownOptionList.length === 0 && true}
|
||||
type="submit"
|
||||
className="op-btn op-btn-primary"
|
||||
>
|
||||
{t("save")}
|
||||
</button>
|
||||
{props.currWidgetsDetails?.options?.values?.length > 0 && (
|
||||
<button
|
||||
type="submit"
|
||||
className="op-btn op-btn-ghost text-base-content ml-1"
|
||||
onClick={() => {
|
||||
props.handleClose && props.handleClose();
|
||||
resetState();
|
||||
}}
|
||||
>
|
||||
{t("cancel")}
|
||||
</button>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
</ModalUi>
|
||||
);
|
||||
}
|
||||
|
||||
export default DropdownWidgetOption;
|
||||
@@ -1,189 +0,0 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { checkIsSubscribed, getFileName } from "../../constant/Utils";
|
||||
import Upgrade from "../../primitives/Upgrade";
|
||||
import { isEnableSubscription } from "../../constant/const";
|
||||
// import SelectFolder from "../../premitives/SelectFolder";
|
||||
|
||||
const EditTemplate = ({ template, onSuccess }) => {
|
||||
// const [folder, setFolder] = useState({ ObjectId: "", Name: "" });
|
||||
const [formData, setFormData] = useState({
|
||||
Name: template?.Name || "",
|
||||
Note: template?.Note || "",
|
||||
Description: template?.Description || "",
|
||||
SendinOrder: template?.SendinOrder ? `${template?.SendinOrder}` : "false",
|
||||
AutomaticReminders: template?.AutomaticReminders || false,
|
||||
RemindOnceInEvery: template?.RemindOnceInEvery || 5
|
||||
});
|
||||
const [isSubscribe, setIsSubscribe] = useState(false);
|
||||
useEffect(() => {
|
||||
fetchSubscription();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
const fetchSubscription = async () => {
|
||||
if (isEnableSubscription) {
|
||||
const getIsSubscribe = await checkIsSubscribed();
|
||||
setIsSubscribe(getIsSubscribe);
|
||||
}
|
||||
};
|
||||
const handleStrInput = (e) => {
|
||||
setFormData({ ...formData, [e.target.name]: e.target.value });
|
||||
};
|
||||
// const handleFolder = (data) => {
|
||||
// console.log("handleFolder ", data)
|
||||
// setFolder(data);
|
||||
// };
|
||||
|
||||
// Define a function to handle form submission
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const isChecked = formData.SendinOrder === "true" ? true : false;
|
||||
const AutoReminder = formData?.AutomaticReminders || false;
|
||||
|
||||
let reminderDate = {};
|
||||
if (AutoReminder) {
|
||||
const RemindOnceInEvery = parseInt(formData?.RemindOnceInEvery);
|
||||
const ReminderDate = new Date(template?.createdAt);
|
||||
ReminderDate.setDate(ReminderDate.getDate() + RemindOnceInEvery);
|
||||
reminderDate = { NextReminderDate: ReminderDate };
|
||||
}
|
||||
const data = { ...formData, SendinOrder: isChecked, ...reminderDate };
|
||||
onSuccess(data);
|
||||
};
|
||||
const handleAutoReminder = () => {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
AutomaticReminders: !formData.AutomaticReminders
|
||||
}));
|
||||
};
|
||||
return (
|
||||
<div className="max-h-[300px] md:max-h-[400px] overflow-y-scroll p-[10px]">
|
||||
<div className="text-base-content">
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="mb-[0.35rem]">
|
||||
<label htmlFor="name" className="text-[13px]">
|
||||
File
|
||||
</label>
|
||||
<div className="op-input op-input-bordered op-input-sm focus:outline-none py-2 font-semibold w-full text-xs">
|
||||
{getFileName(template.URL)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-[0.35rem]">
|
||||
<label htmlFor="name" className="text-[13px]">
|
||||
Name
|
||||
<span className="text-[13px] text-[red]"> *</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="Name"
|
||||
value={formData.Name}
|
||||
onChange={(e) => handleStrInput(e)}
|
||||
required
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-[0.35rem]">
|
||||
<label htmlFor="Note" className="text-[13px]">
|
||||
Note
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="Note"
|
||||
id="Note"
|
||||
value={formData.Note}
|
||||
onChange={(e) => handleStrInput(e)}
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-[0.35rem]">
|
||||
<label htmlFor="Description" className="text-[13px]">
|
||||
Description
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="Description"
|
||||
id="Description"
|
||||
value={formData.Description}
|
||||
onChange={(e) => handleStrInput(e)}
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-[0.35rem]">
|
||||
<label className="text-[13px]">Send In Order</label>
|
||||
<div className="flex items-center gap-[8px] ml-[8px] mb-[5px]">
|
||||
<input
|
||||
type="radio"
|
||||
value={"true"}
|
||||
className="op-radio op-radio-xs"
|
||||
name="SendinOrder"
|
||||
checked={formData.SendinOrder === "true"}
|
||||
onChange={handleStrInput}
|
||||
/>
|
||||
<div className="text-[12px]">Yes</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-[8px] ml-[8px] mb-[5px]">
|
||||
<input
|
||||
type="radio"
|
||||
value={"false"}
|
||||
name="SendinOrder"
|
||||
className="op-radio op-radio-xs"
|
||||
checked={formData.SendinOrder === "false"}
|
||||
onChange={handleStrInput}
|
||||
/>
|
||||
<div className="text-[12px]">No</div>
|
||||
</div>
|
||||
</div>
|
||||
{isEnableSubscription && (
|
||||
<div className="text-xs mt-2">
|
||||
<span
|
||||
className={
|
||||
isSubscribe ? "font-semibold" : "font-semibold text-gray-300"
|
||||
}
|
||||
>
|
||||
Auto reminder{" "}
|
||||
{!isSubscribe && isEnableSubscription && <Upgrade />}
|
||||
</span>
|
||||
<label
|
||||
className={`${
|
||||
isSubscribe
|
||||
? "cursor-pointer "
|
||||
: "pointer-events-none opacity-50"
|
||||
} relative block items-center mb-0`}
|
||||
>
|
||||
<input
|
||||
checked={formData.AutomaticReminders}
|
||||
onChange={handleAutoReminder}
|
||||
type="checkbox"
|
||||
className="op-toggle transition-all checked:[--tglbg:#3368ff] checked:bg-white mt-2"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
{isSubscribe && formData?.AutomaticReminders === true && (
|
||||
<div className="text-xs mt-2">
|
||||
<label className="block">
|
||||
Remind once in every (Days)
|
||||
<span className="text-red-500 text-[13px]">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={formData.RemindOnceInEvery}
|
||||
name="RemindOnceInEvery"
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
onChange={handleStrInput}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-[1rem] flex justify-start">
|
||||
<button type="submit" className="op-btn op-btn-primary">
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditTemplate;
|
||||
@@ -0,0 +1,542 @@
|
||||
import {
|
||||
useState,
|
||||
useRef,
|
||||
} from "react";
|
||||
import {
|
||||
base64ToArrayBuffer,
|
||||
convertBase64ToFile,
|
||||
generatePdfName,
|
||||
getFileName
|
||||
} from "../../constant/Utils";
|
||||
import {
|
||||
maxDescriptionLength,
|
||||
maxNoteLength,
|
||||
maxTitleLength
|
||||
} from "../../constant/const";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Tooltip } from "react-tooltip";
|
||||
import SignersInput from "../shared/fields/SignersInput";
|
||||
import { PDFDocument } from "pdf-lib";
|
||||
import ModalUi from "../../primitives/ModalUi";
|
||||
import { SaveFileSize } from "../../constant/saveFileSize";
|
||||
|
||||
const EditTemplate = ({
|
||||
title,
|
||||
handleClose,
|
||||
pdfbase64,
|
||||
template,
|
||||
onSuccess,
|
||||
setPdfArrayBuffer,
|
||||
setPdfBase64Url,
|
||||
isAddYourSelfCheckbox,
|
||||
}) => {
|
||||
const appName =
|
||||
"OpenSign™";
|
||||
const { t } = useTranslation();
|
||||
const inputFileRef = useRef(null);
|
||||
const [formData, setFormData] = useState({
|
||||
Name: template?.Name || "",
|
||||
Note: template?.Note || "",
|
||||
Description: template?.Description || "",
|
||||
SendinOrder: template?.SendinOrder ? `${template?.SendinOrder}` : "false",
|
||||
AutomaticReminders: template?.AutomaticReminders || false,
|
||||
RemindOnceInEvery: template?.RemindOnceInEvery || 5,
|
||||
IsEnableOTP: template?.IsEnableOTP ? `${template?.IsEnableOTP}` : "false",
|
||||
IsTourEnabled: template?.IsTourEnabled
|
||||
? `${template?.IsTourEnabled}`
|
||||
: "false",
|
||||
NotifyOnSignatures:
|
||||
template?.NotifyOnSignatures !== undefined
|
||||
? template?.NotifyOnSignatures
|
||||
: false,
|
||||
Bcc: template?.Bcc,
|
||||
RedirectUrl: template?.RedirectUrl || "",
|
||||
AllowModifications: template?.AllowModifications || false,
|
||||
TimeToCompleteDays: template?.TimeToCompleteDays || 15,
|
||||
});
|
||||
const [isUpdate, setIsUpdate] = useState(false);
|
||||
const [showConfirm, setShowConfirm] = useState(false);
|
||||
const [uploadPdf, setUploadPdf] = useState({
|
||||
name: "",
|
||||
base64: "",
|
||||
url: ""
|
||||
});
|
||||
const handleDrop = (e) => {
|
||||
e.preventDefault();
|
||||
const file = e.dataTransfer.files[0];
|
||||
handleFile(file);
|
||||
};
|
||||
|
||||
const handleFile = (file) => {
|
||||
if (file && file.type === "application/pdf") {
|
||||
handleReplaceFileValdition(file);
|
||||
// You can handle the file here
|
||||
} else {
|
||||
alert(t("only-pdf-allowed"));
|
||||
if (inputFileRef.current) inputFileRef.current.value = "";
|
||||
}
|
||||
};
|
||||
const handleDragOver = (e) => {
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
// `isValidURL` is used to check valid webhook url
|
||||
function isValidURL(value) {
|
||||
try {
|
||||
const url = new URL(value);
|
||||
return url.protocol === "https:" || url.protocol === "http:";
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const handleStrInput = (e) => {
|
||||
setIsUpdate(true);
|
||||
setFormData({ ...formData, [e.target.name]: e.target.value });
|
||||
};
|
||||
const getPdfMetadataHash = async (pdfBytes) => {
|
||||
const pdfDoc = await PDFDocument.load(pdfBytes);
|
||||
const pages = pdfDoc.getPages();
|
||||
const metaString = pages
|
||||
.map((page, index) => {
|
||||
const { width, height } = page.getSize();
|
||||
return `${index + 1}:${Math.round(width)}x${Math.round(height)}`;
|
||||
})
|
||||
.join("|");
|
||||
const encoder = new TextEncoder();
|
||||
const data = encoder.encode(metaString);
|
||||
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
||||
return Array.from(new Uint8Array(hashBuffer))
|
||||
.map((b) => b.toString(16).padStart(2, "0"))
|
||||
.join("");
|
||||
};
|
||||
|
||||
const handleFileInput = async (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
handleReplaceFileValdition(file);
|
||||
};
|
||||
const handleReplaceFileValdition = async (file) => {
|
||||
try {
|
||||
const basePdfBytes = base64ToArrayBuffer(pdfbase64);
|
||||
const expectedHash = await getPdfMetadataHash(basePdfBytes);
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onload = async (event) => {
|
||||
const uploadedPdfBytes = event.target.result;
|
||||
const uploadedHash = await getPdfMetadataHash(uploadedPdfBytes);
|
||||
|
||||
if (expectedHash === uploadedHash) {
|
||||
const arrayBuffer = uploadedPdfBytes;
|
||||
const uint8Array = new Uint8Array(arrayBuffer);
|
||||
const binaryString = Array.from(uint8Array)
|
||||
.map((b) => String.fromCharCode(b))
|
||||
.join("");
|
||||
const base64 = btoa(binaryString);
|
||||
const pdfName = generatePdfName(16);
|
||||
setIsUpdate(true);
|
||||
setUploadPdf((prev) => ({ ...prev, name: pdfName, base64: base64 }));
|
||||
// alert("✅ PDFs match (based on page number, width, height)");
|
||||
} else {
|
||||
alert("❌ PDF do NOT match based on page number, width, height");
|
||||
if (inputFileRef.current) inputFileRef.current.value = "";
|
||||
}
|
||||
};
|
||||
|
||||
fileReader.readAsArrayBuffer(file);
|
||||
} catch (err) {
|
||||
alert("Error: " + err.message);
|
||||
if (inputFileRef.current) inputFileRef.current.value = "";
|
||||
}
|
||||
};
|
||||
// Define a function to handle form submission
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (formData.RedirectUrl && !isValidURL(formData?.RedirectUrl)) {
|
||||
alert(t("invalid-redirect-url"));
|
||||
return;
|
||||
}
|
||||
if (formData?.Name?.length > maxTitleLength) {
|
||||
alert(t("title-length-alert"));
|
||||
return;
|
||||
}
|
||||
if (formData?.Note?.length > maxNoteLength) {
|
||||
alert(t("note-length-alert"));
|
||||
return;
|
||||
}
|
||||
if (formData?.Description?.length > maxDescriptionLength) {
|
||||
alert(t("description-length-alert"));
|
||||
return;
|
||||
}
|
||||
let pdfUrl;
|
||||
if (uploadPdf?.base64) {
|
||||
pdfUrl = await convertBase64ToFile(
|
||||
uploadPdf.name,
|
||||
uploadPdf.base64,
|
||||
);
|
||||
setUploadPdf((prev) => ({ ...prev, url: pdfUrl }));
|
||||
const pdfBuffer = base64ToArrayBuffer(uploadPdf.base64);
|
||||
setPdfArrayBuffer && setPdfArrayBuffer(pdfBuffer);
|
||||
setPdfBase64Url && setPdfBase64Url(uploadPdf.base64);
|
||||
const tenantId =
|
||||
localStorage.getItem("TenantId") ||
|
||||
template?.ExtUserPtr?.TenantId?.objectId;
|
||||
const buffer = atob(uploadPdf.base64);
|
||||
const userId = template?.ExtUserPtr?.UserId?.objectId;
|
||||
SaveFileSize(buffer.length, pdfUrl, tenantId, userId);
|
||||
}
|
||||
const isChecked = formData.SendinOrder === "true" ? true : false;
|
||||
const isTourEnabled = formData?.IsTourEnabled === "false" ? false : true;
|
||||
const AutoReminder = formData?.AutomaticReminders || false;
|
||||
const IsEnableOTP = formData.IsEnableOTP === "true" ? true : false;
|
||||
const allowModify = formData?.AllowModifications || false;
|
||||
let reminderDate = {};
|
||||
const remindOnceInEvery = formData?.RemindOnceInEvery;
|
||||
const TimeToCompleteDays = parseInt(formData?.TimeToCompleteDays);
|
||||
const reminderCount = TimeToCompleteDays / remindOnceInEvery;
|
||||
if (AutoReminder && reminderCount > 15) {
|
||||
alert(t("only-15-reminder-allowed"));
|
||||
return;
|
||||
}
|
||||
if (AutoReminder) {
|
||||
const RemindOnceInEvery = parseInt(formData?.RemindOnceInEvery);
|
||||
const ReminderDate = new Date(template?.createdAt);
|
||||
ReminderDate.setDate(ReminderDate.getDate() + RemindOnceInEvery);
|
||||
reminderDate = { NextReminderDate: ReminderDate };
|
||||
}
|
||||
const data = {
|
||||
...formData,
|
||||
...(pdfUrl ? { URL: pdfUrl } : {}),
|
||||
SendinOrder: isChecked,
|
||||
IsEnableOTP: IsEnableOTP,
|
||||
IsTourEnabled: isTourEnabled,
|
||||
AllowModifications: allowModify,
|
||||
...reminderDate
|
||||
};
|
||||
onSuccess(data);
|
||||
};
|
||||
|
||||
// `handleNotifySignChange` is trigger when user change radio of notify on signatures
|
||||
const handleNotifySignChange = (value) => {
|
||||
setIsUpdate(true);
|
||||
setFormData((obj) => ({ ...obj, NotifyOnSignatures: value }));
|
||||
};
|
||||
const handleBcc = (data) => {
|
||||
if (data && data.length > 0) {
|
||||
const trimEmail = data.map((item) => ({
|
||||
objectId: item?.value,
|
||||
Name: item?.label,
|
||||
Email: item?.email
|
||||
}));
|
||||
setIsUpdate(true);
|
||||
setFormData((prev) => ({ ...prev, Bcc: trimEmail }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditTemplateClose = () => {
|
||||
if (isUpdate) {
|
||||
setShowConfirm(true);
|
||||
} else {
|
||||
handleClose();
|
||||
}
|
||||
};
|
||||
const discardChanges = () => {
|
||||
setShowConfirm(false);
|
||||
handleClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalUi
|
||||
isOpen
|
||||
title={isUpdate ? `${title} (unsaved)` : title}
|
||||
handleClose={handleEditTemplateClose}
|
||||
>
|
||||
<ModalUi isOpen={showConfirm} showClose={false}>
|
||||
<div className="p-[20px]">
|
||||
<p className="text-base font-normal text-base-content py-[5px] md:py-[6px] px-[5px]">
|
||||
{t("unsaved-changes-discard-them?")}
|
||||
</p>
|
||||
<div className="flex items-center mt-2.5 gap-2 md:gap-3 text-white">
|
||||
<button
|
||||
className="op-btn op-btn-primary px-6"
|
||||
onClick={discardChanges}
|
||||
>
|
||||
{t("yes-discard")}
|
||||
</button>
|
||||
<button
|
||||
className="op-btn op-btn-secondary px-4 md:px-6"
|
||||
onClick={() => setShowConfirm(false)}
|
||||
>
|
||||
{t("cancel")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ModalUi>
|
||||
<div className="max-h-[300px] md:max-h-[400px] overflow-y-scroll p-[10px]">
|
||||
<div className="text-base-content">
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="mb-[0.35rem]">
|
||||
<label htmlFor="name" className="text-[13px]">
|
||||
{t("report-heading.File")}
|
||||
</label>
|
||||
<div
|
||||
className="border-[1.5px] border-dashed border-gray-300 rounded-lg px-4 py-6 text-center text-gray-500 bg-white cursor-pointer hover:border-base-content transition"
|
||||
onDrop={handleDrop}
|
||||
onDragOver={handleDragOver}
|
||||
onClick={() => inputFileRef?.current?.click()}
|
||||
>
|
||||
<label
|
||||
htmlFor="fileUpload"
|
||||
className="cursor-pointer text-center mb-0"
|
||||
>
|
||||
{t("browse-or-drag-to-replace-existing-file")}
|
||||
</label>
|
||||
</div>
|
||||
<input
|
||||
ref={inputFileRef}
|
||||
type="file"
|
||||
className="hidden"
|
||||
accept="application/pdf"
|
||||
onChange={(e) => handleFileInput(e)}
|
||||
onInvalid={(e) =>
|
||||
e.target.setCustomValidity(t("input-required"))
|
||||
}
|
||||
onInput={(e) => e.target.setCustomValidity("")}
|
||||
/>
|
||||
{uploadPdf?.name && (
|
||||
<div
|
||||
onClick={() => inputFileRef?.current?.click()}
|
||||
className="mt-2 cursor-pointer op-input op-input-bordered op-input-sm focus:outline-none py-2 font-semibold w-full text-xs"
|
||||
>
|
||||
selected:{" "}
|
||||
{uploadPdf?.url
|
||||
? `${uploadPdf?.name}.pdf`
|
||||
: getFileName(template.URL)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="mb-[0.35rem]">
|
||||
<label htmlFor="name" className="text-[13px]">
|
||||
{t("Title")}
|
||||
<span className="text-[13px] text-[red]"> *</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="Name"
|
||||
value={formData.Name}
|
||||
onChange={(e) => handleStrInput(e)}
|
||||
onInvalid={(e) =>
|
||||
e.target.setCustomValidity(t("input-required"))
|
||||
}
|
||||
onInput={(e) => e.target.setCustomValidity("")}
|
||||
required
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-[0.35rem]">
|
||||
<label htmlFor="Note" className="text-[13px]">
|
||||
{t("report-heading.Note")}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="Note"
|
||||
id="Note"
|
||||
value={formData.Note}
|
||||
onChange={(e) => handleStrInput(e)}
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-[0.35rem]">
|
||||
<label htmlFor="Description" className="text-[13px]">
|
||||
{t("description")}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="Description"
|
||||
id="Description"
|
||||
value={formData.Description}
|
||||
onChange={(e) => handleStrInput(e)}
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-[0.35rem]">
|
||||
<label className="text-[13px]">{t("send-in-order")}</label>
|
||||
<div className="flex flex-col md:flex-row md:gap-4">
|
||||
<div className="flex items-center gap-[8px] ml-[8px] mb-[5px]">
|
||||
<input
|
||||
type="radio"
|
||||
value={"true"}
|
||||
className="op-radio op-radio-xs"
|
||||
name="SendinOrder"
|
||||
checked={formData.SendinOrder === "true"}
|
||||
onChange={handleStrInput}
|
||||
/>
|
||||
<div className="text-[12px]">{t("yes")}</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-[8px] ml-[8px] mb-[5px]">
|
||||
<input
|
||||
type="radio"
|
||||
value={"false"}
|
||||
name="SendinOrder"
|
||||
className="op-radio op-radio-xs"
|
||||
checked={formData.SendinOrder === "false"}
|
||||
onChange={handleStrInput}
|
||||
/>
|
||||
<div className="text-[12px]">{t("no")}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs mt-3">
|
||||
<label className="block">
|
||||
<span>
|
||||
{t("enable-tour")}
|
||||
<a data-tooltip-id="istourenabled-tooltip" className="ml-1">
|
||||
<sup>
|
||||
<i className="fa-light fa-question rounded-full border-[#33bbff] text-[#33bbff] text-[13px] border-[1px] py-[1.5px] px-[4px]"></i>
|
||||
</sup>
|
||||
</a>{" "}
|
||||
</span>
|
||||
<Tooltip id="istourenabled-tooltip" className="z-50">
|
||||
<div className="max-w-[200px] md:max-w-[450px]">
|
||||
<p className="font-bold">{t("enable-tour")}</p>
|
||||
<div className="p-[5px]">
|
||||
<ol className="list-disc">
|
||||
<li>
|
||||
<span className="font-bold">{t("yes")}: </span>
|
||||
<span>{t("istourenabled-help.p1")}</span>
|
||||
</li>
|
||||
<li>
|
||||
<span className="font-bold">{t("no")}: </span>
|
||||
<span>{t("istourenabled-help.p2")}</span>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
<p>{t("istourenabled-help.p3", { appName: appName })}</p>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</label>
|
||||
<div className="flex flex-col md:flex-row md:gap-4">
|
||||
<div className="flex items-center gap-2 ml-2 mb-1">
|
||||
<input
|
||||
type="radio"
|
||||
value={"true"}
|
||||
className="op-radio op-radio-xs"
|
||||
name="IsTourEnabled"
|
||||
checked={formData.IsTourEnabled === "true"}
|
||||
onChange={handleStrInput}
|
||||
/>
|
||||
<div className="text-center">{t("yes")}</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 ml-2 mb-1">
|
||||
<input
|
||||
type="radio"
|
||||
value={"false"}
|
||||
name="IsTourEnabled"
|
||||
className="op-radio op-radio-xs"
|
||||
checked={formData.IsTourEnabled === "false"}
|
||||
onChange={handleStrInput}
|
||||
/>
|
||||
<div className="text-center">{t("no")}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs mt-3">
|
||||
<label>
|
||||
{t("notify-on-signatures")}
|
||||
<a data-tooltip-id="nos-tooltip" className="ml-1">
|
||||
<sup>
|
||||
<i className="fa-light fa-question rounded-full border-[#33bbff] text-[#33bbff] text-[13px] border-[1px] py-[1.5px] px-[4px]"></i>
|
||||
</sup>
|
||||
</a>{" "}
|
||||
<Tooltip id="nos-tooltip" className="z-[999]">
|
||||
<div className="max-w-[200px] md:max-w-[450px] text-[11px]">
|
||||
<p className="font-bold">{t("notify-on-signatures")}</p>
|
||||
<p>{t("notify-on-signatures-help.p1")}</p>
|
||||
<p>{t("notify-on-signatures-help.note")}</p>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</label>
|
||||
<div className="flex flex-col md:flex-row md:gap-4">
|
||||
<div
|
||||
className={
|
||||
`flex items-center gap-2 ml-2 mb-1`
|
||||
}
|
||||
>
|
||||
<input
|
||||
className="mr-[2px] op-radio op-radio-xs"
|
||||
type="radio"
|
||||
onChange={() => handleNotifySignChange(true)}
|
||||
checked={formData.NotifyOnSignatures === true}
|
||||
/>
|
||||
<div className="text-center">{t("yes")}</div>
|
||||
</div>
|
||||
<div
|
||||
className={
|
||||
`flex items-center gap-2 ml-2 mb-1`
|
||||
}
|
||||
>
|
||||
<input
|
||||
className="mr-[2px] op-radio op-radio-xs"
|
||||
type="radio"
|
||||
onChange={() => handleNotifySignChange(false)}
|
||||
checked={formData.NotifyOnSignatures === false}
|
||||
/>
|
||||
<div className="text-center">{t("no")}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-xs mt-3">
|
||||
<SignersInput
|
||||
label={t("Bcc")}
|
||||
initialData={template?.Bcc}
|
||||
onChange={handleBcc}
|
||||
helptextZindex={50}
|
||||
helpText={t("bcc-help")}
|
||||
isCaptureAllData
|
||||
isAddYourSelfCheckbox={isAddYourSelfCheckbox}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-xs mt-2">
|
||||
<label className="block">{t("redirect-url")}</label>
|
||||
<input
|
||||
name="RedirectUrl"
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
value={formData.RedirectUrl}
|
||||
onChange={handleStrInput}
|
||||
onInvalid={(e) =>
|
||||
e.target.setCustomValidity(t("input-required"))
|
||||
}
|
||||
onInput={(e) => e.target.setCustomValidity("")}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-xs mt-2">
|
||||
<label className="block">
|
||||
{t("time-to-complete")}
|
||||
<span className="text-red-500 text-[13px]">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
name="TimeToCompleteDays"
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
value={formData.TimeToCompleteDays}
|
||||
onChange={(e) => handleStrInput(e)}
|
||||
onInvalid={(e) =>
|
||||
e.target.setCustomValidity(t("input-required"))
|
||||
}
|
||||
onInput={(e) => e.target.setCustomValidity("")}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-[1rem] flex justify-start">
|
||||
<button type="submit" className="op-btn op-btn-primary">
|
||||
{t("submit")}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</ModalUi>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditTemplate;
|
||||
@@ -1,5 +1,10 @@
|
||||
import React from "react";
|
||||
import { Quill } from "react-quill";
|
||||
import { Quill } from "react-quill-new";
|
||||
// Use a third-party plugin so editors can toggle raw HTML view.
|
||||
import htmlEditButton from "quill-html-edit-button";
|
||||
|
||||
// Register the module which adds a "<>" button for HTML editing.
|
||||
Quill.register("modules/htmlEditButton", htmlEditButton);
|
||||
|
||||
// Custom Undo button icon component for Quill editor. You can import it directly
|
||||
// from 'quill/assets/icons/undo.svg' but I found that a number of loaders do not
|
||||
@@ -54,17 +59,24 @@ Quill.register(Font, true);
|
||||
export const module1 = {
|
||||
toolbar: {
|
||||
container: "#toolbar1",
|
||||
handlers: {
|
||||
undo: undoChange,
|
||||
redo: redoChange
|
||||
}
|
||||
handlers: { undo: undoChange, redo: redoChange }
|
||||
},
|
||||
history: {
|
||||
delay: 500,
|
||||
maxStack: 100,
|
||||
userOnly: true
|
||||
}
|
||||
history: { delay: 500, maxStack: 100, userOnly: true },
|
||||
// Enable the "<>" button registered above
|
||||
htmlEditButton: {}
|
||||
};
|
||||
|
||||
// Modules object for setting up the Quill editor
|
||||
export const module2 = {
|
||||
toolbar: {
|
||||
container: "#toolbar2",
|
||||
handlers: { undo: undoChange, redo: redoChange }
|
||||
},
|
||||
history: { delay: 500, maxStack: 100, userOnly: true },
|
||||
// Enable the "<>" button registered above
|
||||
htmlEditButton: {}
|
||||
};
|
||||
|
||||
// Formats objects for setting up the Quill editor
|
||||
export const formats = [
|
||||
"header",
|
||||
@@ -79,7 +91,6 @@ export const formats = [
|
||||
"blockquote",
|
||||
"background",
|
||||
"list",
|
||||
"bullet",
|
||||
"indent",
|
||||
"link",
|
||||
"image",
|
||||
@@ -1,35 +1,42 @@
|
||||
import React from "react";
|
||||
import Tooltip from "../../primitives/Tooltip";
|
||||
import ReactQuill from "react-quill";
|
||||
import "react-quill/dist/quill.snow.css";
|
||||
import ReactQuill from "react-quill-new";
|
||||
import "../../styles/quill.css";
|
||||
import EditorToolbar, { module1, formats } from "./EditorToolbar";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export function EmailBody(props) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<form className="flex flex-col text-base-content text-lg font-normal">
|
||||
<div className="m-2 md:m-10 p-3 md:p-10 shadow-md hover:shadow-lg border-[1px] border-indigo-800 rounded-md">
|
||||
<label className="text-sm ml-2">
|
||||
Subject <Tooltip message={"email subject"} />
|
||||
{t("subject")} <Tooltip message={t("email-subject")} />
|
||||
</label>
|
||||
<input
|
||||
required
|
||||
onInvalid={(e) => e.target.setCustomValidity(t("input-required"))}
|
||||
onInput={(e) => e.target.setCustomValidity("")}
|
||||
value={props.requestSubject}
|
||||
onChange={(e) => props.setRequestSubject(e.target.value)}
|
||||
onChange={(e) => {
|
||||
props?.setRequestSubject(e.target.value);
|
||||
props?.setCustomizeMail((prev) => ({
|
||||
...prev,
|
||||
subject: e.target.value
|
||||
}));
|
||||
}}
|
||||
placeholder='${senderName} has requested you to sign "${documentName}"'
|
||||
className="op-input op-input-bordered op-input-sm focus:outline-none hover:border-base-content w-full text-xs"
|
||||
/>
|
||||
<label className="text-sm ml-2 mt-3">
|
||||
Body <Tooltip message={"email body"} />
|
||||
{t("body")} <Tooltip message={t("email-body")} />
|
||||
</label>
|
||||
<div className="px-3 py-2 w-full focus:outline-none text-xs">
|
||||
<div className="px-1 py-2 w-full focus:outline-none text-xs">
|
||||
<EditorToolbar containerId="toolbar1" />
|
||||
<ReactQuill
|
||||
theme="snow"
|
||||
value={props.requestBody}
|
||||
// value={typedText}
|
||||
placeholder="add body of email "
|
||||
// readOnly={loading}
|
||||
// onChangeSelection={handleTextSelection} // Listen for text selection
|
||||
placeholder={t("email-placeholder")}
|
||||
ref={props.editorRef}
|
||||
modules={module1}
|
||||
formats={formats}
|
||||
@@ -1,312 +0,0 @@
|
||||
import React, { useState } from "react";
|
||||
import { saveAs } from "file-saver";
|
||||
import axios from "axios";
|
||||
import { getBase64FromUrl } from "../../constant/Utils";
|
||||
import { themeColor, emailRegex } from "../../constant/const";
|
||||
import printModule from "print-js";
|
||||
import Loader from "../../primitives/Loader";
|
||||
import ModalUi from "../../primitives/ModalUi";
|
||||
|
||||
function EmailComponent({
|
||||
isEmail,
|
||||
pdfUrl,
|
||||
setIsEmail,
|
||||
setSuccessEmail,
|
||||
pdfName,
|
||||
sender,
|
||||
setIsAlert,
|
||||
extUserId,
|
||||
activeMailAdapter
|
||||
}) {
|
||||
const [emailList, setEmailList] = useState([]);
|
||||
const [emailValue, setEmailValue] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [emailErr, setEmailErr] = useState(false);
|
||||
//function for send email
|
||||
const sendEmail = async () => {
|
||||
setIsLoading(true);
|
||||
|
||||
let sendMail;
|
||||
for (let i = 0; i < emailList.length; i++) {
|
||||
try {
|
||||
const imgPng =
|
||||
"https://qikinnovation.ams3.digitaloceanspaces.com/logo.png";
|
||||
|
||||
let url = `${localStorage.getItem("baseUrl")}functions/sendmailv3/`;
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
"X-Parse-Application-Id": localStorage.getItem("parseAppId"),
|
||||
sessionToken: localStorage.getItem("accesstoken")
|
||||
};
|
||||
const openSignUrl = "https://www.opensignlabs.com/contact-us";
|
||||
const themeBGcolor = themeColor;
|
||||
let params = {
|
||||
mailProvider: activeMailAdapter,
|
||||
extUserId: extUserId,
|
||||
pdfName: pdfName,
|
||||
url: pdfUrl,
|
||||
recipient: emailList[i],
|
||||
subject: `${sender.name} has signed the doc - ${pdfName}`,
|
||||
from: sender.email,
|
||||
html:
|
||||
"<html><head><meta http-equiv='Content-Type' content='text/html; charset=UTF-8' /></head><body> <div style='background-color:#f5f5f5;padding:20px'> <div style='box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 12px;background-color:white;'> <div><img src=" +
|
||||
imgPng +
|
||||
" height='50' style='padding:20px,width:170px,height:40px'/> </div><div style='padding:2px;font-family:system-ui; background-color:" +
|
||||
themeBGcolor +
|
||||
";'> <p style='font-size:20px;font-weight:400;color:white;padding-left:20px',> Document Copy</p></div><div><p style='padding:20px;font-family:system-ui;font-size:14px'>A copy of the document <strong>" +
|
||||
pdfName +
|
||||
" </strong>is attached to this email. Kindly download the document from the attachment.</p></div> </div><div><p>This is an automated email from OpenSign™. For any queries regarding this email, please contact the sender " +
|
||||
sender.email +
|
||||
" directly. If you think this email is inappropriate or spam, you may file a complaint with OpenSign™ <a href= " +
|
||||
openSignUrl +
|
||||
" target=_blank>here</a> </p></div></div></body></html>"
|
||||
};
|
||||
sendMail = await axios.post(url, params, { headers: headers });
|
||||
} catch (error) {
|
||||
console.log("error", error);
|
||||
setIsLoading(false);
|
||||
setIsEmail(false);
|
||||
setIsAlert({
|
||||
isShow: true,
|
||||
alertMessage: "something went wrong"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (sendMail && sendMail.data.result.status === "success") {
|
||||
setSuccessEmail(true);
|
||||
setTimeout(() => {
|
||||
setSuccessEmail(false);
|
||||
setIsEmail(false);
|
||||
setEmailValue("");
|
||||
setEmailList([]);
|
||||
}, 1500);
|
||||
|
||||
setIsLoading(false);
|
||||
} else if (sendMail && sendMail.data.result.status === "error") {
|
||||
setIsLoading(false);
|
||||
setIsEmail(false);
|
||||
setIsAlert({
|
||||
isShow: true,
|
||||
alertMessage: "something went wrong"
|
||||
});
|
||||
setEmailValue("");
|
||||
setEmailList([]);
|
||||
} else {
|
||||
setIsLoading(false);
|
||||
setIsEmail(false);
|
||||
setIsAlert({
|
||||
isShow: true,
|
||||
alertMessage: "something went wrong"
|
||||
});
|
||||
setEmailValue("");
|
||||
setEmailList([]);
|
||||
}
|
||||
};
|
||||
|
||||
//function for remove email
|
||||
const removeChip = (index) => {
|
||||
const updateEmailCount = emailList.filter((data, key) => key !== index);
|
||||
setEmailList(updateEmailCount);
|
||||
};
|
||||
//function for get email value
|
||||
const handleEmailValue = (e) => {
|
||||
const value = e.target.value;
|
||||
setEmailErr(false);
|
||||
setEmailValue(value);
|
||||
};
|
||||
|
||||
//function for save email in array after press enter
|
||||
const handleEnterPress = (e) => {
|
||||
const pattern = emailRegex;
|
||||
const validate = emailValue?.match(pattern);
|
||||
if (e.key === "Enter" && emailValue) {
|
||||
if (validate) {
|
||||
const emailLowerCase = emailValue?.toLowerCase();
|
||||
setEmailList((prev) => [...prev, emailLowerCase]);
|
||||
setEmailValue("");
|
||||
} else {
|
||||
setEmailErr(true);
|
||||
}
|
||||
} else if (e === "add" && emailValue) {
|
||||
if (validate) {
|
||||
const emailLowerCase = emailValue?.toLowerCase();
|
||||
setEmailList((prev) => [...prev, emailLowerCase]);
|
||||
setEmailValue("");
|
||||
} else {
|
||||
setEmailErr(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// function for print signed pdf
|
||||
const handleToPrint = async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const pdf = await getBase64FromUrl(pdfUrl);
|
||||
const isAndroidDevice = navigator.userAgent.match(/Android/i);
|
||||
const isAppleDevice =
|
||||
(/iPad|iPhone|iPod/.test(navigator.platform) ||
|
||||
(navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1)) &&
|
||||
!window.MSStream;
|
||||
if (isAndroidDevice || isAppleDevice) {
|
||||
const byteArray = Uint8Array.from(
|
||||
atob(pdf)
|
||||
.split("")
|
||||
.map((char) => char.charCodeAt(0))
|
||||
);
|
||||
const blob = new Blob([byteArray], { type: "application/pdf" });
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
window.open(blobUrl, "_blank");
|
||||
} else {
|
||||
printModule({ printable: pdf, type: "pdf", base64: true });
|
||||
}
|
||||
};
|
||||
|
||||
//handle download signed pdf
|
||||
const handleDownloadPdf = () => {
|
||||
saveAs(pdfUrl, `${sanitizeFileName(pdfName)}_signed_by_OpenSign™.pdf`);
|
||||
};
|
||||
|
||||
const sanitizeFileName = (pdfName) => {
|
||||
// Replace spaces with underscore
|
||||
return pdfName.replace(/ /g, "_");
|
||||
};
|
||||
|
||||
const isAndroid = /Android/i.test(navigator.userAgent);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* isEmail */}
|
||||
{isEmail && (
|
||||
<ModalUi isOpen showHeader={false}>
|
||||
{isLoading && (
|
||||
<div className="absolute w-full h-full flex flex-col justify-center items-center z-[20] bg-[#e6f2f2]/70">
|
||||
<Loader />
|
||||
<span className="text-[12px] text-base-content">
|
||||
This might take some time
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-between items-center py-[10px] px-[20px] border-b-[1px] border-base-content">
|
||||
<span className="text-base-content font-semibold">
|
||||
Successfully signed!
|
||||
</span>
|
||||
<div className="flex flex-row">
|
||||
{!isAndroid && (
|
||||
<button
|
||||
onClick={handleToPrint}
|
||||
className="op-btn op-btn-neutral op-btn-sm text-[15px]"
|
||||
>
|
||||
<i className="fa-light fa-print" aria-hidden="true"></i>
|
||||
Print
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="op-btn op-btn-primary op-btn-sm text-[15px] ml-2"
|
||||
onClick={() => handleDownloadPdf()}
|
||||
>
|
||||
<i className="fa-light fa-download" aria-hidden="true"></i>
|
||||
Download
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-full p-[20px]">
|
||||
<p className="font-medium text-[15px] mb-[5px] text-base-content align-baseline">
|
||||
Recipients added here will get a copy of the signed document.
|
||||
</p>
|
||||
{emailList.length > 0 ? (
|
||||
<div className="p-0 border-[1.5px] op-border-primary rounded w-full text-[15px]">
|
||||
<div className="flex flex-row flex-wrap">
|
||||
{emailList.map((data, ind) => {
|
||||
return (
|
||||
<div
|
||||
className="flex flex-row items-center op-bg-primary m-[4px] rounded-md py-[5px] px-[10px]"
|
||||
key={ind}
|
||||
>
|
||||
<span className="text-base-100 text-[13px]">
|
||||
{data}
|
||||
</span>
|
||||
<span
|
||||
className="text-base-100 text-[13px] font-semibold ml-[7px] cursor-pointer"
|
||||
onClick={() => removeChip(ind)}
|
||||
>
|
||||
<i className="fa-light fa-xmark"></i>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{emailList.length <= 9 && (
|
||||
<input
|
||||
type="email"
|
||||
value={emailValue}
|
||||
className="p-[10px] pb-[20px] rounded w-full text-[15px] bg-transparent outline-none"
|
||||
onChange={handleEmailValue}
|
||||
onKeyDown={handleEnterPress}
|
||||
onBlur={() => emailValue && handleEnterPress("add")}
|
||||
required
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<input
|
||||
type="email"
|
||||
value={emailValue}
|
||||
className="p-[10px] pb-[20px] rounded w-full text-[15px] outline-none bg-transparent border-[1.5px] op-border-primary"
|
||||
onChange={handleEmailValue}
|
||||
onKeyDown={handleEnterPress}
|
||||
placeholder="Add an email address and hit enter"
|
||||
onBlur={() => emailValue && handleEnterPress("add")}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{emailErr && (
|
||||
<p className="text-xs text-[red] ml-1.5 mt-0.5">
|
||||
please provide correct email address
|
||||
</p>
|
||||
)}
|
||||
<button
|
||||
className={`${
|
||||
emailValue ? "cursor-pointer" : "cursor-default"
|
||||
} op-btn op-btn-primary op-btn-sm m-2 shadow-md`}
|
||||
onClick={() => emailValue && handleEnterPress("add")}
|
||||
>
|
||||
<i className="fa-light fa-plus" aria-hidden="true"></i>
|
||||
</button>
|
||||
|
||||
<div className="bg-[#e3e2e1] mt-[10px] p-[5px] rounded">
|
||||
<span className="font-bold">Note: </span>
|
||||
<span className="text-[15px]">
|
||||
You can only send to ten recipients at a time.
|
||||
</span>
|
||||
</div>
|
||||
<hr className="w-full my-[15px] bg-base-content" />
|
||||
<button
|
||||
type="button"
|
||||
className="op-btn op-btn-secondary"
|
||||
onClick={() => emailList.length > 0 && sendEmail()}
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="op-btn op-btn-ghost ml-2"
|
||||
onClick={() => {
|
||||
setIsEmail(false);
|
||||
setEmailValue("");
|
||||
setEmailList([]);
|
||||
}}
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</ModalUi>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default EmailComponent;
|
||||
@@ -0,0 +1,229 @@
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
handleToPrint,
|
||||
} from "../../constant/Utils";
|
||||
import {
|
||||
emailRegex,
|
||||
} from "../../constant/const";
|
||||
import Loader from "../../primitives/Loader";
|
||||
import ModalUi from "../../primitives/ModalUi";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Parse from "parse";
|
||||
|
||||
function EmailComponent({
|
||||
isEmail,
|
||||
setIsEmail,
|
||||
setSuccessEmail,
|
||||
pdfDetails,
|
||||
setIsAlert,
|
||||
setIsDownloadModal
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [emailList, setEmailList] = useState([]);
|
||||
const [emailValue, setEmailValue] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [emailErr, setEmailErr] = useState(false);
|
||||
const [isDownloading, setIsDownloading] = useState("");
|
||||
const isAndroid = /Android/i.test(navigator.userAgent);
|
||||
|
||||
//function for send email
|
||||
const sendEmail = async () => {
|
||||
setIsLoading(true);
|
||||
const params = { docId: pdfDetails?.[0]?.objectId, recipients: emailList };
|
||||
const sendmail = await Parse.Cloud.run("forwarddoc", params);
|
||||
if (sendmail?.status === "success") {
|
||||
setSuccessEmail(true);
|
||||
setIsEmail(false);
|
||||
setTimeout(() => {
|
||||
setSuccessEmail(false);
|
||||
setEmailValue("");
|
||||
setEmailList([]);
|
||||
}, 1500);
|
||||
setIsLoading(false);
|
||||
}
|
||||
else {
|
||||
setIsLoading(false);
|
||||
setIsEmail(false);
|
||||
setIsAlert({
|
||||
isShow: true,
|
||||
alertMessage: t("something-went-wrong-mssg")
|
||||
});
|
||||
setEmailValue("");
|
||||
setEmailList([]);
|
||||
}
|
||||
};
|
||||
|
||||
//function for remove email
|
||||
const removeChip = (index) => {
|
||||
const updateEmailCount = emailList.filter((data, key) => key !== index);
|
||||
setEmailList(updateEmailCount);
|
||||
};
|
||||
//function for get email value
|
||||
const handleEmailValue = (e) => {
|
||||
const value = e.target.value?.toLowerCase()?.replace(/\s/g, "");
|
||||
setEmailErr(false);
|
||||
setEmailValue(value);
|
||||
};
|
||||
|
||||
//function for save email in array after press enter
|
||||
const handleEnterPress = (e) => {
|
||||
const pattern = emailRegex;
|
||||
const validate = emailValue?.match(pattern);
|
||||
if (e.key === "Enter" && emailValue) {
|
||||
if (validate) {
|
||||
const emailLowerCase = emailValue?.toLowerCase();
|
||||
setEmailList((prev) => [...prev, emailLowerCase]);
|
||||
setEmailValue("");
|
||||
} else {
|
||||
setEmailErr(true);
|
||||
}
|
||||
} else if (e === "add" && emailValue) {
|
||||
if (validate) {
|
||||
const emailLowerCase = emailValue?.toLowerCase();
|
||||
setEmailList((prev) => [...prev, emailLowerCase]);
|
||||
setEmailValue("");
|
||||
} else {
|
||||
setEmailErr(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
const handleClose = () => {
|
||||
setIsEmail(false);
|
||||
setEmailValue("");
|
||||
setEmailList([]);
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
{/* isEmail */}
|
||||
{isEmail && (
|
||||
<ModalUi isOpen showHeader={false}>
|
||||
{isLoading && (
|
||||
<div className="absolute w-full h-full flex flex-col justify-center items-center z-[20] bg-[#e6f2f2]/70">
|
||||
<Loader />
|
||||
<span className="text-[12px] text-base-content">
|
||||
{t("loader")}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{isDownloading === "pdf" && (
|
||||
<div className="fixed z-[200] inset-0 flex justify-center items-center bg-black bg-opacity-30">
|
||||
<Loader />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-between items-center py-[10px] px-[20px] border-b-[1px] border-base-content">
|
||||
<span className="text-base-content font-bold text-sm md:text-lg">
|
||||
{t("successfully-signed")}
|
||||
</span>
|
||||
<div className="flex flex-row">
|
||||
{!isAndroid && (
|
||||
<button
|
||||
onClick={(e) =>
|
||||
handleToPrint(e, setIsDownloading, pdfDetails)
|
||||
}
|
||||
className="op-btn op-btn-neutral op-btn-sm text-xs md:text-[15px]"
|
||||
>
|
||||
<i className="fa-light fa-print" aria-hidden="true"></i>
|
||||
{t("print")}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="op-btn op-btn-primary op-btn-sm text-xs md:text-[15px] ml-2"
|
||||
onClick={() => {
|
||||
handleClose();
|
||||
setIsDownloadModal(true);
|
||||
}}
|
||||
>
|
||||
<i className="fa-light fa-download" aria-hidden="true"></i>
|
||||
{t("download")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-full p-[20px]">
|
||||
<p className="font-medium text-[15px] mb-[5px] text-base-content align-baseline">
|
||||
{t("email-mssg")}
|
||||
</p>
|
||||
{emailList.length > 0 ? (
|
||||
<div className="p-0 border-[1px] op-border-primary w-full rounded-md text-[15px] overflow-hidden">
|
||||
<div className="flex flex-row flex-wrap">
|
||||
{emailList.map((data, ind) => {
|
||||
return (
|
||||
<div
|
||||
className="flex flex-row items-center op-bg-primary mx-[2px] mt-[2px] rounded-md py-[5px] px-[10px]"
|
||||
key={ind}
|
||||
>
|
||||
<span className="text-base-100 text-[13px]">
|
||||
{data}
|
||||
</span>
|
||||
<span
|
||||
className="text-base-100 text-[13px] font-semibold ml-[7px] cursor-pointer"
|
||||
onClick={() => removeChip(ind)}
|
||||
>
|
||||
<i className="fa-light fa-xmark"></i>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{emailList.length <= 9 && (
|
||||
<input
|
||||
type="email"
|
||||
value={emailValue}
|
||||
className="p-[10px] rounded-md w-full text-[15px] bg-transparent outline-none"
|
||||
onChange={handleEmailValue}
|
||||
onKeyDown={handleEnterPress}
|
||||
onBlur={() => emailValue && handleEnterPress("add")}
|
||||
onInvalid={(e) =>
|
||||
e.target.setCustomValidity(t("input-required"))
|
||||
}
|
||||
onInput={(e) => e.target.setCustomValidity("")}
|
||||
required
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<input
|
||||
type="email"
|
||||
value={emailValue}
|
||||
className="p-[10px] pb-[20px] text-base-content rounded-md w-full text-[15px] outline-none bg-transparent border-[1px] op-border-primary"
|
||||
onChange={handleEmailValue}
|
||||
onKeyDown={handleEnterPress}
|
||||
placeholder={t("enter-email-plaholder")}
|
||||
onBlur={() => emailValue && handleEnterPress("add")}
|
||||
onInvalid={(e) =>
|
||||
e.target.setCustomValidity(t("input-required"))
|
||||
}
|
||||
onInput={(e) => e.target.setCustomValidity("")}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{emailErr && (
|
||||
<p className="text-xs text-[red] ml-1.5 mt-0.5">
|
||||
{t("email-error-1")}
|
||||
</p>
|
||||
)}
|
||||
<div className="mt-2">
|
||||
<button
|
||||
type="button"
|
||||
className="op-btn op-btn-secondary"
|
||||
onClick={() => emailList.length > 0 && sendEmail()}
|
||||
>
|
||||
{t("send")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="op-btn op-btn-ghost text-base-content ml-2"
|
||||
onClick={() => handleClose()}
|
||||
>
|
||||
{t("close")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ModalUi>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default EmailComponent;
|
||||
@@ -0,0 +1,30 @@
|
||||
const Guidelines = ({ x1, x2, y1, y2 }) => {
|
||||
return (
|
||||
<>
|
||||
{/* Horizontal guidelines */}
|
||||
{/* top guide */}{" "}
|
||||
<div
|
||||
className="absolute pointer-events-none z-[1000] left-0 w-full border-t-[1px] border-dashed border-[#3b82f6]"
|
||||
style={{ top: y1 }}
|
||||
/>
|
||||
{/* bottom guide */}
|
||||
<div
|
||||
className="absolute pointer-events-none z-[1000] left-0 w-full border-t-[1px] border-dashed border-[#3b82f6]"
|
||||
style={{ top: y2 }}
|
||||
/>
|
||||
{/* Vertical guidelines */}
|
||||
{/* left guide */}
|
||||
<div
|
||||
className="absolute pointer-events-none z-[1000] top-0 h-full border-l-[1px] border-dashed border-[#3b82f6]"
|
||||
style={{ left: x1 }}
|
||||
/>
|
||||
{/* right guide */}
|
||||
<div
|
||||
className="absolute pointer-events-none z-[1000] top-0 h-full border-l-[1px] border-dashed border-[#3b82f6]"
|
||||
style={{ left: x2 }}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Guidelines;
|
||||
@@ -0,0 +1,94 @@
|
||||
import React, { useEffect, useState, useRef } from "react";
|
||||
import ModalUi from "../../primitives/ModalUi";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function PageReorderModal({
|
||||
isOpen,
|
||||
handleClose,
|
||||
totalPages = 0,
|
||||
onSave
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [order, setOrder] = useState([]);
|
||||
// Keeps track of the page order relative to the original PDF
|
||||
const orderRef = useRef([]);
|
||||
// Captures the order when the modal opens
|
||||
const initialOrderRef = useRef([]);
|
||||
|
||||
// Initialize orderRef when total pages change (e.g. after upload)
|
||||
useEffect(() => {
|
||||
if (orderRef.current.length !== totalPages) {
|
||||
orderRef.current = Array.from({ length: totalPages }, (_, i) => i + 1);
|
||||
}
|
||||
}, [totalPages]);
|
||||
|
||||
// When modal opens, display the last saved order and store it as initial
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setOrder(orderRef.current);
|
||||
initialOrderRef.current = [...orderRef.current];
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
const move = (index, dir) => {
|
||||
const swapIndex = index + dir;
|
||||
if (swapIndex < 0 || swapIndex >= order.length) return;
|
||||
const newOrder = [...order];
|
||||
[newOrder[index], newOrder[swapIndex]] = [newOrder[swapIndex], newOrder[index]];
|
||||
setOrder(newOrder);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
const saveOrder = order.map((num) =>
|
||||
initialOrderRef.current.indexOf(num) + 1
|
||||
);
|
||||
// Persist the new display order for next time
|
||||
orderRef.current = [...order];
|
||||
onSave && onSave(saveOrder);
|
||||
};
|
||||
|
||||
const isUnchanged =
|
||||
order.length === initialOrderRef.current.length &&
|
||||
order.every((n, i) => n === initialOrderRef.current[i]);
|
||||
|
||||
return (
|
||||
<ModalUi isOpen={isOpen} handleClose={handleClose} title={t("reorder-pages")}>
|
||||
<div className="p-[20px] flex flex-col gap-2 text-base-content">
|
||||
{order.map((num, i) => (
|
||||
<div key={num} className="flex items-center justify-between">
|
||||
<span>
|
||||
{t("page")} {num}
|
||||
</span>
|
||||
<div className="flex gap-1">
|
||||
<button
|
||||
className="op-btn op-btn-xs op-btn-ghost text-base-content"
|
||||
disabled={i === 0}
|
||||
onClick={() => move(i, -1)}
|
||||
>
|
||||
<i className="fa-light fa-arrow-up"></i>
|
||||
</button>
|
||||
<button
|
||||
className="op-btn op-btn-xs op-btn-ghost text-base-content"
|
||||
disabled={i === order.length - 1}
|
||||
onClick={() => move(i, 1)}
|
||||
>
|
||||
<i className="fa-light fa-arrow-down"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="h-[1px] bg-[#9f9f9f] w-full my-[15px]"></div>
|
||||
<button onClick={handleSave} type="button" className="op-btn op-btn-primary" disabled={isUnchanged}>
|
||||
{t("save")}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleClose}
|
||||
type="button"
|
||||
className="op-btn op-btn-ghost text-base-content ml-1"
|
||||
>
|
||||
{t("close")}
|
||||
</button>
|
||||
</div>
|
||||
</ModalUi>
|
||||
);
|
||||
}
|
||||
@@ -1,481 +0,0 @@
|
||||
import React, { useState } from "react";
|
||||
import PrevNext from "./PrevNext";
|
||||
import {
|
||||
handleDownloadCertificate,
|
||||
handleDownloadPdf,
|
||||
handleToPrint
|
||||
} from "../../constant/Utils";
|
||||
import "../../styles/signature.css";
|
||||
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
|
||||
import ModalUi from "../../primitives/ModalUi";
|
||||
import Loader from "../../primitives/Loader";
|
||||
|
||||
function Header({
|
||||
isPdfRequestFiles,
|
||||
isPlaceholder,
|
||||
setIsDecline,
|
||||
pageNumber,
|
||||
allPages,
|
||||
changePage,
|
||||
pdfUrl,
|
||||
embedWidgetsData,
|
||||
pdfDetails,
|
||||
signerPos,
|
||||
signersdata,
|
||||
isMailSend,
|
||||
alertSendEmail,
|
||||
isCompleted,
|
||||
isShowHeader,
|
||||
decline,
|
||||
currentSigner,
|
||||
alreadySign,
|
||||
isSignYourself,
|
||||
setIsEmail,
|
||||
completeBtnTitle,
|
||||
setIsEditTemplate,
|
||||
isPublicTemplate
|
||||
}) {
|
||||
const filterPrefill =
|
||||
signerPos && signerPos?.filter((data) => data.Role !== "prefill");
|
||||
const isMobile = window.innerWidth < 767;
|
||||
const [isDownloading, setIsDownloading] = useState("");
|
||||
const isGuestSigner = localStorage.getItem("isGuestSigner");
|
||||
|
||||
//function for show decline alert
|
||||
const handleDeclinePdfAlert = async () => {
|
||||
const currentDecline = { currnt: "Sure", isDeclined: true };
|
||||
setIsDecline(currentDecline);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex py-[5px]">
|
||||
{isMobile && isShowHeader ? (
|
||||
<div
|
||||
id="navbar"
|
||||
className={isGuestSigner ? "stickySignerHead" : "stickyHead"}
|
||||
style={{
|
||||
width: isGuestSigner
|
||||
? window.innerWidth
|
||||
: window.innerWidth - 30 + "px"
|
||||
}}
|
||||
>
|
||||
<div className="flex justify-between items-center py-[5px] px-[10px] ">
|
||||
<div onClick={() => window.history.go(-2)}>
|
||||
<i
|
||||
className="fa-light fa-arrow-left text-base-content"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</div>
|
||||
<PrevNext
|
||||
pageNumber={pageNumber}
|
||||
allPages={allPages}
|
||||
changePage={changePage}
|
||||
/>
|
||||
{pdfUrl && alreadySign ? (
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<div className="op-link op-link-primary no-underline text-[16px] font-semibold pr-[3px] pl-[5px]">
|
||||
<i
|
||||
className="fa-light fa-ellipsis-v"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</div>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content
|
||||
className="DropdownMenuContent"
|
||||
sideOffset={5}
|
||||
>
|
||||
<DropdownMenu.Item
|
||||
className="DropdownMenuItem"
|
||||
onClick={() =>
|
||||
handleDownloadPdf(pdfDetails, pdfUrl, setIsDownloading)
|
||||
}
|
||||
>
|
||||
<div className="flex flex-row">
|
||||
<i
|
||||
className="fa-light fa-arrow-down mr-[3px]"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
Download
|
||||
</div>
|
||||
</DropdownMenu.Item>
|
||||
{isCompleted && (
|
||||
<DropdownMenu.Item
|
||||
className="DropdownMenuItem"
|
||||
onClick={() =>
|
||||
handleDownloadCertificate(
|
||||
pdfDetails,
|
||||
setIsDownloading
|
||||
)
|
||||
}
|
||||
>
|
||||
<div className="border-none bg-[#fff]">
|
||||
<i
|
||||
className="fa-light fa-award mr-[3px]"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
Certificate
|
||||
</div>
|
||||
</DropdownMenu.Item>
|
||||
)}
|
||||
{isSignYourself && (
|
||||
<DropdownMenu.Item
|
||||
className="DropdownMenuItem"
|
||||
onClick={() => setIsEmail(true)}
|
||||
>
|
||||
<div className="flex flex-row">
|
||||
<i
|
||||
className="fa-light fa-envelope mr-[3px]"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
Mail
|
||||
</div>
|
||||
</DropdownMenu.Item>
|
||||
)}
|
||||
<DropdownMenu.Item
|
||||
className="DropdownMenuItem"
|
||||
onClick={(e) =>
|
||||
handleToPrint(e, pdfUrl, setIsDownloading)
|
||||
}
|
||||
>
|
||||
<div className="flex flex-row">
|
||||
<i
|
||||
className="fa-light fa-print mr-[3px]"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
Print
|
||||
</div>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Portal>
|
||||
</DropdownMenu.Root>
|
||||
) : (
|
||||
<div className="flex justify-around items-center">
|
||||
{/* current signer is checking user send request and check status of pdf sign than if current
|
||||
user exist than show finish button else no
|
||||
*/}
|
||||
{currentSigner && (
|
||||
<div className="flex items-center" data-tut="reactourFifth">
|
||||
{decline && (
|
||||
<div
|
||||
onClick={() => handleDeclinePdfAlert()}
|
||||
className="text-[red] border-none font-[650] text-[14px] mr-2"
|
||||
>
|
||||
Decline
|
||||
</div>
|
||||
)}
|
||||
{isPlaceholder ? (
|
||||
<div
|
||||
onClick={() => {
|
||||
if (!isMailSend) {
|
||||
alertSendEmail();
|
||||
}
|
||||
}}
|
||||
className={`${
|
||||
isMailSend ? "" : "op-link-primary"
|
||||
} op-link no-underline font-[650] text-[14px]`}
|
||||
data-tut="headerArea"
|
||||
>
|
||||
{completeBtnTitle ? completeBtnTitle : "Send"}
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
data-tut="reactourThird"
|
||||
onClick={() => {
|
||||
if (!pdfUrl) {
|
||||
embedWidgetsData();
|
||||
} else if (isPdfRequestFiles) {
|
||||
embedWidgetsData();
|
||||
}
|
||||
}}
|
||||
className="border-none font-[650] text-[14px] op-link op-link-primary no-underline"
|
||||
>
|
||||
Finish
|
||||
</div>
|
||||
)}
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<div className="font-[650] text-[18px] px-2 ml-[4px] text-base-content no-underline">
|
||||
<i
|
||||
className="fa-light fa-ellipsis-v"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</div>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content
|
||||
className="bg-white shadow-md rounded-full px-3 py-2"
|
||||
sideOffset={5}
|
||||
>
|
||||
<DropdownMenu.Item
|
||||
className="flex flex-row justify-center items-center text-[13px] focus:outline-none cursor-pointer"
|
||||
onClick={() =>
|
||||
handleDownloadPdf(
|
||||
pdfDetails,
|
||||
pdfUrl,
|
||||
setIsDownloading
|
||||
)
|
||||
}
|
||||
>
|
||||
<i
|
||||
className="fa-light fa-arrow-down mr-[3px]"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span className="font-[500]">Download</span>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Portal>
|
||||
</DropdownMenu.Root>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-wrap justify-between items-center w-full gap-y-1 ml-1">
|
||||
<PrevNext
|
||||
pageNumber={pageNumber}
|
||||
allPages={allPages}
|
||||
changePage={changePage}
|
||||
/>
|
||||
{isPlaceholder ? (
|
||||
<>
|
||||
<div className="flex mx-[100px] lg:mx-0 order-last lg:order-none">
|
||||
{!isMailSend &&
|
||||
signersdata.length > 0 &&
|
||||
signersdata.length !== filterPrefill.length && (
|
||||
<div>
|
||||
{filterPrefill.length === 0 ? (
|
||||
<span className="text-[13px] text-[#f5405e]">
|
||||
Add {signersdata.length - filterPrefill.length}{" "}
|
||||
recipients signature
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-[13px] text-[#f5405e]">
|
||||
Add {signersdata.length - filterPrefill.length} more
|
||||
recipients signature
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex">
|
||||
{setIsEditTemplate && (
|
||||
<button
|
||||
onClick={() => setIsEditTemplate(true)}
|
||||
className="outline-none border-none text-center mr-[3px]"
|
||||
>
|
||||
<i className="fa-light fa-gear fa-lg"></i>
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => window.history.go(-2)}
|
||||
type="button"
|
||||
className="op-btn op-btn-ghost op-btn-sm mr-[3px]"
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
<button
|
||||
disabled={isMailSend && true}
|
||||
data-tut="headerArea"
|
||||
className="op-btn op-btn-primary op-btn-sm mr-[3px]"
|
||||
onClick={() => alertSendEmail()}
|
||||
>
|
||||
{completeBtnTitle
|
||||
? completeBtnTitle
|
||||
: isMailSend
|
||||
? "Sent"
|
||||
: "Send"}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : isPdfRequestFiles ? (
|
||||
alreadySign ? (
|
||||
<div className="flex flex-row">
|
||||
<button
|
||||
onClick={(e) => handleToPrint(e, pdfUrl, setIsDownloading)}
|
||||
type="button"
|
||||
className="op-btn op-btn-neutral op-btn-sm mr-[3px] shadow"
|
||||
>
|
||||
<i
|
||||
className="fa-light fa-print py-[3px]"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span className="hidden lg:block">Print</span>
|
||||
</button>
|
||||
{isCompleted && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
handleDownloadCertificate(pdfDetails, setIsDownloading)
|
||||
}
|
||||
className="op-btn op-btn-secondary op-btn-sm mr-[3px] shadow"
|
||||
>
|
||||
<i
|
||||
className="fa-light fa-award py-[3px]"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span className="hidden lg:block">Certificate</span>
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="op-btn op-btn-primary op-btn-sm mr-[3px] shadow"
|
||||
onClick={() =>
|
||||
handleDownloadPdf(pdfDetails, pdfUrl, setIsDownloading)
|
||||
}
|
||||
>
|
||||
<i
|
||||
className="fa-light fa-download py-[3px]"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span className="hidden lg:block">Download</span>
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex" data-tut="reactourFifth">
|
||||
<button
|
||||
onClick={() => window.history.go(-2)}
|
||||
type="button"
|
||||
className="op-btn op-btn-ghost op-btn-sm mr-[3px]"
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
{currentSigner && (
|
||||
<>
|
||||
<button
|
||||
className="op-btn op-btn-secondary op-btn-sm mr-[3px] shadow"
|
||||
onClick={() => handleDeclinePdfAlert()}
|
||||
>
|
||||
Decline
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="op-btn op-btn-primary op-btn-sm mr-[3px] shadow"
|
||||
onClick={() => embedWidgetsData()}
|
||||
>
|
||||
Finish
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="op-btn op-btn-neutral op-btn-sm mr-[3px] shadow"
|
||||
onClick={() =>
|
||||
handleDownloadPdf(pdfDetails, pdfUrl, setIsDownloading)
|
||||
}
|
||||
>
|
||||
<i className="fa-light fa-arrow-down font-semibold lg:hidden"></i>
|
||||
<span className="hidden lg:block">Download</span>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
) : isCompleted ? (
|
||||
<div className="flex flex-row">
|
||||
{isCompleted && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
handleDownloadCertificate(pdfDetails, setIsDownloading)
|
||||
}
|
||||
className="op-btn op-btn-secondary op-btn-sm gap-0 font-medium text-[12px] mr-[3px] shadow"
|
||||
>
|
||||
<i className="fa-light fa-award" aria-hidden="true"></i>
|
||||
<span className="hidden lg:block ml-1">Certificate</span>
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={(e) => handleToPrint(e, pdfUrl, setIsDownloading)}
|
||||
type="button"
|
||||
className="op-btn op-btn-neutral op-btn-sm gap-0 font-medium text-[12px] mr-[3px] shadow"
|
||||
>
|
||||
<i className="fa-light fa-print" aria-hidden="true"></i>
|
||||
<span className="hidden lg:block ml-1">Print</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="op-btn op-btn-primary op-btn-sm gap-0 font-medium text-[12px] mr-[3px] shadow"
|
||||
onClick={() =>
|
||||
handleDownloadPdf(pdfDetails, pdfUrl, setIsDownloading)
|
||||
}
|
||||
>
|
||||
<i className="fa-light fa-download" aria-hidden="true"></i>
|
||||
<span className="hidden lg:block ml-1">Download</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="op-btn op-btn-info op-btn-sm gap-0 font-medium text-[12px] mr-[3px] shadow"
|
||||
onClick={() => setIsEmail(true)}
|
||||
>
|
||||
<i className="fa-light fa-envelope" aria-hidden="true"></i>
|
||||
<span className="hidden lg:block ml-1">Mail</span>
|
||||
</button>
|
||||
</div>
|
||||
) : isPublicTemplate ? (
|
||||
<>
|
||||
<div className="flex">
|
||||
<button
|
||||
type="button"
|
||||
className="op-btn op-btn-primary op-btn-sm shadow"
|
||||
onClick={() => {
|
||||
embedWidgetsData();
|
||||
}}
|
||||
>
|
||||
Sign Now
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex">
|
||||
<button
|
||||
onClick={() => window.history.go(-2)}
|
||||
type="button"
|
||||
className="op-btn op-btn-ghost op-btn-sm mr-[3px]"
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="op-btn op-btn-primary op-btn-sm mr-[3px]"
|
||||
onClick={() => {
|
||||
if (!pdfUrl) {
|
||||
embedWidgetsData();
|
||||
}
|
||||
}}
|
||||
>
|
||||
Finish
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{isDownloading === "pdf" && (
|
||||
<div className="fixed z-[200] inset-0 flex justify-center items-center bg-black bg-opacity-30">
|
||||
<Loader />
|
||||
</div>
|
||||
)}
|
||||
<ModalUi
|
||||
isOpen={isDownloading === "certificate"}
|
||||
title={
|
||||
isDownloading === "certificate"
|
||||
? "Generating certificate"
|
||||
: "PDF Download"
|
||||
}
|
||||
handleClose={() => setIsDownloading("")}
|
||||
>
|
||||
<div className="p-3 md:p-5 text-[13px] md:text-base text-center text-base-content">
|
||||
{isDownloading === "certificate"}{" "}
|
||||
<p>
|
||||
Your completion certificate is being generated. Please wait
|
||||
momentarily. If the download doesn't start shortly, click the
|
||||
button again.
|
||||
</p>
|
||||
</div>
|
||||
</ModalUi>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Header;
|
||||
@@ -0,0 +1,749 @@
|
||||
import React, { useRef, useState } from "react";
|
||||
import PrevNext from "./PrevNext";
|
||||
import {
|
||||
base64ToArrayBuffer,
|
||||
decryptPdf,
|
||||
deletePdfPage,
|
||||
flattenPdf,
|
||||
getFileAsArrayBuffer,
|
||||
handleDownloadCertificate,
|
||||
handleDownloadPdf,
|
||||
handleRemoveWidgets,
|
||||
handleToPrint,
|
||||
reorderPdfPages
|
||||
} from "../../constant/Utils";
|
||||
import "../../styles/signature.css";
|
||||
import { DropdownMenu } from "radix-ui";
|
||||
import ModalUi from "../../primitives/ModalUi";
|
||||
import Loader from "../../primitives/Loader";
|
||||
import PageReorderModal from "./PageReorderModal";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { PDFDocument } from "pdf-lib";
|
||||
import { maxFileSize } from "../../constant/const";
|
||||
|
||||
function Header(props) {
|
||||
const { t } = useTranslation();
|
||||
const isMobile = window.innerWidth < 767;
|
||||
const [isDownloading, setIsDownloading] = useState("");
|
||||
const [isDeletePage, setIsDeletePage] = useState(false);
|
||||
const [isReorderModal, setIsReorderModal] = useState(false);
|
||||
const mergePdfInputRef = useRef(null);
|
||||
const enabledBackBtn = props?.disabledBackBtn === true ? false : true;
|
||||
//function for show decline alert
|
||||
const handleDeclinePdfAlert = async () => {
|
||||
if (props?.handleDecline) {
|
||||
props.handleDecline();
|
||||
} else {
|
||||
const currentDecline = { currnt: "Sure", isDeclined: true };
|
||||
props?.setIsDecline(currentDecline);
|
||||
}
|
||||
};
|
||||
const handleDetelePage = async () => {
|
||||
props?.setIsUploadPdf && props?.setIsUploadPdf(true);
|
||||
const pdfupdatedData = await deletePdfPage(
|
||||
props?.pdfArrayBuffer,
|
||||
props?.pageNumber
|
||||
);
|
||||
if (pdfupdatedData?.totalPages === 1) {
|
||||
alert(t("delete-alert"));
|
||||
} else {
|
||||
props?.setPdfBase64Url(pdfupdatedData.base64);
|
||||
props?.setPdfArrayBuffer(pdfupdatedData.arrayBuffer);
|
||||
setIsDeletePage(false);
|
||||
handleRemoveWidgets(
|
||||
props?.setSignerPos,
|
||||
props?.signerPos,
|
||||
props?.pageNumber
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// `removeFile` is used to remove file if exists
|
||||
const removeFile = (e) => {
|
||||
if (e) {
|
||||
e.target.value = "";
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileUpload = async (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) {
|
||||
alert(t("please-select-pdf"));
|
||||
return;
|
||||
}
|
||||
if (!file.type.includes("pdf")) {
|
||||
alert(t("only-pdf-allowed"));
|
||||
return;
|
||||
}
|
||||
const fileSize =
|
||||
maxFileSize;
|
||||
const pdfsize = file?.size;
|
||||
const fileSizeBytes = fileSize * 1024 * 1024;
|
||||
if (pdfsize > fileSizeBytes) {
|
||||
alert(`${t("file-alert-1")} ${fileSize} MB`);
|
||||
removeFile(e);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
let uploadedPdfBytes = await file.arrayBuffer();
|
||||
try {
|
||||
uploadedPdfBytes = await flattenPdf(uploadedPdfBytes);
|
||||
} catch (err) {
|
||||
if (err?.message?.includes("is encrypted")) {
|
||||
try {
|
||||
const pdfFile = await decryptPdf(file, "");
|
||||
const pdfArrayBuffer = await getFileAsArrayBuffer(pdfFile);
|
||||
uploadedPdfBytes = await flattenPdf(pdfArrayBuffer);
|
||||
} catch (err) {
|
||||
if (err?.response?.status === 401) {
|
||||
const password = prompt(
|
||||
`PDF "${file.name}" is password-protected. Enter password:`
|
||||
);
|
||||
if (password) {
|
||||
try {
|
||||
const pdfFile = await decryptPdf(file, password);
|
||||
const pdfArrayBuffer = await getFileAsArrayBuffer(pdfFile);
|
||||
uploadedPdfBytes = await flattenPdf(pdfArrayBuffer);
|
||||
// Upload the file to Parse Server
|
||||
} catch (err) {
|
||||
console.error("Incorrect password or decryption failed", err);
|
||||
alert(t("incorrect-password-or-decryption-failed"));
|
||||
}
|
||||
} else {
|
||||
alert(t("provide-password"));
|
||||
}
|
||||
} else {
|
||||
console.log("Err ", err);
|
||||
alert(t("error-uploading-pdf"));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
alert(t("error-uploading-pdf"));
|
||||
}
|
||||
}
|
||||
const uploadedPdfDoc = await PDFDocument.load(uploadedPdfBytes, {
|
||||
ignoreEncryption: true
|
||||
});
|
||||
const basePdfDoc = await PDFDocument.load(props.pdfArrayBuffer);
|
||||
|
||||
// Copy pages from the uploaded PDF to the base PDF
|
||||
const uploadedPdfPages = await basePdfDoc.copyPages(
|
||||
uploadedPdfDoc,
|
||||
uploadedPdfDoc.getPageIndices()
|
||||
);
|
||||
uploadedPdfPages.forEach((page) => basePdfDoc.addPage(page));
|
||||
// Save the updated PDF
|
||||
const pdfBase64 = await basePdfDoc.saveAsBase64({
|
||||
useObjectStreams: false
|
||||
});
|
||||
const pdfBuffer = base64ToArrayBuffer(pdfBase64);
|
||||
const pdfsize = pdfBuffer?.byteLength;
|
||||
const fileSizeBytes = fileSize * 1024 * 1024;
|
||||
if (pdfsize > fileSizeBytes) {
|
||||
alert(`${t("file-alert-1")} ${fileSize} MB`);
|
||||
removeFile(e);
|
||||
return;
|
||||
}
|
||||
props.setPdfArrayBuffer(pdfBuffer);
|
||||
props.setPdfBase64Url(pdfBase64);
|
||||
props.setIsUploadPdf && props.setIsUploadPdf(true);
|
||||
mergePdfInputRef.current.value = "";
|
||||
} catch (error) {
|
||||
mergePdfInputRef.current.value = "";
|
||||
console.error("Error merging PDF:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleReorderSave = async (order) => {
|
||||
try {
|
||||
const pdfupdatedData = await reorderPdfPages(props.pdfArrayBuffer, order);
|
||||
if (pdfupdatedData) {
|
||||
props.setPdfArrayBuffer(pdfupdatedData.arrayBuffer);
|
||||
props.setPdfBase64Url(pdfupdatedData.base64);
|
||||
props.setAllPages(pdfupdatedData.totalPages);
|
||||
props.setPageNumber(1);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("error in reorder pdf pages", e);
|
||||
}
|
||||
setIsReorderModal(false);
|
||||
};
|
||||
|
||||
const handleDownloadDoc = async () => {
|
||||
await handleDownloadPdf(
|
||||
props?.pdfDetails,
|
||||
setIsDownloading,
|
||||
props.pdfBase64
|
||||
);
|
||||
};
|
||||
const handleDownloadBtn = async () => {
|
||||
if (
|
||||
props?.isCompleted
|
||||
) {
|
||||
props?.setIsDownloadModal(true);
|
||||
} else {
|
||||
await handleDownloadDoc();
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className="flex py-[5px]">
|
||||
{isMobile && props?.isShowHeader ? (
|
||||
<div
|
||||
id="navbar"
|
||||
className="stickyHead touch-none"
|
||||
style={{
|
||||
width: window.innerWidth + "px"
|
||||
}}
|
||||
>
|
||||
<div className="flex justify-between items-center py-[5px] pl-[10px] ">
|
||||
<div onClick={() => window.history.go(-2)}>
|
||||
<i
|
||||
className="fa-light fa-arrow-left text-base-content"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</div>
|
||||
<PrevNext
|
||||
pageNumber={props?.pageNumber}
|
||||
allPages={props?.allPages}
|
||||
changePage={props?.changePage}
|
||||
/>
|
||||
{props?.isCompleted || props?.alreadySign ? (
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<div className="op-link op-link-primary no-underline text-[16px] font-semibold px-3">
|
||||
<i
|
||||
className="fa-light fa-ellipsis-v"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</div>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content
|
||||
className="DropdownMenuContent"
|
||||
sideOffset={5}
|
||||
>
|
||||
<DropdownMenu.Item
|
||||
className="DropdownMenuItem"
|
||||
onClick={() => handleDownloadBtn()}
|
||||
>
|
||||
<div className="flex flex-row">
|
||||
<i
|
||||
className="fa-light fa-arrow-down mr-[3px]"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
{t("download")}
|
||||
</div>
|
||||
</DropdownMenu.Item>
|
||||
{
|
||||
props?.isCompleted && (
|
||||
<DropdownMenu.Item
|
||||
className="DropdownMenuItem"
|
||||
onClick={() =>
|
||||
handleDownloadCertificate(
|
||||
props?.pdfDetails,
|
||||
setIsDownloading
|
||||
)
|
||||
}
|
||||
>
|
||||
<div className="border-none bg-[#fff]">
|
||||
<i
|
||||
className="fa-light fa-award mr-[3px]"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
{t("certificate")}
|
||||
</div>
|
||||
</DropdownMenu.Item>
|
||||
)
|
||||
}
|
||||
{props?.isSignYourself && (
|
||||
<DropdownMenu.Item
|
||||
className="DropdownMenuItem"
|
||||
onClick={() => props?.setIsEmail(true)}
|
||||
>
|
||||
<div className="flex flex-row">
|
||||
<i
|
||||
className="fa-light fa-envelope mr-[3px]"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
{t("mail")}
|
||||
</div>
|
||||
</DropdownMenu.Item>
|
||||
)}
|
||||
<DropdownMenu.Item
|
||||
className="DropdownMenuItem"
|
||||
onClick={(e) =>
|
||||
handleToPrint(e, setIsDownloading, props?.pdfDetails)
|
||||
}
|
||||
>
|
||||
<div className="flex flex-row">
|
||||
<i
|
||||
className="fa-light fa-print mr-[3px]"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
{t("print")}
|
||||
</div>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Portal>
|
||||
</DropdownMenu.Root>
|
||||
) : (
|
||||
<div className="flex justify-around items-center">
|
||||
{/* current signer is checking user send request and check status of pdf sign than if current
|
||||
user exist than show finish button else no
|
||||
*/}
|
||||
{props?.currentSigner && (
|
||||
<div className="flex items-center" data-tut="reactourFifth">
|
||||
{props?.decline && (
|
||||
<div
|
||||
onClick={() => handleDeclinePdfAlert()}
|
||||
className="text-[red] border-none font-[650] text-[14px] mr-2"
|
||||
>
|
||||
{t("decline")}
|
||||
</div>
|
||||
)}
|
||||
{props?.isPlaceholder ? (
|
||||
<div
|
||||
onClick={() => {
|
||||
if (!props?.isMailSend) {
|
||||
props?.handleSaveDoc();
|
||||
}
|
||||
}}
|
||||
className={`${
|
||||
props?.isMailSend ? "" : "op-link-primary"
|
||||
} op-link no-underline font-[650] text-[14px]`}
|
||||
data-tut="headerArea"
|
||||
>
|
||||
{props?.completeBtnTitle
|
||||
? props?.completeBtnTitle
|
||||
: t("send")}
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
data-tut="reactourThird"
|
||||
onClick={() => props?.embedWidgetsData()}
|
||||
className="border-none font-[650] text-[14px] op-link op-link-primary no-underline"
|
||||
>
|
||||
{t("finish")}
|
||||
</div>
|
||||
)}
|
||||
<input
|
||||
type="file"
|
||||
className="hidden"
|
||||
accept="application/pdf"
|
||||
ref={mergePdfInputRef}
|
||||
onChange={handleFileUpload}
|
||||
/>
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<div className="font-[650] text-[18px] px-3 text-base-content no-underline">
|
||||
<i
|
||||
className="fa-light fa-ellipsis-v"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</div>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content
|
||||
className="bg-white shadow-md rounded-md px-3 py-2"
|
||||
sideOffset={5}
|
||||
>
|
||||
{props?.setIsEditTemplate && (
|
||||
<DropdownMenu.Item
|
||||
className="DropdownMenuItem"
|
||||
onClick={() => props?.setIsEditTemplate(true)}
|
||||
>
|
||||
<div className="flex flex-row">
|
||||
<i
|
||||
className="fa-light fa-gear mr-[3px]"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span className="font-[500]">{t("Edit")}</span>
|
||||
</div>
|
||||
</DropdownMenu.Item>
|
||||
)}
|
||||
<DropdownMenu.Item
|
||||
className="DropdownMenuItem"
|
||||
onClick={() => handleDownloadDoc()}
|
||||
>
|
||||
<div className="flex flex-row">
|
||||
<i
|
||||
className="fa-light fa-arrow-down mr-[3px]"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span className="font-[500]">
|
||||
{t("download")}
|
||||
</span>
|
||||
</div>
|
||||
</DropdownMenu.Item>
|
||||
{!props?.isDisablePdfEditTools && (
|
||||
<>
|
||||
<DropdownMenu.Item
|
||||
className="DropdownMenuItem"
|
||||
onClick={() => mergePdfInputRef.current.click()}
|
||||
>
|
||||
<div className="flex flex-row">
|
||||
<i className="fa-light fa-plus text-gray-500 2xl:text-[30px] mr-[3px]"></i>
|
||||
<span className="font-[500]">
|
||||
{t("add-pages")}
|
||||
</span>
|
||||
</div>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
className="DropdownMenuItem"
|
||||
onClick={() => setIsDeletePage(true)}
|
||||
>
|
||||
<div className="flex flex-row">
|
||||
<i className="fa-light fa-trash text-gray-500 2xl:text-[30px] mr-[3px]"></i>
|
||||
<span className="font-[500]">
|
||||
{t("delete-page")}
|
||||
</span>
|
||||
</div>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
className="DropdownMenuItem"
|
||||
onClick={() => setIsReorderModal(true)}
|
||||
>
|
||||
<div className="flex flex-row">
|
||||
<i className="fa-light fa-list-ol text-gray-500 2xl:text-[30px] mr-[3px]"></i>
|
||||
<span className="font-[500]">
|
||||
{t("reorder-pages")}
|
||||
</span>
|
||||
</div>
|
||||
</DropdownMenu.Item>
|
||||
|
||||
<DropdownMenu.Item
|
||||
className="DropdownMenuItem"
|
||||
onClick={() => props?.handleRotationFun(90)}
|
||||
>
|
||||
<div className="flex flex-row">
|
||||
<i className="fa-light fa-rotate-right text-gray-500 2xl:text-[30px] mr-[3px]"></i>
|
||||
<span className="font-[500]">
|
||||
{t("rotate-right")}
|
||||
</span>
|
||||
</div>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
className="DropdownMenuItem"
|
||||
onClick={() => props?.handleRotationFun(-90)}
|
||||
>
|
||||
<div className="flex flex-row">
|
||||
<i className="fa-light fa-rotate-left text-gray-500 2xl:text-[30px] mr-[3px]"></i>
|
||||
<span className="font-[500]">
|
||||
{t("rotate-left")}
|
||||
</span>
|
||||
</div>
|
||||
</DropdownMenu.Item>
|
||||
</>
|
||||
)}
|
||||
|
||||
<DropdownMenu.Item
|
||||
className="DropdownMenuItem"
|
||||
onClick={() => props?.clickOnZoomIn()}
|
||||
>
|
||||
<div className="flex flex-row">
|
||||
<i className="fa-light fa-magnifying-glass-plus text-gray-500 2xl:text-[30px] mr-[3px]"></i>
|
||||
<span className="font-[500]">{t("zoom-in")}</span>
|
||||
</div>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
className="DropdownMenuItem"
|
||||
onClick={() => props?.clickOnZoomOut()}
|
||||
>
|
||||
<div className="flex flex-row">
|
||||
<i className="fa-light fa-magnifying-glass-minus text-gray-500 2xl:text-[30px] mr-[3px]"></i>
|
||||
<span className="font-[500]">
|
||||
{t("zoom-out")}
|
||||
</span>
|
||||
</div>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Portal>
|
||||
</DropdownMenu.Root>
|
||||
</div>
|
||||
)}
|
||||
{props?.isPublicTemplate && (
|
||||
<div
|
||||
data-tut="reactourThird"
|
||||
onClick={() => props?.embedWidgetsData()}
|
||||
className="border-none font-[650] text-[14px] pr-2 op-link op-link-primary no-underline"
|
||||
>
|
||||
{t("sign-now")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-wrap justify-between items-center w-full gap-y-1 ml-1">
|
||||
<PrevNext
|
||||
pageNumber={props?.pageNumber}
|
||||
allPages={props?.allPages}
|
||||
changePage={props?.changePage}
|
||||
/>
|
||||
{props?.isPlaceholder ? (
|
||||
<>
|
||||
<div className="flex mx-[100px] lg:mx-0 order-last lg:order-none"></div>
|
||||
<div className="flex">
|
||||
{props?.setIsEditTemplate && (
|
||||
<button
|
||||
onClick={() => props?.setIsEditTemplate(true)}
|
||||
className="outline-none border-none text-center mr-[3px]"
|
||||
>
|
||||
<i className="fa-light fa-gear fa-lg text-base-content"></i>
|
||||
</button>
|
||||
)}
|
||||
{enabledBackBtn && (
|
||||
<button
|
||||
onClick={() => window.history.go(-2)}
|
||||
type="button"
|
||||
className="op-btn op-btn-ghost text-base-content op-btn-sm mr-[3px]"
|
||||
>
|
||||
{t("back")}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
disabled={props?.isMailSend && true}
|
||||
data-tut="headerArea"
|
||||
className="op-btn op-btn-primary op-btn-sm mr-[3px]"
|
||||
onClick={() => props?.handleSaveDoc()}
|
||||
>
|
||||
{props?.completeBtnTitle
|
||||
? props?.completeBtnTitle
|
||||
: props?.isMailSend
|
||||
? t("sent")
|
||||
: t("send")}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : props?.isPdfRequestFiles || props?.isSelfSign ? (
|
||||
props?.alreadySign || (props?.isSelfSign && props?.isCompleted) ? (
|
||||
<div className="flex flex-row">
|
||||
<button
|
||||
onClick={(e) =>
|
||||
handleToPrint(e, setIsDownloading, props?.pdfDetails)
|
||||
}
|
||||
type="button"
|
||||
className="op-btn op-btn-neutral op-btn-sm mr-[3px] shadow"
|
||||
>
|
||||
<i
|
||||
className="fa-light fa-print py-[3px]"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span className="hidden lg:block">{t("print")}</span>
|
||||
</button>
|
||||
{
|
||||
props?.isCompleted && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
handleDownloadCertificate(
|
||||
props?.pdfDetails,
|
||||
setIsDownloading
|
||||
)
|
||||
}
|
||||
className="op-btn op-btn-secondary op-btn-sm mr-[3px] shadow"
|
||||
>
|
||||
<i
|
||||
className="fa-light fa-award py-[3px]"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span className="hidden lg:block">
|
||||
{t("certificate")}
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
<button
|
||||
type="button"
|
||||
className="op-btn op-btn-primary op-btn-sm mr-[3px] shadow"
|
||||
onClick={() => handleDownloadBtn()}
|
||||
>
|
||||
<i
|
||||
className="fa-light fa-download py-[3px]"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span className="hidden lg:block">{t("download")}</span>
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex" data-tut="reactourFifth">
|
||||
{props?.currentSigner && (
|
||||
<>
|
||||
{props?.templateId && (
|
||||
<button
|
||||
onClick={() => handleDownloadDoc()}
|
||||
type="button"
|
||||
className="op-btn op-btn-ghost text-base-content op-btn-sm mr-[3px]"
|
||||
>
|
||||
<span className="hidden lg:block">{t("download")}</span>
|
||||
</button>
|
||||
)}
|
||||
{!props?.isSelfSign && (
|
||||
<button
|
||||
className="op-btn op-btn-secondary op-btn-sm mr-[3px] shadow"
|
||||
onClick={() => handleDeclinePdfAlert()}
|
||||
>
|
||||
{t("decline")}
|
||||
</button>
|
||||
)}
|
||||
{!props?.templateId && (
|
||||
<button
|
||||
type="button"
|
||||
className="op-btn op-btn-ghost text-base-content op-btn-sm mr-[3px]"
|
||||
onClick={() => handleDownloadDoc()}
|
||||
>
|
||||
<i className="fa-light fa-arrow-down font-semibold lg:hidden"></i>
|
||||
<span className="hidden lg:block">{t("download")}</span>
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="op-btn op-btn-primary op-btn-sm mr-[3px] shadow"
|
||||
onClick={() => props?.embedWidgetsData()}
|
||||
>
|
||||
{t("finish")}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
) : props?.isCompleted ? (
|
||||
<div className="flex flex-row">
|
||||
{
|
||||
props?.isCompleted && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
handleDownloadCertificate(
|
||||
props?.pdfDetails,
|
||||
setIsDownloading
|
||||
)
|
||||
}
|
||||
className="op-btn op-btn-secondary op-btn-sm gap-0 font-medium text-[12px] mr-[3px] shadow"
|
||||
>
|
||||
<i className="fa-light fa-award" aria-hidden="true"></i>
|
||||
<span className="hidden lg:block ml-1">
|
||||
{t("certificate")}
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
<button
|
||||
onClick={(e) =>
|
||||
handleToPrint(e, setIsDownloading, props?.pdfDetails)
|
||||
}
|
||||
type="button"
|
||||
className="op-btn op-btn-neutral op-btn-sm gap-0 font-medium text-[12px] mr-[3px] shadow"
|
||||
>
|
||||
<i className="fa-light fa-print" aria-hidden="true"></i>
|
||||
<span className="hidden lg:block ml-1">{t("print")}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="op-btn op-btn-primary op-btn-sm gap-0 font-medium text-[12px] mr-[3px] shadow"
|
||||
// onClick={() => props?.setIsDownloadModal(true)}
|
||||
onClick={() => handleDownloadBtn()}
|
||||
>
|
||||
<i className="fa-light fa-download" aria-hidden="true"></i>
|
||||
<span className="hidden lg:block ml-1">{t("download")}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="op-btn op-btn-info op-btn-sm gap-0 font-medium text-[12px] mr-[3px] shadow"
|
||||
onClick={() => props?.setIsEmail(true)}
|
||||
>
|
||||
<i className="fa-light fa-envelope" aria-hidden="true"></i>
|
||||
<span className="hidden lg:block ml-1">{t("mail")}</span>
|
||||
</button>
|
||||
</div>
|
||||
) : props?.isPublicTemplate ? (
|
||||
<div className="flex">
|
||||
<button
|
||||
type="button"
|
||||
className="op-btn op-btn-primary op-btn-sm shadow"
|
||||
onClick={() => props?.embedWidgetsData()}
|
||||
>
|
||||
{t("sign-now")}
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex">
|
||||
<button
|
||||
onClick={() => window.history.go(-2)}
|
||||
type="button"
|
||||
className="op-btn op-btn-ghost text-base-content op-btn-sm mr-[3px]"
|
||||
>
|
||||
{t("back")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="op-btn op-btn-primary op-btn-sm mr-[3px]"
|
||||
onClick={() => props?.embedWidgetsData()}
|
||||
>
|
||||
{t("finish")}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{isDownloading === "pdf" && (
|
||||
<div className="fixed z-[200] inset-0 flex justify-center items-center bg-black bg-opacity-30">
|
||||
<Loader />
|
||||
</div>
|
||||
)}
|
||||
<ModalUi
|
||||
isOpen={
|
||||
isDownloading === "certificate" || isDownloading === "certificate_err"
|
||||
}
|
||||
title={
|
||||
isDownloading === "certificate" || isDownloading === "certificate_err"
|
||||
? t("generating-certificate")
|
||||
: t("pdf-download")
|
||||
}
|
||||
handleClose={() => setIsDownloading("")}
|
||||
>
|
||||
<div className="p-3 md:p-5 text-[13px] md:text-base text-center text-base-content">
|
||||
{isDownloading === "certificate" ? (
|
||||
<p>{t("generate-certificate-alert")}</p>
|
||||
) : (
|
||||
<p>{t("generate-certificate-err")}</p>
|
||||
)}
|
||||
</div>
|
||||
</ModalUi>
|
||||
<ModalUi
|
||||
isOpen={isDeletePage}
|
||||
title={t("delete-page")}
|
||||
handleClose={() => setIsDeletePage(false)}
|
||||
>
|
||||
<div className="h-[100%] p-[20px]">
|
||||
<p className="font-medium text-base-content">{t("delete-alert-2")}</p>
|
||||
<p className="pt-3 text-base-content">{t("delete-note")}</p>
|
||||
<div className="h-[1px] bg-[#9f9f9f] w-full my-[15px]"></div>
|
||||
<button
|
||||
onClick={() => handleDetelePage()}
|
||||
type="button"
|
||||
className="op-btn op-btn-primary"
|
||||
>
|
||||
{t("yes")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setIsDeletePage(false)}
|
||||
type="button"
|
||||
className="op-btn op-btn-ghost text-base-content"
|
||||
>
|
||||
{t("no")}
|
||||
</button>
|
||||
</div>
|
||||
</ModalUi>
|
||||
<PageReorderModal
|
||||
isOpen={isReorderModal}
|
||||
handleClose={() => setIsReorderModal(false)}
|
||||
totalPages={props.allPages}
|
||||
onSave={handleReorderSave}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Header;
|
||||
@@ -0,0 +1,298 @@
|
||||
import { useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
base64ToArrayBuffer,
|
||||
decryptPdf,
|
||||
deletePdfPage,
|
||||
flattenPdf,
|
||||
getFileAsArrayBuffer,
|
||||
handleRemoveWidgets,
|
||||
reorderPdfPages
|
||||
} from "../../constant/Utils";
|
||||
import ModalUi from "../../primitives/ModalUi";
|
||||
import { PDFDocument } from "pdf-lib";
|
||||
import { maxFileSize } from "../../constant/const";
|
||||
import PageReorderModal from "./PageReorderModal";
|
||||
|
||||
function PdfTools(props) {
|
||||
const { t } = useTranslation();
|
||||
const mergePdfInputRef = useRef(null);
|
||||
const [isDeletePage, setIsDeletePage] = useState(false);
|
||||
const [isReorderModal, setIsReorderModal] = useState(false);
|
||||
const handleDetelePage = async () => {
|
||||
props.setIsUploadPdf && props.setIsUploadPdf(true);
|
||||
try {
|
||||
const pdfupdatedData = await deletePdfPage(
|
||||
props.pdfArrayBuffer,
|
||||
props.pageNumber
|
||||
);
|
||||
if (pdfupdatedData?.totalPages === 1) {
|
||||
alert(t("delete-alert"));
|
||||
} else {
|
||||
props.setPdfBase64Url(pdfupdatedData.base64);
|
||||
props.setPdfArrayBuffer(pdfupdatedData.arrayBuffer);
|
||||
setIsDeletePage(false);
|
||||
handleRemoveWidgets(
|
||||
props.setSignerPos,
|
||||
props.signerPos,
|
||||
props.pageNumber
|
||||
);
|
||||
props.setAllPages(pdfupdatedData.remainingPages || 1);
|
||||
if (props.allPages === props.pageNumber) {
|
||||
props.setPageNumber(props.pageNumber - 1);
|
||||
} else if (props.allPages > 2) {
|
||||
props.setPageNumber(props.pageNumber);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("error in delete pdf page", e);
|
||||
}
|
||||
};
|
||||
|
||||
// `removeFile` is used to remove file if exists
|
||||
const removeFile = (e) => {
|
||||
if (e) {
|
||||
e.target.value = "";
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileUpload = async (e) => {
|
||||
props.setIsTour && props.setIsTour(false);
|
||||
const file = e.target.files[0];
|
||||
if (!file) {
|
||||
alert(t("please-select-pdf"));
|
||||
return;
|
||||
}
|
||||
if (!file.type.includes("pdf")) {
|
||||
alert(t("only-pdf-allowed"));
|
||||
return;
|
||||
}
|
||||
const fileSize =
|
||||
maxFileSize;
|
||||
const pdfsize = file?.size;
|
||||
const fileSizeBytes = fileSize * 1024 * 1024;
|
||||
if (pdfsize > fileSizeBytes) {
|
||||
alert(`${t("file-alert-1")} ${fileSize} MB`);
|
||||
removeFile(e);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
let uploadedPdfBytes = await file.arrayBuffer();
|
||||
try {
|
||||
uploadedPdfBytes = await flattenPdf(uploadedPdfBytes);
|
||||
} catch (err) {
|
||||
if (err?.message?.includes("is encrypted")) {
|
||||
try {
|
||||
const pdfFile = await decryptPdf(file, "");
|
||||
const pdfArrayBuffer = await getFileAsArrayBuffer(pdfFile);
|
||||
uploadedPdfBytes = await flattenPdf(pdfArrayBuffer);
|
||||
} catch (err) {
|
||||
if (err?.response?.status === 401) {
|
||||
const password = prompt(
|
||||
`PDF "${file.name}" is password-protected. Enter password:`
|
||||
);
|
||||
if (password) {
|
||||
try {
|
||||
const pdfFile = await decryptPdf(file, password);
|
||||
const pdfArrayBuffer = await getFileAsArrayBuffer(pdfFile);
|
||||
uploadedPdfBytes = await flattenPdf(pdfArrayBuffer);
|
||||
// Upload the file to Parse Server
|
||||
} catch (err) {
|
||||
console.error("Incorrect password or decryption failed", err);
|
||||
alert(t("incorrect-password-or-decryption-failed"));
|
||||
}
|
||||
} else {
|
||||
alert(t("provide-password"));
|
||||
}
|
||||
} else {
|
||||
console.log("Err ", err);
|
||||
alert(t("error-uploading-pdf"));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
alert(t("error-uploading-pdf"));
|
||||
}
|
||||
}
|
||||
const uploadedPdfDoc = await PDFDocument.load(uploadedPdfBytes, {
|
||||
ignoreEncryption: true
|
||||
});
|
||||
const basePdfDoc = await PDFDocument.load(props.pdfArrayBuffer);
|
||||
|
||||
// Copy pages from the uploaded PDF to the base PDF
|
||||
const uploadedPdfPages = await basePdfDoc.copyPages(
|
||||
uploadedPdfDoc,
|
||||
uploadedPdfDoc.getPageIndices()
|
||||
);
|
||||
uploadedPdfPages.forEach((page) => basePdfDoc.addPage(page));
|
||||
// Save the updated PDF
|
||||
const pdfBase64 = await basePdfDoc.saveAsBase64({
|
||||
useObjectStreams: false
|
||||
});
|
||||
const pdfBuffer = base64ToArrayBuffer(pdfBase64);
|
||||
const pdfsize = pdfBuffer?.byteLength;
|
||||
const fileSizeBytes = fileSize * 1024 * 1024;
|
||||
if (pdfsize > fileSizeBytes) {
|
||||
alert(`${t("file-alert-1")} ${fileSize} MB`);
|
||||
removeFile(e);
|
||||
return;
|
||||
}
|
||||
props.setPdfArrayBuffer(pdfBuffer);
|
||||
props.setPdfBase64Url(pdfBase64);
|
||||
props.setIsUploadPdf && props.setIsUploadPdf(true);
|
||||
mergePdfInputRef.current.value = "";
|
||||
} catch (error) {
|
||||
mergePdfInputRef.current.value = "";
|
||||
console.error("Error merging PDF:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleReorderSave = async (order) => {
|
||||
try {
|
||||
const pdfupdatedData = await reorderPdfPages(props.pdfArrayBuffer, order);
|
||||
if (pdfupdatedData) {
|
||||
props.setPdfArrayBuffer(pdfupdatedData.arrayBuffer);
|
||||
props.setPdfBase64Url(pdfupdatedData.base64);
|
||||
props.setAllPages(pdfupdatedData.totalPages);
|
||||
props.setPageNumber(1);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("error in reorder pdf pages", e);
|
||||
}
|
||||
setIsReorderModal(false);
|
||||
};
|
||||
|
||||
const handleDeletePage = () => {
|
||||
setIsDeletePage(true);
|
||||
props.setIsTour && props.setIsTour(false);
|
||||
};
|
||||
|
||||
const handleReorderPages = () => {
|
||||
setIsReorderModal(true);
|
||||
props.setIsTour && props.setIsTour(false);
|
||||
};
|
||||
|
||||
const handleZoomIn = () => {
|
||||
props.clickOnZoomIn();
|
||||
props.setIsTour && props.setIsTour(false);
|
||||
};
|
||||
const handleZoomOut = () => {
|
||||
props.clickOnZoomOut();
|
||||
props.setIsTour && props.setIsTour(false);
|
||||
};
|
||||
const handleRotate = () => {
|
||||
props.handleRotationFun(90);
|
||||
props.setIsTour && props.setIsTour(false);
|
||||
};
|
||||
const handleAntiRotate = () => {
|
||||
props.handleRotationFun(-90);
|
||||
props.setIsTour && props.setIsTour(false);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<span
|
||||
data-tut="pdftools"
|
||||
className="hidden h-max md:flex flex-col gap-1 text-center md:w-[5%] mt-[42px]"
|
||||
>
|
||||
{!props.isDisableEditTools && (
|
||||
<>
|
||||
<span
|
||||
className="bg-gray-50 px-[4px] 2xl:py-[10px] cursor-pointer"
|
||||
onClick={() => mergePdfInputRef.current.click()}
|
||||
title={t("add-pages")}
|
||||
>
|
||||
<input
|
||||
type="file"
|
||||
className="hidden"
|
||||
accept="application/pdf"
|
||||
ref={mergePdfInputRef}
|
||||
onChange={handleFileUpload}
|
||||
/>
|
||||
<i className="fa-light fa-plus text-gray-500 2xl:text-[25px]"></i>
|
||||
</span>
|
||||
<span
|
||||
className="bg-gray-50 px-[4px] 2xl:py-[10px] cursor-pointer"
|
||||
onClick={handleDeletePage}
|
||||
title={t("delete-page")}
|
||||
>
|
||||
<i className="fa-light fa-trash text-gray-500 2xl:text-[25px]"></i>
|
||||
</span>
|
||||
<span
|
||||
className="bg-gray-50 px-[4px] 2xl:py-[10px] cursor-pointer"
|
||||
onClick={handleReorderPages}
|
||||
title={t("reorder-pages")}
|
||||
>
|
||||
<i className="fa-light fa-list-ol text-gray-500 2xl:text-[25px]"></i>
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
<span
|
||||
className="bg-gray-50 px-[4px] 2xl:py-[10px] cursor-pointer"
|
||||
onClick={handleZoomIn}
|
||||
title={t("zoom-in")}
|
||||
>
|
||||
<i className="fa-light fa-magnifying-glass-plus text-gray-500 2xl:text-[25px]"></i>
|
||||
</span>
|
||||
|
||||
{!props.isDisableEditTools && (
|
||||
<>
|
||||
<span
|
||||
className="bg-gray-50 px-[4px] 2xl:py-[10px] cursor-pointer"
|
||||
onClick={handleRotate}
|
||||
title={t("rotate-right")}
|
||||
>
|
||||
<i className="fa-light fa-rotate-right text-gray-500 2xl:text-[25px]"></i>
|
||||
</span>
|
||||
<span
|
||||
className="bg-gray-50 px-[4px] 2xl:py-[10px] cursor-pointer"
|
||||
title={t("rotate-left")}
|
||||
onClick={handleAntiRotate}
|
||||
>
|
||||
<i className="fa-light fa-rotate-left text-gray-500 2xl:text-[25px]"></i>
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
<span
|
||||
className="bg-gray-50 px-[4px] 2xl:py-[10px] cursor-pointer"
|
||||
onClick={handleZoomOut}
|
||||
title={t("zoom-out")}
|
||||
>
|
||||
<i className="fa-light fa-magnifying-glass-minus text-gray-500 2xl:text-[30px]"></i>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<ModalUi
|
||||
isOpen={isDeletePage}
|
||||
title={t("delete-page")}
|
||||
handleClose={() => setIsDeletePage(false)}
|
||||
>
|
||||
<div className="h-[100%] p-[20px]">
|
||||
<p className="font-medium text-base-content">{t("delete-alert-2")}</p>
|
||||
<p className="pt-3 text-base-content">{t("delete-note")}</p>
|
||||
<div className="h-[1px] bg-[#9f9f9f] w-full my-[15px]"></div>
|
||||
<button
|
||||
onClick={() => handleDetelePage()}
|
||||
type="button"
|
||||
className="op-btn op-btn-primary"
|
||||
>
|
||||
{t("yes")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setIsDeletePage(false)}
|
||||
type="button"
|
||||
className="op-btn op-btn-ghost text-base-content ml-1"
|
||||
>
|
||||
{t("no")}
|
||||
</button>
|
||||
</div>
|
||||
</ModalUi>
|
||||
<PageReorderModal
|
||||
isOpen={isReorderModal}
|
||||
handleClose={() => setIsReorderModal(false)}
|
||||
totalPages={props.allPages}
|
||||
onSave={handleReorderSave}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default PdfTools;
|
||||