mirror of
https://github.com/docusealco/docuseal
synced 2026-11-04 14:42:32 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
55d9511c01 | ||
|
|
929d29576d | ||
|
|
6f7128a51c | ||
|
|
fc5c4ff2fc | ||
|
|
a81595db2b | ||
|
|
a5615d500d | ||
|
|
da9266befc | ||
|
|
5b3cffeb73 | ||
|
|
8f7b8a5fdf | ||
|
|
6d93116993 | ||
|
|
a0f4916d3b | ||
|
|
ace897eaf9 | ||
|
|
b8667c5408 | ||
|
|
d87d66632e | ||
|
|
3015c2d156 | ||
|
|
52b9e9ee8e | ||
|
|
5bfad0271c | ||
|
|
58797891c7 | ||
|
|
897750ba1e | ||
|
|
99341e773f | ||
|
|
71319c1548 | ||
|
|
c0a42f6bfc | ||
|
|
d5abb46112 | ||
|
|
2508b5fc8a | ||
|
|
4bd1ef0da3 | ||
|
|
4efd748571 | ||
|
|
859892d1fd | ||
|
|
7adeaf93cd | ||
|
|
5dbe155ecb | ||
|
|
667d065bee | ||
|
|
bb613059de | ||
|
|
1b2dec4c2a | ||
|
|
2c2ef97c30 | ||
|
|
6df1d68237 | ||
|
|
20c9faedff | ||
|
|
d931a40ea9 | ||
|
|
1097b98b85 | ||
|
|
2e597fcf43 | ||
|
|
bb2fa6a261 | ||
|
|
1dc8369487 | ||
|
|
e6d60f1985 | ||
|
|
82e3cb8a4d | ||
|
|
aeec0cbf8e | ||
|
|
7a2d1fc6c5 | ||
|
|
68033ab7d2 | ||
|
|
7595c60ea8 | ||
|
|
d9318c04f8 | ||
|
|
2d76198290 | ||
|
|
02a7b6ea55 | ||
|
|
ab57076b8f | ||
|
|
82fba47c98 | ||
|
|
5f32e2a5af | ||
|
|
70012fbd7c | ||
|
|
668f6ca975 | ||
|
|
e78dd301ef | ||
|
|
f8ec3daf10 | ||
|
|
75c929639d | ||
|
|
e9c1242b8d | ||
|
|
e7eb1d4f12 | ||
|
|
abbd3e809a | ||
|
|
f430b4f5a6 | ||
|
|
e65234b4c8 | ||
|
|
fe04d22c94 | ||
|
|
44a7a5ebdc | ||
|
|
119ef0d0c7 | ||
|
|
cdb8eb7931 | ||
|
|
aef176c0cc | ||
|
|
0c21b55865 | ||
|
|
964b1444a5 | ||
|
|
48de343948 | ||
|
|
2640934c15 | ||
|
|
442dd4d6db | ||
|
|
3168feb3d6 | ||
|
|
fbae4933f6 | ||
|
|
78bb66c344 | ||
|
|
5af65ef74f | ||
|
|
4d354ae9ff | ||
|
|
1e57c1ace7 | ||
|
|
fefd138dfd | ||
|
|
16a77d3536 | ||
|
|
e03857bdb7 | ||
|
|
fc679bcd72 | ||
|
|
652b0da021 | ||
|
|
2d54b0db2f | ||
|
|
7a29894926 | ||
|
|
f9ae79dd5b | ||
|
|
cadb78ca35 | ||
|
|
36a6b7ff1d | ||
|
|
efee46f113 | ||
|
|
b6639738c3 | ||
|
|
aab07c5f49 | ||
|
|
33c0784bc6 | ||
|
|
6f5ebfc8a6 | ||
|
|
811091ff14 | ||
|
|
6666f39928 | ||
|
|
f67db9fe5f | ||
|
|
45f002803e | ||
|
|
8c19f90e88 | ||
|
|
a8f5d5cea7 | ||
|
|
6788071eec | ||
|
|
0c690b3033 | ||
|
|
dcb963b623 | ||
|
|
04e30dbdd6 | ||
|
|
ac6d5a7aed | ||
|
|
3166a31b67 | ||
|
|
fc3283eb3a | ||
|
|
d6a96d4aa3 | ||
|
|
cda349d71c | ||
|
|
87345a3b3d | ||
|
|
b791d0699c | ||
|
|
2cb15f3282 | ||
|
|
8b823ca0d0 | ||
|
|
f3f08797f7 | ||
|
|
cab9672d25 | ||
|
|
c8d3b1bace | ||
|
|
5ee51b2b57 | ||
|
|
525dc503a6 | ||
|
|
12cf28792b | ||
|
|
33466c1d07 | ||
|
|
226ae2172e | ||
|
|
8e2700b8b8 | ||
|
|
753cbd4d27 | ||
|
|
f16405ca70 | ||
|
|
23f052c400 | ||
|
|
90a0a03e15 | ||
|
|
2dba8ae366 | ||
|
|
5bb7ad571c | ||
|
|
0cdb7c2c4f | ||
|
|
11bdc522cd | ||
|
|
014835f90d | ||
|
|
05e66b60ee | ||
|
|
505b185cf2 | ||
|
|
50c32ac6ff | ||
|
|
878ab25fd4 | ||
|
|
1b31772ff6 | ||
|
|
d4940234b7 | ||
|
|
9bdc5d4863 | ||
|
|
a73fd7929c | ||
|
|
8269a61751 | ||
|
|
de072d1437 | ||
|
|
5b24793f2b | ||
|
|
660f7ab8f3 | ||
|
|
6e3fea5fa0 | ||
|
|
29ecbecbc2 | ||
|
|
ad3bbe9892 | ||
|
|
b570ebee69 | ||
|
|
207780fa3f | ||
|
|
8aa219433b | ||
|
|
cb1e9f2be4 | ||
|
|
202943f58d | ||
|
|
6dd15b2091 | ||
|
|
1c55e4f18b | ||
|
|
6cc36b1728 | ||
|
|
37751cd5c1 | ||
|
|
80d882cd20 | ||
|
|
8bef75e570 | ||
|
|
077eab7005 | ||
|
|
715c70d422 | ||
|
|
a3c23f9d53 | ||
|
|
c1e67a8af0 | ||
|
|
2fdc294293 | ||
|
|
1f49ba069e | ||
|
|
43ef0fc0d0 | ||
|
|
a1a920e536 | ||
|
|
dec33b4331 | ||
|
|
432f290633 | ||
|
|
cf98616a27 | ||
|
|
1a720cf1ad | ||
|
|
557c6c34f2 | ||
|
|
943c76505d | ||
|
|
66eb83e6a7 | ||
|
|
0844427357 | ||
|
|
72e9d9ea2d | ||
|
|
3db7d47746 | ||
|
|
95899336b8 | ||
|
|
f6691c110e | ||
|
|
749722a9df | ||
|
|
265b668cb4 | ||
|
|
0527e6a5c1 | ||
|
|
d989b61d19 | ||
|
|
c1f3e6ff13 | ||
|
|
3c0fff2d82 | ||
|
|
bd599c264f | ||
|
|
0274093c89 | ||
|
|
5845b5bdfd | ||
|
|
3dd0ba6b0b | ||
|
|
e79306f8a3 | ||
|
|
0171b0c2dd | ||
|
|
a963ef53d3 | ||
|
|
344f302c76 | ||
|
|
17ded282b2 | ||
|
|
2ce57df485 | ||
|
|
849289287e | ||
|
|
704fbc2393 | ||
|
|
168fc8bd03 | ||
|
|
c5598d9efe | ||
|
|
f02315726d | ||
|
|
e56dcb59c0 | ||
|
|
c74f350ea2 | ||
|
|
af9d015cf2 | ||
|
|
633e5ac5e4 | ||
|
|
2d0f323854 | ||
|
|
cfe5610ec9 | ||
|
|
55616ac321 | ||
|
|
8f2d1deb38 | ||
|
|
e2763ceb80 | ||
|
|
10462f3140 | ||
|
|
3aef92486b | ||
|
|
c1a9ee0bbb | ||
|
|
398fae82cb | ||
|
|
a16443f961 | ||
|
|
ad4e0e1d27 | ||
|
|
493a2108e9 | ||
|
|
c2c9bb64b1 | ||
|
|
bb1426c0af | ||
|
|
2505768e4d | ||
|
|
23a1b70534 | ||
|
|
b34616ecbc | ||
|
|
0111252f6f | ||
|
|
a561cc5f73 | ||
|
|
519e6f1f79 | ||
|
|
da871b5d3a | ||
|
|
ae3f939d23 | ||
|
|
fb6ede564e | ||
|
|
1c701d4fe8 | ||
|
|
c743cc689d | ||
|
|
8432bec3d6 | ||
|
|
8e72b0b0e4 | ||
|
|
19e4bbc01e | ||
|
|
56a9e412c8 | ||
|
|
4b75623b86 | ||
|
|
e1ea42a997 | ||
|
|
ee37710e57 | ||
|
|
ae99218789 | ||
|
|
9fe8214ca0 | ||
|
|
6ca44a5cc3 | ||
|
|
801abd197b | ||
|
|
970977e469 | ||
|
|
f83674b934 | ||
|
|
57c7dc1b5e | ||
|
|
e9277f4eeb | ||
|
|
4052b6fe13 | ||
|
|
608347b1e6 | ||
|
|
345fe674d4 | ||
|
|
92f8901a67 | ||
|
|
fcd40308ee | ||
|
|
2bd023e5d0 | ||
|
|
a697d6330e | ||
|
|
ab56e8be20 | ||
|
|
b9a906b569 | ||
|
|
02adbae758 | ||
|
|
866aab7fac | ||
|
|
c81b4d855b | ||
|
|
4ad58fc285 | ||
|
|
8227dd4f6a | ||
|
|
beb677734f | ||
|
|
03db636d51 | ||
|
|
b83abc9499 | ||
|
|
9738cdcf8f | ||
|
|
179ae01551 | ||
|
|
295860a9de | ||
|
|
0a6c15b6b8 | ||
|
|
18f80020ee | ||
|
|
3768fb94bf | ||
|
|
7b2cadcb91 | ||
|
|
4eb4e8c1f8 | ||
|
|
984df15949 | ||
|
|
aa3bd24332 | ||
|
|
250889623d | ||
|
|
862fa55986 | ||
|
|
0f2889904a | ||
|
|
49f14c7929 | ||
|
|
c3f3c5d5df | ||
|
|
274259f305 | ||
|
|
a15d7a9cf3 | ||
|
|
7d154fc28d | ||
|
|
6727d3be83 | ||
|
|
4bbd5c3bcb | ||
|
|
5995f2e427 | ||
|
|
0910c8e100 | ||
|
|
8df76dfa90 | ||
|
|
78e9e66cb0 | ||
|
|
71c61fc4b3 | ||
|
|
a728aa8689 | ||
|
|
5a3174a720 | ||
|
|
652072d660 | ||
|
|
323f5c0c15 | ||
|
|
229d78c78b | ||
|
|
9d9a955149 | ||
|
|
b40a047190 | ||
|
|
ff9e415823 | ||
|
|
2f7a4fb2b3 | ||
|
|
f8a654a5b6 | ||
|
|
4449475467 | ||
|
|
bcbbf0dd42 | ||
|
|
19b9ba52d4 | ||
|
|
bdc30db2dc | ||
|
|
74634ba96b | ||
|
|
77feb05bad | ||
|
|
a1f05d006e | ||
|
|
cc3dae1335 | ||
|
|
d9debcfb0d | ||
|
|
8770d16d52 | ||
|
|
84953481e4 | ||
|
|
df7aa59f6f | ||
|
|
008b83119d | ||
|
|
15dafcb6ff | ||
|
|
7d291ed888 | ||
|
|
a7221f0750 | ||
|
|
9a5b2a5d30 | ||
|
|
7361a00d42 | ||
|
|
e07d402782 | ||
|
|
a34354bee1 | ||
|
|
84b762b529 | ||
|
|
2a839951b4 | ||
|
|
26d7c2f047 | ||
|
|
151a5b85f3 | ||
|
|
3f506f53b4 | ||
|
|
95f734e4b8 | ||
|
|
ff74574e84 | ||
|
|
843f6c405f | ||
|
|
f8b24ebfce | ||
|
|
d1d26b5424 | ||
|
|
8aa6f45da8 | ||
|
|
48d5bee95b | ||
|
|
98c79d2dec | ||
|
|
0f51238f62 | ||
|
|
b7323b929f | ||
|
|
05eb1537f7 | ||
|
|
bc9f27cfed | ||
|
|
8f457be719 | ||
|
|
6d590e13c9 | ||
|
|
c987ec172a | ||
|
|
49f1637c47 | ||
|
|
13379bd699 | ||
|
|
5ee762c08c | ||
|
|
edb6d561f1 | ||
|
|
1b90f77e82 | ||
|
|
c37785ecbc | ||
|
|
c67188edf8 | ||
|
|
8630c68631 | ||
|
|
ddcb5c5d40 | ||
|
|
07f0fbb50f | ||
|
|
3781b0aa72 | ||
|
|
42a5cea521 | ||
|
|
fb6000ca2c | ||
|
|
59f98db754 | ||
|
|
6add842a27 | ||
|
|
43ca4a6365 | ||
|
|
68196d6af0 | ||
|
|
a582d39bb9 | ||
|
|
7a3e89c4d8 | ||
|
|
87f036ad88 | ||
|
|
5738b8dd58 | ||
|
|
8d73371e0f | ||
|
|
3d870b2305 | ||
|
|
27d8cd57bd | ||
|
|
353b2e7a94 | ||
|
|
d77bd87acb | ||
|
|
bae7b924ae | ||
|
|
89867fdde3 | ||
|
|
c9a1f067ac | ||
|
|
eb26835d57 | ||
|
|
7301661468 | ||
|
|
e4fe984a70 | ||
|
|
12ccf622c1 | ||
|
|
6c35ff6d1c | ||
|
|
1d7c84302e | ||
|
|
f3e8889710 | ||
|
|
4b2f9ff306 | ||
|
|
b4ba642f8e | ||
|
|
5c69b965c0 | ||
|
|
cba196385c | ||
|
|
00abad56cb | ||
|
|
d1070c4955 | ||
|
|
e84be523c6 | ||
|
|
5d598e8268 | ||
|
|
0e9d922204 | ||
|
|
80c1b04e83 | ||
|
|
b2fca3edf3 | ||
|
|
2a292c8e80 | ||
|
|
e6052ad0d8 | ||
|
|
8646e17d5a | ||
|
|
e6785b0afd | ||
|
|
009033a411 | ||
|
|
fdc3d7742a | ||
|
|
f9ccca1376 | ||
|
|
0a00fe6ab8 | ||
|
|
6253e80a70 | ||
|
|
3dcb7c813f | ||
|
|
39cdc41a67 | ||
|
|
d3000487f4 | ||
|
|
80254da1c1 | ||
|
|
73fc2d92f7 | ||
|
|
f69aa33f3d | ||
|
|
e16202c9f7 | ||
|
|
7453d0d274 | ||
|
|
c102828c93 | ||
|
|
afeb6dd318 | ||
|
|
6156623f0e | ||
|
|
ccc06fcc3d | ||
|
|
bbe3942056 | ||
|
|
49cea59b94 | ||
|
|
ce1323e849 | ||
|
|
fc3c1e65e3 | ||
|
|
f1ebd8f912 | ||
|
|
98f5bba832 | ||
|
|
e864e8a47f | ||
|
|
a573545ead | ||
|
|
15ff4386e5 | ||
|
|
7a4aa3bf87 | ||
|
|
c1ba6288c8 | ||
|
|
d0ba180729 | ||
|
|
2edc76414b | ||
|
|
452db5dc39 | ||
|
|
21c4c53feb | ||
|
|
6292789cac | ||
|
|
d445bbc02d | ||
|
|
8627f58cbc | ||
|
|
66ab0059bb | ||
|
|
3f06932277 | ||
|
|
59cf1b435e | ||
|
|
c9a5b0e66e | ||
|
|
bcd5e1da98 | ||
|
|
f74ad050b4 | ||
|
|
b6f43c9b23 | ||
|
|
298c64a119 | ||
|
|
8b80679b3a | ||
|
|
82a2bc561a | ||
|
|
e019542d3f | ||
|
|
9356247ef0 | ||
|
|
4db77c18e3 | ||
|
|
0d0b248988 | ||
|
|
44f3910266 | ||
|
|
35c6798a80 | ||
|
|
0995c7ee63 | ||
|
|
0a25f78329 | ||
|
|
bb8fffc491 | ||
|
|
6f6c51fb32 | ||
|
|
39d14c15d6 | ||
|
|
17de28965b | ||
|
|
ae7865e508 | ||
|
|
dc8e8cc5c9 | ||
|
|
5208470c38 | ||
|
|
e7ef7b552f | ||
|
|
a478112b07 | ||
|
|
7d0f32e1f9 | ||
|
|
d35d8bead3 | ||
|
|
ab9cc52bcc | ||
|
|
7dc342a6f9 | ||
|
|
80a3ef9193 | ||
|
|
bb723d20cb | ||
|
|
0c24a2016c | ||
|
|
2124965996 | ||
|
|
6f1e104067 | ||
|
|
b20f40b5a3 | ||
|
|
1093ca5fc4 | ||
|
|
0d99c55d3f | ||
|
|
86dc1a88ed | ||
|
|
968c20d8ca | ||
|
|
1b2d978449 | ||
|
|
30582b2d75 | ||
|
|
38a8037a4d | ||
|
|
67de45f787 | ||
|
|
f358f24abb | ||
|
|
fd4e15003a | ||
|
|
81df249d5c | ||
|
|
ba60997992 | ||
|
|
8961294a29 | ||
|
|
bc2e0a000a | ||
|
|
1a693fba2d | ||
|
|
05b7acf6aa | ||
|
|
de13d8d397 | ||
|
|
6d7d8ae87c | ||
|
|
cfb1bdcd52 | ||
|
|
48d41833f1 | ||
|
|
01535f1afc | ||
|
|
fe845bfa36 | ||
|
|
b94dbc75b7 | ||
|
|
bdf40640d0 | ||
|
|
496f6c706b | ||
|
|
79f97d5555 | ||
|
|
82f679c84c | ||
|
|
9b789889fa | ||
|
|
dd8dd118da | ||
|
|
8222a4b6a4 | ||
|
|
31f8057bfa | ||
|
|
1d8e0417bd | ||
|
|
8100ca46c2 | ||
|
|
94d745cf1c | ||
|
|
a92bd0ad0f | ||
|
|
94a13ffca2 | ||
|
|
64c8707957 | ||
|
|
d421a73988 | ||
|
|
f7eef1f8d6 | ||
|
|
08c780d309 | ||
|
|
baaf1f510f | ||
|
|
8898dcc054 | ||
|
|
33521c8aeb | ||
|
|
9db03bd7b9 | ||
|
|
bd600c1fba | ||
|
|
82b18b08f1 | ||
|
|
0aefe616b3 | ||
|
|
59a66bbd16 | ||
|
|
155c708e4f | ||
|
|
5502c2ab1b | ||
|
|
e89b42de40 | ||
|
|
2562c4f432 | ||
|
|
17323074ca | ||
|
|
8a4c5c5e09 | ||
|
|
3033faff0d | ||
|
|
0e6aab0e8d | ||
|
|
75e3dd839a | ||
|
|
830e6eb88d | ||
|
|
3c8fae78d1 | ||
|
|
441c1d7044 | ||
|
|
203f4633f7 | ||
|
|
e558f4d9fb | ||
|
|
990e4020e9 | ||
|
|
1ad3e45519 | ||
|
|
442ced8381 | ||
|
|
ba46202a4d | ||
|
|
1c77a1ee90 | ||
|
|
ea74b59535 | ||
|
|
0e9343d9e2 | ||
|
|
af4cf7257f | ||
|
|
c8d4ee58f8 | ||
|
|
65e57c22e5 | ||
|
|
7351ae0a77 | ||
|
|
f1e0d5d137 | ||
|
|
8a1b71688c | ||
|
|
8ef1c309e0 | ||
|
|
a32dcc0dc5 | ||
|
|
16a2fde4b7 | ||
|
|
7856f7a134 | ||
|
|
c545924fe9 | ||
|
|
b1510bb88c | ||
|
|
31d2024ce5 | ||
|
|
23c49f8706 | ||
|
|
62eb0cda6b | ||
|
|
9f48c6e625 | ||
|
|
83ce401cf2 | ||
|
|
541b025ad0 | ||
|
|
5fd018df46 | ||
|
|
2822b0086a | ||
|
|
e27606f443 | ||
|
|
6c95cd5945 | ||
|
|
d723146060 | ||
|
|
3b03e61ae3 | ||
|
|
172513b96c | ||
|
|
68be42a48d | ||
|
|
0370e363bc | ||
|
|
ced88476cc | ||
|
|
c5d71505ef | ||
|
|
489f9859be | ||
|
|
a87ca65839 | ||
|
|
fa78d0976d | ||
|
|
4aaab2563b | ||
|
|
e073cf2c27 | ||
|
|
173d406b1f | ||
|
|
3bcbad6f04 | ||
|
|
e67854fe56 | ||
|
|
7510e0cb6b | ||
|
|
30c913b54f | ||
|
|
2cda9cd736 | ||
|
|
342d29f272 | ||
|
|
13f76c9944 | ||
|
|
ce4e55f387 | ||
|
|
712107ba5a | ||
|
|
1aa867893e | ||
|
|
4f6142f9c0 | ||
|
|
c303349666 | ||
|
|
e85df170f2 | ||
|
|
01333e5b2e | ||
|
|
514987138a | ||
|
|
0814c6d065 | ||
|
|
542a443fb8 | ||
|
|
f2c6bb1c3d | ||
|
|
b85c9cfdb7 | ||
|
|
e07b50c22d | ||
|
|
4dde9890e0 | ||
|
|
66d1ae2f64 | ||
|
|
d130835902 | ||
|
|
52f66368a3 | ||
|
|
89d0810625 | ||
|
|
06d3d1202b | ||
|
|
0698aad8e1 | ||
|
|
3575a25935 | ||
|
|
cdd034e72d | ||
|
|
26f7d5cf0d | ||
|
|
3c69430b94 | ||
|
|
c0162bccc8 | ||
|
|
1adb800f94 | ||
|
|
333a5d6b7b | ||
|
|
9aa68fe955 | ||
|
|
46af87d89a | ||
|
|
1066131143 | ||
|
|
a25022532e | ||
|
|
5162f9a9dc | ||
|
|
cc5694de43 | ||
|
|
f734ba280d | ||
|
|
500d0da563 | ||
|
|
6626a2b602 | ||
|
|
07e66a6e53 | ||
|
|
deef9c7d3b | ||
|
|
2ec8954dc8 | ||
|
|
c1af803e5b | ||
|
|
10abdbeba4 | ||
|
|
1dbde7a6d8 | ||
|
|
a27eb18670 | ||
|
|
44a49a4533 | ||
|
|
fc11078cae | ||
|
|
a42ae0b39e | ||
|
|
3163cdc761 | ||
|
|
5439c63ce7 | ||
|
|
b78287242b | ||
|
|
b869ae38b3 | ||
|
|
4a320e9ba6 | ||
|
|
0025928a35 | ||
|
|
c227a1988a | ||
|
|
3ade45f541 | ||
|
|
17da8bde80 | ||
|
|
59d9a59769 | ||
|
|
7210badde9 | ||
|
|
21659a73f3 | ||
|
|
c0e49ebeea | ||
|
|
2d24eac4a3 | ||
|
|
14c3c8181b | ||
|
|
036f6049a2 | ||
|
|
236e4cb35f | ||
|
|
ee77cb0982 | ||
|
|
d474ad05e5 | ||
|
|
dbf1fa37c5 | ||
|
|
7444bb8b68 | ||
|
|
f7132445a1 | ||
|
|
0127dd738e | ||
|
|
df561a6211 | ||
|
|
50d3404f3b | ||
|
|
607fc8d338 | ||
|
|
8c366dfc38 | ||
|
|
5ed08fa9ab | ||
|
|
ddb557ebef | ||
|
|
b48615df3f | ||
|
|
86c7b92675 | ||
|
|
041f362cc4 | ||
|
|
65f491325e | ||
|
|
57aef5f5e5 | ||
|
|
e704e6fa04 | ||
|
|
4c99bbb58a | ||
|
|
c0647eef2c | ||
|
|
c619b9be93 | ||
|
|
abab3f9688 | ||
|
|
7b15111e8f | ||
|
|
2c7f38c503 | ||
|
|
ed80df1372 | ||
|
|
3614d1796c | ||
|
|
ce39494f84 | ||
|
|
c639a3f733 | ||
|
|
74bef133b3 | ||
|
|
33142966dc | ||
|
|
42ec8d5fc9 | ||
|
|
01d752e811 | ||
|
|
99b33d4165 | ||
|
|
11a90d797a | ||
|
|
9da7463657 | ||
|
|
b6d350ebbd | ||
|
|
9e87a0373f | ||
|
|
aee55165c5 | ||
|
|
9ddbcae153 | ||
|
|
10cd3aa72a | ||
|
|
6684a27e2f | ||
|
|
b6f7a7dd15 | ||
|
|
309e43e18e | ||
|
|
d5b8640a0e | ||
|
|
49b9064d9f | ||
|
|
698031cfc0 | ||
|
|
6dc21e66e1 | ||
|
|
60b32fec94 | ||
|
|
c621166b45 | ||
|
|
8e2da83f9e | ||
|
|
a39ba5b275 | ||
|
|
edf466a6de | ||
|
|
3b8bb8c5bb | ||
|
|
79f1061353 | ||
|
|
745f2160c5 | ||
|
|
2f0c79444b | ||
|
|
69a3782903 | ||
|
|
3dd7c47558 | ||
|
|
761bb07ee6 | ||
|
|
92d7bc65f5 |
No files matched your search
@@ -8,7 +8,8 @@
|
|||||||
},
|
},
|
||||||
"rules": {
|
"rules": {
|
||||||
"vue/no-deprecated-html-element-is": 0,
|
"vue/no-deprecated-html-element-is": 0,
|
||||||
"vue/no-mutating-props": 0
|
"vue/no-mutating-props": 0,
|
||||||
|
"vue/one-component-per-file": 0
|
||||||
},
|
},
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
"ecmaVersion": 2022,
|
"ecmaVersion": 2022,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ jobs:
|
|||||||
- name: Install Ruby
|
- name: Install Ruby
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
ruby-version: 3.2.2
|
ruby-version: 3.3.1
|
||||||
- name: Cache gems
|
- name: Cache gems
|
||||||
uses: actions/cache@v1
|
uses: actions/cache@v1
|
||||||
with:
|
with:
|
||||||
@@ -35,7 +35,7 @@ jobs:
|
|||||||
- name: Install Ruby
|
- name: Install Ruby
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
ruby-version: 3.2.2
|
ruby-version: 3.3.1
|
||||||
- name: Cache gems
|
- name: Cache gems
|
||||||
uses: actions/cache@v1
|
uses: actions/cache@v1
|
||||||
with:
|
with:
|
||||||
@@ -59,7 +59,7 @@ jobs:
|
|||||||
- name: Install Node.js
|
- name: Install Node.js
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 16.13.1
|
node-version: 20.9.0
|
||||||
- name: Cache directory path
|
- name: Cache directory path
|
||||||
id: yarn-cache-dir-path
|
id: yarn-cache-dir-path
|
||||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||||
@@ -100,11 +100,11 @@ jobs:
|
|||||||
- name: Install Ruby
|
- name: Install Ruby
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
ruby-version: 3.2.2
|
ruby-version: 3.3.1
|
||||||
- name: Set up Node
|
- name: Set up Node
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 16.13.1
|
node-version: 20.9.0
|
||||||
- name: Install Chrome
|
- name: Install Chrome
|
||||||
uses: browser-actions/setup-chrome@latest
|
uses: browser-actions/setup-chrome@latest
|
||||||
- name: Cache node_modules
|
- name: Cache node_modules
|
||||||
|
|||||||
@@ -30,6 +30,9 @@ jobs:
|
|||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
|
- name: Create .version file
|
||||||
|
run: echo ${{ github.ref_name }} > .version
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
|
|||||||
+9
-3
@@ -10,7 +10,7 @@ AllCops:
|
|||||||
- node_modules/**/*
|
- node_modules/**/*
|
||||||
- bin/*
|
- bin/*
|
||||||
- vendor/**/*
|
- vendor/**/*
|
||||||
TargetRubyVersion: '3.2'
|
TargetRubyVersion: '3.3'
|
||||||
|
|
||||||
Metrics/BlockLength:
|
Metrics/BlockLength:
|
||||||
Exclude:
|
Exclude:
|
||||||
@@ -30,7 +30,7 @@ Metrics/ParameterLists:
|
|||||||
Max: 10
|
Max: 10
|
||||||
|
|
||||||
Metrics/MethodLength:
|
Metrics/MethodLength:
|
||||||
Max: 25
|
Max: 30
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'db/migrate/**'
|
- 'db/migrate/**'
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ Layout/LineLength:
|
|||||||
AllowedPatterns: ['\A\s*#']
|
AllowedPatterns: ['\A\s*#']
|
||||||
|
|
||||||
Metrics/AbcSize:
|
Metrics/AbcSize:
|
||||||
Max: 35
|
Max: 45
|
||||||
|
|
||||||
Metrics/ModuleLength:
|
Metrics/ModuleLength:
|
||||||
Max: 500
|
Max: 500
|
||||||
@@ -67,8 +67,14 @@ RSpec/MultipleMemoizedHelpers:
|
|||||||
Rails/I18nLocaleTexts:
|
Rails/I18nLocaleTexts:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
|
Rails/FindEach:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
Rails/SkipsModelValidations:
|
Rails/SkipsModelValidations:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
Rails/ApplicationController:
|
Rails/ApplicationController:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
|
Capybara/ClickLinkOrButtonStyle:
|
||||||
|
Enabled: false
|
||||||
File renamed without changes.
+35
-10
@@ -1,4 +1,12 @@
|
|||||||
FROM ruby:3.2.2-alpine as webpack
|
FROM ruby:3.3.1-alpine3.18 as fonts
|
||||||
|
|
||||||
|
WORKDIR /fonts
|
||||||
|
|
||||||
|
RUN apk --no-cache add fontforge wget && wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Regular.ttf && wget https://github.com/satbyy/go-noto-universal/releases/download/v7.0/GoNotoKurrent-Bold.ttf && wget https://github.com/impallari/DancingScript/raw/master/fonts/DancingScript-Regular.otf && wget https://cdn.jsdelivr.net/gh/notofonts/notofonts.github.io/fonts/NotoSansSymbols2/hinted/ttf/NotoSansSymbols2-Regular.ttf && wget https://github.com/Maxattax97/gnu-freefont/raw/master/ttf/FreeSans.ttf && wget https://github.com/impallari/DancingScript/raw/master/OFL.txt
|
||||||
|
|
||||||
|
RUN fontforge -lang=py -c 'font1 = fontforge.open("FreeSans.ttf"); font2 = fontforge.open("NotoSansSymbols2-Regular.ttf"); font1.mergeFonts(font2); font1.generate("FreeSans.ttf")'
|
||||||
|
|
||||||
|
FROM ruby:3.3.1-alpine3.18 as webpack
|
||||||
|
|
||||||
ENV RAILS_ENV=production
|
ENV RAILS_ENV=production
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
@@ -15,27 +23,41 @@ RUN yarn install --network-timeout 1000000
|
|||||||
COPY ./bin/shakapacker ./bin/shakapacker
|
COPY ./bin/shakapacker ./bin/shakapacker
|
||||||
COPY ./config/webpack ./config/webpack
|
COPY ./config/webpack ./config/webpack
|
||||||
COPY ./config/shakapacker.yml ./config/shakapacker.yml
|
COPY ./config/shakapacker.yml ./config/shakapacker.yml
|
||||||
COPY ./postcss.config.js ./postcss.config.js ./
|
COPY ./postcss.config.js ./postcss.config.js
|
||||||
COPY ./tailwind.config.js ./tailwind.config.js ./
|
COPY ./tailwind.config.js ./tailwind.config.js
|
||||||
COPY ./tailwind.form.config.js ./tailwind.form.config.js ./
|
COPY ./tailwind.form.config.js ./tailwind.form.config.js
|
||||||
COPY ./tailwind.application.config.js ./tailwind.application.config.js ./
|
COPY ./tailwind.application.config.js ./tailwind.application.config.js
|
||||||
COPY ./app/javascript ./app/javascript
|
COPY ./app/javascript ./app/javascript
|
||||||
COPY ./app/views ./app/views
|
COPY ./app/views ./app/views
|
||||||
|
|
||||||
RUN echo "gem 'shakapacker'" > Gemfile && ./bin/shakapacker
|
RUN echo "gem 'shakapacker'" > Gemfile && ./bin/shakapacker
|
||||||
|
|
||||||
FROM ruby:3.2.2-alpine as app
|
FROM ruby:3.3.1-alpine3.18 as app
|
||||||
|
|
||||||
ENV RAILS_ENV=production
|
ENV RAILS_ENV=production
|
||||||
ENV BUNDLE_WITHOUT="development:test"
|
ENV BUNDLE_WITHOUT="development:test"
|
||||||
|
ENV LD_PRELOAD=/lib/libgcompat.so.0
|
||||||
|
ENV OPENSSL_CONF=/app/openssl_legacy.cnf
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN apk add --no-cache build-base sqlite-dev libpq-dev mariadb-dev vips-dev vips-poppler vips-heif libc6-compat ttf-freefont ttf-liberation && cp /usr/share/fonts/liberation/LiberationSans-Regular.ttf /usr/share/fonts/liberation/LiberationSans-Bold.ttf / && apk del ttf-liberation
|
RUN apk add --no-cache sqlite-dev libpq-dev mariadb-dev vips-dev vips-poppler poppler-utils vips-heif gcompat ttf-freefont && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf
|
||||||
|
|
||||||
|
RUN echo $'.include = /etc/ssl/openssl.cnf\n\
|
||||||
|
\n\
|
||||||
|
[provider_sect]\n\
|
||||||
|
default = default_sect\n\
|
||||||
|
legacy = legacy_sect\n\
|
||||||
|
\n\
|
||||||
|
[default_sect]\n\
|
||||||
|
activate = 1\n\
|
||||||
|
\n\
|
||||||
|
[legacy_sect]\n\
|
||||||
|
activate = 1' >> /app/openssl_legacy.cnf
|
||||||
|
|
||||||
COPY ./Gemfile ./Gemfile.lock ./
|
COPY ./Gemfile ./Gemfile.lock ./
|
||||||
|
|
||||||
RUN bundle update --bundler && bundle install && rm -rf ~/.bundle
|
RUN apk add --no-cache build-base && bundle install && apk del build-base && rm -rf ~/.bundle /usr/local/bundle/cache && ruby -e "puts Dir['/usr/local/bundle/**/{spec,rdoc,resources/shared,resources/collation,resources/locales}']" | xargs rm -rf
|
||||||
|
|
||||||
COPY ./bin ./bin
|
COPY ./bin ./bin
|
||||||
COPY ./app ./app
|
COPY ./app ./app
|
||||||
@@ -45,14 +67,17 @@ COPY ./log ./log
|
|||||||
COPY ./lib ./lib
|
COPY ./lib ./lib
|
||||||
COPY ./public ./public
|
COPY ./public ./public
|
||||||
COPY ./tmp ./tmp
|
COPY ./tmp ./tmp
|
||||||
COPY LICENSE README.md Rakefile config.ru ./
|
COPY LICENSE README.md Rakefile config.ru .version ./
|
||||||
|
|
||||||
|
COPY --from=fonts /fonts/GoNotoKurrent-Regular.ttf /fonts/GoNotoKurrent-Bold.ttf /fonts/DancingScript-Regular.otf /fonts/OFL.txt /fonts
|
||||||
|
COPY --from=fonts /fonts/FreeSans.ttf /usr/share/fonts/freefont
|
||||||
COPY --from=webpack /app/public/packs ./public/packs
|
COPY --from=webpack /app/public/packs ./public/packs
|
||||||
|
|
||||||
|
RUN ln -s /fonts /app/public/fonts
|
||||||
RUN bundle exec bootsnap precompile --gemfile app/ lib/
|
RUN bundle exec bootsnap precompile --gemfile app/ lib/
|
||||||
|
|
||||||
WORKDIR /data/docuseal
|
WORKDIR /data/docuseal
|
||||||
ENV WORKDIR=/data/docuseal
|
ENV WORKDIR=/data/docuseal
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
CMD ["/app/bin/rails", "server"]
|
CMD ["/app/bin/bundle", "exec", "puma", "-C", "/app/config/puma.rb", "--dir", "/app"]
|
||||||
@@ -2,12 +2,14 @@
|
|||||||
|
|
||||||
source 'https://rubygems.org'
|
source 'https://rubygems.org'
|
||||||
|
|
||||||
ruby '3.2.2'
|
ruby '3.3.1'
|
||||||
|
|
||||||
|
gem 'arabic-letter-connector', require: 'arabic-letter-connector/logic'
|
||||||
gem 'aws-sdk-s3', require: false
|
gem 'aws-sdk-s3', require: false
|
||||||
gem 'azure-storage-blob', require: false
|
gem 'azure-storage-blob', require: false
|
||||||
gem 'bootsnap', require: false
|
gem 'bootsnap', require: false
|
||||||
gem 'cancancan'
|
gem 'cancancan'
|
||||||
|
gem 'csv'
|
||||||
gem 'devise'
|
gem 'devise'
|
||||||
gem 'devise-two-factor'
|
gem 'devise-two-factor'
|
||||||
gem 'dotenv', require: false
|
gem 'dotenv', require: false
|
||||||
@@ -21,26 +23,26 @@ gem 'jwt'
|
|||||||
gem 'lograge'
|
gem 'lograge'
|
||||||
gem 'mysql2', require: false
|
gem 'mysql2', require: false
|
||||||
gem 'oj'
|
gem 'oj'
|
||||||
gem 'omniauth-google-oauth2'
|
|
||||||
gem 'omniauth-rails_csrf_protection'
|
|
||||||
gem 'pagy'
|
gem 'pagy'
|
||||||
gem 'pdf-reader'
|
gem 'pdf-reader'
|
||||||
gem 'pg', require: false
|
gem 'pg', require: false
|
||||||
gem 'premailer-rails'
|
gem 'premailer-rails'
|
||||||
|
gem 'pretender'
|
||||||
gem 'puma'
|
gem 'puma'
|
||||||
|
gem 'rack'
|
||||||
gem 'rails'
|
gem 'rails'
|
||||||
gem 'rails_autolink'
|
gem 'rails_autolink'
|
||||||
gem 'rails-i18n'
|
gem 'rails-i18n'
|
||||||
gem 'rollbar', require: ENV.key?('ROLLBAR_ACCESS_TOKEN')
|
|
||||||
gem 'rotp'
|
gem 'rotp'
|
||||||
gem 'rqrcode'
|
gem 'rqrcode'
|
||||||
gem 'ruby-vips'
|
gem 'ruby-vips'
|
||||||
gem 'rubyXL'
|
gem 'rubyXL'
|
||||||
gem 'shakapacker'
|
gem 'shakapacker'
|
||||||
gem 'sidekiq', require: ENV.key?('REDIS_URL')
|
gem 'sidekiq', require: ENV.key?('REDIS_URL')
|
||||||
gem 'sqlite3', require: false
|
gem 'sqlite3', require: false, force_ruby_platform: true
|
||||||
gem 'strip_attributes'
|
gem 'strip_attributes'
|
||||||
gem 'turbo-rails'
|
gem 'turbo-rails'
|
||||||
|
gem 'twitter_cldr', require: false
|
||||||
gem 'tzinfo-data'
|
gem 'tzinfo-data'
|
||||||
|
|
||||||
group :development, :test do
|
group :development, :test do
|
||||||
|
|||||||
+267
-250
@@ -2,93 +2,103 @@ GEM
|
|||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
Ascii85 (1.1.0)
|
Ascii85 (1.1.0)
|
||||||
actioncable (7.0.7)
|
actioncable (7.1.3.2)
|
||||||
actionpack (= 7.0.7)
|
actionpack (= 7.1.3.2)
|
||||||
activesupport (= 7.0.7)
|
activesupport (= 7.1.3.2)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
websocket-driver (>= 0.6.1)
|
websocket-driver (>= 0.6.1)
|
||||||
actionmailbox (7.0.7)
|
zeitwerk (~> 2.6)
|
||||||
actionpack (= 7.0.7)
|
actionmailbox (7.1.3.2)
|
||||||
activejob (= 7.0.7)
|
actionpack (= 7.1.3.2)
|
||||||
activerecord (= 7.0.7)
|
activejob (= 7.1.3.2)
|
||||||
activestorage (= 7.0.7)
|
activerecord (= 7.1.3.2)
|
||||||
activesupport (= 7.0.7)
|
activestorage (= 7.1.3.2)
|
||||||
|
activesupport (= 7.1.3.2)
|
||||||
mail (>= 2.7.1)
|
mail (>= 2.7.1)
|
||||||
net-imap
|
net-imap
|
||||||
net-pop
|
net-pop
|
||||||
net-smtp
|
net-smtp
|
||||||
actionmailer (7.0.7)
|
actionmailer (7.1.3.2)
|
||||||
actionpack (= 7.0.7)
|
actionpack (= 7.1.3.2)
|
||||||
actionview (= 7.0.7)
|
actionview (= 7.1.3.2)
|
||||||
activejob (= 7.0.7)
|
activejob (= 7.1.3.2)
|
||||||
activesupport (= 7.0.7)
|
activesupport (= 7.1.3.2)
|
||||||
mail (~> 2.5, >= 2.5.4)
|
mail (~> 2.5, >= 2.5.4)
|
||||||
net-imap
|
net-imap
|
||||||
net-pop
|
net-pop
|
||||||
net-smtp
|
net-smtp
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.2)
|
||||||
actionpack (7.0.7)
|
actionpack (7.1.3.2)
|
||||||
actionview (= 7.0.7)
|
actionview (= 7.1.3.2)
|
||||||
activesupport (= 7.0.7)
|
activesupport (= 7.1.3.2)
|
||||||
rack (~> 2.0, >= 2.2.4)
|
nokogiri (>= 1.8.5)
|
||||||
|
racc
|
||||||
|
rack (>= 2.2.4)
|
||||||
|
rack-session (>= 1.0.1)
|
||||||
rack-test (>= 0.6.3)
|
rack-test (>= 0.6.3)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.2)
|
||||||
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
rails-html-sanitizer (~> 1.6)
|
||||||
actiontext (7.0.7)
|
actiontext (7.1.3.2)
|
||||||
actionpack (= 7.0.7)
|
actionpack (= 7.1.3.2)
|
||||||
activerecord (= 7.0.7)
|
activerecord (= 7.1.3.2)
|
||||||
activestorage (= 7.0.7)
|
activestorage (= 7.1.3.2)
|
||||||
activesupport (= 7.0.7)
|
activesupport (= 7.1.3.2)
|
||||||
globalid (>= 0.6.0)
|
globalid (>= 0.6.0)
|
||||||
nokogiri (>= 1.8.5)
|
nokogiri (>= 1.8.5)
|
||||||
actionview (7.0.7)
|
actionview (7.1.3.2)
|
||||||
activesupport (= 7.0.7)
|
activesupport (= 7.1.3.2)
|
||||||
builder (~> 3.1)
|
builder (~> 3.1)
|
||||||
erubi (~> 1.4)
|
erubi (~> 1.11)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.2)
|
||||||
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
rails-html-sanitizer (~> 1.6)
|
||||||
activejob (7.0.7)
|
activejob (7.1.3.2)
|
||||||
activesupport (= 7.0.7)
|
activesupport (= 7.1.3.2)
|
||||||
globalid (>= 0.3.6)
|
globalid (>= 0.3.6)
|
||||||
activemodel (7.0.7)
|
activemodel (7.1.3.2)
|
||||||
activesupport (= 7.0.7)
|
activesupport (= 7.1.3.2)
|
||||||
activerecord (7.0.7)
|
activerecord (7.1.3.2)
|
||||||
activemodel (= 7.0.7)
|
activemodel (= 7.1.3.2)
|
||||||
activesupport (= 7.0.7)
|
activesupport (= 7.1.3.2)
|
||||||
activestorage (7.0.7)
|
timeout (>= 0.4.0)
|
||||||
actionpack (= 7.0.7)
|
activestorage (7.1.3.2)
|
||||||
activejob (= 7.0.7)
|
actionpack (= 7.1.3.2)
|
||||||
activerecord (= 7.0.7)
|
activejob (= 7.1.3.2)
|
||||||
activesupport (= 7.0.7)
|
activerecord (= 7.1.3.2)
|
||||||
|
activesupport (= 7.1.3.2)
|
||||||
marcel (~> 1.0)
|
marcel (~> 1.0)
|
||||||
mini_mime (>= 1.1.0)
|
activesupport (7.1.3.2)
|
||||||
activesupport (7.0.7)
|
base64
|
||||||
|
bigdecimal
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||||
|
connection_pool (>= 2.2.5)
|
||||||
|
drb
|
||||||
i18n (>= 1.6, < 2)
|
i18n (>= 1.6, < 2)
|
||||||
minitest (>= 5.1)
|
minitest (>= 5.1)
|
||||||
|
mutex_m
|
||||||
tzinfo (~> 2.0)
|
tzinfo (~> 2.0)
|
||||||
addressable (2.8.5)
|
addressable (2.8.6)
|
||||||
public_suffix (>= 2.0.2, < 6.0)
|
public_suffix (>= 2.0.2, < 6.0)
|
||||||
afm (0.2.2)
|
afm (0.2.2)
|
||||||
annotate (3.2.0)
|
annotate (3.2.0)
|
||||||
activerecord (>= 3.2, < 8.0)
|
activerecord (>= 3.2, < 8.0)
|
||||||
rake (>= 10.4, < 14.0)
|
rake (>= 10.4, < 14.0)
|
||||||
|
arabic-letter-connector (0.1.1)
|
||||||
ast (2.4.2)
|
ast (2.4.2)
|
||||||
aws-eventstream (1.2.0)
|
aws-eventstream (1.3.0)
|
||||||
aws-partitions (1.807.0)
|
aws-partitions (1.913.0)
|
||||||
aws-sdk-core (3.180.3)
|
aws-sdk-core (3.191.6)
|
||||||
aws-eventstream (~> 1, >= 1.0.2)
|
aws-eventstream (~> 1, >= 1.3.0)
|
||||||
aws-partitions (~> 1, >= 1.651.0)
|
aws-partitions (~> 1, >= 1.651.0)
|
||||||
aws-sigv4 (~> 1.5)
|
aws-sigv4 (~> 1.8)
|
||||||
jmespath (~> 1, >= 1.6.1)
|
jmespath (~> 1, >= 1.6.1)
|
||||||
aws-sdk-kms (1.71.0)
|
aws-sdk-kms (1.79.0)
|
||||||
aws-sdk-core (~> 3, >= 3.177.0)
|
aws-sdk-core (~> 3, >= 3.191.0)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
aws-sdk-s3 (1.132.1)
|
aws-sdk-s3 (1.146.1)
|
||||||
aws-sdk-core (~> 3, >= 3.179.0)
|
aws-sdk-core (~> 3, >= 3.191.0)
|
||||||
aws-sdk-kms (~> 1)
|
aws-sdk-kms (~> 1)
|
||||||
aws-sigv4 (~> 1.6)
|
aws-sigv4 (~> 1.8)
|
||||||
aws-sigv4 (1.6.0)
|
aws-sigv4 (1.8.0)
|
||||||
aws-eventstream (~> 1, >= 1.0.2)
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
azure-storage-blob (2.0.3)
|
azure-storage-blob (2.0.3)
|
||||||
azure-storage-common (~> 2.0)
|
azure-storage-common (~> 2.0)
|
||||||
@@ -98,51 +108,57 @@ GEM
|
|||||||
faraday_middleware (~> 1.0, >= 1.0.0.rc1)
|
faraday_middleware (~> 1.0, >= 1.0.0.rc1)
|
||||||
net-http-persistent (~> 4.0)
|
net-http-persistent (~> 4.0)
|
||||||
nokogiri (~> 1, >= 1.10.8)
|
nokogiri (~> 1, >= 1.10.8)
|
||||||
base64 (0.1.1)
|
base64 (0.2.0)
|
||||||
bcrypt (3.1.19)
|
bcrypt (3.1.20)
|
||||||
better_html (2.0.2)
|
better_html (2.1.1)
|
||||||
actionview (>= 6.0)
|
actionview (>= 6.0)
|
||||||
activesupport (>= 6.0)
|
activesupport (>= 6.0)
|
||||||
ast (~> 2.0)
|
ast (~> 2.0)
|
||||||
erubi (~> 1.4)
|
erubi (~> 1.4)
|
||||||
parser (>= 2.4)
|
parser (>= 2.4)
|
||||||
smart_properties
|
smart_properties
|
||||||
|
bigdecimal (3.1.7)
|
||||||
bindex (0.8.1)
|
bindex (0.8.1)
|
||||||
bootsnap (1.16.0)
|
bootsnap (1.18.3)
|
||||||
msgpack (~> 1.2)
|
msgpack (~> 1.2)
|
||||||
builder (3.2.4)
|
builder (3.2.4)
|
||||||
bullet (7.0.7)
|
bullet (7.1.6)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
uniform_notifier (~> 1.11)
|
uniform_notifier (~> 1.11)
|
||||||
|
camertron-eprun (1.1.1)
|
||||||
cancancan (3.5.0)
|
cancancan (3.5.0)
|
||||||
capybara (3.39.2)
|
capybara (3.40.0)
|
||||||
addressable
|
addressable
|
||||||
matrix
|
matrix
|
||||||
mini_mime (>= 0.1.3)
|
mini_mime (>= 0.1.3)
|
||||||
nokogiri (~> 1.8)
|
nokogiri (~> 1.11)
|
||||||
rack (>= 1.6.0)
|
rack (>= 1.6.0)
|
||||||
rack-test (>= 0.6.3)
|
rack-test (>= 0.6.3)
|
||||||
regexp_parser (>= 1.5, < 3.0)
|
regexp_parser (>= 1.5, < 3.0)
|
||||||
xpath (~> 3.2)
|
xpath (~> 3.2)
|
||||||
|
childprocess (5.0.0)
|
||||||
chunky_png (1.4.0)
|
chunky_png (1.4.0)
|
||||||
|
cldr-plurals-runtime-rb (1.1.0)
|
||||||
cmdparse (3.0.7)
|
cmdparse (3.0.7)
|
||||||
coderay (1.1.3)
|
coderay (1.1.3)
|
||||||
concurrent-ruby (1.2.2)
|
concurrent-ruby (1.2.3)
|
||||||
connection_pool (2.4.1)
|
connection_pool (2.4.1)
|
||||||
crack (0.4.5)
|
crack (1.0.0)
|
||||||
|
bigdecimal
|
||||||
rexml
|
rexml
|
||||||
crass (1.0.6)
|
crass (1.0.6)
|
||||||
css_parser (1.14.0)
|
css_parser (1.17.1)
|
||||||
addressable
|
addressable
|
||||||
cuprite (0.14.3)
|
csv (3.3.0)
|
||||||
|
cuprite (0.15)
|
||||||
capybara (~> 3.0)
|
capybara (~> 3.0)
|
||||||
ferrum (~> 0.13.0)
|
ferrum (~> 0.14.0)
|
||||||
date (3.3.3)
|
date (3.3.4)
|
||||||
debug (1.8.0)
|
debug (1.9.2)
|
||||||
irb (>= 1.5.0)
|
irb (~> 1.10)
|
||||||
reline (>= 0.3.1)
|
reline (>= 0.3.8)
|
||||||
declarative (0.0.20)
|
declarative (0.0.20)
|
||||||
devise (4.9.2)
|
devise (4.9.4)
|
||||||
bcrypt (~> 3.0)
|
bcrypt (~> 3.0)
|
||||||
orm_adapter (~> 0.1)
|
orm_adapter (~> 0.1)
|
||||||
railties (>= 4.1.0)
|
railties (>= 4.1.0)
|
||||||
@@ -153,13 +169,14 @@ GEM
|
|||||||
devise (~> 4.0)
|
devise (~> 4.0)
|
||||||
railties (~> 7.0)
|
railties (~> 7.0)
|
||||||
rotp (~> 6.0)
|
rotp (~> 6.0)
|
||||||
diff-lcs (1.5.0)
|
diff-lcs (1.5.1)
|
||||||
digest-crc (0.6.5)
|
digest-crc (0.6.5)
|
||||||
rake (>= 12.0.0, < 14.0.0)
|
rake (>= 12.0.0, < 14.0.0)
|
||||||
docile (1.4.0)
|
docile (1.4.0)
|
||||||
dotenv (2.8.1)
|
dotenv (3.1.0)
|
||||||
|
drb (2.2.1)
|
||||||
email_typo (0.2.3)
|
email_typo (0.2.3)
|
||||||
erb_lint (0.4.0)
|
erb_lint (0.5.0)
|
||||||
activesupport
|
activesupport
|
||||||
better_html (>= 2.0.1)
|
better_html (>= 2.0.1)
|
||||||
parser (>= 2.7.1.4)
|
parser (>= 2.7.1.4)
|
||||||
@@ -167,12 +184,12 @@ GEM
|
|||||||
rubocop
|
rubocop
|
||||||
smart_properties
|
smart_properties
|
||||||
erubi (1.12.0)
|
erubi (1.12.0)
|
||||||
factory_bot (6.2.1)
|
factory_bot (6.4.6)
|
||||||
activesupport (>= 5.0.0)
|
activesupport (>= 5.0.0)
|
||||||
factory_bot_rails (6.2.0)
|
factory_bot_rails (6.4.3)
|
||||||
factory_bot (~> 6.2.0)
|
factory_bot (~> 6.4)
|
||||||
railties (>= 5.0.0)
|
railties (>= 5.0.0)
|
||||||
faker (3.2.1)
|
faker (3.3.1)
|
||||||
i18n (>= 1.8.11, < 2)
|
i18n (>= 1.8.11, < 2)
|
||||||
faraday (1.10.3)
|
faraday (1.10.3)
|
||||||
faraday-em_http (~> 1.0)
|
faraday-em_http (~> 1.0)
|
||||||
@@ -201,85 +218,87 @@ GEM
|
|||||||
faraday-retry (1.0.3)
|
faraday-retry (1.0.3)
|
||||||
faraday_middleware (1.2.0)
|
faraday_middleware (1.2.0)
|
||||||
faraday (~> 1.0)
|
faraday (~> 1.0)
|
||||||
ferrum (0.13)
|
ferrum (0.14)
|
||||||
addressable (~> 2.5)
|
addressable (~> 2.5)
|
||||||
concurrent-ruby (~> 1.1)
|
concurrent-ruby (~> 1.1)
|
||||||
webrick (~> 1.7)
|
webrick (~> 1.7)
|
||||||
websocket-driver (>= 0.6, < 0.8)
|
websocket-driver (>= 0.6, < 0.8)
|
||||||
ffi (1.15.5)
|
ffi (1.16.3)
|
||||||
geom2d (0.4.1)
|
geom2d (0.4.1)
|
||||||
globalid (1.1.0)
|
globalid (1.2.1)
|
||||||
activesupport (>= 5.0)
|
activesupport (>= 6.1)
|
||||||
google-apis-core (0.11.1)
|
google-apis-core (0.14.1)
|
||||||
addressable (~> 2.5, >= 2.5.1)
|
addressable (~> 2.5, >= 2.5.1)
|
||||||
googleauth (>= 0.16.2, < 2.a)
|
googleauth (~> 1.9)
|
||||||
httpclient (>= 2.8.1, < 3.a)
|
httpclient (>= 2.8.1, < 3.a)
|
||||||
mini_mime (~> 1.0)
|
mini_mime (~> 1.0)
|
||||||
representable (~> 3.0)
|
representable (~> 3.0)
|
||||||
retriable (>= 2.0, < 4.a)
|
retriable (>= 2.0, < 4.a)
|
||||||
rexml
|
rexml
|
||||||
webrick
|
google-apis-iamcredentials_v1 (0.20.0)
|
||||||
google-apis-iamcredentials_v1 (0.17.0)
|
google-apis-core (>= 0.14.0, < 2.a)
|
||||||
google-apis-core (>= 0.11.0, < 2.a)
|
google-apis-storage_v1 (0.37.0)
|
||||||
google-apis-storage_v1 (0.19.0)
|
google-apis-core (>= 0.14.0, < 2.a)
|
||||||
google-apis-core (>= 0.9.0, < 2.a)
|
google-cloud-core (1.7.0)
|
||||||
google-cloud-core (1.6.0)
|
google-cloud-env (>= 1.0, < 3.a)
|
||||||
google-cloud-env (~> 1.0)
|
|
||||||
google-cloud-errors (~> 1.0)
|
google-cloud-errors (~> 1.0)
|
||||||
google-cloud-env (1.6.0)
|
google-cloud-env (2.1.1)
|
||||||
faraday (>= 0.17.3, < 3.0)
|
faraday (>= 1.0, < 3.a)
|
||||||
google-cloud-errors (1.3.1)
|
google-cloud-errors (1.4.0)
|
||||||
google-cloud-storage (1.44.0)
|
google-cloud-storage (1.49.0)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
digest-crc (~> 0.4)
|
digest-crc (~> 0.4)
|
||||||
google-apis-iamcredentials_v1 (~> 0.1)
|
google-apis-core (~> 0.13)
|
||||||
google-apis-storage_v1 (~> 0.19.0)
|
google-apis-iamcredentials_v1 (~> 0.18)
|
||||||
|
google-apis-storage_v1 (~> 0.33)
|
||||||
google-cloud-core (~> 1.6)
|
google-cloud-core (~> 1.6)
|
||||||
googleauth (>= 0.16.2, < 2.a)
|
googleauth (~> 1.9)
|
||||||
mini_mime (~> 1.0)
|
mini_mime (~> 1.0)
|
||||||
googleauth (1.7.0)
|
googleauth (1.11.0)
|
||||||
faraday (>= 0.17.3, < 3.a)
|
faraday (>= 1.0, < 3.a)
|
||||||
|
google-cloud-env (~> 2.1)
|
||||||
jwt (>= 1.4, < 3.0)
|
jwt (>= 1.4, < 3.0)
|
||||||
memoist (~> 0.16)
|
|
||||||
multi_json (~> 1.11)
|
multi_json (~> 1.11)
|
||||||
os (>= 0.9, < 2.0)
|
os (>= 0.9, < 2.0)
|
||||||
signet (>= 0.16, < 2.a)
|
signet (>= 0.16, < 2.a)
|
||||||
hashdiff (1.0.1)
|
hashdiff (1.1.0)
|
||||||
hashery (2.1.2)
|
hashery (2.1.2)
|
||||||
hashie (5.0.0)
|
hexapdf (0.40.0)
|
||||||
hexapdf (0.33.0)
|
|
||||||
cmdparse (~> 3.0, >= 3.0.3)
|
cmdparse (~> 3.0, >= 3.0.3)
|
||||||
geom2d (~> 0.4, >= 0.4.1)
|
geom2d (~> 0.4, >= 0.4.1)
|
||||||
openssl (>= 2.2.1)
|
openssl (>= 2.2.1)
|
||||||
htmlentities (4.3.4)
|
htmlentities (4.3.4)
|
||||||
httpclient (2.8.3)
|
httpclient (2.8.3)
|
||||||
i18n (1.14.1)
|
i18n (1.14.4)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
image_processing (1.12.2)
|
image_processing (1.12.2)
|
||||||
mini_magick (>= 4.9.5, < 5)
|
mini_magick (>= 4.9.5, < 5)
|
||||||
ruby-vips (>= 2.0.17, < 3)
|
ruby-vips (>= 2.0.17, < 3)
|
||||||
io-console (0.6.0)
|
io-console (0.7.2)
|
||||||
irb (1.7.4)
|
irb (1.12.0)
|
||||||
reline (>= 0.3.6)
|
rdoc
|
||||||
|
reline (>= 0.4.2)
|
||||||
jmespath (1.6.2)
|
jmespath (1.6.2)
|
||||||
json (2.6.3)
|
json (2.7.2)
|
||||||
jwt (2.7.1)
|
jwt (2.8.1)
|
||||||
|
base64
|
||||||
language_server-protocol (3.17.0.3)
|
language_server-protocol (3.17.0.3)
|
||||||
launchy (2.5.2)
|
launchy (3.0.0)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
letter_opener (1.8.1)
|
childprocess (~> 5.0)
|
||||||
launchy (>= 2.2, < 3)
|
letter_opener (1.10.0)
|
||||||
|
launchy (>= 2.2, < 4)
|
||||||
letter_opener_web (2.0.0)
|
letter_opener_web (2.0.0)
|
||||||
actionmailer (>= 5.2)
|
actionmailer (>= 5.2)
|
||||||
letter_opener (~> 1.7)
|
letter_opener (~> 1.7)
|
||||||
railties (>= 5.2)
|
railties (>= 5.2)
|
||||||
rexml
|
rexml
|
||||||
lograge (0.13.0)
|
lograge (0.14.0)
|
||||||
actionpack (>= 4)
|
actionpack (>= 4)
|
||||||
activesupport (>= 4)
|
activesupport (>= 4)
|
||||||
railties (>= 4)
|
railties (>= 4)
|
||||||
request_store (~> 1.0)
|
request_store (~> 1.0)
|
||||||
loofah (2.21.3)
|
loofah (2.22.0)
|
||||||
crass (~> 1.0.2)
|
crass (~> 1.0.2)
|
||||||
nokogiri (>= 1.12.0)
|
nokogiri (>= 1.12.0)
|
||||||
mail (2.8.1)
|
mail (2.8.1)
|
||||||
@@ -287,75 +306,56 @@ GEM
|
|||||||
net-imap
|
net-imap
|
||||||
net-pop
|
net-pop
|
||||||
net-smtp
|
net-smtp
|
||||||
marcel (1.0.2)
|
marcel (1.0.4)
|
||||||
matrix (0.4.2)
|
matrix (0.4.2)
|
||||||
memoist (0.16.2)
|
method_source (1.1.0)
|
||||||
method_source (1.0.0)
|
|
||||||
mini_magick (4.12.0)
|
mini_magick (4.12.0)
|
||||||
mini_mime (1.1.5)
|
mini_mime (1.1.5)
|
||||||
mini_portile2 (2.8.4)
|
mini_portile2 (2.8.6)
|
||||||
minitest (5.19.0)
|
minitest (5.22.3)
|
||||||
msgpack (1.7.2)
|
msgpack (1.7.2)
|
||||||
multi_json (1.15.0)
|
multi_json (1.15.0)
|
||||||
multi_xml (0.6.0)
|
multipart-post (2.4.0)
|
||||||
multipart-post (2.3.0)
|
mutex_m (0.2.0)
|
||||||
mysql2 (0.5.5)
|
mysql2 (0.5.6)
|
||||||
net-http-persistent (4.0.2)
|
net-http-persistent (4.0.2)
|
||||||
connection_pool (~> 2.2)
|
connection_pool (~> 2.2)
|
||||||
net-imap (0.3.7)
|
net-imap (0.4.10)
|
||||||
date
|
date
|
||||||
net-protocol
|
net-protocol
|
||||||
net-pop (0.1.2)
|
net-pop (0.1.2)
|
||||||
net-protocol
|
net-protocol
|
||||||
net-protocol (0.2.1)
|
net-protocol (0.2.2)
|
||||||
timeout
|
timeout
|
||||||
net-smtp (0.3.3)
|
net-smtp (0.4.0)
|
||||||
net-protocol
|
net-protocol
|
||||||
nio4r (2.5.9)
|
nio4r (2.7.1)
|
||||||
nokogiri (1.15.4)
|
nokogiri (1.16.4)
|
||||||
mini_portile2 (~> 2.8.2)
|
mini_portile2 (~> 2.8.2)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.15.4-arm64-darwin)
|
nokogiri (1.16.4-aarch64-linux)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
oauth2 (2.0.9)
|
nokogiri (1.16.4-x86_64-linux)
|
||||||
faraday (>= 0.17.3, < 3.0)
|
racc (~> 1.4)
|
||||||
jwt (>= 1.0, < 3.0)
|
oj (3.16.3)
|
||||||
multi_xml (~> 0.5)
|
bigdecimal (>= 3.0)
|
||||||
rack (>= 1.2, < 4)
|
openssl (3.2.0)
|
||||||
snaky_hash (~> 2.0)
|
|
||||||
version_gem (~> 1.1)
|
|
||||||
oj (3.16.0)
|
|
||||||
omniauth (2.1.1)
|
|
||||||
hashie (>= 3.4.6)
|
|
||||||
rack (>= 2.2.3)
|
|
||||||
rack-protection
|
|
||||||
omniauth-google-oauth2 (1.1.1)
|
|
||||||
jwt (>= 2.0)
|
|
||||||
oauth2 (~> 2.0.6)
|
|
||||||
omniauth (~> 2.0)
|
|
||||||
omniauth-oauth2 (~> 1.8.0)
|
|
||||||
omniauth-oauth2 (1.8.0)
|
|
||||||
oauth2 (>= 1.4, < 3)
|
|
||||||
omniauth (~> 2.0)
|
|
||||||
omniauth-rails_csrf_protection (1.0.1)
|
|
||||||
actionpack (>= 4.2)
|
|
||||||
omniauth (~> 2.0)
|
|
||||||
openssl (3.1.0)
|
|
||||||
orm_adapter (0.5.0)
|
orm_adapter (0.5.0)
|
||||||
os (1.1.4)
|
os (1.1.4)
|
||||||
pagy (6.0.4)
|
package_json (0.1.0)
|
||||||
parallel (1.23.0)
|
pagy (8.1.2)
|
||||||
parser (3.2.2.3)
|
parallel (1.24.0)
|
||||||
|
parser (3.3.1.0)
|
||||||
ast (~> 2.4.1)
|
ast (~> 2.4.1)
|
||||||
racc
|
racc
|
||||||
pdf-reader (2.11.0)
|
pdf-reader (2.12.0)
|
||||||
Ascii85 (~> 1.0)
|
Ascii85 (~> 1.0)
|
||||||
afm (~> 0.2.1)
|
afm (~> 0.2.1)
|
||||||
hashery (~> 2.0)
|
hashery (~> 2.0)
|
||||||
ruby-rc4
|
ruby-rc4
|
||||||
ttfunk
|
ttfunk
|
||||||
pg (1.5.3)
|
pg (1.5.6)
|
||||||
premailer (1.21.0)
|
premailer (1.23.0)
|
||||||
addressable
|
addressable
|
||||||
css_parser (>= 1.12.0)
|
css_parser (>= 1.12.0)
|
||||||
htmlentities (>= 4.0.0)
|
htmlentities (>= 4.0.0)
|
||||||
@@ -363,36 +363,43 @@ GEM
|
|||||||
actionmailer (>= 3)
|
actionmailer (>= 3)
|
||||||
net-smtp
|
net-smtp
|
||||||
premailer (~> 1.7, >= 1.7.9)
|
premailer (~> 1.7, >= 1.7.9)
|
||||||
|
pretender (0.5.0)
|
||||||
|
actionpack (>= 6.1)
|
||||||
pry (0.14.2)
|
pry (0.14.2)
|
||||||
coderay (~> 1.1)
|
coderay (~> 1.1)
|
||||||
method_source (~> 1.0)
|
method_source (~> 1.0)
|
||||||
pry-rails (0.3.9)
|
pry-rails (0.3.9)
|
||||||
pry (>= 0.10.4)
|
pry (>= 0.10.4)
|
||||||
public_suffix (5.0.3)
|
psych (5.1.2)
|
||||||
puma (6.3.1)
|
stringio
|
||||||
|
public_suffix (5.0.5)
|
||||||
|
puma (6.4.2)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
racc (1.7.1)
|
racc (1.7.3)
|
||||||
rack (2.2.8)
|
rack (2.2.9)
|
||||||
rack-protection (3.1.0)
|
rack-proxy (0.7.7)
|
||||||
rack (~> 2.2, >= 2.2.4)
|
|
||||||
rack-proxy (0.7.6)
|
|
||||||
rack
|
rack
|
||||||
|
rack-session (1.0.2)
|
||||||
|
rack (< 3)
|
||||||
rack-test (2.1.0)
|
rack-test (2.1.0)
|
||||||
rack (>= 1.3)
|
rack (>= 1.3)
|
||||||
rails (7.0.7)
|
rackup (1.0.0)
|
||||||
actioncable (= 7.0.7)
|
rack (< 3)
|
||||||
actionmailbox (= 7.0.7)
|
webrick
|
||||||
actionmailer (= 7.0.7)
|
rails (7.1.3.2)
|
||||||
actionpack (= 7.0.7)
|
actioncable (= 7.1.3.2)
|
||||||
actiontext (= 7.0.7)
|
actionmailbox (= 7.1.3.2)
|
||||||
actionview (= 7.0.7)
|
actionmailer (= 7.1.3.2)
|
||||||
activejob (= 7.0.7)
|
actionpack (= 7.1.3.2)
|
||||||
activemodel (= 7.0.7)
|
actiontext (= 7.1.3.2)
|
||||||
activerecord (= 7.0.7)
|
actionview (= 7.1.3.2)
|
||||||
activestorage (= 7.0.7)
|
activejob (= 7.1.3.2)
|
||||||
activesupport (= 7.0.7)
|
activemodel (= 7.1.3.2)
|
||||||
|
activerecord (= 7.1.3.2)
|
||||||
|
activestorage (= 7.1.3.2)
|
||||||
|
activesupport (= 7.1.3.2)
|
||||||
bundler (>= 1.15.0)
|
bundler (>= 1.15.0)
|
||||||
railties (= 7.0.7)
|
railties (= 7.1.3.2)
|
||||||
rails-dom-testing (2.2.0)
|
rails-dom-testing (2.2.0)
|
||||||
activesupport (>= 5.0.0)
|
activesupport (>= 5.0.0)
|
||||||
minitest
|
minitest
|
||||||
@@ -400,111 +407,117 @@ GEM
|
|||||||
rails-html-sanitizer (1.6.0)
|
rails-html-sanitizer (1.6.0)
|
||||||
loofah (~> 2.21)
|
loofah (~> 2.21)
|
||||||
nokogiri (~> 1.14)
|
nokogiri (~> 1.14)
|
||||||
rails-i18n (7.0.8)
|
rails-i18n (7.0.9)
|
||||||
i18n (>= 0.7, < 2)
|
i18n (>= 0.7, < 2)
|
||||||
railties (>= 6.0.0, < 8)
|
railties (>= 6.0.0, < 8)
|
||||||
rails_autolink (1.1.8)
|
rails_autolink (1.1.8)
|
||||||
actionview (> 3.1)
|
actionview (> 3.1)
|
||||||
activesupport (> 3.1)
|
activesupport (> 3.1)
|
||||||
railties (> 3.1)
|
railties (> 3.1)
|
||||||
railties (7.0.7)
|
railties (7.1.3.2)
|
||||||
actionpack (= 7.0.7)
|
actionpack (= 7.1.3.2)
|
||||||
activesupport (= 7.0.7)
|
activesupport (= 7.1.3.2)
|
||||||
method_source
|
irb
|
||||||
|
rackup (>= 1.0.0)
|
||||||
rake (>= 12.2)
|
rake (>= 12.2)
|
||||||
thor (~> 1.0)
|
thor (~> 1.0, >= 1.2.2)
|
||||||
zeitwerk (~> 2.5)
|
zeitwerk (~> 2.6)
|
||||||
rainbow (3.1.1)
|
rainbow (3.1.1)
|
||||||
rake (13.0.6)
|
rake (13.2.1)
|
||||||
redis-client (0.16.0)
|
rdoc (6.6.3.1)
|
||||||
|
psych (>= 4.0.0)
|
||||||
|
redis-client (0.22.0)
|
||||||
connection_pool
|
connection_pool
|
||||||
regexp_parser (2.8.1)
|
regexp_parser (2.9.0)
|
||||||
reline (0.3.8)
|
reline (0.5.1)
|
||||||
io-console (~> 0.5)
|
io-console (~> 0.5)
|
||||||
representable (3.2.0)
|
representable (3.2.0)
|
||||||
declarative (< 0.1.0)
|
declarative (< 0.1.0)
|
||||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||||
uber (< 0.2.0)
|
uber (< 0.2.0)
|
||||||
request_store (1.5.1)
|
request_store (1.6.0)
|
||||||
rack (>= 1.4)
|
rack (>= 1.4)
|
||||||
responders (3.1.0)
|
responders (3.1.1)
|
||||||
actionpack (>= 5.2)
|
actionpack (>= 5.2)
|
||||||
railties (>= 5.2)
|
railties (>= 5.2)
|
||||||
retriable (3.1.2)
|
retriable (3.1.2)
|
||||||
rexml (3.2.6)
|
rexml (3.2.6)
|
||||||
rollbar (3.4.0)
|
|
||||||
rotp (6.3.0)
|
rotp (6.3.0)
|
||||||
rqrcode (2.2.0)
|
rqrcode (2.2.0)
|
||||||
chunky_png (~> 1.0)
|
chunky_png (~> 1.0)
|
||||||
rqrcode_core (~> 1.0)
|
rqrcode_core (~> 1.0)
|
||||||
rqrcode_core (1.2.0)
|
rqrcode_core (1.2.0)
|
||||||
rspec-core (3.12.2)
|
rspec-core (3.13.0)
|
||||||
rspec-support (~> 3.12.0)
|
rspec-support (~> 3.13.0)
|
||||||
rspec-expectations (3.12.3)
|
rspec-expectations (3.13.0)
|
||||||
diff-lcs (>= 1.2.0, < 2.0)
|
diff-lcs (>= 1.2.0, < 2.0)
|
||||||
rspec-support (~> 3.12.0)
|
rspec-support (~> 3.13.0)
|
||||||
rspec-mocks (3.12.6)
|
rspec-mocks (3.13.0)
|
||||||
diff-lcs (>= 1.2.0, < 2.0)
|
diff-lcs (>= 1.2.0, < 2.0)
|
||||||
rspec-support (~> 3.12.0)
|
rspec-support (~> 3.13.0)
|
||||||
rspec-rails (6.0.3)
|
rspec-rails (6.1.2)
|
||||||
actionpack (>= 6.1)
|
actionpack (>= 6.1)
|
||||||
activesupport (>= 6.1)
|
activesupport (>= 6.1)
|
||||||
railties (>= 6.1)
|
railties (>= 6.1)
|
||||||
rspec-core (~> 3.12)
|
rspec-core (~> 3.13)
|
||||||
rspec-expectations (~> 3.12)
|
rspec-expectations (~> 3.13)
|
||||||
rspec-mocks (~> 3.12)
|
rspec-mocks (~> 3.13)
|
||||||
rspec-support (~> 3.12)
|
rspec-support (~> 3.13)
|
||||||
rspec-support (3.12.1)
|
rspec-support (3.13.1)
|
||||||
rubocop (1.56.1)
|
rubocop (1.63.3)
|
||||||
base64 (~> 0.1.1)
|
|
||||||
json (~> 2.3)
|
json (~> 2.3)
|
||||||
language_server-protocol (>= 3.17.0)
|
language_server-protocol (>= 3.17.0)
|
||||||
parallel (~> 1.10)
|
parallel (~> 1.10)
|
||||||
parser (>= 3.2.2.3)
|
parser (>= 3.3.0.2)
|
||||||
rainbow (>= 2.2.2, < 4.0)
|
rainbow (>= 2.2.2, < 4.0)
|
||||||
regexp_parser (>= 1.8, < 3.0)
|
regexp_parser (>= 1.8, < 3.0)
|
||||||
rexml (>= 3.2.5, < 4.0)
|
rexml (>= 3.2.5, < 4.0)
|
||||||
rubocop-ast (>= 1.28.1, < 2.0)
|
rubocop-ast (>= 1.31.1, < 2.0)
|
||||||
ruby-progressbar (~> 1.7)
|
ruby-progressbar (~> 1.7)
|
||||||
unicode-display_width (>= 2.4.0, < 3.0)
|
unicode-display_width (>= 2.4.0, < 3.0)
|
||||||
rubocop-ast (1.29.0)
|
rubocop-ast (1.31.2)
|
||||||
parser (>= 3.2.1.0)
|
parser (>= 3.3.0.4)
|
||||||
rubocop-capybara (2.18.0)
|
rubocop-capybara (2.20.0)
|
||||||
rubocop (~> 1.41)
|
rubocop (~> 1.41)
|
||||||
rubocop-factory_bot (2.23.1)
|
rubocop-factory_bot (2.25.1)
|
||||||
rubocop (~> 1.33)
|
rubocop (~> 1.41)
|
||||||
rubocop-performance (1.19.0)
|
rubocop-performance (1.21.0)
|
||||||
rubocop (>= 1.7.0, < 2.0)
|
rubocop (>= 1.48.1, < 2.0)
|
||||||
rubocop-ast (>= 0.4.0)
|
rubocop-ast (>= 1.31.1, < 2.0)
|
||||||
rubocop-rails (2.20.2)
|
rubocop-rails (2.24.1)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
rack (>= 1.1)
|
rack (>= 1.1)
|
||||||
rubocop (>= 1.33.0, < 2.0)
|
rubocop (>= 1.33.0, < 2.0)
|
||||||
rubocop-rspec (2.23.2)
|
rubocop-ast (>= 1.31.1, < 2.0)
|
||||||
rubocop (~> 1.33)
|
rubocop-rspec (2.29.1)
|
||||||
|
rubocop (~> 1.40)
|
||||||
rubocop-capybara (~> 2.17)
|
rubocop-capybara (~> 2.17)
|
||||||
rubocop-factory_bot (~> 2.22)
|
rubocop-factory_bot (~> 2.22)
|
||||||
|
rubocop-rspec_rails (~> 2.28)
|
||||||
|
rubocop-rspec_rails (2.28.3)
|
||||||
|
rubocop (~> 1.40)
|
||||||
ruby-progressbar (1.13.0)
|
ruby-progressbar (1.13.0)
|
||||||
ruby-rc4 (0.1.5)
|
ruby-rc4 (0.1.5)
|
||||||
ruby-vips (2.1.4)
|
ruby-vips (2.2.1)
|
||||||
ffi (~> 1.12)
|
ffi (~> 1.12)
|
||||||
ruby2_keywords (0.0.5)
|
ruby2_keywords (0.0.5)
|
||||||
rubyXL (3.4.25)
|
rubyXL (3.4.27)
|
||||||
nokogiri (>= 1.10.8)
|
nokogiri (>= 1.10.8)
|
||||||
rubyzip (>= 1.3.0)
|
rubyzip (>= 1.3.0)
|
||||||
rubyzip (2.3.2)
|
rubyzip (2.3.2)
|
||||||
semantic_range (3.0.0)
|
semantic_range (3.0.0)
|
||||||
shakapacker (7.0.3)
|
shakapacker (7.2.3)
|
||||||
activesupport (>= 5.2)
|
activesupport (>= 5.2)
|
||||||
|
package_json
|
||||||
rack-proxy (>= 0.6.1)
|
rack-proxy (>= 0.6.1)
|
||||||
railties (>= 5.2)
|
railties (>= 5.2)
|
||||||
semantic_range (>= 2.3.0)
|
semantic_range (>= 2.3.0)
|
||||||
sidekiq (7.1.2)
|
sidekiq (7.2.2)
|
||||||
concurrent-ruby (< 2)
|
concurrent-ruby (< 2)
|
||||||
connection_pool (>= 2.3.0)
|
connection_pool (>= 2.3.0)
|
||||||
rack (>= 2.2.4)
|
rack (>= 2.2.4)
|
||||||
redis-client (>= 0.14.0)
|
redis-client (>= 0.19.0)
|
||||||
signet (0.17.0)
|
signet (0.19.0)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
faraday (>= 0.17.5, < 3.a)
|
faraday (>= 0.17.5, < 3.a)
|
||||||
jwt (>= 1.5, < 3.0)
|
jwt (>= 1.5, < 3.0)
|
||||||
@@ -516,38 +529,39 @@ GEM
|
|||||||
simplecov-html (0.12.3)
|
simplecov-html (0.12.3)
|
||||||
simplecov_json_formatter (0.1.4)
|
simplecov_json_formatter (0.1.4)
|
||||||
smart_properties (1.17.0)
|
smart_properties (1.17.0)
|
||||||
snaky_hash (2.0.1)
|
sqlite3 (1.7.3)
|
||||||
hashie
|
|
||||||
version_gem (~> 1.1, >= 1.1.1)
|
|
||||||
sqlite3 (1.6.3)
|
|
||||||
mini_portile2 (~> 2.8.0)
|
mini_portile2 (~> 2.8.0)
|
||||||
sqlite3 (1.6.3-arm64-darwin)
|
stringio (3.1.0)
|
||||||
strip_attributes (1.13.0)
|
strip_attributes (1.13.0)
|
||||||
activemodel (>= 3.0, < 8.0)
|
activemodel (>= 3.0, < 8.0)
|
||||||
thor (1.2.2)
|
thor (1.3.1)
|
||||||
timeout (0.4.0)
|
timeout (0.4.1)
|
||||||
trailblazer-option (0.1.2)
|
trailblazer-option (0.1.2)
|
||||||
ttfunk (1.7.0)
|
ttfunk (1.8.0)
|
||||||
turbo-rails (1.4.0)
|
bigdecimal (~> 3.1)
|
||||||
|
turbo-rails (2.0.5)
|
||||||
actionpack (>= 6.0.0)
|
actionpack (>= 6.0.0)
|
||||||
activejob (>= 6.0.0)
|
activejob (>= 6.0.0)
|
||||||
railties (>= 6.0.0)
|
railties (>= 6.0.0)
|
||||||
|
twitter_cldr (6.12.0)
|
||||||
|
camertron-eprun
|
||||||
|
cldr-plurals-runtime-rb (~> 1.1)
|
||||||
|
tzinfo
|
||||||
tzinfo (2.0.6)
|
tzinfo (2.0.6)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
tzinfo-data (1.2023.3)
|
tzinfo-data (1.2024.1)
|
||||||
tzinfo (>= 1.0.0)
|
tzinfo (>= 1.0.0)
|
||||||
uber (0.1.0)
|
uber (0.1.0)
|
||||||
unicode-display_width (2.4.2)
|
unicode-display_width (2.5.0)
|
||||||
uniform_notifier (1.16.0)
|
uniform_notifier (1.16.0)
|
||||||
version_gem (1.1.3)
|
|
||||||
warden (1.2.9)
|
warden (1.2.9)
|
||||||
rack (>= 2.0.9)
|
rack (>= 2.0.9)
|
||||||
web-console (4.2.0)
|
web-console (4.2.1)
|
||||||
actionview (>= 6.0.0)
|
actionview (>= 6.0.0)
|
||||||
activemodel (>= 6.0.0)
|
activemodel (>= 6.0.0)
|
||||||
bindex (>= 0.4.0)
|
bindex (>= 0.4.0)
|
||||||
railties (>= 6.0.0)
|
railties (>= 6.0.0)
|
||||||
webmock (3.18.1)
|
webmock (3.23.0)
|
||||||
addressable (>= 2.8.0)
|
addressable (>= 2.8.0)
|
||||||
crack (>= 0.3.2)
|
crack (>= 0.3.2)
|
||||||
hashdiff (>= 0.4.0, < 2.0.0)
|
hashdiff (>= 0.4.0, < 2.0.0)
|
||||||
@@ -557,14 +571,16 @@ GEM
|
|||||||
websocket-extensions (0.1.5)
|
websocket-extensions (0.1.5)
|
||||||
xpath (3.2.0)
|
xpath (3.2.0)
|
||||||
nokogiri (~> 1.8)
|
nokogiri (~> 1.8)
|
||||||
zeitwerk (2.6.11)
|
zeitwerk (2.6.13)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
arm64-darwin-22
|
aarch64-linux
|
||||||
ruby
|
ruby
|
||||||
|
x86_64-linux
|
||||||
|
|
||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
annotate
|
annotate
|
||||||
|
arabic-letter-connector
|
||||||
aws-sdk-s3
|
aws-sdk-s3
|
||||||
azure-storage-blob
|
azure-storage-blob
|
||||||
better_html
|
better_html
|
||||||
@@ -572,6 +588,7 @@ DEPENDENCIES
|
|||||||
bullet
|
bullet
|
||||||
cancancan
|
cancancan
|
||||||
capybara
|
capybara
|
||||||
|
csv
|
||||||
cuprite
|
cuprite
|
||||||
debug
|
debug
|
||||||
devise
|
devise
|
||||||
@@ -591,18 +608,17 @@ DEPENDENCIES
|
|||||||
lograge
|
lograge
|
||||||
mysql2
|
mysql2
|
||||||
oj
|
oj
|
||||||
omniauth-google-oauth2
|
|
||||||
omniauth-rails_csrf_protection
|
|
||||||
pagy
|
pagy
|
||||||
pdf-reader
|
pdf-reader
|
||||||
pg
|
pg
|
||||||
premailer-rails
|
premailer-rails
|
||||||
|
pretender
|
||||||
pry-rails
|
pry-rails
|
||||||
puma
|
puma
|
||||||
|
rack
|
||||||
rails
|
rails
|
||||||
rails-i18n
|
rails-i18n
|
||||||
rails_autolink
|
rails_autolink
|
||||||
rollbar
|
|
||||||
rotp
|
rotp
|
||||||
rqrcode
|
rqrcode
|
||||||
rspec-rails
|
rspec-rails
|
||||||
@@ -618,12 +634,13 @@ DEPENDENCIES
|
|||||||
sqlite3
|
sqlite3
|
||||||
strip_attributes
|
strip_attributes
|
||||||
turbo-rails
|
turbo-rails
|
||||||
|
twitter_cldr
|
||||||
tzinfo-data
|
tzinfo-data
|
||||||
web-console
|
web-console
|
||||||
webmock
|
webmock
|
||||||
|
|
||||||
RUBY VERSION
|
RUBY VERSION
|
||||||
ruby 3.2.2p53
|
ruby 3.3.1p55
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
2.4.10
|
2.5.3
|
||||||
@@ -33,17 +33,32 @@ DocuSeal is an open source platform that provides secure and efficient digital d
|
|||||||
[](https://demo.docuseal.co)
|
[](https://demo.docuseal.co)
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
- [x] PDF form fields builder (WYSIWYG)
|
- PDF form fields builder (WYSIWYG)
|
||||||
- [x] 10 field types available (Signature, Date, File, Checkbox etc.)
|
- 12 field types available (Signature, Date, File, Checkbox etc.)
|
||||||
- [x] Multiple submitters per document
|
- Multiple submitters per document
|
||||||
- [x] Automated emails via SMTP
|
- Automated emails via SMTP
|
||||||
- [x] Files storage on AWS S3, Google Storage, or Azure
|
- Files storage on disk or AWS S3, Google Storage, Azure Cloud
|
||||||
- [x] Automatic PDF eSignature
|
- Automatic PDF eSignature
|
||||||
- [x] PDF signature verification
|
- PDF signature verification
|
||||||
- [x] Users management
|
- Users management
|
||||||
- [x] Mobile-optimized
|
- Mobile-optimized
|
||||||
- [x] API and Webhooks for integrations
|
- Signing available in 13 languages
|
||||||
- [x] Easy to deploy in minutes
|
- API and Webhooks for integrations
|
||||||
|
- Easy to deploy in minutes
|
||||||
|
|
||||||
|
## Pro Features
|
||||||
|
- Company logo and white-label
|
||||||
|
- User roles
|
||||||
|
- Automated reminders
|
||||||
|
- Invitation and identify verification via SMS
|
||||||
|
- Conditional fields and formulas
|
||||||
|
- Bulk send with CSV, XLSX spreadsheet import
|
||||||
|
- SSO / SAML
|
||||||
|
- Template creation with HTML API ([Guide](https://www.docuseal.co/guides/create-pdf-document-fillable-form-with-html-api))
|
||||||
|
- Template creation with PDF or DOCX and field tags API ([Guide](https://www.docuseal.co/guides/use-embedded-text-field-tags-in-the-pdf-to-create-a-fillable-form))
|
||||||
|
- Embedded signing form ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue), [Angular](https://github.com/docusealco/docuseal-angular) or [JavaScript](https://www.docuseal.co/docs/embedded))
|
||||||
|
- Embedded document form builder ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue), [Angular](https://github.com/docusealco/docuseal-angular) or [JavaScript](https://www.docuseal.co/docs/embedded))
|
||||||
|
- [Learn more](https://www.docuseal.co/pricing)
|
||||||
|
|
||||||
## Deploy
|
## Deploy
|
||||||
|
|
||||||
@@ -52,9 +67,6 @@ DocuSeal is an open source platform that provides secure and efficient digital d
|
|||||||
| [<img alt="Deploy on Heroku" src="https://www.herokucdn.com/deploy/button.svg" height="40">](https://heroku.com/deploy?template=https://github.com/docusealco/docuseal-heroku) | [<img alt="Deploy on Railway" src="https://railway.app/button.svg" height="40">](https://railway.app/template/IGoDnc?referralCode=ruU7JR)|
|
| [<img alt="Deploy on Heroku" src="https://www.herokucdn.com/deploy/button.svg" height="40">](https://heroku.com/deploy?template=https://github.com/docusealco/docuseal-heroku) | [<img alt="Deploy on Railway" src="https://railway.app/button.svg" height="40">](https://railway.app/template/IGoDnc?referralCode=ruU7JR)|
|
||||||
|**DigitalOcean**|**Render**|
|
|**DigitalOcean**|**Render**|
|
||||||
| [<img alt="Deploy on DigitalOcean" src="https://www.deploytodo.com/do-btn-blue.svg" height="40">](https://cloud.digitalocean.com/apps/new?repo=https://github.com/docusealco/docuseal-digitalocean/tree/master&refcode=421d50f53990) | [<img alt="Deploy to Render" src="https://render.com/images/deploy-to-render-button.svg" height="40">](https://render.com/deploy?repo=https://github.com/docusealco/docuseal-render)
|
| [<img alt="Deploy on DigitalOcean" src="https://www.deploytodo.com/do-btn-blue.svg" height="40">](https://cloud.digitalocean.com/apps/new?repo=https://github.com/docusealco/docuseal-digitalocean/tree/master&refcode=421d50f53990) | [<img alt="Deploy to Render" src="https://render.com/images/deploy-to-render-button.svg" height="40">](https://render.com/deploy?repo=https://github.com/docusealco/docuseal-render)
|
||||||
|**Koyeb**|**Elestio**|
|
|
||||||
| [<img alt="Deploy on Koyeb" src="https://www.koyeb.com/static/images/deploy/button.svg" height="40">](https://app.koyeb.com/deploy?name=docuseal&type=docker&image=docker.io/docuseal/docuseal&env[PORT]=8000&env[DATABASE_URL]=CHANGE_ME&env[SECRET_KEY_BASE]=CHANGE_ME&ports=8000;http;/) | [<img alt="Deploy on Elestio" src="https://pub-da36157c854648669813f3f76c526c2b.r2.dev/deploy-on-elestio-black.png">](https://dash.elest.io/deploy?soft=DocuSeal&id=339) |
|
|
||||||
|
|
||||||
|
|
||||||
#### Docker
|
#### Docker
|
||||||
|
|
||||||
@@ -73,17 +85,23 @@ curl https://raw.githubusercontent.com/docusealco/docuseal/master/docker-compose
|
|||||||
|
|
||||||
Run the app under a custom domain over https using docker compose (make sure your DNS points to the server to automatically issue ssl certs with Caddy):
|
Run the app under a custom domain over https using docker compose (make sure your DNS points to the server to automatically issue ssl certs with Caddy):
|
||||||
```sh
|
```sh
|
||||||
HOST=your-domain-name.com docker-compose up
|
sudo HOST=your-domain-name.com docker compose up
|
||||||
```
|
```
|
||||||
|
|
||||||
## For Companies
|
## For Businesses
|
||||||
### Integrate seamless document signing into your web or mobile apps with DocuSeal!
|
### Integrate seamless document signing into your web or mobile apps with DocuSeal
|
||||||
|
|
||||||
At DocuSeal we have expertise and technologies to make documents creation, filling, signing and processing seamlessly integrated with your product. We specialize in working with various industries, including **Banking, Healthcare, Transport, Real Estate, eCommerce, KYC, CRM, and other software products** that require bulk document signing. By leveraging DocuSeal, we can assist in reducing the overall cost of developing and processing electronic documents while ensuring security and compliance with local electronic document laws.
|
At DocuSeal we have expertise and technologies to make documents creation, filling, signing and processing seamlessly integrated with your product. We specialize in working with various industries, including **Banking, Healthcare, Transport, Real Estate, eCommerce, KYC, CRM, and other software products** that require bulk document signing. By leveraging DocuSeal, we can assist in reducing the overall cost of developing and processing electronic documents while ensuring security and compliance with local electronic document laws.
|
||||||
|
|
||||||
[Book a Meeting](https://calendly.com/kriti-docuseal/30min)
|
[Book a Meeting](https://www.docuseal.co/contact)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Distributed under the AGPLv3 License. See [LICENSE](https://github.com/docusealco/docuseal/blob/master/LICENSE) for more information.
|
Distributed under the AGPLv3 License. See [LICENSE](https://github.com/docusealco/docuseal/blob/master/LICENSE) for more information.
|
||||||
Unless otherwise noted, all files © 2023 DocuSeal LLC.
|
Unless otherwise noted, all files © 2023 DocuSeal LLC.
|
||||||
|
|
||||||
|
## Tools
|
||||||
|
|
||||||
|
- [Signature Maker](https://www.docuseal.co/online-signature)
|
||||||
|
- [Sign Document Online](https://www.docuseal.co/sign-documents-online)
|
||||||
|
- [Fill PDF Online](https://www.docuseal.co/fill-pdf)
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
# Reporting a Vulnerability
|
||||||
|
|
||||||
|
If you come across any security concern or vulnarability, please report the information via email to security@docuseal.co instead of opening a GitHub issue. We will promptly respond and will collaborate with you to validate the issue, and resolve it ASAP.
|
||||||
|
|
||||||
|
**We have a bug bounty program to reward security researchers.**
|
||||||
|
|
||||||
|
Out of scope vulnerabilities:
|
||||||
|
|
||||||
|
- CSRF
|
||||||
|
- DNSSEC, CAA, CSP headers
|
||||||
|
- DNS or email security related
|
||||||
|
- Rate Limiting
|
||||||
|
|
||||||
|
Note: We reserve the right to classify any reported vulnerability as out of scope for the bug bounty program.
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AccountConfigsController < ApplicationController
|
||||||
|
before_action :load_account_config
|
||||||
|
authorize_resource :account_config
|
||||||
|
|
||||||
|
ALLOWED_KEYS = [
|
||||||
|
AccountConfig::ALLOW_TYPED_SIGNATURE,
|
||||||
|
AccountConfig::FORCE_MFA,
|
||||||
|
AccountConfig::ALLOW_TO_RESUBMIT,
|
||||||
|
AccountConfig::ESIGNING_PREFERENCE_KEY,
|
||||||
|
AccountConfig::FORM_WITH_CONFETTI_KEY,
|
||||||
|
AccountConfig::DOWNLOAD_LINKS_AUTH_KEY,
|
||||||
|
AccountConfig::FORCE_SSO_AUTH_KEY,
|
||||||
|
AccountConfig::FLATTEN_RESULT_PDF_KEY
|
||||||
|
].freeze
|
||||||
|
|
||||||
|
InvalidKey = Class.new(StandardError)
|
||||||
|
|
||||||
|
def create
|
||||||
|
@account_config.update!(account_config_params)
|
||||||
|
|
||||||
|
head :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def load_account_config
|
||||||
|
raise InvalidKey unless ALLOWED_KEYS.include?(account_config_params[:key])
|
||||||
|
|
||||||
|
@account_config =
|
||||||
|
AccountConfig.find_or_initialize_by(account: current_account, key: account_config_params[:key])
|
||||||
|
end
|
||||||
|
|
||||||
|
def account_config_params
|
||||||
|
params.required(:account_config).permit!.tap do |attrs|
|
||||||
|
attrs[:value] = attrs[:value] == '1' if attrs[:value].in?(%w[1 0])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -4,6 +4,7 @@ class AccountsController < ApplicationController
|
|||||||
LOCALE_OPTIONS = {
|
LOCALE_OPTIONS = {
|
||||||
'en-US' => 'English (United States)',
|
'en-US' => 'English (United States)',
|
||||||
'en-GB' => 'English (United Kingdom)',
|
'en-GB' => 'English (United Kingdom)',
|
||||||
|
'fr-FR' => 'French (France)',
|
||||||
'es-ES' => 'Spanish (Spain)',
|
'es-ES' => 'Spanish (Spain)',
|
||||||
'pt-PT' => 'Portuguese (Portugal)',
|
'pt-PT' => 'Portuguese (Portugal)',
|
||||||
'de-DE' => 'German (Germany)'
|
'de-DE' => 'German (Germany)'
|
||||||
@@ -36,6 +37,18 @@ class AccountsController < ApplicationController
|
|||||||
render :show, status: :unprocessable_entity
|
render :show, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
authorize!(:manage, current_account)
|
||||||
|
|
||||||
|
true_user.update!(locked_at: Time.current)
|
||||||
|
|
||||||
|
render turbo_stream: turbo_stream.replace(
|
||||||
|
:account_delete_button,
|
||||||
|
html: helpers.tag.p('Your account removal request will be processed within 2 weeks. ' \
|
||||||
|
'Please contact us if you want to keep your account.')
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def load_account
|
def load_account
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Api
|
||||||
|
class ActiveStorageBlobsProxyController < ApiBaseController
|
||||||
|
include ActiveStorage::Streaming
|
||||||
|
|
||||||
|
skip_before_action :authenticate_user!
|
||||||
|
skip_authorization_check
|
||||||
|
|
||||||
|
before_action :set_cors_headers
|
||||||
|
before_action :set_noindex_headers
|
||||||
|
|
||||||
|
def show
|
||||||
|
blob_uuid, purp, exp = ApplicationRecord.signed_id_verifier.verified(params[:signed_uuid])
|
||||||
|
|
||||||
|
if blob_uuid.blank? || (purp.present? && purp != 'blob') || (exp && exp < Time.current.to_i)
|
||||||
|
Rollbar.error('Blob not found') if defined?(Rollbar)
|
||||||
|
|
||||||
|
return head :not_found
|
||||||
|
end
|
||||||
|
|
||||||
|
blob = ActiveStorage::Blob.find_by!(uuid: blob_uuid)
|
||||||
|
|
||||||
|
authorization_check!(blob) if exp.blank?
|
||||||
|
|
||||||
|
if request.headers['Range'].present?
|
||||||
|
send_blob_byte_range_data blob, request.headers['Range']
|
||||||
|
else
|
||||||
|
http_cache_forever public: true do
|
||||||
|
response.headers['Accept-Ranges'] = 'bytes'
|
||||||
|
response.headers['Content-Length'] = blob.byte_size.to_s
|
||||||
|
|
||||||
|
send_blob_stream blob, disposition: params[:disposition]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def authorization_check!(blob)
|
||||||
|
is_authorized =
|
||||||
|
blob.attachments.all? do |a|
|
||||||
|
a.name.in?(%w[logo preview_images]) ||
|
||||||
|
(current_user && a.record.account.id == current_user.account_id) ||
|
||||||
|
!a.record.account.account_configs.find_or_initialize_by(key: AccountConfig::DOWNLOAD_LINKS_AUTH_KEY).value
|
||||||
|
end
|
||||||
|
|
||||||
|
return if is_authorized
|
||||||
|
|
||||||
|
Rollbar.error('Blob aunauthorized') if defined?(Rollbar)
|
||||||
|
|
||||||
|
raise CanCan::AccessDenied
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Api
|
||||||
|
class ActiveStorageBlobsProxyLegacyController < ApiBaseController
|
||||||
|
include ActiveStorage::Streaming
|
||||||
|
|
||||||
|
skip_before_action :authenticate_user!
|
||||||
|
skip_authorization_check
|
||||||
|
|
||||||
|
before_action :set_cors_headers
|
||||||
|
before_action :set_noindex_headers
|
||||||
|
|
||||||
|
# rubocop:disable Metrics
|
||||||
|
def show
|
||||||
|
Rollbar.info('Blob legacy') if defined?(Rollbar)
|
||||||
|
|
||||||
|
blob = ActiveStorage::Blob.find_signed(params[:signed_blob_id] || params[:signed_id])
|
||||||
|
|
||||||
|
return head :not_found unless blob
|
||||||
|
|
||||||
|
is_permitted = blob.attachments.any? do |a|
|
||||||
|
(current_user && a.record.account.id == current_user.account_id) ||
|
||||||
|
a.record.account.account_configs.any? { |e| e.key == 'legacy_blob_proxy' } ||
|
||||||
|
a.name == 'logo'
|
||||||
|
end
|
||||||
|
|
||||||
|
unless is_permitted
|
||||||
|
Rollbar.error("Blob account not found: #{blob.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
|
return render json: { error: 'Not authenticated' }, status: :unauthorized
|
||||||
|
end
|
||||||
|
|
||||||
|
if request.headers['Range'].present?
|
||||||
|
send_blob_byte_range_data blob, request.headers['Range']
|
||||||
|
else
|
||||||
|
http_cache_forever public: true do
|
||||||
|
response.headers['Accept-Ranges'] = 'bytes'
|
||||||
|
response.headers['Content-Length'] = blob.byte_size.to_s
|
||||||
|
|
||||||
|
send_blob_stream blob, disposition: params[:disposition]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
# rubocop:enable Metrics
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -8,33 +8,74 @@ module Api
|
|||||||
DEFAULT_LIMIT = 10
|
DEFAULT_LIMIT = 10
|
||||||
MAX_LIMIT = 100
|
MAX_LIMIT = 100
|
||||||
|
|
||||||
|
impersonates :user, with: ->(uuid) { User.find_by(uuid:) }
|
||||||
|
|
||||||
wrap_parameters false
|
wrap_parameters false
|
||||||
|
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
check_authorization
|
check_authorization
|
||||||
|
|
||||||
|
rescue_from Params::BaseValidator::InvalidParameterError do |e|
|
||||||
|
render json: { error: e.message }, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
|
||||||
|
rescue_from RateLimit::LimitApproached do |e|
|
||||||
|
Rollbar.error(e) if defined?(Rollbar)
|
||||||
|
|
||||||
|
render json: { error: 'Too many requests' }, status: :too_many_requests
|
||||||
|
end
|
||||||
|
|
||||||
if Rails.env.production?
|
if Rails.env.production?
|
||||||
rescue_from CanCan::AccessDenied do |e|
|
rescue_from CanCan::AccessDenied do |e|
|
||||||
Rollbar.error(e) if defined?(Rollbar)
|
|
||||||
|
|
||||||
render json: { error: e.message }, status: :forbidden
|
render json: { error: e.message }, status: :forbidden
|
||||||
end
|
end
|
||||||
|
|
||||||
|
rescue_from JSON::ParserError do |e|
|
||||||
|
Rollbar.warning(e) if defined?(Rollbar)
|
||||||
|
|
||||||
|
render json: { error: "JSON parse error: #{e.message}" }, status: :unprocessable_entity
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def paginate(relation)
|
def paginate(relation)
|
||||||
result = relation.order(id: :desc)
|
result = relation.order(id: :desc)
|
||||||
.limit([params[:limit] || DEFAULT_LIMIT, MAX_LIMIT].min)
|
.limit([params.fetch(:limit, DEFAULT_LIMIT).to_i, MAX_LIMIT].min)
|
||||||
|
|
||||||
result = result.where('id < ?', params[:after]) if params[:after].present?
|
result = result.where(id: ...params[:after].to_i) if params[:after].present?
|
||||||
result = result.where('id > ?', params[:before]) if params[:before].present?
|
result = result.where(id: (params[:before].to_i + 1)...) if params[:before].present?
|
||||||
|
|
||||||
result
|
result
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def authenticate_user!
|
||||||
|
render json: { error: 'Not authenticated' }, status: :unauthorized unless current_user
|
||||||
|
end
|
||||||
|
|
||||||
|
def current_user
|
||||||
|
super || @current_user ||=
|
||||||
|
if request.headers['X-Auth-Token'].present?
|
||||||
|
sha256 = Digest::SHA256.hexdigest(request.headers['X-Auth-Token'])
|
||||||
|
|
||||||
|
User.joins(:access_token).active.find_by(access_token: { sha256: })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def current_account
|
def current_account
|
||||||
current_user&.account
|
current_user&.account
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def set_noindex_headers
|
||||||
|
headers['X-Robots-Tag'] = 'noindex'
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_cors_headers
|
||||||
|
headers['Access-Control-Allow-Origin'] = '*'
|
||||||
|
headers['Access-Control-Allow-Methods'] = 'POST, GET, PUT, PATCH, DELETE, OPTIONS'
|
||||||
|
headers['Access-Control-Allow-Headers'] = '*'
|
||||||
|
headers['Access-Control-Max-Age'] = '1728000'
|
||||||
|
headers['Access-Control-Allow-Credentials'] = true
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
module Api
|
module Api
|
||||||
class SubmissionsController < ApiBaseController
|
class SubmissionsController < ApiBaseController
|
||||||
load_and_authorize_resource :template, only: :create
|
load_and_authorize_resource :template, only: :create
|
||||||
load_and_authorize_resource :submission, only: %i[show index]
|
load_and_authorize_resource :submission, only: %i[show index destroy]
|
||||||
|
|
||||||
before_action only: :create do
|
before_action only: :create do
|
||||||
authorize!(:create, Submission)
|
authorize!(:create, Submission)
|
||||||
@@ -13,7 +13,13 @@ module Api
|
|||||||
submissions = Submissions.search(@submissions, params[:q])
|
submissions = Submissions.search(@submissions, params[:q])
|
||||||
submissions = submissions.where(template_id: params[:template_id]) if params[:template_id].present?
|
submissions = submissions.where(template_id: params[:template_id]) if params[:template_id].present?
|
||||||
|
|
||||||
submissions = paginate(submissions.preload(:created_by_user, :template, :submitters))
|
if params[:template_folder].present?
|
||||||
|
submissions = submissions.joins(template: :folder).where(folder: { name: params[:template_folder] })
|
||||||
|
end
|
||||||
|
|
||||||
|
submissions = paginate(submissions.preload(:created_by_user, :submitters,
|
||||||
|
template: :folder,
|
||||||
|
audit_trail_attachment: :blob))
|
||||||
|
|
||||||
render json: {
|
render json: {
|
||||||
data: submissions.as_json(serialize_params),
|
data: submissions.as_json(serialize_params),
|
||||||
@@ -26,126 +32,165 @@ module Api
|
|||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
serialized_subbmitters =
|
submitters = @submission.submitters.preload(documents_attachments: :blob, attachments_attachments: :blob)
|
||||||
@submission.submitters.preload(documents_attachments: :blob, attachments_attachments: :blob).map do |submitter|
|
|
||||||
Submissions::EnsureResultGenerated.call(submitter) if submitter.completed_at?
|
|
||||||
|
|
||||||
Submitters::SerializeForApi.call(submitter)
|
submitters.each do |submitter|
|
||||||
|
if submitter.completed_at? && submitter.documents_attachments.blank?
|
||||||
|
submitter.documents_attachments = Submissions::EnsureResultGenerated.call(submitter)
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if @submission.audit_trail_attachment.blank? && submitters.all?(&:completed_at?)
|
||||||
|
@submission.audit_trail_attachment = Submissions::GenerateAuditTrail.call(@submission)
|
||||||
|
end
|
||||||
|
|
||||||
|
serialized_submitters = submitters.map { |submitter| Submitters::SerializeForApi.call(submitter) }
|
||||||
|
|
||||||
json = @submission.as_json(
|
json = @submission.as_json(
|
||||||
serialize_params.deep_merge(
|
serialize_params.deep_merge(
|
||||||
include: {
|
include: { submission_events: { only: %i[id submitter_id event_type event_timestamp] } }
|
||||||
submission_events: {
|
|
||||||
only: %i[id submitter_id event_type event_timestamp]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
json[:submitters] = serialized_subbmitters
|
if submitters.all?(&:completed_at?)
|
||||||
|
last_submitter = submitters.max_by(&:completed_at)
|
||||||
|
|
||||||
|
json[:documents] = serialized_submitters.find { |e| e['id'] == last_submitter.id }['documents']
|
||||||
|
json[:status] = 'completed'
|
||||||
|
json[:completed_at] = last_submitter.completed_at
|
||||||
|
else
|
||||||
|
json[:documents] = []
|
||||||
|
json[:status] = 'pending'
|
||||||
|
json[:completed_at] = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
json[:submitters] = serialized_submitters
|
||||||
|
|
||||||
render json:
|
render json:
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
is_send_email = !params[:send_email].in?(['false', false])
|
Params::SubmissionCreateValidator.call(params)
|
||||||
|
|
||||||
submissions =
|
return render json: { error: 'Template not found' }, status: :unprocessable_entity if @template.nil?
|
||||||
if (emails = (params[:emails] || params[:email]).presence) && params[:submission].blank?
|
|
||||||
Submissions.create_from_emails(template: @template,
|
|
||||||
user: current_user,
|
|
||||||
source: :api,
|
|
||||||
mark_as_sent: is_send_email,
|
|
||||||
emails:)
|
|
||||||
else
|
|
||||||
submissions_attrs, attachments = normalize_submissions_params!(submissions_params[:submission], @template)
|
|
||||||
|
|
||||||
Submissions.create_from_submitters(
|
if @template.fields.blank?
|
||||||
template: @template,
|
Rollbar.warning("Template does not contain fields: #{@template.id}") if defined?(Rollbar)
|
||||||
user: current_user,
|
|
||||||
source: :api,
|
return render json: { error: 'Template does not contain fields' }, status: :unprocessable_entity
|
||||||
mark_as_sent: is_send_email,
|
end
|
||||||
submitters_order: params[:submitters_order] || 'preserved',
|
|
||||||
submissions_attrs:
|
params[:send_email] = true unless params.key?(:send_email)
|
||||||
)
|
params[:send_sms] = false unless params.key?(:send_sms)
|
||||||
|
|
||||||
|
submissions = create_submissions(@template, params)
|
||||||
|
|
||||||
|
Submissions.send_signature_requests(submissions)
|
||||||
|
|
||||||
|
submissions.each do |submission|
|
||||||
|
if submission.submitters.all?(&:completed_at?) && submission.submitters.last
|
||||||
|
ProcessSubmitterCompletionJob.perform_later(submission.submitters.last)
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
Submissions.send_signature_requests(submissions, send_email: is_send_email)
|
json = submissions.flat_map do |submission|
|
||||||
|
submission.submitters.map do |s|
|
||||||
|
Submitters::SerializeForApi.call(s, with_documents: false, with_urls: true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
submitters = submissions.flat_map(&:submitters)
|
render json:
|
||||||
|
rescue Submitters::NormalizeValues::BaseError => e
|
||||||
|
Rollbar.warning(e) if defined?(Rollbar)
|
||||||
|
|
||||||
save_default_value_attachments!(attachments, submitters)
|
|
||||||
|
|
||||||
render json: submitters
|
|
||||||
rescue Submitters::NormalizeValues::UnknownFieldName, Submitters::NormalizeValues::UnknownSubmitterName => e
|
|
||||||
render json: { error: e.message }, status: :unprocessable_entity
|
render json: { error: e.message }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@submission.update!(deleted_at: Time.current)
|
@submission.update!(archived_at: Time.current)
|
||||||
|
|
||||||
render json: @submission.as_json(only: %i[id deleted_at])
|
SendSubmissionArchivedWebhookRequestJob.perform_later(@submission)
|
||||||
|
|
||||||
|
render json: @submission.as_json(only: %i[id], methods: %i[archived_at])
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def create_submissions(template, params)
|
||||||
|
is_send_email = !params[:send_email].in?(['false', false])
|
||||||
|
|
||||||
|
if (emails = (params[:emails] || params[:email]).presence) &&
|
||||||
|
(params[:submission].blank? && params[:submitters].blank?)
|
||||||
|
Submissions.create_from_emails(template:,
|
||||||
|
user: current_user,
|
||||||
|
source: :api,
|
||||||
|
mark_as_sent: is_send_email,
|
||||||
|
emails:,
|
||||||
|
params:)
|
||||||
|
else
|
||||||
|
submissions_attrs, attachments =
|
||||||
|
Submissions::NormalizeParamUtils.normalize_submissions_params!(submissions_params, template)
|
||||||
|
|
||||||
|
submissions = Submissions.create_from_submitters(
|
||||||
|
template:,
|
||||||
|
user: current_user,
|
||||||
|
source: :api,
|
||||||
|
mark_as_sent: is_send_email,
|
||||||
|
submitters_order: params[:submitters_order] || params[:order] || 'preserved',
|
||||||
|
submissions_attrs:,
|
||||||
|
params:
|
||||||
|
)
|
||||||
|
|
||||||
|
submitters = submissions.flat_map(&:submitters)
|
||||||
|
|
||||||
|
Submissions::NormalizeParamUtils.save_default_value_attachments!(attachments, submitters)
|
||||||
|
|
||||||
|
submitters.each do |submitter|
|
||||||
|
SubmissionEvents.create_with_tracking_data(submitter, 'api_complete_form', request) if submitter.completed_at?
|
||||||
|
end
|
||||||
|
|
||||||
|
submissions
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def serialize_params
|
def serialize_params
|
||||||
{
|
{
|
||||||
only: %i[id source submitters_order created_at updated_at],
|
only: %i[id source submitters_order created_at updated_at archived_at],
|
||||||
|
methods: %i[audit_log_url],
|
||||||
include: {
|
include: {
|
||||||
submitters: { only: %i[id slug uuid name email phone
|
submitters: { only: %i[id slug uuid name email phone
|
||||||
completed_at opened_at sent_at
|
completed_at opened_at sent_at
|
||||||
created_at updated_at],
|
created_at updated_at external_id metadata],
|
||||||
methods: %i[status] },
|
methods: %i[status application_key] },
|
||||||
template: { only: %i[id name created_at updated_at] },
|
template: { only: %i[id name external_id created_at updated_at],
|
||||||
|
methods: %i[folder_name] },
|
||||||
created_by_user: { only: %i[id email first_name last_name] }
|
created_by_user: { only: %i[id email first_name last_name] }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def submissions_params
|
def submissions_params
|
||||||
params.permit(submission: [{
|
permitted_attrs = [
|
||||||
submitters: [[:uuid, :name, :email, :role, :completed, :phone, { values: {} }]]
|
:send_email, :send_sms, :bcc_completed, :completed_redirect_url, :reply_to, :go_to_last,
|
||||||
}])
|
{
|
||||||
end
|
message: %i[subject body],
|
||||||
|
submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role,
|
||||||
|
:completed, :phone, :application_key, :external_id, :reply_to, :go_to_last,
|
||||||
|
{ metadata: {}, values: {}, readonly_fields: [], message: %i[subject body],
|
||||||
|
fields: [:name, :uuid, :default_value, :value, :title, :description,
|
||||||
|
:readonly, :redacted, :validation_pattern, :invalid_message,
|
||||||
|
{ default_value: [], value: [] }] }]]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
def normalize_submissions_params!(submissions_params, template)
|
if params.key?(:submitters)
|
||||||
attachments = []
|
params.permit(*permitted_attrs)
|
||||||
|
else
|
||||||
|
key = params.key?(:submission) ? :submission : :submissions
|
||||||
|
|
||||||
Array.wrap(submissions_params).each do |submission|
|
params.permit(
|
||||||
submission[:submitters].each_with_index do |submitter, index|
|
key => [permitted_attrs]
|
||||||
next if submitter[:values].blank?
|
).fetch(key, [])
|
||||||
|
|
||||||
values, new_attachments =
|
|
||||||
Submitters::NormalizeValues.call(template,
|
|
||||||
submitter[:values],
|
|
||||||
submitter[:role] || template.submitters[index]['name'])
|
|
||||||
|
|
||||||
attachments.push(*new_attachments)
|
|
||||||
|
|
||||||
submitter[:values] = values
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
[submissions_params, attachments]
|
|
||||||
end
|
|
||||||
|
|
||||||
def save_default_value_attachments!(attachments, submitters)
|
|
||||||
return if attachments.blank?
|
|
||||||
|
|
||||||
attachments_index = attachments.index_by(&:uuid)
|
|
||||||
|
|
||||||
submitters.each do |submitter|
|
|
||||||
submitter.values.to_a.each do |_, value|
|
|
||||||
attachment = attachments_index[value]
|
|
||||||
|
|
||||||
next unless attachment
|
|
||||||
|
|
||||||
attachment.record = submitter
|
|
||||||
|
|
||||||
attachment.save!
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ module Api
|
|||||||
def create
|
def create
|
||||||
submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
||||||
|
|
||||||
|
submitter.opened_at = Time.current
|
||||||
|
submitter.save
|
||||||
|
|
||||||
SubmissionEvents.create_with_tracking_data(submitter, 'view_form', request)
|
SubmissionEvents.create_with_tracking_data(submitter, 'view_form', request)
|
||||||
|
|
||||||
SendFormViewedWebhookRequestJob.perform_later(submitter)
|
SendFormViewedWebhookRequestJob.perform_later(submitter)
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module Api
|
|
||||||
class SubmittersAutocompleteController < ApiBaseController
|
|
||||||
load_and_authorize_resource :submitter, parent: false
|
|
||||||
|
|
||||||
SELECT_COLUMNS = %w[email phone name].freeze
|
|
||||||
LIMIT = 100
|
|
||||||
|
|
||||||
def index
|
|
||||||
submitters = search_submitters(@submitters)
|
|
||||||
|
|
||||||
values = submitters.limit(LIMIT).group(SELECT_COLUMNS.join(', ')).pluck(SELECT_COLUMNS.join(', '))
|
|
||||||
|
|
||||||
attrs = values.map { |row| SELECT_COLUMNS.zip(row).to_h }
|
|
||||||
attrs = attrs.uniq { |e| e[params[:field]] } if params[:field].present?
|
|
||||||
|
|
||||||
render json: attrs
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def search_submitters(submitters)
|
|
||||||
if SELECT_COLUMNS.include?(params[:field])
|
|
||||||
column = Submitter.arel_table[params[:field].to_sym]
|
|
||||||
|
|
||||||
term = "%#{params[:q].downcase}%"
|
|
||||||
|
|
||||||
submitters.where(column.lower.matches(term))
|
|
||||||
else
|
|
||||||
Submitters.search(submitters, params[:q])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -4,10 +4,170 @@ module Api
|
|||||||
class SubmittersController < ApiBaseController
|
class SubmittersController < ApiBaseController
|
||||||
load_and_authorize_resource :submitter
|
load_and_authorize_resource :submitter
|
||||||
|
|
||||||
|
def index
|
||||||
|
submitters = Submitters.search(@submitters, params[:q])
|
||||||
|
|
||||||
|
submitters = submitters.where(external_id: params[:application_key]) if params[:application_key].present?
|
||||||
|
submitters = submitters.where(external_id: params[:external_id]) if params[:external_id].present?
|
||||||
|
submitters = submitters.where(submission_id: params[:submission_id]) if params[:submission_id].present?
|
||||||
|
|
||||||
|
if params[:template_id].present?
|
||||||
|
submitters = submitters.joins(:submission).where(submission: { template_id: params[:template_id] })
|
||||||
|
end
|
||||||
|
|
||||||
|
submitters = maybe_filder_by_completed_at(submitters, params)
|
||||||
|
|
||||||
|
submitters = paginate(
|
||||||
|
submitters.preload(:template, :submission, :submission_events,
|
||||||
|
documents_attachments: :blob, attachments_attachments: :blob)
|
||||||
|
)
|
||||||
|
|
||||||
|
render json: {
|
||||||
|
data: submitters.map { |s| Submitters::SerializeForApi.call(s, with_template: true, with_events: true) },
|
||||||
|
pagination: {
|
||||||
|
count: submitters.size,
|
||||||
|
next: submitters.last&.id,
|
||||||
|
prev: submitters.first&.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
Submissions::EnsureResultGenerated.call(@submitter) if @submitter.completed_at?
|
Submissions::EnsureResultGenerated.call(@submitter) if @submitter.completed_at?
|
||||||
|
|
||||||
render json: Submitters::SerializeForApi.call(@submitter, with_template: true, with_events: true)
|
render json: Submitters::SerializeForApi.call(@submitter, with_template: true, with_events: true)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
if @submitter.completed_at?
|
||||||
|
return render json: { error: 'Submitter has already completed the submission.' }, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
|
||||||
|
role = @submitter.submission.template_submitters.find { |e| e['uuid'] == @submitter.uuid }['name']
|
||||||
|
|
||||||
|
normalized_params, new_attachments =
|
||||||
|
Submissions::NormalizeParamUtils.normalize_submitter_params!(submitter_params.merge(role:), @submitter.template,
|
||||||
|
for_submitter: @submitter)
|
||||||
|
|
||||||
|
Submissions::CreateFromSubmitters.maybe_set_template_fields(@submitter.submission,
|
||||||
|
[normalized_params],
|
||||||
|
default_submitter_uuid: @submitter.uuid)
|
||||||
|
|
||||||
|
assign_submitter_attrs(@submitter, normalized_params)
|
||||||
|
|
||||||
|
ApplicationRecord.transaction do
|
||||||
|
Submissions::NormalizeParamUtils.save_default_value_attachments!(new_attachments, [@submitter])
|
||||||
|
|
||||||
|
@submitter.save!
|
||||||
|
|
||||||
|
@submitter.submission.save!
|
||||||
|
|
||||||
|
SubmissionEvents.create_with_tracking_data(@submitter, 'api_complete_form', request) if @submitter.completed_at?
|
||||||
|
end
|
||||||
|
|
||||||
|
if @submitter.completed_at?
|
||||||
|
ProcessSubmitterCompletionJob.perform_later(@submitter)
|
||||||
|
elsif normalized_params[:send_email] || normalized_params[:send_sms]
|
||||||
|
Submitters.send_signature_requests([@submitter])
|
||||||
|
end
|
||||||
|
|
||||||
|
render json: Submitters::SerializeForApi.call(@submitter, with_template: false,
|
||||||
|
with_urls: true,
|
||||||
|
with_events: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
def submitter_params
|
||||||
|
submitter_params = params.key?(:submitter) ? params.require(:submitter) : params
|
||||||
|
|
||||||
|
submitter_params.permit(
|
||||||
|
:send_email, :send_sms, :reply_to, :completed_redirect_url, :uuid, :name, :email, :role,
|
||||||
|
:completed, :phone, :application_key, :external_id, :go_to_last,
|
||||||
|
{ metadata: {}, values: {}, readonly_fields: [], message: %i[subject body],
|
||||||
|
fields: [[:name, :uuid, :default_value, :value,
|
||||||
|
:readonly, :redacted, :validation_pattern, :invalid_message,
|
||||||
|
{ default_value: [], value: [] }]] }
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def maybe_filder_by_completed_at(submitters, params)
|
||||||
|
if params[:completed_after].present?
|
||||||
|
submitters = submitters.where(completed_at: Time.zone.parse(params[:completed_after])..)
|
||||||
|
end
|
||||||
|
|
||||||
|
if params[:completed_before].present?
|
||||||
|
submitters = submitters.where(completed_at: ..Time.zone.parse(params[:completed_before]))
|
||||||
|
end
|
||||||
|
|
||||||
|
submitters
|
||||||
|
end
|
||||||
|
|
||||||
|
def assign_submitter_attrs(submitter, attrs)
|
||||||
|
values = attrs[:values]&.to_unsafe_h || {}
|
||||||
|
|
||||||
|
assign_submission_fields(submitter.submission)
|
||||||
|
|
||||||
|
phone_field_uuid = submitter.submission.template_fields.find do |f|
|
||||||
|
values[f['uuid']].present? && f['type'] == 'phone'
|
||||||
|
end&.dig('uuid')
|
||||||
|
|
||||||
|
submitter.email = Submissions.normalize_email(attrs[:email]) if attrs.key?(:email)
|
||||||
|
|
||||||
|
if attrs.key?(:phone)
|
||||||
|
submitter.phone = attrs[:phone].to_s.gsub(/[^0-9+]/, '')
|
||||||
|
elsif values[phone_field_uuid].present?
|
||||||
|
submitter.phone = values[phone_field_uuid].to_s.gsub(/[^0-9+]/, '')
|
||||||
|
end
|
||||||
|
|
||||||
|
values = values.except(phone_field_uuid)
|
||||||
|
|
||||||
|
submitter.values = submitter.values.merge(values) if values.present?
|
||||||
|
submitter.completed_at = attrs[:completed] ? Time.current : submitter.completed_at
|
||||||
|
submitter.metadata = attrs[:metadata] if attrs.key?(:metadata)
|
||||||
|
|
||||||
|
assign_external_id(submitter, attrs)
|
||||||
|
assign_preferences(submitter, attrs)
|
||||||
|
|
||||||
|
submitter
|
||||||
|
end
|
||||||
|
|
||||||
|
def assign_external_id(submitter, attrs)
|
||||||
|
submitter.external_id = attrs[:application_key] if attrs.key?(:application_key)
|
||||||
|
submitter.external_id = attrs[:external_id] if attrs.key?(:external_id)
|
||||||
|
|
||||||
|
submitter
|
||||||
|
end
|
||||||
|
|
||||||
|
def assign_submission_fields(submission)
|
||||||
|
submission.template_fields ||= submission.template.fields
|
||||||
|
submission.template_schema ||= submission.template.schema
|
||||||
|
end
|
||||||
|
|
||||||
|
def assign_preferences(submitter, attrs)
|
||||||
|
submitter_preferences = Submitters.normalize_preferences(submitter.account, current_user, attrs)
|
||||||
|
|
||||||
|
submitter.preferences['default_values'] = attrs[:values].to_unsafe_h if attrs[:values].present?
|
||||||
|
|
||||||
|
if submitter_preferences.key?('send_email')
|
||||||
|
submitter.preferences['send_email'] = submitter_preferences['send_email']
|
||||||
|
end
|
||||||
|
|
||||||
|
submitter.preferences['send_sms'] = submitter_preferences['send_sms'] if submitter_preferences.key?('send_sms')
|
||||||
|
submitter.preferences['reply_to'] = submitter_preferences['reply_to'] if submitter_preferences.key?('reply_to')
|
||||||
|
if submitter_preferences.key?('go_to_last')
|
||||||
|
submitter.preferences['go_to_last'] = submitter_preferences['go_to_last']
|
||||||
|
end
|
||||||
|
|
||||||
|
if submitter_preferences.key?('completed_redirect_url')
|
||||||
|
submitter.preferences['completed_redirect_url'] = submitter_preferences['completed_redirect_url']
|
||||||
|
end
|
||||||
|
|
||||||
|
return unless submitter_preferences.key?('email_message_uuid')
|
||||||
|
|
||||||
|
submitter.preferences['email_message_uuid'] = submitter_preferences['email_message_uuid']
|
||||||
|
|
||||||
|
submitter
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module Api
|
|
||||||
class TemplateFoldersAutocompleteController < ApiBaseController
|
|
||||||
load_and_authorize_resource :template_folder, parent: false
|
|
||||||
|
|
||||||
LIMIT = 100
|
|
||||||
|
|
||||||
def index
|
|
||||||
template_folders = @template_folders.joins(:templates).where(templates: { deleted_at: nil }).distinct
|
|
||||||
template_folders = TemplateFolders.search(template_folders, params[:q]).limit(LIMIT)
|
|
||||||
|
|
||||||
render json: template_folders.as_json(only: %i[name deleted_at])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Api
|
||||||
|
class TemplatesCloneController < ApiBaseController
|
||||||
|
load_and_authorize_resource :template
|
||||||
|
|
||||||
|
def create
|
||||||
|
authorize!(:manage, @template)
|
||||||
|
|
||||||
|
cloned_template = Templates::Clone.call(
|
||||||
|
@template,
|
||||||
|
author: current_user,
|
||||||
|
name: params[:name],
|
||||||
|
external_id: params[:external_id].presence || params[:application_key],
|
||||||
|
folder_name: params[:folder_name]
|
||||||
|
)
|
||||||
|
|
||||||
|
cloned_template.source = :api
|
||||||
|
cloned_template.save!
|
||||||
|
|
||||||
|
Templates::CloneAttachments.call(template: cloned_template, original_template: @template)
|
||||||
|
|
||||||
|
SendTemplateCreatedWebhookRequestJob.perform_later(cloned_template)
|
||||||
|
|
||||||
|
render json: Templates::SerializeForApi.call(cloned_template)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -5,14 +5,33 @@ module Api
|
|||||||
load_and_authorize_resource :template
|
load_and_authorize_resource :template
|
||||||
|
|
||||||
def index
|
def index
|
||||||
templates = Templates.search(@templates, params[:q])
|
templates = filter_templates(@templates, params)
|
||||||
|
|
||||||
templates = params[:archived] ? templates.archived : templates.active
|
templates = paginate(templates.preload(:author, :folder))
|
||||||
|
|
||||||
templates = paginate(templates.preload(:author, documents_attachments: :blob))
|
schema_documents =
|
||||||
|
ActiveStorage::Attachment.where(record_id: templates.map(&:id),
|
||||||
|
record_type: 'Template',
|
||||||
|
name: :documents,
|
||||||
|
uuid: templates.flat_map { |t| t.schema.pluck('attachment_uuid') })
|
||||||
|
.preload(:blob)
|
||||||
|
|
||||||
|
preview_image_attachments =
|
||||||
|
ActiveStorage::Attachment.joins(:blob)
|
||||||
|
.where(blob: { filename: '0.jpg' })
|
||||||
|
.where(record_id: schema_documents.map(&:id),
|
||||||
|
record_type: 'ActiveStorage::Attachment',
|
||||||
|
name: :preview_images)
|
||||||
|
.preload(:blob)
|
||||||
|
|
||||||
render json: {
|
render json: {
|
||||||
data: templates.as_json(serialize_params),
|
data: templates.map do |t|
|
||||||
|
Templates::SerializeForApi.call(
|
||||||
|
t,
|
||||||
|
schema_documents.select { |e| e.record_id == t.id },
|
||||||
|
preview_image_attachments
|
||||||
|
)
|
||||||
|
end,
|
||||||
pagination: {
|
pagination: {
|
||||||
count: templates.size,
|
count: templates.size,
|
||||||
next: templates.last&.id,
|
next: templates.last&.id,
|
||||||
@@ -22,40 +41,78 @@ module Api
|
|||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
render json: @template.as_json(serialize_params)
|
render json: Templates::SerializeForApi.call(@template)
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
if (folder_name = params.dig(:template, :folder_name))
|
if (folder_name = params[:folder_name] || params.dig(:template, :folder_name))
|
||||||
@template.folder = TemplateFolders.find_or_create_by_name(current_user, folder_name)
|
@template.folder = TemplateFolders.find_or_create_by_name(current_user, folder_name)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Array.wrap(params[:roles].presence || params.dig(:template, :roles).presence).each_with_index do |role, index|
|
||||||
|
if (item = @template.submitters[index])
|
||||||
|
item['name'] = role
|
||||||
|
else
|
||||||
|
@template.submitters << { 'name' => role, 'uuid' => SecureRandom.uuid }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
archived = params.key?(:archived) ? params[:archived] : params.dig(:template, :archived)
|
||||||
|
|
||||||
|
if archived.in?([true, false])
|
||||||
|
@template.archived_at = archived == true ? Time.current : nil
|
||||||
|
end
|
||||||
|
|
||||||
@template.update!(template_params)
|
@template.update!(template_params)
|
||||||
|
|
||||||
|
SendTemplateUpdatedWebhookRequestJob.perform_later(@template)
|
||||||
|
|
||||||
render json: @template.as_json(only: %i[id updated_at])
|
render json: @template.as_json(only: %i[id updated_at])
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@template.update!(deleted_at: Time.current)
|
if params[:permanently] == 'true' && !Docuseal.multitenant?
|
||||||
|
@template.destroy!
|
||||||
|
else
|
||||||
|
@template.update!(archived_at: Time.current)
|
||||||
|
end
|
||||||
|
|
||||||
render json: @template.as_json(only: %i[id deleted_at])
|
render json: @template.as_json(only: %i[id archived_at])
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def serialize_params
|
def filter_templates(templates, params)
|
||||||
{
|
templates = Templates.search(templates, params[:q])
|
||||||
include: { author: { only: %i[id email first_name last_name] },
|
templates = params[:archived] ? templates.archived : templates.active
|
||||||
documents: { only: %i[id uuid], methods: %i[url filename] } }
|
templates = templates.where(external_id: params[:application_key]) if params[:application_key].present?
|
||||||
}
|
templates = templates.where(external_id: params[:external_id]) if params[:external_id].present?
|
||||||
|
templates = templates.joins(:folder).where(folder: { name: params[:folder] }) if params[:folder].present?
|
||||||
|
|
||||||
|
templates
|
||||||
end
|
end
|
||||||
|
|
||||||
def template_params
|
def template_params
|
||||||
params.require(:template).permit(:name,
|
permitted_params = [
|
||||||
schema: [%i[attachment_uuid name]],
|
:name,
|
||||||
submitters: [%i[name uuid]],
|
{
|
||||||
fields: [[:uuid, :submitter_uuid, :name, :type, :required,
|
submitters: [%i[name uuid]],
|
||||||
{ options: [], areas: [%i[x y w h cell_w attachment_uuid page]] }]])
|
fields: [[:uuid, :submitter_uuid, :name, :type,
|
||||||
|
:required, :readonly, :default_value,
|
||||||
|
:title, :description,
|
||||||
|
{ preferences: {},
|
||||||
|
conditions: [%i[field_uuid value action]],
|
||||||
|
options: [%i[value uuid]],
|
||||||
|
validation: %i[message pattern],
|
||||||
|
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
if params.key?(:template)
|
||||||
|
params.require(:template).permit(permitted_params)
|
||||||
|
else
|
||||||
|
params.permit(permitted_params)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module Api
|
|
||||||
class TemplatesDocumentsController < ApiBaseController
|
|
||||||
load_and_authorize_resource :template
|
|
||||||
|
|
||||||
def create
|
|
||||||
return head :unprocessable_entity if params[:blobs].blank? && params[:files].blank?
|
|
||||||
|
|
||||||
documents = Templates::CreateAttachments.call(@template, params)
|
|
||||||
|
|
||||||
schema = documents.map do |doc|
|
|
||||||
{ attachment_uuid: doc.uuid, name: doc.filename.base }
|
|
||||||
end
|
|
||||||
|
|
||||||
render json: {
|
|
||||||
schema:,
|
|
||||||
documents: documents.as_json(
|
|
||||||
include: {
|
|
||||||
preview_images: { methods: %i[url metadata filename] }
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -4,4 +4,14 @@ class ApiSettingsController < ApplicationController
|
|||||||
def index
|
def index
|
||||||
authorize!(:read, current_user.access_token)
|
authorize!(:read, current_user.access_token)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
authorize!(:manage, current_user.access_token)
|
||||||
|
|
||||||
|
current_user.access_token.token = SecureRandom.base58(AccessToken::TOKEN_LENGTH)
|
||||||
|
|
||||||
|
current_user.access_token.save!
|
||||||
|
|
||||||
|
redirect_back(fallback_location: settings_api_index_path, notice: 'API token as been updated.')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class ApplicationController < ActionController::Base
|
class ApplicationController < ActionController::Base
|
||||||
|
BROWSER_LOCALE_REGEXP = /\A\w{2}(?:-\w{2})?/
|
||||||
|
|
||||||
include ActiveStorage::SetCurrent
|
include ActiveStorage::SetCurrent
|
||||||
include Pagy::Backend
|
include Pagy::Backend
|
||||||
|
|
||||||
@@ -14,15 +16,23 @@ class ApplicationController < ActionController::Base
|
|||||||
:current_account,
|
:current_account,
|
||||||
:svg_icon
|
:svg_icon
|
||||||
|
|
||||||
|
impersonates :user, with: ->(uuid) { User.find_by(uuid:) }
|
||||||
|
|
||||||
rescue_from Pagy::OverflowError do
|
rescue_from Pagy::OverflowError do
|
||||||
redirect_to request.path
|
redirect_to request.path
|
||||||
end
|
end
|
||||||
|
|
||||||
|
rescue_from RateLimit::LimitApproached do |e|
|
||||||
|
Rollbar.error(e) if defined?(Rollbar)
|
||||||
|
|
||||||
|
redirect_to request.referer, alert: 'Too many requests', status: :too_many_requests
|
||||||
|
end
|
||||||
|
|
||||||
if Rails.env.production?
|
if Rails.env.production?
|
||||||
rescue_from CanCan::AccessDenied do |e|
|
rescue_from CanCan::AccessDenied do |e|
|
||||||
Rollbar.error(e) if defined?(Rollbar)
|
Rollbar.warning(e) if defined?(Rollbar)
|
||||||
|
|
||||||
redirect_back(fallback_location: root_path, alert: e.message)
|
redirect_to root_path, alert: e.message
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -30,10 +40,34 @@ class ApplicationController < ActionController::Base
|
|||||||
Docuseal.default_url_options
|
Docuseal.default_url_options
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def impersonate_user(user)
|
||||||
|
raise ArgumentError unless user
|
||||||
|
raise Pretender::Error unless true_user
|
||||||
|
|
||||||
|
@impersonated_user = user
|
||||||
|
|
||||||
|
request.session[:impersonated_user_id] = user.uuid
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def with_browser_locale(&)
|
||||||
|
locale = request.env['HTTP_ACCEPT_LANGUAGE'].to_s[BROWSER_LOCALE_REGEXP].to_s
|
||||||
|
|
||||||
|
locale =
|
||||||
|
if locale.starts_with?('en-') && locale != 'en-US'
|
||||||
|
'en-GB'
|
||||||
|
else
|
||||||
|
locale.split('-').first.presence || 'en-GB'
|
||||||
|
end
|
||||||
|
|
||||||
|
locale = 'en-GB' unless I18n.locale_available?(locale)
|
||||||
|
|
||||||
|
I18n.with_locale(locale, &)
|
||||||
|
end
|
||||||
|
|
||||||
def sign_in_for_demo
|
def sign_in_for_demo
|
||||||
sign_in(User.order('random()').take) unless signed_in?
|
sign_in(User.active.order('random()').take) unless signed_in?
|
||||||
end
|
end
|
||||||
|
|
||||||
def current_account
|
def current_account
|
||||||
@@ -44,8 +78,9 @@ class ApplicationController < ActionController::Base
|
|||||||
redirect_to setup_index_path unless User.exists?
|
redirect_to setup_index_path unless User.exists?
|
||||||
end
|
end
|
||||||
|
|
||||||
def button_title(title: 'Submit', disabled_with: 'Submitting', icon: nil, icon_disabled: nil)
|
def button_title(title: 'Submit', disabled_with: 'Submitting', title_class: '', icon: nil, icon_disabled: nil)
|
||||||
render_to_string(partial: 'shared/button_title', locals: { title:, disabled_with:, icon:, icon_disabled: })
|
render_to_string(partial: 'shared/button_title',
|
||||||
|
locals: { title:, disabled_with:, title_class:, icon:, icon_disabled: })
|
||||||
end
|
end
|
||||||
|
|
||||||
def svg_icon(icon_name, class: '')
|
def svg_icon(icon_name, class: '')
|
||||||
|
|||||||
@@ -5,9 +5,13 @@ class ConsoleRedirectController < ApplicationController
|
|||||||
skip_authorization_check
|
skip_authorization_check
|
||||||
|
|
||||||
def index
|
def index
|
||||||
return redirect_to(new_user_session_path({ redir: params[:redir] }.compact)) if current_user.blank?
|
if request.path == '/upgrade'
|
||||||
|
params[:redir] = Docuseal.multitenant? ? "#{Docuseal::CONSOLE_URL}/plans" : "#{Docuseal::CONSOLE_URL}/on_premise"
|
||||||
|
end
|
||||||
|
|
||||||
auth = JsonWebToken.encode(uuid: current_user.uuid,
|
return redirect_to(new_user_session_path({ redir: params[:redir] }.compact)) if true_user.blank?
|
||||||
|
|
||||||
|
auth = JsonWebToken.encode(uuid: true_user.uuid,
|
||||||
scope: :console,
|
scope: :console,
|
||||||
exp: 1.minute.from_now.to_i)
|
exp: 1.minute.from_now.to_i)
|
||||||
|
|
||||||
|
|||||||
@@ -5,63 +5,37 @@ class DashboardController < ApplicationController
|
|||||||
|
|
||||||
before_action :maybe_redirect_product_url
|
before_action :maybe_redirect_product_url
|
||||||
before_action :maybe_render_landing
|
before_action :maybe_render_landing
|
||||||
|
before_action :maybe_redirect_mfa_setup
|
||||||
|
|
||||||
load_and_authorize_resource :template_folder, parent: false
|
skip_authorization_check
|
||||||
load_and_authorize_resource :template, parent: false
|
|
||||||
|
|
||||||
SHOW_TEMPLATES_FOLDERS_THRESHOLD = 9
|
|
||||||
TEMPLATES_PER_PAGE = 12
|
|
||||||
FOLDERS_PER_PAGE = 18
|
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@template_folders = filter_template_folders(@template_folders)
|
if cookies.permanent[:dashboard_view] == 'submissions'
|
||||||
|
SubmissionsDashboardController.dispatch(:index, request, response)
|
||||||
@pagy, @template_folders = pagy(
|
|
||||||
@template_folders,
|
|
||||||
items: FOLDERS_PER_PAGE,
|
|
||||||
page: @template_folders.count > SHOW_TEMPLATES_FOLDERS_THRESHOLD ? params[:page] : 1
|
|
||||||
)
|
|
||||||
|
|
||||||
if @pagy.count > SHOW_TEMPLATES_FOLDERS_THRESHOLD
|
|
||||||
@templates = @templates.none
|
|
||||||
else
|
else
|
||||||
@template_folders = @template_folders.reject { |e| e.name == TemplateFolder::DEFAULT_NAME }
|
TemplatesDashboardController.dispatch(:index, request, response)
|
||||||
@templates = filter_templates(@templates)
|
|
||||||
|
|
||||||
items =
|
|
||||||
if @template_folders.size < 4
|
|
||||||
TEMPLATES_PER_PAGE
|
|
||||||
else
|
|
||||||
(@template_folders.size < 7 ? 9 : 6)
|
|
||||||
end
|
|
||||||
|
|
||||||
@pagy, @templates = pagy(@templates, items:)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def filter_template_folders(template_folders)
|
|
||||||
rel = template_folders.joins(:active_templates)
|
|
||||||
.order(id: :desc)
|
|
||||||
.distinct
|
|
||||||
|
|
||||||
TemplateFolders.search(rel, params[:q])
|
|
||||||
end
|
|
||||||
|
|
||||||
def filter_templates(templates)
|
|
||||||
rel = templates.active.preload(:author).order(id: :desc)
|
|
||||||
rel = rel.where(folder_id: current_account.default_template_folder.id) if params[:q].blank?
|
|
||||||
|
|
||||||
Templates.search(rel, params[:q])
|
|
||||||
end
|
|
||||||
|
|
||||||
def maybe_redirect_product_url
|
def maybe_redirect_product_url
|
||||||
return if !Docuseal.multitenant? || signed_in?
|
return if !Docuseal.multitenant? || signed_in?
|
||||||
|
|
||||||
redirect_to Docuseal::PRODUCT_URL, allow_other_host: true
|
redirect_to Docuseal::PRODUCT_URL, allow_other_host: true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def maybe_redirect_mfa_setup
|
||||||
|
return unless signed_in?
|
||||||
|
return if current_user.otp_required_for_login
|
||||||
|
|
||||||
|
return if !current_user.otp_required_for_login && !AccountConfig.exists?(value: true,
|
||||||
|
account_id: current_user.account_id,
|
||||||
|
key: AccountConfig::FORCE_MFA)
|
||||||
|
|
||||||
|
redirect_to mfa_setup_path, notice: 'Setup 2FA to continue'
|
||||||
|
end
|
||||||
|
|
||||||
def maybe_render_landing
|
def maybe_render_landing
|
||||||
return if signed_in?
|
return if signed_in?
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class EncryptedUserConfigsController < ApplicationController
|
||||||
|
load_and_authorize_resource :encrypted_user_config
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
@encrypted_user_config.destroy!
|
||||||
|
|
||||||
|
redirect_back(fallback_location: root_path)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class EnquiriesController < ApplicationController
|
||||||
|
skip_before_action :authenticate_user!
|
||||||
|
skip_authorization_check
|
||||||
|
|
||||||
|
def create
|
||||||
|
if params[:talk_to_sales] == 'on'
|
||||||
|
Faraday.post(Docuseal::ENQUIRIES_URL,
|
||||||
|
enquiry_params.merge(type: :talk_to_sales).to_json,
|
||||||
|
'Content-Type' => 'application/json')
|
||||||
|
end
|
||||||
|
|
||||||
|
head :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def enquiry_params
|
||||||
|
params.require(:user).permit(:email)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class ErrorsController < ActionController::Base
|
||||||
|
ENTERPRISE_FEATURE_MESSAGE =
|
||||||
|
'This feature is available in Pro Edition: https://www.docuseal.co/pricing'
|
||||||
|
|
||||||
|
ENTERPRISE_PATHS = [
|
||||||
|
'/templates/html',
|
||||||
|
'/api/templates/html',
|
||||||
|
'/templates/pdf',
|
||||||
|
'/api/templates/pdf',
|
||||||
|
'/templates/doc',
|
||||||
|
'/api/templates/doc',
|
||||||
|
'/templates/docx',
|
||||||
|
'/api/templates/docx'
|
||||||
|
].freeze
|
||||||
|
|
||||||
|
SAFE_ERROR_MESSAGE_CLASSES = [
|
||||||
|
ActionDispatch::Http::Parameters::ParseError,
|
||||||
|
JSON::ParserError
|
||||||
|
].freeze
|
||||||
|
|
||||||
|
def show
|
||||||
|
if request.original_fullpath.in?(ENTERPRISE_PATHS) && error_status_code == 404
|
||||||
|
return render json: { status: 404, message: ENTERPRISE_FEATURE_MESSAGE }, status: :not_found
|
||||||
|
end
|
||||||
|
|
||||||
|
respond_to do |f|
|
||||||
|
f.json do
|
||||||
|
set_cors_headers
|
||||||
|
|
||||||
|
exception = request.env['action_dispatch.exception']
|
||||||
|
|
||||||
|
error = exception.message if exception.class.in?(SAFE_ERROR_MESSAGE_CLASSES)
|
||||||
|
|
||||||
|
render json: { status: error_status_code, error: }.compact, status: error_status_code
|
||||||
|
end
|
||||||
|
|
||||||
|
f.html { render error_status_code.to_s, status: error_status_code }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_cors_headers
|
||||||
|
headers['Access-Control-Allow-Origin'] = '*'
|
||||||
|
headers['Access-Control-Allow-Methods'] = 'POST, GET, PUT, PATCH, DELETE, OPTIONS'
|
||||||
|
headers['Access-Control-Allow-Headers'] = '*'
|
||||||
|
headers['Access-Control-Max-Age'] = '1728000'
|
||||||
|
headers['Access-Control-Allow-Credentials'] = true
|
||||||
|
end
|
||||||
|
|
||||||
|
def error_status_code
|
||||||
|
@error_status_code ||=
|
||||||
|
ActionDispatch::ExceptionWrapper.new(request.env,
|
||||||
|
request.env['action_dispatch.exception']).status_code
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -57,8 +57,8 @@ class EsignSettingsController < ApplicationController
|
|||||||
save_new_cert!(@encrypted_config, @cert_record)
|
save_new_cert!(@encrypted_config, @cert_record)
|
||||||
|
|
||||||
redirect_to settings_esign_path, notice: 'Certificate has been successfully added!'
|
redirect_to settings_esign_path, notice: 'Certificate has been successfully added!'
|
||||||
rescue OpenSSL::PKCS12::PKCS12Error
|
rescue OpenSSL::PKCS12::PKCS12Error => e
|
||||||
@cert_record.errors.add(:password, "is invalid. Make sure you're uploading a valid .p12 file")
|
@cert_record.errors.add(:password, e.message)
|
||||||
|
|
||||||
render turbo_stream: turbo_stream.replace(:modal, template: 'esign_settings/new'), status: :unprocessable_entity
|
render turbo_stream: turbo_stream.replace(:modal, template: 'esign_settings/new'), status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -5,13 +5,11 @@ class MfaSetupController < ApplicationController
|
|||||||
authorize!(:update, current_user)
|
authorize!(:update, current_user)
|
||||||
end
|
end
|
||||||
|
|
||||||
def new
|
before_action :set_provision_url, only: %i[show new]
|
||||||
current_user.otp_secret ||= User.generate_otp_secret
|
|
||||||
|
|
||||||
current_user.save!
|
def show; end
|
||||||
|
|
||||||
@provision_url = current_user.otp_provisioning_uri(current_user.email, issuer: Docuseal::PRODUCT_NAME)
|
def new; end
|
||||||
end
|
|
||||||
|
|
||||||
def edit; end
|
def edit; end
|
||||||
|
|
||||||
@@ -22,11 +20,11 @@ class MfaSetupController < ApplicationController
|
|||||||
|
|
||||||
redirect_to settings_profile_index_path, notice: '2FA has been configured'
|
redirect_to settings_profile_index_path, notice: '2FA has been configured'
|
||||||
else
|
else
|
||||||
@provision_url = current_user.otp_provisioning_uri(current_user.email, issuer: Docuseal::PRODUCT_NAME)
|
@provision_url = current_user.otp_provisioning_uri(current_user.email, issuer: Docuseal.product_name)
|
||||||
|
|
||||||
@error_message = 'Code is invalid'
|
@error_message = 'Code is invalid'
|
||||||
|
|
||||||
render turbo_stream: turbo_stream.replace(:modal, template: 'mfa_setup/new'), status: :unprocessable_entity
|
render turbo_stream: turbo_stream.replace(:mfa_form, partial: 'mfa_setup/form'), status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -41,4 +39,16 @@ class MfaSetupController < ApplicationController
|
|||||||
render turbo_stream: turbo_stream.replace(:modal, template: 'mfa_setup/edit'), status: :unprocessable_entity
|
render turbo_stream: turbo_stream.replace(:modal, template: 'mfa_setup/edit'), status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_provision_url
|
||||||
|
return redirect_to root_path, alert: '2FA has been set up already' if current_user.otp_required_for_login
|
||||||
|
|
||||||
|
current_user.otp_secret ||= User.generate_otp_secret
|
||||||
|
|
||||||
|
current_user.save!
|
||||||
|
|
||||||
|
@provision_url = current_user.otp_provisioning_uri(current_user.email, issuer: Docuseal.product_name)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -12,7 +12,7 @@ class NotificationsSettingsController < ApplicationController
|
|||||||
def index; end
|
def index; end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
if @account_config.save
|
if @account_config.value.present? ? @account_config.save : @account_config.delete
|
||||||
redirect_back fallback_location: settings_notifications_path, notice: 'Changes have been saved'
|
redirect_back fallback_location: settings_notifications_path, notice: 'Changes have been saved'
|
||||||
else
|
else
|
||||||
redirect_back fallback_location: settings_notifications_path, alert: 'Unable to save'
|
redirect_back fallback_location: settings_notifications_path, alert: 'Unable to save'
|
||||||
@@ -35,12 +35,12 @@ class NotificationsSettingsController < ApplicationController
|
|||||||
|
|
||||||
def load_reminder_config
|
def load_reminder_config
|
||||||
@reminder_config =
|
@reminder_config =
|
||||||
AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::SUBMITTER_REMAILERS)
|
AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::SUBMITTER_REMINDERS)
|
||||||
end
|
end
|
||||||
|
|
||||||
def email_config_params
|
def email_config_params
|
||||||
params.require(:account_config).permit!.tap do |attrs|
|
params.require(:account_config).permit!.tap do |attrs|
|
||||||
attrs[:key] = nil unless attrs[:key].in?([AccountConfig::BCC_EMAILS, AccountConfig::SUBMITTER_REMAILERS])
|
attrs[:key] = nil unless attrs[:key].in?([AccountConfig::BCC_EMAILS, AccountConfig::SUBMITTER_REMINDERS])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
|
||||||
def google_oauth2
|
|
||||||
@user = Users.from_omniauth(request.env['omniauth.auth'])
|
|
||||||
|
|
||||||
if @user.persisted?
|
|
||||||
flash[:notice] = I18n.t('devise.omniauth_callbacks.success', kind: 'Google')
|
|
||||||
|
|
||||||
sign_in_and_redirect @user, event: :authentication
|
|
||||||
else
|
|
||||||
redirect_to new_registration_path(oauth_callback: true, user: @user.slice(:email, :first_name, :last_name)),
|
|
||||||
notice: 'Please complete registration with Google auth'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class PasswordsController < Devise::PasswordsController
|
||||||
|
class Current < ActiveSupport::CurrentAttributes
|
||||||
|
attribute :user
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
super do |resource|
|
||||||
|
resource.errors.clear unless Docuseal.multitenant?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
super do |resource|
|
||||||
|
Current.user = resource
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,24 +1,66 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class PersonalizationSettingsController < ApplicationController
|
class PersonalizationSettingsController < ApplicationController
|
||||||
|
ALLOWED_KEYS = [
|
||||||
|
AccountConfig::FORM_COMPLETED_BUTTON_KEY,
|
||||||
|
AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY,
|
||||||
|
AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY,
|
||||||
|
AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY,
|
||||||
|
AccountConfig::FORM_COMPLETED_MESSAGE_KEY
|
||||||
|
].freeze
|
||||||
|
|
||||||
|
InvalidKey = Class.new(StandardError)
|
||||||
|
|
||||||
|
before_action :load_and_authorize_account_config, only: :create
|
||||||
|
|
||||||
def show
|
def show
|
||||||
authorize!(:read, AccountConfig)
|
authorize!(:read, AccountConfig)
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
account_config =
|
if @account_config.value.is_a?(Hash)
|
||||||
current_account.account_configs.find_or_initialize_by(key: encrypted_config_params[:key])
|
@account_config.value = @account_config.value.reject do |_, v|
|
||||||
|
v.blank? && v != false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
authorize!(:create, account_config)
|
if @account_config.value != false && @account_config.value.blank?
|
||||||
|
@account_config.destroy!
|
||||||
account_config.update!(encrypted_config_params)
|
else
|
||||||
|
@account_config.save!
|
||||||
|
end
|
||||||
|
|
||||||
redirect_back(fallback_location: settings_personalization_path, notice: 'Settings have been saved.')
|
redirect_back(fallback_location: settings_personalization_path, notice: 'Settings have been saved.')
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def encrypted_config_params
|
def load_and_authorize_account_config
|
||||||
params.require(:account_config).permit!
|
@account_config =
|
||||||
|
current_account.account_configs.find_or_initialize_by(key: account_config_params[:key])
|
||||||
|
|
||||||
|
@account_config.assign_attributes(account_config_params)
|
||||||
|
|
||||||
|
authorize!(:create, @account_config)
|
||||||
|
|
||||||
|
raise InvalidKey unless ALLOWED_KEYS.include?(@account_config.key)
|
||||||
|
|
||||||
|
@account_config
|
||||||
|
end
|
||||||
|
|
||||||
|
def account_config_params
|
||||||
|
attrs = params.require(:account_config).permit!
|
||||||
|
|
||||||
|
return attrs if attrs[:value].is_a?(String)
|
||||||
|
|
||||||
|
attrs[:value]&.transform_values! do |value|
|
||||||
|
if value.in?(%w[true false])
|
||||||
|
value == 'true'
|
||||||
|
else
|
||||||
|
value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
attrs
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class PreviewDocumentPageController < ActionController::API
|
||||||
|
include ActiveStorage::SetCurrent
|
||||||
|
|
||||||
|
FORMAT = Templates::ProcessDocument::FORMAT
|
||||||
|
|
||||||
|
def show
|
||||||
|
attachment_uuid = ApplicationRecord.signed_id_verifier.verified(params[:signed_uuid], purpose: :attachment)
|
||||||
|
|
||||||
|
attachment = ActiveStorage::Attachment.find_by(uuid: attachment_uuid) if attachment_uuid
|
||||||
|
|
||||||
|
return head :not_found unless attachment
|
||||||
|
|
||||||
|
preview_image = attachment.preview_images.joins(:blob).find_by(blob: { filename: "#{params[:id]}#{FORMAT}" })
|
||||||
|
|
||||||
|
return redirect_to preview_image.url, allow_other_host: true if preview_image
|
||||||
|
|
||||||
|
file_path =
|
||||||
|
if attachment.service.name == :disk
|
||||||
|
ActiveStorage::Blob.service.path_for(attachment.key)
|
||||||
|
else
|
||||||
|
find_or_create_document_tempfile_path(attachment)
|
||||||
|
end
|
||||||
|
|
||||||
|
preview_image =
|
||||||
|
Templates::ProcessDocument.generate_pdf_preview_from_file(attachment, file_path, params[:id].to_i)
|
||||||
|
|
||||||
|
redirect_to preview_image.url, allow_other_host: true
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_or_create_document_tempfile_path(attachment)
|
||||||
|
file_path = "#{Dir.tmpdir}/#{attachment.uuid}"
|
||||||
|
|
||||||
|
File.open(file_path, File::RDWR | File::CREAT, 0o644) do |f|
|
||||||
|
f.flock(File::LOCK_EX)
|
||||||
|
|
||||||
|
# rubocop:disable Style/ZeroLengthPredicate
|
||||||
|
if f.size.zero?
|
||||||
|
f.binmode
|
||||||
|
|
||||||
|
f.write(attachment.download)
|
||||||
|
end
|
||||||
|
# rubocop:enable Style/ZeroLengthPredicate
|
||||||
|
end
|
||||||
|
|
||||||
|
file_path
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class RegistrationsController < Devise::RegistrationsController
|
|
||||||
prepend_before_action :require_no_authentication, only: [:show]
|
|
||||||
prepend_before_action :maybe_redirect_if_signed_in, only: [:show]
|
|
||||||
|
|
||||||
def show; end
|
|
||||||
|
|
||||||
def create
|
|
||||||
super
|
|
||||||
|
|
||||||
Accounts.create_default_template(resource.account) if resource.account.persisted?
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def after_sign_up_path_for(...)
|
|
||||||
if params[:redir].present?
|
|
||||||
return console_redirect_index_path(redir: params[:redir]) if params[:redir].starts_with?(Docuseal::CONSOLE_URL)
|
|
||||||
|
|
||||||
return params[:redir]
|
|
||||||
end
|
|
||||||
|
|
||||||
super
|
|
||||||
end
|
|
||||||
|
|
||||||
def maybe_redirect_if_signed_in
|
|
||||||
return unless signed_in?
|
|
||||||
return if params[:redir].blank?
|
|
||||||
|
|
||||||
redirect_to after_sign_up_path_for(current_user), allow_other_host: true
|
|
||||||
end
|
|
||||||
|
|
||||||
def require_no_authentication
|
|
||||||
super
|
|
||||||
|
|
||||||
flash.clear
|
|
||||||
end
|
|
||||||
|
|
||||||
def build_resource(_hash = {})
|
|
||||||
account = Account.new(account_params)
|
|
||||||
account.timezone = Accounts.normalize_timezone(account.timezone)
|
|
||||||
|
|
||||||
self.resource = account.users.new(user_params)
|
|
||||||
|
|
||||||
account.name ||= "#{resource.full_name}'s Company" if params[:action] == 'create'
|
|
||||||
end
|
|
||||||
|
|
||||||
def user_params
|
|
||||||
return {} if params[:user].blank?
|
|
||||||
|
|
||||||
params.require(:user).permit(:first_name, :last_name, :email, :password).compact_blank.tap do |attrs|
|
|
||||||
attrs[:password] ||= SecureRandom.hex if params[:action] == 'create'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def account_params
|
|
||||||
return {} if params[:account].blank?
|
|
||||||
|
|
||||||
params.require(:account).permit(:name, :timezone).compact_blank
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -12,13 +12,18 @@ class SendSubmissionEmailController < ApplicationController
|
|||||||
def create
|
def create
|
||||||
@submitter =
|
@submitter =
|
||||||
if params[:template_slug]
|
if params[:template_slug]
|
||||||
Submitter.joins(submission: :template).find_by!(email: params[:email],
|
Submitter.joins(submission: :template).find_by!(email: params[:email].to_s.downcase,
|
||||||
template: { slug: params[:template_slug] })
|
template: { slug: params[:template_slug] })
|
||||||
|
elsif params[:submission_slug]
|
||||||
|
Submitter.joins(:submission).find_by!(email: params[:email].to_s.downcase,
|
||||||
|
submission: { slug: params[:submission_slug] })
|
||||||
else
|
else
|
||||||
Submitter.find_by!(slug: params[:submitter_slug])
|
Submitter.find_by!(slug: params[:submitter_slug])
|
||||||
end
|
end
|
||||||
|
|
||||||
SubmitterMailer.documents_copy_email(@submitter).deliver_later!
|
RateLimit.call("send-email-#{@submitter.id}", limit: 2, ttl: 5.minutes)
|
||||||
|
|
||||||
|
SubmitterMailer.documents_copy_email(@submitter, sig: true).deliver_later!
|
||||||
|
|
||||||
respond_to do |f|
|
respond_to do |f|
|
||||||
f.html { redirect_to success_send_submission_email_index_path }
|
f.html { redirect_to success_send_submission_email_index_path }
|
||||||
|
|||||||
@@ -4,12 +4,16 @@ class SessionsController < Devise::SessionsController
|
|||||||
before_action :configure_permitted_parameters
|
before_action :configure_permitted_parameters
|
||||||
|
|
||||||
def create
|
def create
|
||||||
if Docuseal.multitenant? && !User.exists?(email: sign_in_params[:email])
|
email = sign_in_params[:email].to_s.downcase
|
||||||
|
|
||||||
|
if Docuseal.multitenant? && !User.exists?(email:)
|
||||||
|
Rollbar.warning('Sign in new user') if defined?(Rollbar)
|
||||||
|
|
||||||
return redirect_to new_registration_path(sign_up: true, user: sign_in_params.slice(:email)),
|
return redirect_to new_registration_path(sign_up: true, user: sign_in_params.slice(:email)),
|
||||||
notice: 'Create a new account'
|
notice: 'Create a new account'
|
||||||
end
|
end
|
||||||
|
|
||||||
if User.exists?(email: sign_in_params[:email], otp_required_for_login: true) && sign_in_params[:otp_attempt].blank?
|
if User.exists?(email:, otp_required_for_login: true) && sign_in_params[:otp_attempt].blank?
|
||||||
return render :otp, locals: { resource: User.new(sign_in_params) }, status: :unprocessable_entity
|
return render :otp, locals: { resource: User.new(sign_in_params) }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -32,9 +36,9 @@ class SessionsController < Devise::SessionsController
|
|||||||
devise_parameter_sanitizer.permit(:sign_in, keys: [:otp_attempt])
|
devise_parameter_sanitizer.permit(:sign_in, keys: [:otp_attempt])
|
||||||
end
|
end
|
||||||
|
|
||||||
def require_no_authentication
|
def set_flash_message(key, kind, options = {})
|
||||||
super
|
return if key == :alert && kind == 'already_authenticated'
|
||||||
|
|
||||||
flash.clear
|
super
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SsoSettingsController < ApplicationController
|
||||||
|
before_action :load_encrypted_config
|
||||||
|
authorize_resource :encrypted_config, only: :index
|
||||||
|
|
||||||
|
def index; end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def load_encrypted_config
|
||||||
|
@encrypted_config =
|
||||||
|
EncryptedConfig.find_or_initialize_by(account: current_account, key: 'saml_configs')
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -6,28 +6,32 @@ class StartFormController < ApplicationController
|
|||||||
skip_before_action :authenticate_user!
|
skip_before_action :authenticate_user!
|
||||||
skip_authorization_check
|
skip_authorization_check
|
||||||
|
|
||||||
|
around_action :with_browser_locale, only: %i[show completed]
|
||||||
before_action :load_template
|
before_action :load_template
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@submitter = @template.submissions.new.submitters.new(uuid: @template.submitters.first['uuid'])
|
@submitter = @template.submissions.new(account_id: @template.account_id)
|
||||||
|
.submitters.new(uuid: @template.submitters.first['uuid'])
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
@submitter = Submitter.where(submission: @template.submissions.where(deleted_at: nil))
|
return redirect_to start_form_path(@template.slug) if @template.archived_at?
|
||||||
.find_or_initialize_by(email: submitter_params[:email])
|
|
||||||
|
@submitter = Submitter.where(submission: @template.submissions.where(archived_at: nil))
|
||||||
|
.order(id: :desc)
|
||||||
|
.then { |rel| params[:resubmit].present? ? rel.where(completed_at: nil) : rel }
|
||||||
|
.find_or_initialize_by(**submitter_params.compact_blank)
|
||||||
|
|
||||||
if @submitter.completed_at?
|
if @submitter.completed_at?
|
||||||
redirect_to start_form_completed_path(@template.slug, email: submitter_params[:email])
|
redirect_to start_form_completed_path(@template.slug, email: submitter_params[:email])
|
||||||
else
|
else
|
||||||
@submitter.assign_attributes(
|
if @template.submitters.to_a.size > 1 && @submitter.new_record?
|
||||||
uuid: @template.submitters.first['uuid'],
|
@error_message = 'Not found'
|
||||||
ip: request.remote_ip,
|
|
||||||
ua: request.user_agent
|
|
||||||
)
|
|
||||||
|
|
||||||
@submitter.submission ||= Submission.new(template: @template,
|
return render :show
|
||||||
template_submitters: @template.submitters,
|
end
|
||||||
source: :link)
|
|
||||||
|
assign_submission_attributes(@submitter, @template) if @submitter.new_record?
|
||||||
|
|
||||||
if @submitter.save
|
if @submitter.save
|
||||||
redirect_to submit_form_path(@submitter.slug)
|
redirect_to submit_form_path(@submitter.slug)
|
||||||
@@ -45,8 +49,37 @@ class StartFormController < ApplicationController
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def assign_submission_attributes(submitter, template)
|
||||||
|
resubmit_submitter =
|
||||||
|
if params[:resubmit].present?
|
||||||
|
Submitter.where(submission: @template.submissions).find_by(slug: params[:resubmit])
|
||||||
|
end
|
||||||
|
|
||||||
|
submitter.assign_attributes(
|
||||||
|
uuid: template.submitters.first['uuid'],
|
||||||
|
ip: request.remote_ip,
|
||||||
|
ua: request.user_agent,
|
||||||
|
values: resubmit_submitter&.preferences&.fetch('default_values', nil) || {},
|
||||||
|
preferences: resubmit_submitter&.preferences.presence || { 'send_email' => true },
|
||||||
|
metadata: resubmit_submitter&.metadata.presence || {}
|
||||||
|
)
|
||||||
|
|
||||||
|
if submitter.values.present?
|
||||||
|
resubmit_submitter.attachments.each do |attachment|
|
||||||
|
submitter.attachments << attachment.dup if submitter.values.value?(attachment.uuid)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
submitter.submission ||= Submission.new(template:,
|
||||||
|
account_id: template.account_id,
|
||||||
|
template_submitters: template.submitters,
|
||||||
|
source: :link)
|
||||||
|
|
||||||
|
submitter
|
||||||
|
end
|
||||||
|
|
||||||
def submitter_params
|
def submitter_params
|
||||||
params.require(:submitter).permit(:email).tap do |attrs|
|
params.require(:submitter).permit(:email, :phone, :name).tap do |attrs|
|
||||||
attrs[:email] = Submissions.normalize_email(attrs[:email])
|
attrs[:email] = Submissions.normalize_email(attrs[:email])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SubmissionsArchivedController < ApplicationController
|
||||||
|
load_and_authorize_resource :submission, parent: false
|
||||||
|
|
||||||
|
def index
|
||||||
|
@submissions = @submissions.joins(:template)
|
||||||
|
@submissions = @submissions.where.not(archived_at: nil)
|
||||||
|
.or(@submissions.where.not(templates: { archived_at: nil }))
|
||||||
|
.preload(:created_by_user, template: :author)
|
||||||
|
@submissions = Submissions.search(@submissions, params[:q], search_template: true)
|
||||||
|
|
||||||
|
@pagy, @submissions = pagy(@submissions.preload(:submitters).order(id: :desc))
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -7,10 +7,7 @@ class SubmissionsController < ApplicationController
|
|||||||
load_and_authorize_resource :submission, only: %i[show destroy]
|
load_and_authorize_resource :submission, only: %i[show destroy]
|
||||||
|
|
||||||
def show
|
def show
|
||||||
ActiveRecord::Associations::Preloader.new(
|
@submission = Submissions.preload_with_pages(@submission)
|
||||||
records: [@submission],
|
|
||||||
associations: [:template, { template_schema_documents: [:blob, { preview_images_attachments: :blob }] }]
|
|
||||||
).call
|
|
||||||
|
|
||||||
render :show, layout: 'plain'
|
render :show, layout: 'plain'
|
||||||
end
|
end
|
||||||
@@ -22,40 +19,58 @@ class SubmissionsController < ApplicationController
|
|||||||
def create
|
def create
|
||||||
authorize!(:create, Submission)
|
authorize!(:create, Submission)
|
||||||
|
|
||||||
|
save_template_message(@template, params) if params[:save_message] == '1'
|
||||||
|
|
||||||
|
if params[:is_custom_message] != '1'
|
||||||
|
params.delete(:subject)
|
||||||
|
params.delete(:body)
|
||||||
|
end
|
||||||
|
|
||||||
submissions =
|
submissions =
|
||||||
if params[:emails].present?
|
if params[:emails].present?
|
||||||
Submissions.create_from_emails(template: @template,
|
Submissions.create_from_emails(template: @template,
|
||||||
user: current_user,
|
user: current_user,
|
||||||
source: :invite,
|
source: :invite,
|
||||||
mark_as_sent: params[:send_email] == '1',
|
mark_as_sent: params[:send_email] == '1',
|
||||||
emails: params[:emails])
|
emails: params[:emails],
|
||||||
|
params: params.merge('send_completed_email' => true))
|
||||||
else
|
else
|
||||||
Submissions.create_from_submitters(template: @template,
|
Submissions.create_from_submitters(template: @template,
|
||||||
user: current_user,
|
user: current_user,
|
||||||
source: :invite,
|
source: :invite,
|
||||||
submitters_order: params[:preserve_order] == '1' ? 'preserved' : 'random',
|
submitters_order: params[:preserve_order] == '1' ? 'preserved' : 'random',
|
||||||
mark_as_sent: params[:send_email] == '1',
|
mark_as_sent: params[:send_email] == '1',
|
||||||
submissions_attrs: submissions_params[:submission].to_h.values)
|
submissions_attrs: submissions_params[:submission].to_h.values,
|
||||||
|
params: params.merge('send_completed_email' => true))
|
||||||
end
|
end
|
||||||
|
|
||||||
Submissions.send_signature_requests(submissions, params)
|
Submissions.send_signature_requests(submissions)
|
||||||
|
|
||||||
redirect_to template_path(@template), notice: 'New recipients have been added'
|
redirect_to template_path(@template), notice: 'New recipients have been added'
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
@submission.update!(deleted_at: Time.current)
|
@submission.update!(archived_at: Time.current)
|
||||||
|
|
||||||
|
SendSubmissionArchivedWebhookRequestJob.perform_later(@submission)
|
||||||
|
|
||||||
redirect_back(fallback_location: template_path(@submission.template), notice: 'Submission has been archived')
|
redirect_back(fallback_location: template_path(@submission.template), notice: 'Submission has been archived')
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def save_template_message(template, params)
|
||||||
|
template.preferences['request_email_subject'] = params[:subject] if params[:subject].present?
|
||||||
|
template.preferences['request_email_body'] = params[:body] if params[:body].present?
|
||||||
|
|
||||||
|
template.save!
|
||||||
|
end
|
||||||
|
|
||||||
def submissions_params
|
def submissions_params
|
||||||
params.permit(submission: { submitters: [%i[uuid email phone name]] })
|
params.permit(submission: { submitters: [%i[uuid email phone name]] })
|
||||||
end
|
end
|
||||||
|
|
||||||
def load_template
|
def load_template
|
||||||
@template = current_account.templates.find(params[:template_id])
|
@template = Template.accessible_by(current_ability).find(params[:template_id])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SubmissionsDashboardController < ApplicationController
|
||||||
|
load_and_authorize_resource :submission, parent: false
|
||||||
|
|
||||||
|
def index
|
||||||
|
@submissions = @submissions.joins(:template)
|
||||||
|
|
||||||
|
@submissions = @submissions.where(archived_at: nil)
|
||||||
|
.where(templates: { archived_at: nil })
|
||||||
|
.preload(:created_by_user, template: :author)
|
||||||
|
|
||||||
|
@submissions = Submissions.search(@submissions, params[:q], search_template: true)
|
||||||
|
|
||||||
|
@submissions = @submissions.pending if params[:status] == 'pending'
|
||||||
|
@submissions = @submissions.completed if params[:status] == 'completed'
|
||||||
|
|
||||||
|
@pagy, @submissions = pagy(@submissions.preload(:submitters).order(id: :desc))
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -4,8 +4,20 @@ class SubmissionsDownloadController < ApplicationController
|
|||||||
skip_before_action :authenticate_user!
|
skip_before_action :authenticate_user!
|
||||||
skip_authorization_check
|
skip_authorization_check
|
||||||
|
|
||||||
|
TTL = 40.minutes
|
||||||
|
FILES_TTL = 5.minutes
|
||||||
|
|
||||||
def index
|
def index
|
||||||
submitter = Submitter.find_by(slug: params[:submitter_slug])
|
submitter = Submitter.find_signed(params[:sig], purpose: :download_completed) if params[:sig].present?
|
||||||
|
|
||||||
|
signature_valid =
|
||||||
|
if submitter&.slug == params[:submitter_slug]
|
||||||
|
true
|
||||||
|
else
|
||||||
|
submitter = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
submitter ||= Submitter.find_by!(slug: params[:submitter_slug])
|
||||||
|
|
||||||
Submissions::EnsureResultGenerated.call(submitter)
|
Submissions::EnsureResultGenerated.call(submitter)
|
||||||
|
|
||||||
@@ -13,11 +25,26 @@ class SubmissionsDownloadController < ApplicationController
|
|||||||
|
|
||||||
Submissions::EnsureResultGenerated.call(last_submitter)
|
Submissions::EnsureResultGenerated.call(last_submitter)
|
||||||
|
|
||||||
urls =
|
return head :not_found unless last_submitter.completed_at?
|
||||||
Submitters.select_attachments_for_download(last_submitter).map do |attachment|
|
|
||||||
helpers.rails_blob_url(attachment)
|
|
||||||
end
|
|
||||||
|
|
||||||
render json: urls
|
if last_submitter.completed_at < TTL.ago && !signature_valid && !current_user_submitter?(last_submitter)
|
||||||
|
Rollbar.info("TTL: #{last_submitter.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
|
return head :not_found
|
||||||
|
end
|
||||||
|
|
||||||
|
render json: build_urls(last_submitter)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def current_user_submitter?(submitter)
|
||||||
|
current_user && current_user.account.submitters.exists?(id: submitter.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_urls(submitter)
|
||||||
|
Submitters.select_attachments_for_download(submitter).map do |attachment|
|
||||||
|
ActiveStorage::Blob.proxy_url(attachment.blob, expires_at: FILES_TTL.from_now.to_i)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SubmissionsPreviewController < ApplicationController
|
||||||
|
skip_before_action :authenticate_user!
|
||||||
|
skip_authorization_check
|
||||||
|
|
||||||
|
TTL = 40.minutes
|
||||||
|
|
||||||
|
def show
|
||||||
|
submitter = Submitter.find_signed(params[:sig], purpose: :download_completed) if params[:sig].present?
|
||||||
|
|
||||||
|
signature_valid =
|
||||||
|
if submitter && submitter.submission.slug == params[:slug]
|
||||||
|
@submission = submitter.submission
|
||||||
|
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
@submission ||= Submission.find_by!(slug: params[:slug])
|
||||||
|
|
||||||
|
if !@submission.submitters.all?(&:completed_at?) && current_user.blank?
|
||||||
|
raise ActionController::RoutingError, 'Not Found'
|
||||||
|
end
|
||||||
|
|
||||||
|
if !submission_valid_ttl?(@submission) && !signature_valid
|
||||||
|
Rollbar.info("TTL: #{@submission.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
|
return redirect_to submissions_preview_completed_path(@submission.slug)
|
||||||
|
end
|
||||||
|
|
||||||
|
@submission = Submissions.preload_with_pages(@submission)
|
||||||
|
|
||||||
|
render 'submissions/show', layout: 'plain'
|
||||||
|
end
|
||||||
|
|
||||||
|
def completed
|
||||||
|
@submission = Submission.find_by!(slug: params[:submissions_preview_slug])
|
||||||
|
|
||||||
|
render :completed, layout: 'form'
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def submission_valid_ttl?(submission)
|
||||||
|
return true if current_user && current_user.account.submissions.exists?(id: submission.id)
|
||||||
|
|
||||||
|
last_submitter = submission.submitters.select(&:completed_at?).max_by(&:completed_at)
|
||||||
|
|
||||||
|
last_submitter && last_submitter.completed_at > TTL.ago
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -3,28 +3,50 @@
|
|||||||
class SubmitFormController < ApplicationController
|
class SubmitFormController < ApplicationController
|
||||||
layout 'form'
|
layout 'form'
|
||||||
|
|
||||||
|
around_action :with_browser_locale, only: %i[show completed success]
|
||||||
skip_before_action :authenticate_user!
|
skip_before_action :authenticate_user!
|
||||||
skip_authorization_check
|
skip_authorization_check
|
||||||
|
|
||||||
|
PRELOAD_ALL_PAGES_AMOUNT = 200
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@submitter =
|
@submitter = Submitter.find_by!(slug: params[:slug])
|
||||||
Submitter.preload(submission: [
|
|
||||||
:template, { template_schema_documents: [:blob, { preview_images_attachments: :blob }] }
|
|
||||||
])
|
|
||||||
.find_by!(slug: params[:slug])
|
|
||||||
|
|
||||||
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
|
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
|
||||||
|
|
||||||
|
ActiveRecord::Associations::Preloader.new(
|
||||||
|
records: [@submitter],
|
||||||
|
associations: [submission: [:template, { template_schema_documents: :blob }]]
|
||||||
|
).call
|
||||||
|
|
||||||
|
total_pages =
|
||||||
|
@submitter.submission.template_schema_documents.sum { |e| e.metadata.dig('pdf', 'number_of_pages').to_i }
|
||||||
|
|
||||||
|
if total_pages < PRELOAD_ALL_PAGES_AMOUNT
|
||||||
|
ActiveRecord::Associations::Preloader.new(
|
||||||
|
records: @submitter.submission.template_schema_documents,
|
||||||
|
associations: [:blob, { preview_images_attachments: :blob }]
|
||||||
|
).call
|
||||||
|
end
|
||||||
|
|
||||||
Submitters::MaybeUpdateDefaultValues.call(@submitter, current_user)
|
Submitters::MaybeUpdateDefaultValues.call(@submitter, current_user)
|
||||||
|
|
||||||
cookies[:submitter_sid] = @submitter.signed_id
|
render(@submitter.submission.template.archived_at? || @submitter.submission.archived_at? ? :archived : :show)
|
||||||
|
|
||||||
render @submitter.submission.template.deleted_at? ? :archived : :show
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
submitter = Submitter.find_by!(slug: params[:slug])
|
submitter = Submitter.find_by!(slug: params[:slug])
|
||||||
|
|
||||||
|
if submitter.completed_at?
|
||||||
|
return render json: { error: 'Form has been completed already.' }, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
|
||||||
|
if submitter.template.archived_at? || submitter.submission.archived_at?
|
||||||
|
Rollbar.info("Archived template: #{submitter.template.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
|
return render json: { error: 'Form has been archived.' }, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
|
||||||
Submitters::SubmitValues.call(submitter, params, request)
|
Submitters::SubmitValues.call(submitter, params, request)
|
||||||
|
|
||||||
head :ok
|
head :ok
|
||||||
@@ -33,6 +55,8 @@ class SubmitFormController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def completed
|
def completed
|
||||||
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
@submitter = Submitter.completed.find_by!(slug: params[:submit_form_slug])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def success; end
|
||||||
end
|
end
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SubmittersAutocompleteController < ApplicationController
|
||||||
|
load_and_authorize_resource :submitter, parent: false
|
||||||
|
|
||||||
|
SELECT_COLUMNS = %w[email phone name].freeze
|
||||||
|
LIMIT = 100
|
||||||
|
|
||||||
|
def index
|
||||||
|
submitters = search_submitters(@submitters)
|
||||||
|
|
||||||
|
values = submitters.limit(LIMIT).group(SELECT_COLUMNS.join(', ')).pluck(SELECT_COLUMNS.join(', '))
|
||||||
|
|
||||||
|
attrs = values.map { |row| SELECT_COLUMNS.zip(row).to_h }
|
||||||
|
attrs = attrs.uniq { |e| e[params[:field]] } if params[:field].present?
|
||||||
|
|
||||||
|
render json: attrs
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def search_submitters(submitters)
|
||||||
|
if SELECT_COLUMNS.include?(params[:field])
|
||||||
|
column = Submitter.arel_table[params[:field].to_sym]
|
||||||
|
|
||||||
|
term = "#{params[:q].downcase}%"
|
||||||
|
|
||||||
|
submitters.where(column.matches(term, false, true))
|
||||||
|
else
|
||||||
|
Submitters.search(submitters, params[:q])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -4,9 +4,16 @@ class SubmittersSendEmailController < ApplicationController
|
|||||||
load_and_authorize_resource :submitter, id_param: :submitter_slug, find_by: :slug
|
load_and_authorize_resource :submitter, id_param: :submitter_slug, find_by: :slug
|
||||||
|
|
||||||
def create
|
def create
|
||||||
SubmitterMailer.invitation_email(@submitter).deliver_later!
|
if Docuseal.multitenant? && SubmissionEvent.exists?(submitter: @submitter,
|
||||||
|
event_type: 'send_email',
|
||||||
|
created_at: 24.hours.ago..Time.current)
|
||||||
|
Rollbar.warning("Already sent: #{@submitter.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
SubmissionEvent.create!(submitter: @submitter, event_type: 'send_email')
|
return redirect_back(fallback_location: submission_path(@submitter.submission),
|
||||||
|
alert: 'Email has been sent already.')
|
||||||
|
end
|
||||||
|
|
||||||
|
SendSubmitterInvitationEmailJob.perform_later('submitter_id' => @submitter.id)
|
||||||
|
|
||||||
@submitter.sent_at ||= Time.current
|
@submitter.sent_at ||= Time.current
|
||||||
@submitter.save!
|
@submitter.save!
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class TemplateDocumentsController < ApplicationController
|
||||||
|
load_and_authorize_resource :template
|
||||||
|
|
||||||
|
def create
|
||||||
|
return head :unprocessable_entity if params[:blobs].blank? && params[:files].blank?
|
||||||
|
|
||||||
|
old_fields_hash = @template.fields.hash
|
||||||
|
|
||||||
|
documents = Templates::CreateAttachments.call(@template, params)
|
||||||
|
|
||||||
|
schema = documents.map do |doc|
|
||||||
|
{ attachment_uuid: doc.uuid, name: doc.filename.base }
|
||||||
|
end
|
||||||
|
|
||||||
|
render json: {
|
||||||
|
schema:,
|
||||||
|
fields: old_fields_hash == @template.fields.hash ? nil : @template.fields,
|
||||||
|
submitters: old_fields_hash == @template.fields.hash ? nil : @template.submitters,
|
||||||
|
documents: documents.as_json(
|
||||||
|
methods: %i[metadata signed_uuid],
|
||||||
|
include: {
|
||||||
|
preview_images: { methods: %i[url metadata filename] }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
rescue Templates::CreateAttachments::PdfEncrypted
|
||||||
|
render json: { error: 'PDF encrypted' }, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class TemplateFoldersAutocompleteController < ApplicationController
|
||||||
|
load_and_authorize_resource :template_folder, parent: false
|
||||||
|
|
||||||
|
LIMIT = 100
|
||||||
|
|
||||||
|
def index
|
||||||
|
template_folders = @template_folders.joins(:templates).where(templates: { archived_at: nil }).distinct
|
||||||
|
template_folders = TemplateFolders.search(template_folders, params[:q]).limit(LIMIT)
|
||||||
|
|
||||||
|
render json: template_folders.as_json(only: %i[name archived_at])
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class TemplateSharingsTestingController < ApplicationController
|
||||||
|
load_and_authorize_resource :template, parent: true
|
||||||
|
|
||||||
|
before_action do
|
||||||
|
authorize!(:manage, TemplateSharing.new(template: @template))
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
testing_account = Accounts.find_or_create_testing_user(true_user.account).account
|
||||||
|
|
||||||
|
if params[:value] == '1'
|
||||||
|
TemplateSharing.create!(ability: :manage, account: testing_account, template: @template)
|
||||||
|
else
|
||||||
|
TemplateSharing.find_by(template: @template, account: testing_account)&.destroy!
|
||||||
|
end
|
||||||
|
|
||||||
|
head :ok
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -4,7 +4,7 @@ class TemplatesArchivedController < ApplicationController
|
|||||||
load_and_authorize_resource :template, parent: false
|
load_and_authorize_resource :template, parent: false
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@templates = @templates.where.not(deleted_at: nil).preload(:author, :folder).order(id: :desc)
|
@templates = @templates.where.not(archived_at: nil).preload(:author, :folder).order(id: :desc)
|
||||||
@templates = Templates.search(@templates, params[:q])
|
@templates = Templates.search(@templates, params[:q])
|
||||||
|
|
||||||
@pagy, @templates = pagy(@templates, items: 12)
|
@pagy, @templates = pagy(@templates, items: 12)
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ class TemplatesArchivedSubmissionsController < ApplicationController
|
|||||||
load_and_authorize_resource :submission, through: :template, parent: false
|
load_and_authorize_resource :submission, through: :template, parent: false
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@submissions = @submissions.where.not(deleted_at: nil)
|
@submissions = @submissions.where.not(archived_at: nil)
|
||||||
@submissions = Submissions.search(@submissions, params[:q])
|
@submissions = Submissions.search(@submissions, params[:q], search_values: true)
|
||||||
|
|
||||||
@pagy, @submissions = pagy(@submissions.preload(:submitters).order(id: :desc))
|
@pagy, @submissions = pagy(@submissions.preload(:submitters).order(id: :desc))
|
||||||
rescue ActiveRecord::RecordNotFound
|
rescue ActiveRecord::RecordNotFound
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class TemplatesCodeModalController < ApplicationController
|
||||||
|
load_and_authorize_resource :template
|
||||||
|
|
||||||
|
def show; end
|
||||||
|
end
|
||||||
@@ -6,9 +6,14 @@ class TemplatesController < ApplicationController
|
|||||||
before_action :load_base_template, only: %i[new create]
|
before_action :load_base_template, only: %i[new create]
|
||||||
|
|
||||||
def show
|
def show
|
||||||
submissions = @template.submissions
|
submissions = @template.submissions.accessible_by(current_ability)
|
||||||
submissions = submissions.active if @template.deleted_at.blank?
|
submissions = submissions.active if @template.archived_at.blank?
|
||||||
submissions = Submissions.search(submissions, params[:q])
|
submissions = Submissions.search(submissions, params[:q], search_values: true)
|
||||||
|
|
||||||
|
@base_submissions = submissions
|
||||||
|
|
||||||
|
submissions = submissions.pending if params[:status] == 'pending'
|
||||||
|
submissions = submissions.completed if params[:status] == 'completed'
|
||||||
|
|
||||||
@pagy, @submissions = pagy(submissions.preload(:submitters).order(id: :desc))
|
@pagy, @submissions = pagy(submissions.preload(:submitters).order(id: :desc))
|
||||||
rescue ActiveRecord::RecordNotFound
|
rescue ActiveRecord::RecordNotFound
|
||||||
@@ -25,39 +30,103 @@ class TemplatesController < ApplicationController
|
|||||||
associations: [schema_documents: { preview_images_attachments: :blob }]
|
associations: [schema_documents: { preview_images_attachments: :blob }]
|
||||||
).call
|
).call
|
||||||
|
|
||||||
|
@template_data =
|
||||||
|
@template.as_json.merge(
|
||||||
|
documents: @template.schema_documents.as_json(
|
||||||
|
methods: %i[metadata signed_uuid],
|
||||||
|
include: { preview_images: { methods: %i[url metadata filename] } }
|
||||||
|
)
|
||||||
|
).to_json
|
||||||
|
|
||||||
render :edit, layout: 'plain'
|
render :edit, layout: 'plain'
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@template.account = current_account
|
if @base_template
|
||||||
@template.author = current_user
|
@template = Templates::Clone.call(@base_template, author: current_user,
|
||||||
@template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name])
|
name: params.dig(:template, :name),
|
||||||
@template.assign_attributes(@base_template.slice(:fields, :schema, :submitters)) if @base_template
|
folder_name: params[:folder_name])
|
||||||
|
else
|
||||||
|
@template.author = current_user
|
||||||
|
@template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name])
|
||||||
|
end
|
||||||
|
|
||||||
|
if params[:account_id].present? && authorized_clone_account_id?(params[:account_id])
|
||||||
|
@template.account_id = params[:account_id]
|
||||||
|
@template.folder = @template.account.default_template_folder
|
||||||
|
else
|
||||||
|
@template.account = current_account
|
||||||
|
end
|
||||||
|
|
||||||
if @template.save
|
if @template.save
|
||||||
Templates::CloneAttachments.call(template: @template, original_template: @base_template) if @base_template
|
Templates::CloneAttachments.call(template: @template, original_template: @base_template) if @base_template
|
||||||
|
|
||||||
redirect_to edit_template_path(@template)
|
SendTemplateUpdatedWebhookRequestJob.perform_later(@template)
|
||||||
|
|
||||||
|
maybe_redirect_to_template(@template)
|
||||||
else
|
else
|
||||||
render turbo_stream: turbo_stream.replace(:modal, template: 'templates/new'), status: :unprocessable_entity
|
render turbo_stream: turbo_stream.replace(:modal, template: 'templates/new'), status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def update
|
||||||
@template.update!(deleted_at: Time.current)
|
@template.update!(template_params)
|
||||||
|
|
||||||
redirect_back(fallback_location: root_path, notice: 'Template has been archived.')
|
SendTemplateUpdatedWebhookRequestJob.perform_later(@template)
|
||||||
|
|
||||||
|
head :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
notice =
|
||||||
|
if params[:permanently].present?
|
||||||
|
@template.destroy!
|
||||||
|
|
||||||
|
Rollbar.info("Remove template: #{@template.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
|
'Template has been removed.'
|
||||||
|
else
|
||||||
|
@template.update!(archived_at: Time.current)
|
||||||
|
|
||||||
|
'Template has been archived.'
|
||||||
|
end
|
||||||
|
|
||||||
|
redirect_back(fallback_location: root_path, notice:)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def template_params
|
def template_params
|
||||||
params.require(:template).permit(:name)
|
params.require(:template).permit(
|
||||||
|
:name,
|
||||||
|
{ schema: [%i[attachment_uuid name]],
|
||||||
|
submitters: [%i[name uuid]],
|
||||||
|
fields: [[:uuid, :submitter_uuid, :name, :type,
|
||||||
|
:required, :readonly, :default_value,
|
||||||
|
:title, :description,
|
||||||
|
{ preferences: {},
|
||||||
|
conditions: [%i[field_uuid value action]],
|
||||||
|
options: [%i[value uuid]],
|
||||||
|
validation: %i[message pattern],
|
||||||
|
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]] }
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def authorized_clone_account_id?(account_id)
|
||||||
|
true_user.account_id.to_s == account_id.to_s || true_user.account.linked_accounts.exists?(id: account_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def maybe_redirect_to_template(template)
|
||||||
|
if template.account == current_account
|
||||||
|
redirect_to(edit_template_path(@template))
|
||||||
|
else
|
||||||
|
redirect_back(fallback_location: root_path, notice: 'Template has been clonned')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def load_base_template
|
def load_base_template
|
||||||
return if params[:base_template_id].blank?
|
return if params[:base_template_id].blank?
|
||||||
|
|
||||||
@base_template = current_account.templates.find_by(id: params[:base_template_id])
|
@base_template = Template.accessible_by(current_ability).find_by(id: params[:base_template_id])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class TemplatesDashboardController < ApplicationController
|
||||||
|
load_and_authorize_resource :template_folder, parent: false
|
||||||
|
load_and_authorize_resource :template, parent: false
|
||||||
|
|
||||||
|
SHOW_TEMPLATES_FOLDERS_THRESHOLD = 9
|
||||||
|
TEMPLATES_PER_PAGE = 12
|
||||||
|
FOLDERS_PER_PAGE = 18
|
||||||
|
|
||||||
|
def index
|
||||||
|
@template_folders = filter_template_folders(@template_folders)
|
||||||
|
|
||||||
|
@pagy, @template_folders = pagy(
|
||||||
|
@template_folders,
|
||||||
|
items: FOLDERS_PER_PAGE,
|
||||||
|
page: @template_folders.count > SHOW_TEMPLATES_FOLDERS_THRESHOLD ? params[:page] : 1
|
||||||
|
)
|
||||||
|
|
||||||
|
if @pagy.count > SHOW_TEMPLATES_FOLDERS_THRESHOLD
|
||||||
|
@templates = @templates.none
|
||||||
|
else
|
||||||
|
@template_folders = @template_folders.reject { |e| e.name == TemplateFolder::DEFAULT_NAME }
|
||||||
|
@templates = filter_templates(@templates)
|
||||||
|
|
||||||
|
items =
|
||||||
|
if @template_folders.size < 4
|
||||||
|
TEMPLATES_PER_PAGE
|
||||||
|
else
|
||||||
|
(@template_folders.size < 7 ? 9 : 6)
|
||||||
|
end
|
||||||
|
|
||||||
|
@pagy, @templates = pagy(@templates, items:)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def filter_template_folders(template_folders)
|
||||||
|
rel = template_folders.joins(:active_templates)
|
||||||
|
.order(id: :desc)
|
||||||
|
.distinct
|
||||||
|
|
||||||
|
TemplateFolders.search(rel, params[:q])
|
||||||
|
end
|
||||||
|
|
||||||
|
def filter_templates(templates)
|
||||||
|
rel = templates.active.preload(:author).order(id: :desc)
|
||||||
|
|
||||||
|
if params[:q].blank?
|
||||||
|
if Docuseal.multitenant? && !current_account.testing?
|
||||||
|
rel = rel.where(folder_id: current_account.default_template_folder.id)
|
||||||
|
else
|
||||||
|
shared_template_ids =
|
||||||
|
TemplateSharing.where(account_id: [current_account.id, TemplateSharing::ALL_ID]).select(:template_id)
|
||||||
|
|
||||||
|
rel = rel.where(folder_id: current_account.default_template_folder.id).or(rel.where(id: shared_template_ids))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Templates.search(rel, params[:q])
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class TemplatesPreferencesController < ApplicationController
|
||||||
|
load_and_authorize_resource :template
|
||||||
|
|
||||||
|
def show; end
|
||||||
|
|
||||||
|
def create
|
||||||
|
authorize!(:update, @template)
|
||||||
|
|
||||||
|
@template.preferences = @template.preferences.merge(template_params[:preferences])
|
||||||
|
@template.save!
|
||||||
|
|
||||||
|
head :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def template_params
|
||||||
|
params.require(:template).permit(preferences: %i[bcc_completed request_email_subject request_email_body])
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class TemplatesPreviewController < ApplicationController
|
||||||
|
load_and_authorize_resource :template
|
||||||
|
|
||||||
|
def show
|
||||||
|
ActiveRecord::Associations::Preloader.new(
|
||||||
|
records: [@template],
|
||||||
|
associations: [schema_documents: { preview_images_attachments: :blob }]
|
||||||
|
).call
|
||||||
|
|
||||||
|
@template_data =
|
||||||
|
@template.as_json.merge(
|
||||||
|
documents: @template.schema_documents.as_json(
|
||||||
|
methods: %i[metadata signed_uuid],
|
||||||
|
include: { preview_images: { methods: %i[url metadata filename] } }
|
||||||
|
)
|
||||||
|
).to_json
|
||||||
|
|
||||||
|
render :show, layout: 'plain'
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -4,7 +4,7 @@ class TemplatesRestoreController < ApplicationController
|
|||||||
load_and_authorize_resource :template
|
load_and_authorize_resource :template
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@template.update!(deleted_at: nil)
|
@template.update!(archived_at: nil)
|
||||||
|
|
||||||
redirect_to template_path(@template), notice: 'Template has been unarchived'
|
redirect_to template_path(@template), notice: 'Template has been unarchived'
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -3,20 +3,65 @@
|
|||||||
class TemplatesUploadsController < ApplicationController
|
class TemplatesUploadsController < ApplicationController
|
||||||
load_and_authorize_resource :template, parent: false
|
load_and_authorize_resource :template, parent: false
|
||||||
|
|
||||||
|
layout 'plain'
|
||||||
|
|
||||||
|
def show; end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@template.account = current_account
|
url_params = create_file_params_from_url if params[:url].present?
|
||||||
@template.author = current_user
|
|
||||||
@template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name])
|
|
||||||
@template.name = File.basename(params[:files].first.original_filename, '.*')
|
|
||||||
|
|
||||||
@template.save!
|
save_template!(@template, url_params)
|
||||||
|
|
||||||
documents = Templates::CreateAttachments.call(@template, params)
|
documents = Templates::CreateAttachments.call(@template, url_params || params)
|
||||||
|
|
||||||
schema = documents.map { |doc| { attachment_uuid: doc.uuid, name: doc.filename.base } }
|
schema = documents.map { |doc| { attachment_uuid: doc.uuid, name: doc.filename.base } }
|
||||||
|
|
||||||
@template.update!(schema:)
|
@template.update!(schema:)
|
||||||
|
|
||||||
|
SendTemplateCreatedWebhookRequestJob.perform_later(@template)
|
||||||
|
|
||||||
redirect_to edit_template_path(@template)
|
redirect_to edit_template_path(@template)
|
||||||
|
rescue Templates::CreateAttachments::PdfEncrypted
|
||||||
|
render turbo_stream: turbo_stream.append(params[:form_id], html: helpers.tag.prompt_password)
|
||||||
|
rescue StandardError => e
|
||||||
|
Rollbar.error(e) if defined?(Rollbar)
|
||||||
|
|
||||||
|
redirect_to root_path, alert: 'Unable to upload file'
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def save_template!(template, url_params)
|
||||||
|
template.account = current_account
|
||||||
|
template.author = current_user
|
||||||
|
template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name])
|
||||||
|
template.name = File.basename((url_params || params)[:files].first.original_filename, '.*')
|
||||||
|
|
||||||
|
template.save!
|
||||||
|
|
||||||
|
template
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_file_params_from_url
|
||||||
|
tempfile = Tempfile.new
|
||||||
|
tempfile.binmode
|
||||||
|
tempfile.write(conn.get(Addressable::URI.parse(params[:url]).display_uri.to_s).body)
|
||||||
|
tempfile.rewind
|
||||||
|
|
||||||
|
file = ActionDispatch::Http::UploadedFile.new(
|
||||||
|
tempfile:,
|
||||||
|
filename: File.basename(
|
||||||
|
URI.decode_www_form_component(params[:filename].presence || params[:url])
|
||||||
|
),
|
||||||
|
type: Marcel::MimeType.for(tempfile)
|
||||||
|
)
|
||||||
|
|
||||||
|
{ files: [file] }
|
||||||
|
end
|
||||||
|
|
||||||
|
def conn
|
||||||
|
Faraday.new do |faraday|
|
||||||
|
faraday.response :follow_redirects
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class TestingAccountsController < ApplicationController
|
||||||
|
skip_authorization_check only: :destroy
|
||||||
|
|
||||||
|
def show
|
||||||
|
authorize!(:manage, current_account)
|
||||||
|
authorize!(:manage, current_user)
|
||||||
|
|
||||||
|
impersonate_user(Accounts.find_or_create_testing_user(true_user.account))
|
||||||
|
|
||||||
|
redirect_back(fallback_location: root_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
stop_impersonating_user
|
||||||
|
|
||||||
|
redirect_back(fallback_location: root_path)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class TestingApiSettingsController < ApplicationController
|
||||||
|
def index
|
||||||
|
authorize!(:manage, current_user.access_token)
|
||||||
|
|
||||||
|
@webhook_config =
|
||||||
|
current_account.encrypted_configs.find_or_initialize_by(key: EncryptedConfig::WEBHOOK_URL_KEY)
|
||||||
|
|
||||||
|
authorize!(:manage, @webhook_config)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class TimestampServerController < ApplicationController
|
||||||
|
before_action :build_encrypted_config
|
||||||
|
authorize_resource :encrypted_config
|
||||||
|
|
||||||
|
def create
|
||||||
|
return head :not_found if Docuseal.multitenant?
|
||||||
|
|
||||||
|
test_timeserver_url(@encrypted_config.value) if @encrypted_config.value.present?
|
||||||
|
|
||||||
|
if @encrypted_config.value.present? ? @encrypted_config.save : @encrypted_config.delete
|
||||||
|
redirect_back fallback_location: settings_notifications_path, notice: 'Changes have been saved'
|
||||||
|
else
|
||||||
|
redirect_back fallback_location: settings_notifications_path, alert: 'Unable to save'
|
||||||
|
end
|
||||||
|
rescue HexaPDF::Error, SocketError, Submissions::TimestampHandler::TimestampError, OpenSSL::Timestamp::TimestampError
|
||||||
|
redirect_back fallback_location: settings_notifications_path, alert: 'Invalid Timeserver'
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def test_timeserver_url(url)
|
||||||
|
pdf = HexaPDF::Document.new
|
||||||
|
pdf.pages.add
|
||||||
|
|
||||||
|
pkcs = Accounts.load_signing_pkcs(current_account)
|
||||||
|
|
||||||
|
pdf.sign(StringIO.new,
|
||||||
|
reason: 'Test',
|
||||||
|
certificate: pkcs.certificate,
|
||||||
|
key: pkcs.key,
|
||||||
|
signature_size: 10_000,
|
||||||
|
certificate_chain: pkcs.ca_certs || [],
|
||||||
|
timestamp_handler: Submissions::TimestampHandler.new(tsa_url: url))
|
||||||
|
end
|
||||||
|
|
||||||
|
def load_encrypted_config
|
||||||
|
@encrypted_config
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_encrypted_config
|
||||||
|
@encrypted_config =
|
||||||
|
EncryptedConfig.find_or_initialize_by(account: current_account,
|
||||||
|
key: EncryptedConfig::TIMESTAMP_SERVER_URL_KEY)
|
||||||
|
|
||||||
|
@encrypted_config.assign_attributes(encrypted_config_params)
|
||||||
|
end
|
||||||
|
|
||||||
|
def encrypted_config_params
|
||||||
|
params.require(:encrypted_config).permit(:value)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class UserConfigsController < ApplicationController
|
||||||
|
before_action :load_user_config
|
||||||
|
authorize_resource :user_config
|
||||||
|
|
||||||
|
ALLOWED_KEYS = [
|
||||||
|
UserConfig::RECEIVE_COMPLETED_EMAIL
|
||||||
|
].freeze
|
||||||
|
|
||||||
|
InvalidKey = Class.new(StandardError)
|
||||||
|
|
||||||
|
def create
|
||||||
|
@user_config.update!(user_config_params)
|
||||||
|
|
||||||
|
head :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def load_user_config
|
||||||
|
raise InvalidKey unless ALLOWED_KEYS.include?(user_config_params[:key])
|
||||||
|
|
||||||
|
@user_config =
|
||||||
|
UserConfig.find_or_initialize_by(user: current_user, key: user_config_params[:key])
|
||||||
|
end
|
||||||
|
|
||||||
|
def user_config_params
|
||||||
|
params.required(:user_config).permit!.tap do |attrs|
|
||||||
|
attrs[:value] = attrs[:value] == '1' if attrs[:value].in?(%w[1 0])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -28,6 +28,12 @@ class UserSignaturesController < ApplicationController
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
@user_config.destroy
|
||||||
|
|
||||||
|
redirect_to settings_profile_index_path, notice: 'Signature has been removed'
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def load_user_config
|
def load_user_config
|
||||||
|
|||||||
@@ -1,13 +1,20 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class UsersController < ApplicationController
|
class UsersController < ApplicationController
|
||||||
load_and_authorize_resource :user, only: %i[index edit new update destroy]
|
load_and_authorize_resource :user, only: %i[index edit update destroy]
|
||||||
|
|
||||||
before_action :build_user, only: :create
|
before_action :build_user, only: %i[new create]
|
||||||
authorize_resource :user, only: :create
|
authorize_resource :user, only: %i[new create]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@pagy, @users = pagy(@users.active.order(id: :desc))
|
@users =
|
||||||
|
if params[:status] == 'archived'
|
||||||
|
@users.archived
|
||||||
|
else
|
||||||
|
@users.active
|
||||||
|
end
|
||||||
|
|
||||||
|
@pagy, @users = pagy(@users.where(account: current_account).order(id: :desc))
|
||||||
end
|
end
|
||||||
|
|
||||||
def new; end
|
def new; end
|
||||||
@@ -15,10 +22,19 @@ class UsersController < ApplicationController
|
|||||||
def edit; end
|
def edit; end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
|
existing_user = User.accessible_by(current_ability).find_by(email: @user.email)
|
||||||
|
|
||||||
|
if existing_user
|
||||||
|
existing_user.archived_at = nil
|
||||||
|
existing_user.assign_attributes(user_params)
|
||||||
|
|
||||||
|
@user = existing_user
|
||||||
|
end
|
||||||
|
|
||||||
if @user.save
|
if @user.save
|
||||||
UserMailer.invitation_email(@user).deliver_later!
|
UserMailer.invitation_email(@user).deliver_later!
|
||||||
|
|
||||||
redirect_to settings_users_path, notice: 'User has been invited'
|
redirect_back fallback_location: settings_users_path, notice: 'User has been invited'
|
||||||
else
|
else
|
||||||
render turbo_stream: turbo_stream.replace(:modal, template: 'users/new'), status: :unprocessable_entity
|
render turbo_stream: turbo_stream.replace(:modal, template: 'users/new'), status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
@@ -27,11 +43,11 @@ class UsersController < ApplicationController
|
|||||||
def update
|
def update
|
||||||
return redirect_to settings_users_path, notice: 'Unable to update user.' if Docuseal.demo?
|
return redirect_to settings_users_path, notice: 'Unable to update user.' if Docuseal.demo?
|
||||||
|
|
||||||
attrs = user_params.compact_blank
|
attrs = user_params.compact_blank.merge(user_params.slice(:archived_at))
|
||||||
attrs.delete(:role) if User::ROLES.exclude?(attrs[:role])
|
attrs.delete(:role) if !role_valid?(attrs[:role]) || current_user == @user
|
||||||
|
|
||||||
if @user.update(attrs.except(current_user == @user ? :role : nil))
|
if @user.update(attrs)
|
||||||
redirect_to settings_users_path, notice: 'User has been updated'
|
redirect_back fallback_location: settings_users_path, notice: 'User has been updated'
|
||||||
else
|
else
|
||||||
render turbo_stream: turbo_stream.replace(:modal, template: 'users/edit'), status: :unprocessable_entity
|
render turbo_stream: turbo_stream.replace(:modal, template: 'users/edit'), status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
@@ -42,17 +58,21 @@ class UsersController < ApplicationController
|
|||||||
return redirect_to settings_users_path, notice: 'Unable to remove user'
|
return redirect_to settings_users_path, notice: 'Unable to remove user'
|
||||||
end
|
end
|
||||||
|
|
||||||
@user.update!(deleted_at: Time.current)
|
@user.update!(archived_at: Time.current)
|
||||||
|
|
||||||
redirect_to settings_users_path, notice: 'User has been removed'
|
redirect_back fallback_location: settings_users_path, notice: 'User has been removed'
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def role_valid?(role)
|
||||||
|
User::ROLES.include?(role)
|
||||||
|
end
|
||||||
|
|
||||||
def build_user
|
def build_user
|
||||||
@user = current_account.users.find_by(email: user_params[:email])&.tap do |user|
|
@user = current_account.users.find_by(email: user_params[:email])&.tap do |user|
|
||||||
user.assign_attributes(user_params)
|
user.assign_attributes(user_params)
|
||||||
user.deleted_at = nil
|
user.archived_at = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
@user ||= current_account.users.new(user_params)
|
@user ||= current_account.users.new(user_params)
|
||||||
@@ -61,6 +81,11 @@ class UsersController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def user_params
|
def user_params
|
||||||
params.require(:user).permit(:email, :first_name, :last_name, :password, :role)
|
if params.key?(:user)
|
||||||
|
params.require(:user).permit(:email, :first_name, :last_name, :password,
|
||||||
|
:role, :archived_at, :account_id)
|
||||||
|
else
|
||||||
|
{}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -9,11 +9,14 @@ class VerifyPdfSignatureController < ApplicationController
|
|||||||
HexaPDF::Document.new(io: file.open)
|
HexaPDF::Document.new(io: file.open)
|
||||||
end
|
end
|
||||||
|
|
||||||
cert_data = if Docuseal.multitenant?
|
cert_data =
|
||||||
Docuseal::CERTS
|
if Docuseal.multitenant?
|
||||||
else
|
value = EncryptedConfig.find_by(account: current_account, key: EncryptedConfig::ESIGN_CERTS_KEY)&.value || {}
|
||||||
EncryptedConfig.find_by(account: current_account, key: EncryptedConfig::ESIGN_CERTS_KEY)&.value || {}
|
|
||||||
end
|
Docuseal::CERTS.merge(value)
|
||||||
|
else
|
||||||
|
EncryptedConfig.find_by(key: EncryptedConfig::ESIGN_CERTS_KEY)&.value || {}
|
||||||
|
end
|
||||||
|
|
||||||
default_pkcs = GenerateCertificate.load_pkcs(cert_data)
|
default_pkcs = GenerateCertificate.load_pkcs(cert_data)
|
||||||
|
|
||||||
@@ -24,7 +27,8 @@ class VerifyPdfSignatureController < ApplicationController
|
|||||||
trusted_certs = [default_pkcs.certificate,
|
trusted_certs = [default_pkcs.certificate,
|
||||||
*default_pkcs.ca_certs,
|
*default_pkcs.ca_certs,
|
||||||
*custom_certs.map(&:certificate),
|
*custom_certs.map(&:certificate),
|
||||||
*custom_certs.flat_map(&:ca_certs).compact]
|
*custom_certs.flat_map(&:ca_certs).compact,
|
||||||
|
*Docuseal.trusted_certs]
|
||||||
|
|
||||||
render turbo_stream: turbo_stream.replace('result', partial: 'result',
|
render turbo_stream: turbo_stream.replace('result', partial: 'result',
|
||||||
locals: { pdfs:, files: params[:files], trusted_certs: })
|
locals: { pdfs:, files: params[:files], trusted_certs: })
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class WebhookPreferencesController < ApplicationController
|
||||||
|
EVENTS = %w[
|
||||||
|
form.viewed
|
||||||
|
form.started
|
||||||
|
form.completed
|
||||||
|
template.created
|
||||||
|
template.updated
|
||||||
|
submission.archived
|
||||||
|
].freeze
|
||||||
|
|
||||||
|
before_action :load_account_config
|
||||||
|
authorize_resource :account_config, parent: false
|
||||||
|
|
||||||
|
def create
|
||||||
|
@account_config.value[account_config_params[:event]] = account_config_params[:value] == '1'
|
||||||
|
|
||||||
|
@account_config.save!
|
||||||
|
|
||||||
|
head :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def load_account_config
|
||||||
|
@account_config =
|
||||||
|
current_account.account_configs.find_or_initialize_by(key: AccountConfig::WEBHOOK_PREFERENCES_KEY)
|
||||||
|
@account_config.value ||= {}
|
||||||
|
end
|
||||||
|
|
||||||
|
def account_config_params
|
||||||
|
params.permit(:event, :value)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -7,7 +7,9 @@ class WebhookSettingsController < ApplicationController
|
|||||||
def show; end
|
def show; end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@encrypted_config.update!(encrypted_config_params)
|
@encrypted_config.assign_attributes(encrypted_config_params)
|
||||||
|
|
||||||
|
@encrypted_config.value.present? ? @encrypted_config.save! : @encrypted_config.delete
|
||||||
|
|
||||||
redirect_back(fallback_location: settings_webhooks_path, notice: 'Webhook URL has been saved.')
|
redirect_back(fallback_location: settings_webhooks_path, notice: 'Webhook URL has been saved.')
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ import { encodeMethodIntoRequestBody } from '@hotwired/turbo-rails/app/javascrip
|
|||||||
|
|
||||||
import { createApp, reactive } from 'vue'
|
import { createApp, reactive } from 'vue'
|
||||||
import TemplateBuilder from './template_builder/builder'
|
import TemplateBuilder from './template_builder/builder'
|
||||||
|
import ImportList from './template_builder/import_list'
|
||||||
|
|
||||||
import ToggleVisible from './elements/toggle_visible'
|
import ToggleVisible from './elements/toggle_visible'
|
||||||
|
import ToggleCookies from './elements/toggle_cookies'
|
||||||
import DisableHidden from './elements/disable_hidden'
|
import DisableHidden from './elements/disable_hidden'
|
||||||
import TurboModal from './elements/turbo_modal'
|
import TurboModal from './elements/turbo_modal'
|
||||||
import FileDropzone from './elements/file_dropzone'
|
import FileDropzone from './elements/file_dropzone'
|
||||||
@@ -18,6 +20,10 @@ import AutoresizeTextarea from './elements/autoresize_textarea'
|
|||||||
import SubmittersAutocomplete from './elements/submitter_autocomplete'
|
import SubmittersAutocomplete from './elements/submitter_autocomplete'
|
||||||
import FolderAutocomplete from './elements/folder_autocomplete'
|
import FolderAutocomplete from './elements/folder_autocomplete'
|
||||||
import SignatureForm from './elements/signature_form'
|
import SignatureForm from './elements/signature_form'
|
||||||
|
import SubmitForm from './elements/submit_form'
|
||||||
|
import PromptPassword from './elements/prompt_password'
|
||||||
|
import EmailsTextarea from './elements/emails_textarea'
|
||||||
|
import ToggleOnSubmit from './elements/toggle_on_submit'
|
||||||
|
|
||||||
import * as TurboInstantClick from './lib/turbo_instant_click'
|
import * as TurboInstantClick from './lib/turbo_instant_click'
|
||||||
|
|
||||||
@@ -49,6 +55,11 @@ window.customElements.define('autoresize-textarea', AutoresizeTextarea)
|
|||||||
window.customElements.define('submitters-autocomplete', SubmittersAutocomplete)
|
window.customElements.define('submitters-autocomplete', SubmittersAutocomplete)
|
||||||
window.customElements.define('folder-autocomplete', FolderAutocomplete)
|
window.customElements.define('folder-autocomplete', FolderAutocomplete)
|
||||||
window.customElements.define('signature-form', SignatureForm)
|
window.customElements.define('signature-form', SignatureForm)
|
||||||
|
window.customElements.define('submit-form', SubmitForm)
|
||||||
|
window.customElements.define('prompt-password', PromptPassword)
|
||||||
|
window.customElements.define('emails-textarea', EmailsTextarea)
|
||||||
|
window.customElements.define('toggle-cookies', ToggleCookies)
|
||||||
|
window.customElements.define('toggle-on-submit', ToggleOnSubmit)
|
||||||
|
|
||||||
document.addEventListener('turbo:before-fetch-request', encodeMethodIntoRequestBody)
|
document.addEventListener('turbo:before-fetch-request', encodeMethodIntoRequestBody)
|
||||||
document.addEventListener('turbo:submit-end', async (event) => {
|
document.addEventListener('turbo:submit-end', async (event) => {
|
||||||
@@ -77,13 +88,41 @@ window.customElements.define('template-builder', class extends HTMLElement {
|
|||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
this.appElem = document.createElement('div')
|
this.appElem = document.createElement('div')
|
||||||
|
|
||||||
|
this.appElem.classList.add('md:h-screen')
|
||||||
|
|
||||||
this.app = createApp(TemplateBuilder, {
|
this.app = createApp(TemplateBuilder, {
|
||||||
template: reactive(JSON.parse(this.dataset.template)),
|
template: reactive(JSON.parse(this.dataset.template)),
|
||||||
backgroundColor: '#faf7f5',
|
backgroundColor: '#faf7f5',
|
||||||
withPhone: this.dataset.withPhone === 'true',
|
withPhone: this.dataset.withPhone === 'true',
|
||||||
withLogo: this.dataset.withLogo !== 'false',
|
withLogo: this.dataset.withLogo !== 'false',
|
||||||
acceptFileTypes: this.dataset.acceptFileTypes,
|
editable: this.dataset.editable !== 'false',
|
||||||
isDirectUpload: this.dataset.isDirectUpload === 'true'
|
authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content,
|
||||||
|
withPayment: this.dataset.withPayment === 'true',
|
||||||
|
isPaymentConnected: this.dataset.isPaymentConnected === 'true',
|
||||||
|
withFormula: this.dataset.withFormula === 'true',
|
||||||
|
withConditions: this.dataset.withConditions === 'true',
|
||||||
|
currencies: (this.dataset.currencies || '').split(',').filter(Boolean),
|
||||||
|
acceptFileTypes: this.dataset.acceptFileTypes
|
||||||
|
})
|
||||||
|
|
||||||
|
this.app.mount(this.appElem)
|
||||||
|
|
||||||
|
this.appendChild(this.appElem)
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback () {
|
||||||
|
this.app?.unmount()
|
||||||
|
this.appElem?.remove()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
window.customElements.define('import-list', class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
this.appElem = document.createElement('div')
|
||||||
|
|
||||||
|
this.app = createApp(ImportList, {
|
||||||
|
template: JSON.parse(this.dataset.template),
|
||||||
|
authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content
|
||||||
})
|
})
|
||||||
|
|
||||||
this.app.mount(this.appElem)
|
this.app.mount(this.appElem)
|
||||||
|
|||||||
@@ -83,6 +83,11 @@ button[disabled] .enabled {
|
|||||||
bottom: auto;
|
bottom: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tooltip-pre:before {
|
||||||
|
white-space: pre;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
.tooltip-bottom-end:after {
|
.tooltip-bottom-end:after {
|
||||||
transform: translateX(-25%);
|
transform: translateX(-25%);
|
||||||
border-color: transparent transparent var(--tooltip-color) transparent;
|
border-color: transparent transparent var(--tooltip-color) transparent;
|
||||||
@@ -125,3 +130,7 @@ button[disabled] .enabled {
|
|||||||
outline-offset: 3px;
|
outline-offset: 3px;
|
||||||
outline-color: hsl(var(--bc) / 0.2);
|
outline-color: hsl(var(--bc) / 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
select:required:invalid {
|
||||||
|
color: gray !important;
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ export default class extends HTMLElement {
|
|||||||
|
|
||||||
resize () {
|
resize () {
|
||||||
if (this.textarea.clientHeight < this.textarea.scrollHeight) {
|
if (this.textarea.clientHeight < this.textarea.scrollHeight) {
|
||||||
this.textarea.style.height = `${this.textarea.scrollHeight}px`
|
this.textarea.style.height = `${this.textarea.scrollHeight + 5}px`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,27 +18,64 @@ export default targetable(class extends HTMLElement {
|
|||||||
this.toggleState()
|
this.toggleState()
|
||||||
|
|
||||||
fetch(this.dataset.src).then((response) => response.json()).then((urls) => {
|
fetch(this.dataset.src).then((response) => response.json()).then((urls) => {
|
||||||
const fileRequests = urls.map((url) => {
|
const isSafariIos = /iPhone|iPad|iPod/i.test(navigator.userAgent)
|
||||||
return () => {
|
|
||||||
return fetch(url).then(async (resp) => {
|
|
||||||
const blobUrl = URL.createObjectURL(await resp.blob())
|
|
||||||
const link = document.createElement('a')
|
|
||||||
|
|
||||||
link.href = blobUrl
|
if (isSafariIos && urls.length > 1) {
|
||||||
link.setAttribute('download', decodeURI(url.split('/').pop()))
|
this.downloadSafariIos(urls)
|
||||||
|
} else {
|
||||||
|
this.downloadUrls(urls)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
link.click()
|
downloadUrls (urls) {
|
||||||
|
const fileRequests = urls.map((url) => {
|
||||||
|
return () => {
|
||||||
|
return fetch(url).then(async (resp) => {
|
||||||
|
const blobUrl = URL.createObjectURL(await resp.blob())
|
||||||
|
const link = document.createElement('a')
|
||||||
|
|
||||||
URL.revokeObjectURL(url)
|
link.href = blobUrl
|
||||||
})
|
link.setAttribute('download', decodeURI(url.split('/').pop()))
|
||||||
}
|
|
||||||
|
link.click()
|
||||||
|
|
||||||
|
URL.revokeObjectURL(blobUrl)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
fileRequests.reduce(
|
||||||
|
(prevPromise, request) => prevPromise.then(() => request()),
|
||||||
|
Promise.resolve()
|
||||||
|
).finally(() => {
|
||||||
|
this.toggleState()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadSafariIos (urls) {
|
||||||
|
const fileRequests = urls.map((url) => {
|
||||||
|
return fetch(url).then(async (resp) => {
|
||||||
|
const blob = await resp.blob()
|
||||||
|
const blobUrl = URL.createObjectURL(blob.slice(0, blob.size, 'application/octet-stream'))
|
||||||
|
const link = document.createElement('a')
|
||||||
|
|
||||||
|
link.href = blobUrl
|
||||||
|
link.setAttribute('download', decodeURI(url.split('/').pop()))
|
||||||
|
|
||||||
|
return link
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
|
||||||
fileRequests.reduce(
|
Promise.all(fileRequests).then((links) => {
|
||||||
(prevPromise, request) => prevPromise.then(() => request()),
|
links.forEach((link, index) => {
|
||||||
Promise.resolve()
|
setTimeout(() => {
|
||||||
)
|
link.click()
|
||||||
|
|
||||||
|
URL.revokeObjectURL(link.href)
|
||||||
|
}, index * 50)
|
||||||
|
})
|
||||||
|
}).finally(() => {
|
||||||
this.toggleState()
|
this.toggleState()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
const emailRegexp = /([^@;,<>\s]+@[^@;,<>\s]+)/g
|
||||||
|
|
||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
if (this.dataset.limit) {
|
||||||
|
this.textarea.addEventListener('input', () => {
|
||||||
|
const emails = this.textarea.value.match(emailRegexp) || []
|
||||||
|
|
||||||
|
this.updateCounter(emails.length)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCounter (count) {
|
||||||
|
let counter = document.getElementById('emails_counter')
|
||||||
|
let bulkMessage = document.getElementById('bulk_message')
|
||||||
|
|
||||||
|
if (count < 2) {
|
||||||
|
counter?.remove()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((count + 10) > this.dataset.limit) {
|
||||||
|
if (!counter) {
|
||||||
|
counter = document.createElement('span')
|
||||||
|
|
||||||
|
counter.id = 'emails_counter'
|
||||||
|
counter.classList.add('text-xs', 'right-0', 'absolute')
|
||||||
|
counter.style.bottom = '-15px'
|
||||||
|
|
||||||
|
this.textarea.parentNode.append(counter)
|
||||||
|
}
|
||||||
|
|
||||||
|
counter.innerText = `${count} / ${this.dataset.limit}`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.dataset.bulkEnabled !== 'true') {
|
||||||
|
if (!bulkMessage) {
|
||||||
|
bulkMessage = document.createElement('span')
|
||||||
|
|
||||||
|
bulkMessage.id = 'bulk_message'
|
||||||
|
bulkMessage.classList.add('text-xs', 'left-0', 'absolute')
|
||||||
|
bulkMessage.style.bottom = '-15px'
|
||||||
|
|
||||||
|
this.textarea.parentNode.append(bulkMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
bulkMessage.innerHTML = '<a class="link" data-turbo="false" href="/upgrade">Upgrade</a> to bulk send multiple recipients'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get textarea () {
|
||||||
|
return this.querySelector('textarea')
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,10 +9,6 @@ export default actionable(targetable(class extends HTMLElement {
|
|||||||
]
|
]
|
||||||
|
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
if (this.dataset.isDirectUpload === 'true') {
|
|
||||||
import('@rails/activestorage')
|
|
||||||
}
|
|
||||||
|
|
||||||
this.addEventListener('drop', this.onDrop)
|
this.addEventListener('drop', this.onDrop)
|
||||||
|
|
||||||
this.addEventListener('dragover', (e) => e.preventDefault())
|
this.addEventListener('dragover', (e) => e.preventDefault())
|
||||||
@@ -48,61 +44,11 @@ export default actionable(targetable(class extends HTMLElement {
|
|||||||
this.classList.toggle('opacity-50')
|
this.classList.toggle('opacity-50')
|
||||||
}
|
}
|
||||||
|
|
||||||
async uploadFiles (files) {
|
uploadFiles () {
|
||||||
this.toggleLoading()
|
this.toggleLoading()
|
||||||
|
|
||||||
if (this.dataset.isDirectUpload === 'true') {
|
if (this.dataset.submitOnUpload) {
|
||||||
const { DirectUpload } = await import('@rails/activestorage')
|
this.closest('form').querySelector('button[type="submit"]').click()
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
Array.from(files).map(async (file) => {
|
|
||||||
const upload = new DirectUpload(
|
|
||||||
file,
|
|
||||||
'/direct_uploads',
|
|
||||||
this.input
|
|
||||||
)
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
upload.create((error, blob) => {
|
|
||||||
if (error) {
|
|
||||||
console.error(error)
|
|
||||||
|
|
||||||
return reject(error)
|
|
||||||
} else {
|
|
||||||
return resolve(blob)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}).catch((error) => {
|
|
||||||
console.error(error)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
).then((blobs) => {
|
|
||||||
if (this.dataset.submitOnUpload) {
|
|
||||||
this.querySelectorAll('[name="blob_signed_ids[]"]').forEach((e) => e.remove())
|
|
||||||
}
|
|
||||||
|
|
||||||
blobs.forEach((blob) => {
|
|
||||||
const input = document.createElement('input')
|
|
||||||
|
|
||||||
input.type = 'hidden'
|
|
||||||
input.name = 'blob_signed_ids[]'
|
|
||||||
input.value = blob.signed_id
|
|
||||||
|
|
||||||
this.append(input)
|
|
||||||
})
|
|
||||||
|
|
||||||
if (this.dataset.submitOnUpload === 'true') {
|
|
||||||
this.closest('form').querySelector('button[type="submit"]').click()
|
|
||||||
}
|
|
||||||
}).finally(() => {
|
|
||||||
if (this.dataset.submitOnUpload !== 'true') {
|
|
||||||
this.toggleLoading()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
if (this.dataset.submitOnUpload) {
|
|
||||||
this.closest('form').querySelector('button[type="submit"]').click()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
@@ -20,7 +20,7 @@ export default class extends HTMLElement {
|
|||||||
fetch = (text, resolve) => {
|
fetch = (text, resolve) => {
|
||||||
const queryParams = new URLSearchParams({ q: text })
|
const queryParams = new URLSearchParams({ q: text })
|
||||||
|
|
||||||
fetch('/api/template_folders_autocomplete?' + queryParams).then(async (resp) => {
|
fetch('/template_folders_autocomplete?' + queryParams).then(async (resp) => {
|
||||||
const items = await resp.json()
|
const items = await resp.json()
|
||||||
|
|
||||||
resolve(items)
|
resolve(items)
|
||||||
@@ -32,6 +32,8 @@ export default class extends HTMLElement {
|
|||||||
render = (item) => {
|
render = (item) => {
|
||||||
const div = document.createElement('div')
|
const div = document.createElement('div')
|
||||||
|
|
||||||
|
div.setAttribute('dir', 'auto')
|
||||||
|
|
||||||
div.textContent = item.name
|
div.textContent = item.name
|
||||||
|
|
||||||
return div
|
return div
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
const input = document.createElement('input')
|
||||||
|
|
||||||
|
input.type = 'hidden'
|
||||||
|
input.name = 'password'
|
||||||
|
input.value = prompt('Enter PDF password')
|
||||||
|
|
||||||
|
this.form.append(input)
|
||||||
|
|
||||||
|
this.form.requestSubmit()
|
||||||
|
|
||||||
|
this.remove()
|
||||||
|
}
|
||||||
|
|
||||||
|
get form () {
|
||||||
|
return this.closest('form')
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,21 @@
|
|||||||
export default class extends HTMLElement {
|
export default class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
if (this.dataset.inputId) {
|
if (this.dataset.inputId) {
|
||||||
document.getElementById(this.dataset.inputId).value = Intl.DateTimeFormat().resolvedOptions().timeZone
|
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||||
|
|
||||||
|
if (this.dataset.params === 'true') {
|
||||||
|
const params = new URLSearchParams(this.input.value)
|
||||||
|
|
||||||
|
params.set('timezone', timezone)
|
||||||
|
|
||||||
|
this.input.value = params.toString()
|
||||||
|
} else {
|
||||||
|
this.input.value = timezone
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get input () {
|
||||||
|
return document.getElementById(this.dataset.inputId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
this.querySelector('form').requestSubmit()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ export default class extends HTMLElement {
|
|||||||
preventSubmit: 1,
|
preventSubmit: 1,
|
||||||
minLength: 1,
|
minLength: 1,
|
||||||
showOnFocus: true,
|
showOnFocus: true,
|
||||||
|
debounceWaitMs: 200,
|
||||||
onSelect: this.onSelect,
|
onSelect: this.onSelect,
|
||||||
render: this.render,
|
render: this.render,
|
||||||
fetch: this.fetch
|
fetch: this.fetch
|
||||||
@@ -27,6 +28,8 @@ export default class extends HTMLElement {
|
|||||||
|
|
||||||
if (textarea && item[field]) {
|
if (textarea && item[field]) {
|
||||||
textarea.value = textarea.value.replace(/[^;,\s]+$/, item[field] + ' ')
|
textarea.value = textarea.value.replace(/[^;,\s]+$/, item[field] + ' ')
|
||||||
|
|
||||||
|
textarea.dispatchEvent(new Event('input', { bubbles: true }))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -37,7 +40,9 @@ export default class extends HTMLElement {
|
|||||||
if (q) {
|
if (q) {
|
||||||
const queryParams = new URLSearchParams({ q, field: this.dataset.field })
|
const queryParams = new URLSearchParams({ q, field: this.dataset.field })
|
||||||
|
|
||||||
fetch('/api/submitters_autocomplete?' + queryParams).then(async (resp) => {
|
this.currentFetch ||= fetch('/submitters_autocomplete?' + queryParams)
|
||||||
|
|
||||||
|
this.currentFetch.then(async (resp) => {
|
||||||
const items = await resp.json()
|
const items = await resp.json()
|
||||||
|
|
||||||
if (q.length < 3) {
|
if (q.length < 3) {
|
||||||
@@ -47,6 +52,8 @@ export default class extends HTMLElement {
|
|||||||
}
|
}
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
resolve([])
|
resolve([])
|
||||||
|
}).finally(() => {
|
||||||
|
this.currentFetch = null
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
resolve([])
|
resolve([])
|
||||||
@@ -56,6 +63,8 @@ export default class extends HTMLElement {
|
|||||||
render = (item) => {
|
render = (item) => {
|
||||||
const div = document.createElement('div')
|
const div = document.createElement('div')
|
||||||
|
|
||||||
|
div.setAttribute('dir', 'auto')
|
||||||
|
|
||||||
div.textContent = item[this.dataset.field]
|
div.textContent = item[this.dataset.field]
|
||||||
|
|
||||||
return div
|
return div
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
this.button.addEventListener('click', () => {
|
||||||
|
const expirationDate = new Date()
|
||||||
|
|
||||||
|
expirationDate.setFullYear(expirationDate.getFullYear() + 10)
|
||||||
|
|
||||||
|
const expires = expirationDate.toUTCString()
|
||||||
|
|
||||||
|
document.cookie = this.dataset.key + '=' + this.dataset.value + '; expires=' + expires + '; path=/'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
get button () {
|
||||||
|
return this.querySelector('button')
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
document.addEventListener('turbo:submit-end', this.onSubmitEnd)
|
||||||
|
|
||||||
|
this.form.addEventListener('submit', this.onSubmit)
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback () {
|
||||||
|
document.removeEventListener('turbo:submit-end', this.onSubmitEnd)
|
||||||
|
|
||||||
|
this.form.removeEventListener('submit', this.onSubmit)
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubmit = () => {
|
||||||
|
this.element.classList.add('invisible')
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubmitEnd = (event) => {
|
||||||
|
if (event.target === this.form) {
|
||||||
|
const resp = event.detail?.formSubmission?.result?.fetchResponse?.response
|
||||||
|
|
||||||
|
if (resp?.status / 100 === 2) {
|
||||||
|
this.element.classList.remove('invisible')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get element () {
|
||||||
|
return document.getElementById(this.dataset.elementId)
|
||||||
|
}
|
||||||
|
|
||||||
|
get form () {
|
||||||
|
return this.closest('form')
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,15 +10,18 @@ window.customElements.define('submission-form', class extends HTMLElement {
|
|||||||
|
|
||||||
this.app = createApp(Form, {
|
this.app = createApp(Form, {
|
||||||
submitter: JSON.parse(this.dataset.submitter),
|
submitter: JSON.parse(this.dataset.submitter),
|
||||||
authenticityToken: this.dataset.authenticityToken,
|
|
||||||
canSendEmail: this.dataset.canSendEmail === 'true',
|
canSendEmail: this.dataset.canSendEmail === 'true',
|
||||||
isDirectUpload: this.dataset.isDirectUpload === 'true',
|
|
||||||
goToLast: this.dataset.goToLast === 'true',
|
goToLast: this.dataset.goToLast === 'true',
|
||||||
isDemo: this.dataset.isDemo === 'true',
|
isDemo: this.dataset.isDemo === 'true',
|
||||||
attribution: this.dataset.attribution !== 'false',
|
attribution: this.dataset.attribution !== 'false',
|
||||||
withConfetti: true,
|
withConfetti: this.dataset.withConfetti !== 'false',
|
||||||
|
withDisclosure: this.dataset.withDisclosure === 'true',
|
||||||
|
withTypedSignature: this.dataset.withTypedSignature !== 'false',
|
||||||
|
authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content,
|
||||||
values: reactive(JSON.parse(this.dataset.values)),
|
values: reactive(JSON.parse(this.dataset.values)),
|
||||||
completedButton: JSON.parse(this.dataset.completedButton),
|
completedButton: JSON.parse(this.dataset.completedButton || '{}'),
|
||||||
|
completedMessage: JSON.parse(this.dataset.completedMessage || '{}'),
|
||||||
|
completedRedirectUrl: this.dataset.completedRedirectUrl,
|
||||||
attachments: reactive(JSON.parse(this.dataset.attachments)),
|
attachments: reactive(JSON.parse(this.dataset.attachments)),
|
||||||
fields: JSON.parse(this.dataset.fields)
|
fields: JSON.parse(this.dataset.fields)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -47,6 +47,10 @@ select:required:invalid {
|
|||||||
@apply border-base-content/20;
|
@apply border-base-content/20;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.base-textarea {
|
||||||
|
@apply textarea textarea-bordered bg-white rounded-3xl;
|
||||||
|
}
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
@apply no-animation;
|
@apply no-animation;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="uniqueAreas.length > 1"
|
||||||
|
dir="auto"
|
||||||
|
class="mb-2"
|
||||||
|
>
|
||||||
|
<div class="flex space-x-1 text-sm ml-1 -mt-3">
|
||||||
|
<span>
|
||||||
|
{{ t('appears_on') }}:
|
||||||
|
</span>
|
||||||
|
<a
|
||||||
|
v-for="area in uniqueAreas"
|
||||||
|
:key="area.uuid"
|
||||||
|
href="#"
|
||||||
|
class="link pr-0.5"
|
||||||
|
@click.prevent="scrollIntoArea(area)"
|
||||||
|
>
|
||||||
|
{{ t('page') }} <template v-if="isMultipleDocs">{{ attachmentUuids.indexOf(area.attachment_uuid) + 1 }}-</template>{{ area.page + 1 }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'AppearsOn',
|
||||||
|
inject: ['t', 'scrollIntoArea'],
|
||||||
|
props: {
|
||||||
|
field: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isMultipleDocs () {
|
||||||
|
return this.attachmentUuids.length > 1
|
||||||
|
},
|
||||||
|
attachmentUuids () {
|
||||||
|
return [...new Set(this.uniqueAreas.map((e) => e.attachment_uuid))]
|
||||||
|
},
|
||||||
|
uniqueAreas () {
|
||||||
|
const areas = {}
|
||||||
|
|
||||||
|
this.field.areas?.forEach((area) => {
|
||||||
|
areas[area.attachment_uuid + area.page] ||= area
|
||||||
|
})
|
||||||
|
|
||||||
|
return Object.values(areas).slice(0, 6)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="flex absolute text-[1.5vw] lg:text-base"
|
class="field-area flex absolute lg:text-base -outline-offset-1"
|
||||||
|
dir="auto"
|
||||||
:style="computedStyle"
|
:style="computedStyle"
|
||||||
:class="{ 'cursor-default': !submittable, 'bg-red-100 border cursor-pointer ': submittable, 'border-red-100': !isActive && submittable, 'bg-opacity-70': !isActive && !isValueSet && submittable, 'border-red-500 border-dashed z-10': isActive && submittable, 'bg-opacity-30': (isActive || isValueSet) && submittable }"
|
:class="{ 'text-[1.5vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars, 'cursor-default': !submittable, 'border border-red-100 bg-red-100 cursor-pointer': submittable, 'border border-red-100': !isActive && submittable, 'bg-opacity-80': !isActive && !isValueSet && submittable, 'field-area-active outline-red-500 outline-dashed outline-2 z-10': isActive && submittable, 'bg-opacity-40': (isActive || isValueSet) && submittable }"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="!isActive && !isValueSet && field.type !== 'checkbox' && submittable"
|
v-if="!isActive && !isValueSet && field.type !== 'checkbox' && submittable && !area.option_uuid"
|
||||||
class="absolute top-0 bottom-0 right-0 left-0 items-center justify-center h-full w-full"
|
class="absolute top-0 bottom-0 right-0 left-0 items-center justify-center h-full w-full"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@@ -21,15 +22,20 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="isActive && withLabel"
|
v-if="isActive && withLabel && (!area.option_uuid || !option.value)"
|
||||||
class="absolute -top-7 rounded bg-base-content text-base-100 px-2 text-sm whitespace-nowrap"
|
class="absolute -top-7 rounded bg-base-content text-base-100 px-2 text-sm whitespace-nowrap pointer-events-none"
|
||||||
>
|
>
|
||||||
{{ field.name || fieldNames[field.type] }}
|
<template v-if="area.option_uuid && !option.value">
|
||||||
<template v-if="field.type === 'checkbox' && !field.name">
|
{{ optionValue(option) }}
|
||||||
{{ fieldIndex + 1 }}
|
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="!field.required && field.type !== 'checkbox'">
|
<template v-else>
|
||||||
(optional)
|
{{ field.name || fieldNames[field.type] }}
|
||||||
|
<template v-if="field.type === 'checkbox' && !field.name">
|
||||||
|
{{ fieldIndex + 1 }}
|
||||||
|
</template>
|
||||||
|
<template v-else-if="!field.required && field.type !== 'checkbox'">
|
||||||
|
(optional)
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -42,6 +48,11 @@
|
|||||||
class="object-contain mx-auto"
|
class="object-contain mx-auto"
|
||||||
:src="image.url"
|
:src="image.url"
|
||||||
>
|
>
|
||||||
|
<img
|
||||||
|
v-else-if="field.type === 'stamp' && stamp"
|
||||||
|
class="object-contain mx-auto"
|
||||||
|
:src="stamp.url"
|
||||||
|
>
|
||||||
<img
|
<img
|
||||||
v-else-if="field.type === 'signature' && signature"
|
v-else-if="field.type === 'signature' && signature"
|
||||||
class="object-contain mx-auto"
|
class="object-contain mx-auto"
|
||||||
@@ -53,7 +64,7 @@
|
|||||||
:src="initials.url"
|
:src="initials.url"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-else-if="field.type === 'file'"
|
v-else-if="field.type === 'file' || field.type === 'payment'"
|
||||||
class="px-0.5 flex flex-col justify-center"
|
class="px-0.5 flex flex-col justify-center"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
@@ -70,7 +81,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else-if="field.type === 'checkbox'"
|
v-else-if="field.type === 'checkbox'"
|
||||||
class="w-full p-[0.2vw] flex items-center justify-center"
|
class="w-full p-[1px] flex items-center justify-center"
|
||||||
|
@click="$event.target.querySelector('input')?.click()"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
v-if="submittable"
|
v-if="submittable"
|
||||||
@@ -87,6 +99,46 @@
|
|||||||
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
|
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="field.type === 'radio' && area.option_uuid"
|
||||||
|
class="w-full p-[1px] flex items-center justify-center"
|
||||||
|
@click="$event.target.querySelector('input')?.click()"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-if="submittable"
|
||||||
|
type="radio"
|
||||||
|
:value="false"
|
||||||
|
class="aspect-square checked:checkbox checked:checkbox-xs"
|
||||||
|
:class="{ 'base-radio': !modelValue || modelValue !== optionValue(option), '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
|
||||||
|
:checked="!!modelValue && modelValue === optionValue(option)"
|
||||||
|
@click="$emit('update:model-value', optionValue(option))"
|
||||||
|
>
|
||||||
|
<IconCheck
|
||||||
|
v-else-if="!!modelValue && modelValue === optionValue(option)"
|
||||||
|
class="aspect-square"
|
||||||
|
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="field.type === 'multiple' && area.option_uuid"
|
||||||
|
class="w-full p-[1px] flex items-center justify-center"
|
||||||
|
@click="$event.target.querySelector('input')?.click()"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-if="submittable"
|
||||||
|
type="checkbox"
|
||||||
|
:value="false"
|
||||||
|
class="aspect-square base-checkbox"
|
||||||
|
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
|
||||||
|
:checked="!!modelValue && modelValue.includes(optionValue(option))"
|
||||||
|
@change="updateMultipleSelectValue(optionValue(option))"
|
||||||
|
>
|
||||||
|
<IconCheck
|
||||||
|
v-else-if="!!modelValue && modelValue.includes(optionValue(option))"
|
||||||
|
class="aspect-square"
|
||||||
|
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else-if="field.type === 'cells'"
|
v-else-if="field.type === 'cells'"
|
||||||
class="w-full flex items-center"
|
class="w-full flex items-center"
|
||||||
@@ -102,7 +154,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
class="flex items-center px-0.5"
|
ref="textContainer"
|
||||||
|
dir="auto"
|
||||||
|
class="flex items-center px-0.5 w-full"
|
||||||
|
:class="alignClasses[field.preferences?.align]"
|
||||||
>
|
>
|
||||||
<span v-if="Array.isArray(modelValue)">
|
<span v-if="Array.isArray(modelValue)">
|
||||||
{{ modelValue.join(', ') }}
|
{{ modelValue.join(', ') }}
|
||||||
@@ -110,15 +165,20 @@
|
|||||||
<span v-else-if="field.type === 'date'">
|
<span v-else-if="field.type === 'date'">
|
||||||
{{ formattedDate }}
|
{{ formattedDate }}
|
||||||
</span>
|
</span>
|
||||||
<span v-else>
|
<span v-else-if="field.type === 'number'">
|
||||||
{{ modelValue }}
|
{{ formatNumber(modelValue, field.preferences?.format) }}
|
||||||
</span>
|
</span>
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
class="whitespace-pre-wrap"
|
||||||
|
:class="{ 'w-full': field.preferences?.align }"
|
||||||
|
>{{ modelValue }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconCheck, IconColumns3, IconPhoneCheck, IconLetterCaseUpper } from '@tabler/icons-vue'
|
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconCheck, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp, IconSquareNumber1 } from '@tabler/icons-vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FieldArea',
|
name: 'FieldArea',
|
||||||
@@ -126,6 +186,7 @@ export default {
|
|||||||
IconPaperclip,
|
IconPaperclip,
|
||||||
IconCheck
|
IconCheck
|
||||||
},
|
},
|
||||||
|
inject: ['t'],
|
||||||
props: {
|
props: {
|
||||||
field: {
|
field: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -172,37 +233,58 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['update:model-value'],
|
emits: ['update:model-value'],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
textOverflowChars: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
fieldNames () {
|
fieldNames () {
|
||||||
return {
|
return {
|
||||||
text: 'Text',
|
text: this.t('text'),
|
||||||
cells: 'Text',
|
signature: this.t('signature'),
|
||||||
signature: 'Signature',
|
initials: this.t('initials'),
|
||||||
date: 'Date',
|
date: this.t('date'),
|
||||||
image: 'Image',
|
number: this.t('number'),
|
||||||
initials: 'Initials',
|
image: this.t('image'),
|
||||||
file: 'File',
|
file: this.t('file'),
|
||||||
select: 'Select',
|
select: this.t('select'),
|
||||||
checkbox: 'Checkbox',
|
checkbox: this.t('checkbox'),
|
||||||
radio: 'Radio',
|
multiple: this.t('multiple'),
|
||||||
multiple: 'Multiple Select',
|
radio: this.t('radio'),
|
||||||
phone: 'Phone'
|
cells: this.t('cells'),
|
||||||
|
stamp: this.t('stamp'),
|
||||||
|
payment: this.t('payment'),
|
||||||
|
phone: this.t('phone')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
alignClasses () {
|
||||||
|
return {
|
||||||
|
center: 'text-center',
|
||||||
|
left: 'text-left',
|
||||||
|
right: 'text-right'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
option () {
|
||||||
|
return this.field.options.find((o) => o.uuid === this.area.option_uuid)
|
||||||
|
},
|
||||||
fieldIcons () {
|
fieldIcons () {
|
||||||
return {
|
return {
|
||||||
text: IconTextSize,
|
text: IconTextSize,
|
||||||
signature: IconWritingSign,
|
signature: IconWritingSign,
|
||||||
date: IconCalendarEvent,
|
date: IconCalendarEvent,
|
||||||
|
number: IconSquareNumber1,
|
||||||
image: IconPhoto,
|
image: IconPhoto,
|
||||||
initials: IconLetterCaseUpper,
|
initials: IconLetterCaseUpper,
|
||||||
file: IconPaperclip,
|
file: IconPaperclip,
|
||||||
select: IconSelect,
|
select: IconSelect,
|
||||||
checkbox: IconCheckbox,
|
checkbox: IconCheckbox,
|
||||||
radio: IconCircleDot,
|
radio: IconCircleDot,
|
||||||
|
stamp: IconRubberStamp,
|
||||||
cells: IconColumns3,
|
cells: IconColumns3,
|
||||||
multiple: IconChecks,
|
multiple: IconChecks,
|
||||||
phone: IconPhoneCheck
|
phone: IconPhoneCheck,
|
||||||
|
payment: IconCreditCard
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
image () {
|
image () {
|
||||||
@@ -212,6 +294,13 @@ export default {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
stamp () {
|
||||||
|
if (this.field.type === 'stamp') {
|
||||||
|
return this.attachmentsIndex[this.modelValue]
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
},
|
||||||
signature () {
|
signature () {
|
||||||
if (this.field.type === 'signature') {
|
if (this.field.type === 'signature') {
|
||||||
return this.attachmentsIndex[this.modelValue]
|
return this.attachmentsIndex[this.modelValue]
|
||||||
@@ -226,9 +315,15 @@ export default {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
locale () {
|
||||||
|
return Intl.DateTimeFormat().resolvedOptions()?.locale
|
||||||
|
},
|
||||||
formattedDate () {
|
formattedDate () {
|
||||||
if (this.field.type === 'date' && this.modelValue) {
|
if (this.field.type === 'date' && this.modelValue) {
|
||||||
return new Intl.DateTimeFormat([], { year: 'numeric', month: 'long', day: 'numeric', timeZone: 'UTC' }).format(new Date(this.modelValue))
|
return this.formatDate(
|
||||||
|
this.modelValue === '{{date}}' ? new Date() : new Date(this.modelValue),
|
||||||
|
this.field.preferences?.format || (this.locale.endsWith('-US') ? 'MM/DD/YYYY' : 'DD/MM/YYYY')
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
@@ -236,6 +331,8 @@ export default {
|
|||||||
attachments () {
|
attachments () {
|
||||||
if (this.field.type === 'file') {
|
if (this.field.type === 'file') {
|
||||||
return (this.modelValue || []).map((uuid) => this.attachmentsIndex[uuid])
|
return (this.modelValue || []).map((uuid) => this.attachmentsIndex[uuid])
|
||||||
|
} else if (this.field.type === 'payment') {
|
||||||
|
return [this.attachmentsIndex[this.modelValue]].filter(Boolean)
|
||||||
} else {
|
} else {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
@@ -250,6 +347,92 @@ export default {
|
|||||||
height: h * 100 + '%'
|
height: h * 100 + '%'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
modelValue () {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (['date', 'text', 'number'].includes(this.field.type) && this.$refs.textContainer && (this.textOverflowChars === 0 || (this.textOverflowChars - 4) > `${this.modelValue}`.length)) {
|
||||||
|
this.textOverflowChars = this.$refs.textContainer.scrollHeight > this.$refs.textContainer.clientHeight ? `${this.modelValue}`.length : 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (['date', 'text', 'number'].includes(this.field.type) && this.$refs.textContainer) {
|
||||||
|
this.textOverflowChars = this.$refs.textContainer.scrollHeight > this.$refs.textContainer.clientHeight ? `${this.modelValue}`.length : 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
optionValue (option) {
|
||||||
|
if (option) {
|
||||||
|
if (option.value) {
|
||||||
|
return option.value
|
||||||
|
} else {
|
||||||
|
const index = this.field.options.indexOf(option)
|
||||||
|
|
||||||
|
return `${this.t('option')} ${index + 1}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
formatNumber (number, format) {
|
||||||
|
if (format === 'comma') {
|
||||||
|
return new Intl.NumberFormat('en-US').format(number)
|
||||||
|
} else if (format === 'dot') {
|
||||||
|
return new Intl.NumberFormat('de-DE').format(number)
|
||||||
|
} else if (format === 'space') {
|
||||||
|
return new Intl.NumberFormat('fr-FR').format(number)
|
||||||
|
} else {
|
||||||
|
return number
|
||||||
|
}
|
||||||
|
},
|
||||||
|
formatDate (date, format) {
|
||||||
|
const monthFormats = {
|
||||||
|
M: 'numeric',
|
||||||
|
MM: '2-digit',
|
||||||
|
MMM: 'short',
|
||||||
|
MMMM: 'long'
|
||||||
|
}
|
||||||
|
|
||||||
|
const dayFormats = {
|
||||||
|
D: 'numeric',
|
||||||
|
DD: '2-digit'
|
||||||
|
}
|
||||||
|
|
||||||
|
const yearFormats = {
|
||||||
|
YYYY: 'numeric',
|
||||||
|
YYY: 'numeric',
|
||||||
|
YY: '2-digit'
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = new Intl.DateTimeFormat([], {
|
||||||
|
day: dayFormats[format.match(/D+/)],
|
||||||
|
month: monthFormats[format.match(/M+/)],
|
||||||
|
year: yearFormats[format.match(/Y+/)],
|
||||||
|
timeZone: 'UTC'
|
||||||
|
}).formatToParts(date)
|
||||||
|
|
||||||
|
return format
|
||||||
|
.replace(/D+/, parts.find((p) => p.type === 'day').value)
|
||||||
|
.replace(/M+/, parts.find((p) => p.type === 'month').value)
|
||||||
|
.replace(/Y+/, parts.find((p) => p.type === 'year').value)
|
||||||
|
},
|
||||||
|
updateMultipleSelectValue (value) {
|
||||||
|
if (this.modelValue?.includes(value)) {
|
||||||
|
const newValue = [...this.modelValue]
|
||||||
|
|
||||||
|
newValue.splice(newValue.indexOf(value), 1)
|
||||||
|
|
||||||
|
this.$emit('update:model-value', newValue)
|
||||||
|
} else {
|
||||||
|
const newValue = this.modelValue ? [...this.modelValue] : []
|
||||||
|
|
||||||
|
newValue.push(value)
|
||||||
|
|
||||||
|
this.$emit('update:model-value', newValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
:key="areaIndex"
|
:key="areaIndex"
|
||||||
>
|
>
|
||||||
<Teleport
|
<Teleport
|
||||||
|
v-if="findPageElementForArea(area)"
|
||||||
:to="findPageElementForArea(area)"
|
:to="findPageElementForArea(area)"
|
||||||
>
|
>
|
||||||
<FieldArea
|
<FieldArea
|
||||||
@@ -82,22 +83,25 @@ export default {
|
|||||||
return (this.$root.$el?.parentNode?.getRootNode() || document).getElementById(`page-${area.attachment_uuid}-${area.page}`)
|
return (this.$root.$el?.parentNode?.getRootNode() || document).getElementById(`page-${area.attachment_uuid}-${area.page}`)
|
||||||
},
|
},
|
||||||
scrollIntoField (field) {
|
scrollIntoField (field) {
|
||||||
this.areaRefs.find((area) => {
|
if (field?.areas) {
|
||||||
if (area.field === field) {
|
this.scrollIntoArea(field.areas[0])
|
||||||
const root = this.$root.$el.parentNode.getRootNode()
|
}
|
||||||
const container = root.body || root.querySelector('div')
|
},
|
||||||
|
scrollIntoArea (area) {
|
||||||
|
const areaRef = this.areaRefs.find((a) => a.area === area)
|
||||||
|
|
||||||
if (container.style.overflow === 'hidden') {
|
if (areaRef) {
|
||||||
this.scrollInContainer(area.$el)
|
const root = this.$root.$el.parentNode.getRootNode()
|
||||||
} else {
|
const container = root.body || root.querySelector('div')
|
||||||
area.$refs.scrollToElem.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
if (container.style.overflow === 'hidden') {
|
||||||
|
this.scrollInContainer(areaRef.$el)
|
||||||
} else {
|
} else {
|
||||||
return null
|
areaRef.$refs.scrollToElem.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
},
|
},
|
||||||
scrollInContainer (target) {
|
scrollInContainer (target) {
|
||||||
const root = this.$root.$el.parentNode.getRootNode()
|
const root = this.$root.$el.parentNode.getRootNode()
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div v-if="modelValue.length">
|
<div v-if="value.length">
|
||||||
<div
|
<div
|
||||||
v-for="(val, index) in modelValue"
|
v-for="(val, index) in value"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="flex mb-2"
|
class="flex mb-2"
|
||||||
>
|
>
|
||||||
@@ -26,10 +26,7 @@
|
|||||||
{{ attachmentsIndex[val].filename }}
|
{{ attachmentsIndex[val].filename }}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
<button
|
<button @click.prevent="removeAttachment(val)">
|
||||||
v-if="modelValue"
|
|
||||||
@click.prevent="removeAttachment(val)"
|
|
||||||
>
|
|
||||||
<IconTrashX
|
<IconTrashX
|
||||||
:width="18"
|
:width="18"
|
||||||
:heigh="19"
|
:heigh="19"
|
||||||
@@ -44,10 +41,16 @@
|
|||||||
:name="`values[${field.uuid}][]`"
|
:name="`values[${field.uuid}][]`"
|
||||||
>
|
>
|
||||||
</template>
|
</template>
|
||||||
|
<div
|
||||||
|
v-if="field.description && !modelValue.length"
|
||||||
|
dir="auto"
|
||||||
|
class="mb-3 px-1"
|
||||||
|
>
|
||||||
|
<MarkdownContent :string="field.description" />
|
||||||
|
</div>
|
||||||
<FileDropzone
|
<FileDropzone
|
||||||
:message="`Upload ${field.name || 'Attachments'}${field.required ? '' : ' (optional)'}`"
|
:message="`${t('upload')} ${field.name || t('files')}${field.required ? '' : ` (${t('optional')})`}`"
|
||||||
:submitter-slug="submitterSlug"
|
:submitter-slug="submitterSlug"
|
||||||
:is-direct-upload="isDirectUpload"
|
|
||||||
:multiple="true"
|
:multiple="true"
|
||||||
@upload="onUpload"
|
@upload="onUpload"
|
||||||
/>
|
/>
|
||||||
@@ -56,15 +59,18 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import FileDropzone from './dropzone'
|
import FileDropzone from './dropzone'
|
||||||
|
import MarkdownContent from './markdown_content'
|
||||||
import { IconPaperclip, IconTrashX } from '@tabler/icons-vue'
|
import { IconPaperclip, IconTrashX } from '@tabler/icons-vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'AttachmentStep',
|
name: 'AttachmentStep',
|
||||||
components: {
|
components: {
|
||||||
FileDropzone,
|
FileDropzone,
|
||||||
|
MarkdownContent,
|
||||||
IconPaperclip,
|
IconPaperclip,
|
||||||
IconTrashX
|
IconTrashX
|
||||||
},
|
},
|
||||||
|
inject: ['t'],
|
||||||
props: {
|
props: {
|
||||||
field: {
|
field: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -79,11 +85,6 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: () => ({})
|
default: () => ({})
|
||||||
},
|
},
|
||||||
isDirectUpload: {
|
|
||||||
type: Boolean,
|
|
||||||
required: true,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -91,18 +92,28 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['attached', 'update:model-value'],
|
emits: ['attached', 'update:model-value'],
|
||||||
|
computed: {
|
||||||
|
value: {
|
||||||
|
set (value) {
|
||||||
|
this.$emit('update:model-value', this.modelValue || [])
|
||||||
|
},
|
||||||
|
get () {
|
||||||
|
return this.modelValue || []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
removeAttachment (uuid) {
|
removeAttachment (uuid) {
|
||||||
this.modelValue.splice(this.modelValue.indexOf(uuid), 1)
|
this.value.splice(this.value.indexOf(uuid), 1)
|
||||||
|
|
||||||
this.$emit('update:model-value', this.modelValue)
|
this.$emit('update:model-value', this.value)
|
||||||
},
|
},
|
||||||
onUpload (attachments) {
|
onUpload (attachments) {
|
||||||
attachments.forEach((attachment) => {
|
attachments.forEach((attachment) => {
|
||||||
this.$emit('attached', attachment)
|
this.$emit('attached', attachment)
|
||||||
})
|
})
|
||||||
|
|
||||||
this.$emit('update:model-value', [...this.modelValue, ...attachments.map(a => a.uuid)])
|
this.$emit('update:model-value', [...this.value, ...attachments.map(a => a.uuid)])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,31 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="mx-auto max-w-md flex flex-col">
|
<div
|
||||||
<p class="font-medium text-2xl flex items-center space-x-1.5 mx-auto">
|
class="mx-auto max-w-md flex flex-col"
|
||||||
|
dir="auto"
|
||||||
|
>
|
||||||
|
<div class="font-medium text-2xl flex items-center space-x-1.5 mx-auto">
|
||||||
<IconCircleCheck
|
<IconCircleCheck
|
||||||
class="inline text-green-600"
|
class="inline text-green-600"
|
||||||
:width="30"
|
:width="30"
|
||||||
:height="30"
|
:height="30"
|
||||||
/>
|
/>
|
||||||
<span>
|
<span>
|
||||||
{{ t('form_has_been_completed') }}
|
{{ completedMessage.title || t('form_has_been_completed') }}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="completedMessage.body"
|
||||||
|
class="mt-2"
|
||||||
|
>
|
||||||
|
<MarkdownContent
|
||||||
|
:string="completedMessage.body"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div class="space-y-3 mt-5">
|
<div class="space-y-3 mt-5">
|
||||||
<a
|
<a
|
||||||
v-if="completedButton.url"
|
v-if="completedButton.url"
|
||||||
:href="completedButton.url"
|
:href="sanitizeHref(completedButton.url)"
|
||||||
|
rel="noopener noreferrer nofollow"
|
||||||
class="white-button flex items-center w-full"
|
class="white-button flex items-center w-full"
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
@@ -21,7 +33,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
<button
|
<button
|
||||||
v-if="canSendEmail && !isDemo"
|
v-if="canSendEmail && !isDemo && withSendCopyButton"
|
||||||
class="white-button !h-auto flex items-center space-x-1 w-full"
|
class="white-button !h-auto flex items-center space-x-1 w-full"
|
||||||
:disabled="isSendingCopy"
|
:disabled="isSendingCopy"
|
||||||
@click.prevent="sendCopyToEmail"
|
@click.prevent="sendCopyToEmail"
|
||||||
@@ -36,7 +48,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="!isWebView"
|
v-if="!isWebView && withDownloadButton"
|
||||||
class="base-button flex items-center space-x-1 w-full"
|
class="base-button flex items-center space-x-1 w-full"
|
||||||
:disabled="isDownloading"
|
:disabled="isDownloading"
|
||||||
@click.prevent="download"
|
@click.prevent="download"
|
||||||
@@ -78,7 +90,7 @@
|
|||||||
>
|
>
|
||||||
{{ t('signed_with') }}
|
{{ t('signed_with') }}
|
||||||
<a
|
<a
|
||||||
href="https://www.docuseal.co"
|
href="https://www.docuseal.co/start"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="underline"
|
class="underline"
|
||||||
>DocuSeal</a> - {{ t('open_source_documents_software') }}
|
>DocuSeal</a> - {{ t('open_source_documents_software') }}
|
||||||
@@ -88,10 +100,12 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { IconCircleCheck, IconBrandGithub, IconMail, IconDownload, IconInnerShadowTop, IconLogin } from '@tabler/icons-vue'
|
import { IconCircleCheck, IconBrandGithub, IconMail, IconDownload, IconInnerShadowTop, IconLogin } from '@tabler/icons-vue'
|
||||||
|
import MarkdownContent from './markdown_content'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FormCompleted',
|
name: 'FormCompleted',
|
||||||
components: {
|
components: {
|
||||||
|
MarkdownContent,
|
||||||
IconCircleCheck,
|
IconCircleCheck,
|
||||||
IconInnerShadowTop,
|
IconInnerShadowTop,
|
||||||
IconBrandGithub,
|
IconBrandGithub,
|
||||||
@@ -115,6 +129,16 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
|
withDownloadButton: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
withSendCopyButton: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
withConfetti: {
|
withConfetti: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -129,6 +153,11 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: false,
|
required: false,
|
||||||
default: () => ({})
|
default: () => ({})
|
||||||
|
},
|
||||||
|
completedMessage: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
@@ -169,27 +198,67 @@ export default {
|
|||||||
this.isDownloading = true
|
this.isDownloading = true
|
||||||
|
|
||||||
fetch(this.baseUrl + `/submitters/${this.submitterSlug}/download`).then((response) => response.json()).then((urls) => {
|
fetch(this.baseUrl + `/submitters/${this.submitterSlug}/download`).then((response) => response.json()).then((urls) => {
|
||||||
const fileRequests = urls.map((url) => {
|
const isSafariIos = /iPhone|iPad|iPod/i.test(navigator.userAgent)
|
||||||
return () => {
|
|
||||||
return fetch(url).then(async (resp) => {
|
|
||||||
const blobUrl = URL.createObjectURL(await resp.blob())
|
|
||||||
const link = document.createElement('a')
|
|
||||||
|
|
||||||
link.href = blobUrl
|
if (isSafariIos && urls.length > 1) {
|
||||||
link.setAttribute('download', decodeURI(url.split('/').pop()))
|
this.downloadSafariIos(urls)
|
||||||
|
} else {
|
||||||
|
this.downloadUrls(urls)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
downloadUrls (urls) {
|
||||||
|
const fileRequests = urls.map((url) => {
|
||||||
|
return () => {
|
||||||
|
return fetch(url).then(async (resp) => {
|
||||||
|
const blobUrl = URL.createObjectURL(await resp.blob())
|
||||||
|
const link = document.createElement('a')
|
||||||
|
|
||||||
link.click()
|
link.href = blobUrl
|
||||||
|
link.setAttribute('download', decodeURI(url.split('/').pop()))
|
||||||
|
|
||||||
URL.revokeObjectURL(url)
|
link.click()
|
||||||
})
|
|
||||||
}
|
URL.revokeObjectURL(blobUrl)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
fileRequests.reduce(
|
||||||
|
(prevPromise, request) => prevPromise.then(() => request()),
|
||||||
|
Promise.resolve()
|
||||||
|
).finally(() => {
|
||||||
|
this.isDownloading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
sanitizeHref (href) {
|
||||||
|
if (href && href.trim().match(/^((?:https?:\/\/)|\/)/)) {
|
||||||
|
return href.replace(/javascript:/g, '')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
downloadSafariIos (urls) {
|
||||||
|
const fileRequests = urls.map((url) => {
|
||||||
|
return fetch(url).then(async (resp) => {
|
||||||
|
const blob = await resp.blob()
|
||||||
|
const blobUrl = URL.createObjectURL(blob.slice(0, blob.size, 'application/octet-stream'))
|
||||||
|
const link = document.createElement('a')
|
||||||
|
|
||||||
|
link.href = blobUrl
|
||||||
|
link.setAttribute('download', decodeURI(url.split('/').pop()))
|
||||||
|
|
||||||
|
return link
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
|
||||||
fileRequests.reduce(
|
Promise.all(fileRequests).then((links) => {
|
||||||
(prevPromise, request) => prevPromise.then(() => request()),
|
links.forEach((link, index) => {
|
||||||
Promise.resolve()
|
setTimeout(() => {
|
||||||
)
|
link.click()
|
||||||
|
|
||||||
|
URL.revokeObjectURL(link.href)
|
||||||
|
}, index * 50)
|
||||||
|
})
|
||||||
|
}).finally(() => {
|
||||||
this.isDownloading = false
|
this.isDownloading = false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
function cropCanvasAndExportToPNG (canvas) {
|
function cropCanvasAndExportToPNG (canvas, { errorOnTooSmall } = { errorOnTooSmall: false }) {
|
||||||
const ctx = canvas.getContext('2d')
|
const ctx = canvas.getContext('2d')
|
||||||
|
|
||||||
const width = canvas.width
|
const width = canvas.width
|
||||||
@@ -33,6 +33,10 @@ function cropCanvasAndExportToPNG (canvas) {
|
|||||||
croppedCanvas.height = croppedHeight
|
croppedCanvas.height = croppedHeight
|
||||||
const croppedCtx = croppedCanvas.getContext('2d')
|
const croppedCtx = croppedCanvas.getContext('2d')
|
||||||
|
|
||||||
|
if (errorOnTooSmall && (croppedWidth < 20 || croppedHeight < 20)) {
|
||||||
|
return Promise.reject(new Error('Image too small'))
|
||||||
|
}
|
||||||
|
|
||||||
croppedCtx.drawImage(canvas, leftmost, topmost, croppedWidth, croppedHeight, 0, 0, croppedWidth, croppedHeight)
|
croppedCtx.drawImage(canvas, leftmost, topmost, croppedWidth, croppedHeight, 0, 0, croppedWidth, croppedHeight)
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
<template>
|
||||||
|
<div dir="auto">
|
||||||
|
<div
|
||||||
|
class="flex justify-between items-center w-full"
|
||||||
|
:class="{ 'mb-2': !field.description }"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
:for="field.uuid"
|
||||||
|
class="label text-2xl"
|
||||||
|
>
|
||||||
|
<MarkdownContent
|
||||||
|
v-if="field.title"
|
||||||
|
:string="field.title"
|
||||||
|
/>
|
||||||
|
<template v-else>
|
||||||
|
{{ field.name && showFieldNames ? field.name : t('date') }}
|
||||||
|
<template v-if="!field.required">({{ t('optional') }})</template>
|
||||||
|
</template>
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
class="btn btn-outline btn-sm !normal-case font-normal"
|
||||||
|
@click.prevent="[setCurrentDate(), $emit('focus')]"
|
||||||
|
>
|
||||||
|
<IconCalendarCheck :width="16" />
|
||||||
|
{{ t('set_today') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="field.description"
|
||||||
|
class="mb-3 px-1"
|
||||||
|
dir="auto"
|
||||||
|
>
|
||||||
|
<MarkdownContent :string="field.description" />
|
||||||
|
</div>
|
||||||
|
<AppearsOn :field="field" />
|
||||||
|
<div class="text-center">
|
||||||
|
<input
|
||||||
|
ref="input"
|
||||||
|
v-model="value"
|
||||||
|
class="base-input !text-2xl text-center w-full"
|
||||||
|
:required="field.required"
|
||||||
|
type="date"
|
||||||
|
:name="`values[${field.uuid}]`"
|
||||||
|
@focus="$emit('focus')"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { IconCalendarCheck } from '@tabler/icons-vue'
|
||||||
|
import AppearsOn from './appears_on'
|
||||||
|
import MarkdownContent from './markdown_content'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'DateStep',
|
||||||
|
components: {
|
||||||
|
IconCalendarCheck,
|
||||||
|
MarkdownContent,
|
||||||
|
AppearsOn
|
||||||
|
},
|
||||||
|
inject: ['t'],
|
||||||
|
props: {
|
||||||
|
field: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
showFieldNames: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
modelValue: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['update:model-value', 'focus'],
|
||||||
|
computed: {
|
||||||
|
value: {
|
||||||
|
set (value) {
|
||||||
|
this.$emit('update:model-value', value)
|
||||||
|
},
|
||||||
|
get () {
|
||||||
|
return this.modelValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
setCurrentDate () {
|
||||||
|
const inputEl = this.$refs.input
|
||||||
|
|
||||||
|
inputEl.valueAsDate = new Date(new Date().getTime() - new Date().getTimezoneOffset() * 60000)
|
||||||
|
|
||||||
|
inputEl.dispatchEvent(new Event('input', { bubbles: true }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -70,11 +70,6 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: '*/*'
|
default: '*/*'
|
||||||
},
|
},
|
||||||
isDirectUpload: {
|
|
||||||
type: Boolean,
|
|
||||||
required: true,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
multiple: {
|
multiple: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -92,11 +87,6 @@ export default {
|
|||||||
return 'el' + Math.random().toString(32).split('.')[1]
|
return 'el' + Math.random().toString(32).split('.')[1]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted () {
|
|
||||||
if (this.isDirectUpload) {
|
|
||||||
import('@rails/activestorage')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
onDropFiles (e) {
|
onDropFiles (e) {
|
||||||
this.uploadFiles(e.dataTransfer.files)
|
this.uploadFiles(e.dataTransfer.files)
|
||||||
@@ -113,72 +103,55 @@ export default {
|
|||||||
async uploadFiles (files) {
|
async uploadFiles (files) {
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
|
|
||||||
if (this.isDirectUpload) {
|
return await Promise.all(
|
||||||
const { DirectUpload } = await import('@rails/activestorage')
|
Array.from(files).map(async (file) => {
|
||||||
|
const formData = new FormData()
|
||||||
|
|
||||||
const blobs = await Promise.all(
|
if (file.type === 'image/bmp') {
|
||||||
Array.from(files).map(async (file) => {
|
file = await this.convertBmpToPng(file)
|
||||||
const upload = new DirectUpload(
|
}
|
||||||
file,
|
|
||||||
'/direct_uploads',
|
|
||||||
this.$refs.input
|
|
||||||
)
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
formData.append('file', file)
|
||||||
upload.create((error, blob) => {
|
formData.append('submitter_slug', this.submitterSlug)
|
||||||
if (error) {
|
formData.append('name', 'attachments')
|
||||||
console.error(error)
|
|
||||||
|
|
||||||
return reject(error)
|
return fetch(this.baseUrl + '/api/attachments', {
|
||||||
} else {
|
method: 'POST',
|
||||||
return resolve(blob)
|
body: formData
|
||||||
}
|
}).then(resp => resp.json()).then((data) => {
|
||||||
})
|
return data
|
||||||
}).catch((error) => {
|
|
||||||
console.error(error)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
)
|
})).then((result) => {
|
||||||
|
this.$emit('upload', result)
|
||||||
|
}).finally(() => {
|
||||||
|
this.isLoading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
convertBmpToPng (bmpFile) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader()
|
||||||
|
|
||||||
return await Promise.all(
|
reader.onload = function (event) {
|
||||||
blobs.map((blob) => {
|
const img = new Image()
|
||||||
return fetch(this.baseUrl + '/api/attachments', {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({
|
|
||||||
name: 'attachments',
|
|
||||||
blob_signed_id: blob.signed_id,
|
|
||||||
submitter_slug: this.submitterSlug
|
|
||||||
}),
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
}).then(resp => resp.json()).then((data) => {
|
|
||||||
return data
|
|
||||||
})
|
|
||||||
})).then((result) => {
|
|
||||||
this.$emit('upload', result)
|
|
||||||
}).finally(() => {
|
|
||||||
this.isLoading = false
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
return await Promise.all(
|
|
||||||
Array.from(files).map((file) => {
|
|
||||||
const formData = new FormData()
|
|
||||||
|
|
||||||
formData.append('file', file)
|
img.onload = function () {
|
||||||
formData.append('submitter_slug', this.submitterSlug)
|
const canvas = document.createElement('canvas')
|
||||||
formData.append('name', 'attachments')
|
const ctx = canvas.getContext('2d')
|
||||||
|
|
||||||
return fetch(this.baseUrl + '/api/attachments', {
|
canvas.width = img.width
|
||||||
method: 'POST',
|
canvas.height = img.height
|
||||||
body: formData
|
ctx.drawImage(img, 0, 0)
|
||||||
}).then(resp => resp.json()).then((data) => {
|
canvas.toBlob(function (blob) {
|
||||||
return data
|
const newFile = new File([blob], bmpFile.name.replace(/\.\w+$/, '.png'), { type: 'image/png' })
|
||||||
})
|
resolve(newFile)
|
||||||
})).then((result) => {
|
}, 'image/png')
|
||||||
this.$emit('upload', result)
|
}
|
||||||
}).finally(() => {
|
|
||||||
this.isLoading = false
|
img.src = event.target.result
|
||||||
})
|
}
|
||||||
}
|
reader.onerror = reject
|
||||||
|
reader.readAsDataURL(bmpFile)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,97 @@
|
|||||||
|
<template>
|
||||||
|
<template
|
||||||
|
v-for="(field, fieldIndex) in fields"
|
||||||
|
:key="field.uuid"
|
||||||
|
>
|
||||||
|
<template
|
||||||
|
v-for="(area, areaIndex) in field.areas"
|
||||||
|
:key="areaIndex"
|
||||||
|
>
|
||||||
|
<Teleport
|
||||||
|
v-if="findPageElementForArea(area)"
|
||||||
|
:to="findPageElementForArea(area)"
|
||||||
|
>
|
||||||
|
<FieldArea
|
||||||
|
v-if="isMathLoaded"
|
||||||
|
:model-value="calculateFormula(field)"
|
||||||
|
:field="field"
|
||||||
|
:area="area"
|
||||||
|
:submittable="false"
|
||||||
|
:field-index="fieldIndex"
|
||||||
|
/>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import FieldArea from './area'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'FormulaFieldAreas',
|
||||||
|
components: {
|
||||||
|
FieldArea
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
fields: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
values: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
isMathLoaded: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async mounted () {
|
||||||
|
const {
|
||||||
|
create,
|
||||||
|
evaluateDependencies,
|
||||||
|
addDependencies,
|
||||||
|
subtractDependencies,
|
||||||
|
divideDependencies,
|
||||||
|
multiplyDependencies,
|
||||||
|
powDependencies,
|
||||||
|
roundDependencies,
|
||||||
|
absDependencies,
|
||||||
|
sinDependencies,
|
||||||
|
tanDependencies,
|
||||||
|
cosDependencies
|
||||||
|
} = await import('mathjs')
|
||||||
|
|
||||||
|
this.math = create({
|
||||||
|
evaluateDependencies,
|
||||||
|
addDependencies,
|
||||||
|
subtractDependencies,
|
||||||
|
divideDependencies,
|
||||||
|
multiplyDependencies,
|
||||||
|
powDependencies,
|
||||||
|
roundDependencies,
|
||||||
|
absDependencies,
|
||||||
|
sinDependencies,
|
||||||
|
tanDependencies,
|
||||||
|
cosDependencies
|
||||||
|
})
|
||||||
|
|
||||||
|
this.isMathLoaded = true
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
findPageElementForArea (area) {
|
||||||
|
return (this.$root.$el?.parentNode?.getRootNode() || document).getElementById(`page-${area.attachment_uuid}-${area.page}`)
|
||||||
|
},
|
||||||
|
calculateFormula (field) {
|
||||||
|
const transformedFormula = field.preferences.formula.replace(/{{(.*?)}}/g, (match, uuid) => {
|
||||||
|
return this.values[uuid] || 0.0
|
||||||
|
})
|
||||||
|
|
||||||
|
return this.math.evaluate(transformedFormula.toLowerCase())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,17 +1,39 @@
|
|||||||
const en = {
|
const en = {
|
||||||
|
text: 'Text',
|
||||||
|
by_clicking_you_agree_to_the: 'By clicking "{button}", you agree to the',
|
||||||
|
electronic_signature_disclosure: 'Electronic Signature Disclosure',
|
||||||
|
esignature_disclosure: 'eSignature Disclosure',
|
||||||
|
signature: 'Signature',
|
||||||
|
initials: 'Initials',
|
||||||
|
date: 'Date',
|
||||||
|
number: 'Number',
|
||||||
|
image: 'Image',
|
||||||
|
take_photo: 'Take photo',
|
||||||
|
file: 'File',
|
||||||
|
select: 'Select',
|
||||||
|
checkbox: 'Checkbox',
|
||||||
|
multiple: 'Multiple',
|
||||||
|
radio: 'Radio',
|
||||||
|
cells: 'Cells',
|
||||||
|
stamp: 'Stamp',
|
||||||
|
minimize: 'Minimize',
|
||||||
|
payment: 'Payment',
|
||||||
|
phone: 'Phone',
|
||||||
submit_form: 'Submit Form',
|
submit_form: 'Submit Form',
|
||||||
type_here: 'Type here',
|
sign_now: 'Sign Now',
|
||||||
|
type_here_: 'Type here...',
|
||||||
optional: 'optional',
|
optional: 'optional',
|
||||||
|
option: 'Option',
|
||||||
|
appears_on: 'Appears on',
|
||||||
|
page: 'Page',
|
||||||
select_your_option: 'Select your option',
|
select_your_option: 'Select your option',
|
||||||
complete_hightlighted_checkboxes_and_click: 'Complete hightlighted checkboxes and click',
|
complete_hightlighted_checkboxes_and_click: 'Complete highlighted checkboxes and click',
|
||||||
submit: 'submit',
|
submit: 'submit',
|
||||||
next: 'next',
|
next: 'next',
|
||||||
click_to_upload: 'Click to upload',
|
click_to_upload: 'Click to upload',
|
||||||
or_drag_and_drop_files: 'or drag and drop files',
|
or_drag_and_drop_files: 'or drag and drop files',
|
||||||
send_copy_via_email: 'Send copy via email',
|
send_copy_via_email: 'Send copy via email',
|
||||||
download: 'Download',
|
download: 'Download',
|
||||||
signature: 'Signature',
|
|
||||||
initials: 'Initials',
|
|
||||||
clear: 'Clear',
|
clear: 'Clear',
|
||||||
redraw: 'Redraw',
|
redraw: 'Redraw',
|
||||||
draw_initials: 'Draw initials',
|
draw_initials: 'Draw initials',
|
||||||
@@ -23,19 +45,55 @@ const en = {
|
|||||||
please_check_the_box_to_continue: 'Please check the box to continue',
|
please_check_the_box_to_continue: 'Please check the box to continue',
|
||||||
open_source_documents_software: 'open source documents software',
|
open_source_documents_software: 'open source documents software',
|
||||||
verified_phone_number: 'Verify Phone Number',
|
verified_phone_number: 'Verify Phone Number',
|
||||||
use_international_format: 'Use internatioanl format: +1xxx',
|
use_international_format: 'Use international format: +1xxx',
|
||||||
six_digits_code: '6-digit code',
|
six_digits_code: '6-digit code',
|
||||||
change_phone_number: 'Change phone number',
|
change_phone_number: 'Change phone number',
|
||||||
sending: 'Sending...',
|
sending: 'Sending...',
|
||||||
resend_code: 'Re-send code',
|
resend_code: 'Re-send code',
|
||||||
verification_code_has_been_resent: 'Verification code has been re-sent via SMS',
|
verification_code_has_been_resent: 'Verification code has been re-sent via SMS',
|
||||||
email_has_been_sent: 'Email has been sent'
|
please_fill_all_required_fields: 'Please fill all required fields',
|
||||||
|
set_today: 'Set Today',
|
||||||
|
toggle_multiline_text: 'Toggle Multiline Text',
|
||||||
|
draw_signature: 'Draw signature',
|
||||||
|
type_initial: 'Type initials',
|
||||||
|
draw: 'Draw',
|
||||||
|
type: 'Type',
|
||||||
|
type_text: 'Type text',
|
||||||
|
email_has_been_sent: 'Email has been sent',
|
||||||
|
processing: 'Processing',
|
||||||
|
pay_with_strip: 'Pay with Stripe',
|
||||||
|
reupload: 'Reupload',
|
||||||
|
upload: 'Upload',
|
||||||
|
files: 'Files'
|
||||||
}
|
}
|
||||||
|
|
||||||
const es = {
|
const es = {
|
||||||
|
by_clicking_you_agree_to_the: 'Al hacer clic en "{button}", usted acepta el',
|
||||||
|
electronic_signature_disclosure: 'Divulgación de Firma Electrónica',
|
||||||
|
esignature_disclosure: 'Divulgación de eFirma',
|
||||||
|
minimize: 'Minimizar',
|
||||||
|
text: 'Texto',
|
||||||
|
signature: 'Firma',
|
||||||
|
initials: 'Iniciales',
|
||||||
|
date: 'Fecha',
|
||||||
|
number: 'Número',
|
||||||
|
image: 'Imagen',
|
||||||
|
file: 'Archivo',
|
||||||
|
select: 'Seleccionar',
|
||||||
|
checkbox: 'Casilla',
|
||||||
|
multiple: 'Múltiple',
|
||||||
|
radio: 'Radio',
|
||||||
|
cells: 'Celdas',
|
||||||
|
stamp: 'Sello',
|
||||||
|
payment: 'Pago',
|
||||||
|
phone: 'Teléfono',
|
||||||
|
take_photo: 'Tomar foto',
|
||||||
submit_form: 'Enviar Formulario',
|
submit_form: 'Enviar Formulario',
|
||||||
type_here: 'Escribe aquí',
|
sign_now: 'Firmar ahora',
|
||||||
|
type_here_: 'Escribe aquí...',
|
||||||
optional: 'opcional',
|
optional: 'opcional',
|
||||||
|
appears_on: 'Aparece en',
|
||||||
|
page: 'Página',
|
||||||
select_your_option: 'Selecciona tu opción',
|
select_your_option: 'Selecciona tu opción',
|
||||||
complete_hightlighted_checkboxes_and_click: 'Completa las casillas resaltadas y haz clic',
|
complete_hightlighted_checkboxes_and_click: 'Completa las casillas resaltadas y haz clic',
|
||||||
submit: 'enviar',
|
submit: 'enviar',
|
||||||
@@ -44,8 +102,6 @@ const es = {
|
|||||||
or_drag_and_drop_files: 'o arrastra y suelta archivos',
|
or_drag_and_drop_files: 'o arrastra y suelta archivos',
|
||||||
send_copy_via_email: 'Enviar copia por correo electrónico',
|
send_copy_via_email: 'Enviar copia por correo electrónico',
|
||||||
download: 'Descargar',
|
download: 'Descargar',
|
||||||
signature: 'Firma',
|
|
||||||
initials: 'Iniciales',
|
|
||||||
clear: 'Borrar',
|
clear: 'Borrar',
|
||||||
redraw: 'Redibujar',
|
redraw: 'Redibujar',
|
||||||
draw_initials: 'Dibujar iniciales',
|
draw_initials: 'Dibujar iniciales',
|
||||||
@@ -63,13 +119,49 @@ const es = {
|
|||||||
sending: 'Enviando...',
|
sending: 'Enviando...',
|
||||||
resend_code: 'Reenviar código',
|
resend_code: 'Reenviar código',
|
||||||
verification_code_has_been_resent: 'El código de verificación ha sido reenviado por SMS',
|
verification_code_has_been_resent: 'El código de verificación ha sido reenviado por SMS',
|
||||||
email_has_been_sent: 'El correo electrónico ha sido enviado'
|
please_fill_all_required_fields: 'Por favor, complete todos los campos obligatorios',
|
||||||
|
set_today: 'Establecer Hoy',
|
||||||
|
toggle_multiline_text: 'Alternar Texto Multilínea',
|
||||||
|
draw_signature: 'Dibujar firma',
|
||||||
|
type_initial: 'Escribir iniciales',
|
||||||
|
draw: 'Dibujar',
|
||||||
|
type: 'Escribir',
|
||||||
|
type_text: 'Escribir texto',
|
||||||
|
email_has_been_sent: 'El correo electrónico ha sido enviado',
|
||||||
|
processing: 'Procesando',
|
||||||
|
pay_with_strip: 'Pagar con Stripe',
|
||||||
|
reupload: 'Volver a subir',
|
||||||
|
upload: 'Subir',
|
||||||
|
files: 'Archivos'
|
||||||
}
|
}
|
||||||
|
|
||||||
const it = {
|
const it = {
|
||||||
|
by_clicking_you_agree_to_the: 'Cliccando su "{button}", accetti il',
|
||||||
|
electronic_signature_disclosure: 'Divulgazione della Firma Elettronica',
|
||||||
|
esignature_disclosure: 'Divulgazione della eFirma',
|
||||||
|
minimize: 'Minimizza',
|
||||||
|
text: 'Testo',
|
||||||
|
signature: 'Firma',
|
||||||
|
initials: 'Iniziali',
|
||||||
|
date: 'Data',
|
||||||
|
number: 'Numero',
|
||||||
|
image: 'Immagine',
|
||||||
|
file: 'File',
|
||||||
|
select: 'Seleziona',
|
||||||
|
checkbox: 'Casella di controllo',
|
||||||
|
multiple: 'Multiplo',
|
||||||
|
radio: 'Radio',
|
||||||
|
cells: 'Celle',
|
||||||
|
stamp: 'Timbro',
|
||||||
|
payment: 'Pagamento',
|
||||||
|
phone: 'Telefono',
|
||||||
submit_form: 'Invia Modulo',
|
submit_form: 'Invia Modulo',
|
||||||
type_here: 'Digita qui',
|
sign_now: 'Firma ora',
|
||||||
|
type_here_: 'Digita qui...',
|
||||||
optional: 'opzionale',
|
optional: 'opzionale',
|
||||||
|
appears_on: 'Compare su',
|
||||||
|
page: 'Pagina',
|
||||||
|
take_photo: 'Scattare una foto',
|
||||||
select_your_option: 'Seleziona la tua opzione',
|
select_your_option: 'Seleziona la tua opzione',
|
||||||
complete_hightlighted_checkboxes_and_click: 'Completa le caselle evidenziate e fai clic',
|
complete_hightlighted_checkboxes_and_click: 'Completa le caselle evidenziate e fai clic',
|
||||||
submit: 'invia',
|
submit: 'invia',
|
||||||
@@ -78,8 +170,6 @@ const it = {
|
|||||||
or_drag_and_drop_files: 'oppure trascina e rilascia i file',
|
or_drag_and_drop_files: 'oppure trascina e rilascia i file',
|
||||||
send_copy_via_email: 'Invia copia via email',
|
send_copy_via_email: 'Invia copia via email',
|
||||||
download: 'Scarica',
|
download: 'Scarica',
|
||||||
signature: 'Firma',
|
|
||||||
initials: 'Iniziali',
|
|
||||||
clear: 'Cancella',
|
clear: 'Cancella',
|
||||||
redraw: 'Ridisegna',
|
redraw: 'Ridisegna',
|
||||||
draw_initials: 'Disegna iniziali',
|
draw_initials: 'Disegna iniziali',
|
||||||
@@ -97,13 +187,49 @@ const it = {
|
|||||||
sending: 'Invio in corso...',
|
sending: 'Invio in corso...',
|
||||||
resend_code: 'Rinvia codice',
|
resend_code: 'Rinvia codice',
|
||||||
verification_code_has_been_resent: 'Il codice di verifica è stato rinviato tramite SMS',
|
verification_code_has_been_resent: 'Il codice di verifica è stato rinviato tramite SMS',
|
||||||
email_has_been_sent: "L'email è stata inviata"
|
please_fill_all_required_fields: 'Si prega di compilare tutti i campi obbligatori',
|
||||||
|
set_today: 'Imposta Oggi',
|
||||||
|
draw_signature: 'Disegna firma',
|
||||||
|
type_initial: 'Inserisci iniziali',
|
||||||
|
draw: 'Disegna',
|
||||||
|
type: 'Inserisci',
|
||||||
|
type_text: 'Inserisci testo',
|
||||||
|
toggle_multiline_text: 'Attiva Testo Multilinea',
|
||||||
|
email_has_been_sent: "L'email è stata inviata",
|
||||||
|
processing: 'Elaborazione',
|
||||||
|
pay_with_strip: 'Paga con Stripe',
|
||||||
|
reupload: 'Ricarica',
|
||||||
|
upload: 'Carica',
|
||||||
|
files: 'File'
|
||||||
}
|
}
|
||||||
|
|
||||||
const de = {
|
const de = {
|
||||||
|
by_clicking_you_agree_to_the: 'Durch Klicken auf "{button}" stimmen Sie zu, dass Sie die',
|
||||||
|
electronic_signature_disclosure: 'Elektronische Unterschriftenoffenlegung',
|
||||||
|
esignature_disclosure: 'eSignatur Offenlegung',
|
||||||
|
minimize: 'Minimieren',
|
||||||
|
text: 'Text',
|
||||||
|
signature: 'Unterschrift',
|
||||||
|
initials: 'Initialen',
|
||||||
|
date: 'Datum',
|
||||||
|
number: 'Nummer',
|
||||||
|
image: 'Bild',
|
||||||
|
file: 'Datei',
|
||||||
|
select: 'Auswählen',
|
||||||
|
checkbox: 'Checkbox',
|
||||||
|
multiple: 'Mehrere',
|
||||||
|
radio: 'Radio',
|
||||||
|
cells: 'Zellen',
|
||||||
|
stamp: 'Stempel',
|
||||||
|
payment: 'Zahlung',
|
||||||
|
phone: 'Telefon',
|
||||||
submit_form: 'Formular absenden',
|
submit_form: 'Formular absenden',
|
||||||
type_here: 'Hier eingeben',
|
sign_now: 'Jetzt unterschreiben',
|
||||||
|
type_here_: 'Hier eingeben...',
|
||||||
optional: 'optional',
|
optional: 'optional',
|
||||||
|
appears_on: 'Erscheint auf',
|
||||||
|
page: 'Seite',
|
||||||
|
take_photo: 'Foto aufnehmen',
|
||||||
select_your_option: 'Wähle deine Option',
|
select_your_option: 'Wähle deine Option',
|
||||||
complete_hightlighted_checkboxes_and_click: 'Markierte Kontrollkästchen ausfüllen und klicken',
|
complete_hightlighted_checkboxes_and_click: 'Markierte Kontrollkästchen ausfüllen und klicken',
|
||||||
submit: 'absenden',
|
submit: 'absenden',
|
||||||
@@ -112,8 +238,6 @@ const de = {
|
|||||||
or_drag_and_drop_files: 'oder Dateien hierher ziehen und ablegen',
|
or_drag_and_drop_files: 'oder Dateien hierher ziehen und ablegen',
|
||||||
send_copy_via_email: 'Kopie per E-Mail senden',
|
send_copy_via_email: 'Kopie per E-Mail senden',
|
||||||
download: 'Herunterladen',
|
download: 'Herunterladen',
|
||||||
signature: 'Unterschrift',
|
|
||||||
initials: 'Initialen',
|
|
||||||
clear: 'Löschen',
|
clear: 'Löschen',
|
||||||
redraw: 'Neu zeichnen',
|
redraw: 'Neu zeichnen',
|
||||||
draw_initials: 'Initialen zeichnen',
|
draw_initials: 'Initialen zeichnen',
|
||||||
@@ -131,13 +255,49 @@ const de = {
|
|||||||
sending: 'Senden...',
|
sending: 'Senden...',
|
||||||
resend_code: 'Code erneut senden',
|
resend_code: 'Code erneut senden',
|
||||||
verification_code_has_been_resent: 'Die Verifizierungscode wurde erneut per SMS gesendet',
|
verification_code_has_been_resent: 'Die Verifizierungscode wurde erneut per SMS gesendet',
|
||||||
email_has_been_sent: 'Die E-Mail wurde gesendet'
|
please_fill_all_required_fields: 'Bitte füllen Sie alle erforderlichen Felder aus',
|
||||||
|
set_today: 'Heute einstellen',
|
||||||
|
draw_signature: 'Unterschrift zeichnen',
|
||||||
|
type_initial: 'Initialen eingeben',
|
||||||
|
draw: 'Zeichnen',
|
||||||
|
type: 'Eingeben',
|
||||||
|
type_text: 'Text eingeben',
|
||||||
|
toggle_multiline_text: 'Mehrzeiligen Text umschalten',
|
||||||
|
email_has_been_sent: 'Die E-Mail wurde gesendet',
|
||||||
|
processing: 'Verarbeitung',
|
||||||
|
pay_with_strip: 'Mit Stripe bezahlen',
|
||||||
|
reupload: 'Erneut hochladen',
|
||||||
|
upload: 'Hochladen',
|
||||||
|
files: 'Dateien'
|
||||||
}
|
}
|
||||||
|
|
||||||
const fr = {
|
const fr = {
|
||||||
|
by_clicking_you_agree_to_the: 'En cliquant sur "{button}", vous acceptez la',
|
||||||
|
electronic_signature_disclosure: 'Divulgation de Signature Électronique',
|
||||||
|
esignature_disclosure: 'Divulgation de la eSignature',
|
||||||
|
minimize: 'Réduire',
|
||||||
|
text: 'Texte',
|
||||||
|
signature: 'Signature',
|
||||||
|
initials: 'Initiales',
|
||||||
|
date: 'Date',
|
||||||
|
number: 'Numéro',
|
||||||
|
image: 'Image',
|
||||||
|
file: 'Fichier',
|
||||||
|
select: 'Choisir',
|
||||||
|
checkbox: 'Coche',
|
||||||
|
multiple: 'Multiple',
|
||||||
|
radio: 'Radio',
|
||||||
|
cells: 'Cellules',
|
||||||
|
stamp: 'Tampon',
|
||||||
|
payment: 'Paiement',
|
||||||
|
phone: 'Téléphone',
|
||||||
submit_form: 'Envoyer le Formulaire',
|
submit_form: 'Envoyer le Formulaire',
|
||||||
type_here: 'Tapez ici',
|
sign_now: 'Signer maintenant',
|
||||||
|
type_here_: 'Tapez ici...',
|
||||||
optional: 'facultatif',
|
optional: 'facultatif',
|
||||||
|
appears_on: 'Apparaît sur',
|
||||||
|
page: 'Page',
|
||||||
|
take_photo: 'Prendre une photo',
|
||||||
select_your_option: 'Sélectionnez votre option',
|
select_your_option: 'Sélectionnez votre option',
|
||||||
complete_hightlighted_checkboxes_and_click: 'Complétez les cases à cocher en surbrillance et cliquez',
|
complete_hightlighted_checkboxes_and_click: 'Complétez les cases à cocher en surbrillance et cliquez',
|
||||||
submit: 'envoyer',
|
submit: 'envoyer',
|
||||||
@@ -146,8 +306,6 @@ const fr = {
|
|||||||
or_drag_and_drop_files: 'ou faites glisser-déposer les fichiers',
|
or_drag_and_drop_files: 'ou faites glisser-déposer les fichiers',
|
||||||
send_copy_via_email: 'Envoyer une copie par e-mail',
|
send_copy_via_email: 'Envoyer une copie par e-mail',
|
||||||
download: 'Télécharger',
|
download: 'Télécharger',
|
||||||
signature: 'Signature',
|
|
||||||
initials: 'Initiales',
|
|
||||||
clear: 'Effacer',
|
clear: 'Effacer',
|
||||||
redraw: 'Redessiner',
|
redraw: 'Redessiner',
|
||||||
draw_initials: 'Dessiner les initiales',
|
draw_initials: 'Dessiner les initiales',
|
||||||
@@ -165,23 +323,57 @@ const fr = {
|
|||||||
sending: 'Envoi en cours...',
|
sending: 'Envoi en cours...',
|
||||||
resend_code: 'Renvoyer le code',
|
resend_code: 'Renvoyer le code',
|
||||||
verification_code_has_been_resent: 'Le code de vérification a été renvoyé par SMS',
|
verification_code_has_been_resent: 'Le code de vérification a été renvoyé par SMS',
|
||||||
email_has_been_sent: "L'email a été envoyé"
|
please_fill_all_required_fields: 'Veuillez remplir tous les champs obligatoires',
|
||||||
|
set_today: "Définir Aujourd'hui",
|
||||||
|
draw_signature: 'Dessiner une signature',
|
||||||
|
type_initial: 'Saisir les initiales',
|
||||||
|
draw: 'Dessiner',
|
||||||
|
type: 'Saisir',
|
||||||
|
type_text: 'Saisir du texte',
|
||||||
|
toggle_multiline_text: 'Basculer le Texte Multiligne',
|
||||||
|
email_has_been_sent: "L'email a été envoyé",
|
||||||
|
processing: 'Traitement',
|
||||||
|
pay_with_strip: 'Paiement avec Stripe',
|
||||||
|
reupload: 'Recharger',
|
||||||
|
upload: 'Télécharger',
|
||||||
|
files: 'Fichiers'
|
||||||
}
|
}
|
||||||
|
|
||||||
const pl = {
|
const pl = {
|
||||||
|
by_clicking_you_agree_to_the: 'Klikając na "{button}", zgadzasz się na',
|
||||||
|
electronic_signature_disclosure: 'Ujawnienie Elektronicznej Sygnatury',
|
||||||
|
esignature_disclosure: 'Ujawnienie ePodpisu',
|
||||||
|
minimize: 'Zminimalizuj',
|
||||||
|
text: 'Tekst',
|
||||||
|
signature: 'Podpis',
|
||||||
|
initials: 'Inicjały',
|
||||||
|
date: 'Data',
|
||||||
|
number: 'Numer',
|
||||||
|
image: 'Obraz',
|
||||||
|
file: 'Plik',
|
||||||
|
select: 'Wybierz',
|
||||||
|
checkbox: 'Pole wyboru',
|
||||||
|
multiple: 'Wielokrotne',
|
||||||
|
radio: 'Radio',
|
||||||
|
cells: 'Komórki',
|
||||||
|
stamp: 'Pieczątka',
|
||||||
|
payment: 'Płatność',
|
||||||
|
phone: 'Telefon',
|
||||||
submit_form: 'Wyślij Formularz',
|
submit_form: 'Wyślij Formularz',
|
||||||
type_here: 'Wpisz tutaj',
|
sign_now: 'Podpisz teraz',
|
||||||
|
type_here_: 'Wpisz tutaj...',
|
||||||
optional: 'opcjonalny',
|
optional: 'opcjonalny',
|
||||||
|
appears_on: 'Pojawia się na',
|
||||||
|
page: 'Strona',
|
||||||
select_your_option: 'Wybierz swoją opcję',
|
select_your_option: 'Wybierz swoją opcję',
|
||||||
complete_hightlighted_checkboxes_and_click: 'Wypełnij zaznaczone pola wyboru i kliknij',
|
complete_hightlighted_checkboxes_and_click: 'Wypełnij zaznaczone pola wyboru i kliknij',
|
||||||
submit: 'wyślij',
|
submit: 'wyślij',
|
||||||
next: 'dalej',
|
next: 'dalej',
|
||||||
|
take_photo: 'Zrób zdjęcie',
|
||||||
click_to_upload: 'Kliknij, aby przesłać',
|
click_to_upload: 'Kliknij, aby przesłać',
|
||||||
or_drag_and_drop_files: 'lub przeciągnij i upuść pliki',
|
or_drag_and_drop_files: 'lub przeciągnij i upuść pliki',
|
||||||
send_copy_via_email: 'Wyślij kopię drogą mailową',
|
send_copy_via_email: 'Wyślij kopię drogą mailową',
|
||||||
download: 'Pobierz',
|
download: 'Pobierz',
|
||||||
signature: 'Podpis',
|
|
||||||
initials: 'Inicjały',
|
|
||||||
clear: 'Wyczyść',
|
clear: 'Wyczyść',
|
||||||
redraw: 'Przerysuj',
|
redraw: 'Przerysuj',
|
||||||
draw_initials: 'Narysuj inicjały',
|
draw_initials: 'Narysuj inicjały',
|
||||||
@@ -199,13 +391,49 @@ const pl = {
|
|||||||
sending: 'Wysyłanie...',
|
sending: 'Wysyłanie...',
|
||||||
resend_code: 'Ponownie wyślij kod',
|
resend_code: 'Ponownie wyślij kod',
|
||||||
verification_code_has_been_resent: 'Kod weryfikacyjny został ponownie wysłany',
|
verification_code_has_been_resent: 'Kod weryfikacyjny został ponownie wysłany',
|
||||||
email_has_been_sent: 'E-mail został wysłany'
|
please_fill_all_required_fields: 'Proszę wypełnić wszystkie wymagane pola',
|
||||||
|
set_today: 'Ustaw Dziś',
|
||||||
|
draw_signature: 'Rysuj podpis',
|
||||||
|
type_initial: 'Wprowadź inicjały',
|
||||||
|
draw: 'Rysuj',
|
||||||
|
type: 'Wprowadź',
|
||||||
|
type_text: 'Wprowadź tekst',
|
||||||
|
toggle_multiline_text: 'Przełącz Tekst Wielolinijkowy',
|
||||||
|
email_has_been_sent: 'E-mail został wysłany',
|
||||||
|
processing: 'Przetwarzanie',
|
||||||
|
pay_with_strip: 'Płatność za pomocą Stripe',
|
||||||
|
reupload: 'Ponowne przesłanie',
|
||||||
|
upload: 'Przesyłanie',
|
||||||
|
files: 'Pliki'
|
||||||
}
|
}
|
||||||
|
|
||||||
const uk = {
|
const uk = {
|
||||||
|
by_clicking_you_agree_to_the: 'Натиснувши на "{button}", ви погоджуєтеся з',
|
||||||
|
electronic_signature_disclosure: 'Розголошення Електронного Підпису',
|
||||||
|
esignature_disclosure: 'Розголошення еПідпису',
|
||||||
|
minimize: 'Зменшити',
|
||||||
|
text: 'Текст',
|
||||||
|
signature: 'Підпис',
|
||||||
|
initials: 'Ініціали',
|
||||||
|
date: 'Дата',
|
||||||
|
number: 'Число',
|
||||||
|
image: 'Зображення',
|
||||||
|
file: 'Файл',
|
||||||
|
select: 'Вибрати',
|
||||||
|
checkbox: 'Прапорець',
|
||||||
|
multiple: 'Множинний',
|
||||||
|
radio: 'Радіо',
|
||||||
|
cells: 'Клітинки',
|
||||||
|
stamp: 'Штамп',
|
||||||
|
payment: 'Платіж',
|
||||||
|
phone: 'Телефон',
|
||||||
submit_form: 'Надіслати Форму',
|
submit_form: 'Надіслати Форму',
|
||||||
type_here: 'Введіть тут',
|
sign_now: 'Підписати зараз',
|
||||||
|
type_here_: 'Введіть тут',
|
||||||
optional: 'необов’язково',
|
optional: 'необов’язково',
|
||||||
|
appears_on: "З'являється на",
|
||||||
|
page: 'Сторінка',
|
||||||
|
take_photo: 'Зробити фото',
|
||||||
select_your_option: 'Виберіть варіант',
|
select_your_option: 'Виберіть варіант',
|
||||||
complete_hightlighted_checkboxes_and_click: 'Заповніть позначені прапорці та натисніть',
|
complete_hightlighted_checkboxes_and_click: 'Заповніть позначені прапорці та натисніть',
|
||||||
submit: 'надіслати',
|
submit: 'надіслати',
|
||||||
@@ -214,8 +442,6 @@ const uk = {
|
|||||||
or_drag_and_drop_files: 'або перетягніть файли сюди',
|
or_drag_and_drop_files: 'або перетягніть файли сюди',
|
||||||
send_copy_via_email: 'Надіслати копію електронною поштою',
|
send_copy_via_email: 'Надіслати копію електронною поштою',
|
||||||
download: 'Завантажити',
|
download: 'Завантажити',
|
||||||
signature: 'Підпис',
|
|
||||||
initials: 'Ініціали',
|
|
||||||
clear: 'Очистити',
|
clear: 'Очистити',
|
||||||
redraw: 'Перемалювати',
|
redraw: 'Перемалювати',
|
||||||
draw_initials: 'Намалювати ініціали',
|
draw_initials: 'Намалювати ініціали',
|
||||||
@@ -233,14 +459,50 @@ const uk = {
|
|||||||
sending: 'Надсилаю...',
|
sending: 'Надсилаю...',
|
||||||
resend_code: 'Повторно відправити код',
|
resend_code: 'Повторно відправити код',
|
||||||
verification_code_has_been_resent: 'Код підтвердження був повторно надісланий',
|
verification_code_has_been_resent: 'Код підтвердження був повторно надісланий',
|
||||||
email_has_been_sent: 'Електронний лист був відправлений'
|
please_fill_all_required_fields: "Будь ласка, заповніть всі обов'язкові поля",
|
||||||
|
set_today: 'Задати Сьогодні',
|
||||||
|
draw_signature: 'Намалюйте підпис',
|
||||||
|
type_initial: 'Введіть ініціали',
|
||||||
|
draw: 'Підпис',
|
||||||
|
type: 'Текст',
|
||||||
|
type_text: 'Введіть текст',
|
||||||
|
toggle_multiline_text: 'Перемкнути Багаторядковий Текст',
|
||||||
|
email_has_been_sent: 'Електронний лист був відправлений',
|
||||||
|
processing: 'Обробка',
|
||||||
|
pay_with_strip: 'Сплатити за допомогою Stripe',
|
||||||
|
reupload: 'Перезавантажити',
|
||||||
|
upload: 'Завантажити',
|
||||||
|
files: 'Файли'
|
||||||
}
|
}
|
||||||
|
|
||||||
const cs = {
|
const cs = {
|
||||||
|
by_clicking_you_agree_to_the: 'Kliknutím na "{button}" souhlasíte s',
|
||||||
|
electronic_signature_disclosure: 'Zveřejněním Elektronického Podpisu',
|
||||||
|
esignature_disclosure: 'Zveřejnění ePodpisu',
|
||||||
|
minimize: 'Minimalizovat',
|
||||||
|
text: 'Text',
|
||||||
|
signature: 'Podpis',
|
||||||
|
initials: 'Iniciály',
|
||||||
|
date: 'Datum',
|
||||||
|
number: 'Číslo',
|
||||||
|
image: 'Obrázek',
|
||||||
|
file: 'Soubor',
|
||||||
|
select: 'Vybrat',
|
||||||
|
checkbox: 'Zaškrtávací políčko',
|
||||||
|
multiple: 'Více možností',
|
||||||
|
radio: 'Rádio',
|
||||||
|
cells: 'Buňky',
|
||||||
|
stamp: 'Razítko',
|
||||||
|
payment: 'Platba',
|
||||||
|
phone: 'Telefon',
|
||||||
submit_form: 'Odeslat formulář',
|
submit_form: 'Odeslat formulář',
|
||||||
type_here: 'Zadejte zde',
|
sign_now: 'Podepsat nyní',
|
||||||
|
type_here_: 'Zadejte zde',
|
||||||
optional: 'volitelné',
|
optional: 'volitelné',
|
||||||
|
appears_on: 'Zobrazuje se na',
|
||||||
|
page: 'Stránka',
|
||||||
select_your_option: 'Vyberte svou volbu',
|
select_your_option: 'Vyberte svou volbu',
|
||||||
|
take_photo: 'Fotit',
|
||||||
complete_hightlighted_checkboxes_and_click: 'Označte zvýrazněné zaškrtávací políčka a klikněte na',
|
complete_hightlighted_checkboxes_and_click: 'Označte zvýrazněné zaškrtávací políčka a klikněte na',
|
||||||
submit: 'odeslat',
|
submit: 'odeslat',
|
||||||
next: 'další',
|
next: 'další',
|
||||||
@@ -248,8 +510,6 @@ const cs = {
|
|||||||
or_drag_and_drop_files: 'nebo přetáhněte soubory sem',
|
or_drag_and_drop_files: 'nebo přetáhněte soubory sem',
|
||||||
send_copy_via_email: 'Odeslat kopii e-mailem',
|
send_copy_via_email: 'Odeslat kopii e-mailem',
|
||||||
download: 'Stáhnout',
|
download: 'Stáhnout',
|
||||||
signature: 'Podpis',
|
|
||||||
initials: 'Iniciály',
|
|
||||||
clear: 'Smazat',
|
clear: 'Smazat',
|
||||||
redraw: 'Překreslit',
|
redraw: 'Překreslit',
|
||||||
draw_initials: 'Nakreslit iniciály',
|
draw_initials: 'Nakreslit iniciály',
|
||||||
@@ -267,13 +527,49 @@ const cs = {
|
|||||||
sending: 'Odesílání...',
|
sending: 'Odesílání...',
|
||||||
resend_code: 'Znovu odeslat kód',
|
resend_code: 'Znovu odeslat kód',
|
||||||
verification_code_has_been_resent: 'Ověřovací kód byl znovu odeslán',
|
verification_code_has_been_resent: 'Ověřovací kód byl znovu odeslán',
|
||||||
email_has_been_sent: 'E-mail byl odeslán'
|
please_fill_all_required_fields: 'Prosím vyplňte všechny povinné položky',
|
||||||
|
set_today: 'Nastavit Dnes',
|
||||||
|
draw_signature: 'Nakreslit podpis',
|
||||||
|
type_initial: 'Zadat iniciály',
|
||||||
|
draw: 'Kreslit',
|
||||||
|
type: 'Zadat',
|
||||||
|
type_text: 'Zadat text',
|
||||||
|
toggle_multiline_text: 'Přepnout Víceřádkový Text',
|
||||||
|
email_has_been_sent: 'E-mail byl odeslán',
|
||||||
|
processing: 'Zpracování',
|
||||||
|
pay_with_strip: 'Zaplacení přes Stripe',
|
||||||
|
reupload: 'Znovu nahrát',
|
||||||
|
upload: 'Nahrát',
|
||||||
|
files: 'Soubory'
|
||||||
}
|
}
|
||||||
|
|
||||||
const pt = {
|
const pt = {
|
||||||
|
by_clicking_you_agree_to_the: 'Ao clicar em "{button}", você concorda com o',
|
||||||
|
electronic_signature_disclosure: 'Divulgação de Assinatura Eletrônica',
|
||||||
|
esignature_disclosure: 'Divulgação da eAssinatura',
|
||||||
|
minimize: 'Minimizar',
|
||||||
|
text: 'Texto',
|
||||||
|
signature: 'Assinatura',
|
||||||
|
initials: 'Iniciais',
|
||||||
|
date: 'Data',
|
||||||
|
number: 'Número',
|
||||||
|
image: 'Imagem',
|
||||||
|
file: 'Arquivo',
|
||||||
|
select: 'Selecionar',
|
||||||
|
checkbox: 'Caixa',
|
||||||
|
multiple: 'Múltiplo',
|
||||||
|
radio: 'Rádio',
|
||||||
|
cells: 'Células',
|
||||||
|
stamp: 'Carimbo',
|
||||||
|
payment: 'Pagamento',
|
||||||
|
phone: 'Telefone',
|
||||||
submit_form: 'Enviar Formulário',
|
submit_form: 'Enviar Formulário',
|
||||||
type_here: 'Digite aqui',
|
sign_now: 'Assinar agora',
|
||||||
|
type_here_: 'Digite aqui',
|
||||||
optional: 'opcional',
|
optional: 'opcional',
|
||||||
|
appears_on: 'Aparece em',
|
||||||
|
page: 'Página',
|
||||||
|
take_photo: 'Tirar foto',
|
||||||
select_your_option: 'Selecione sua opção',
|
select_your_option: 'Selecione sua opção',
|
||||||
complete_hightlighted_checkboxes_and_click: 'Complete as caixas de seleção destacadas e clique',
|
complete_hightlighted_checkboxes_and_click: 'Complete as caixas de seleção destacadas e clique',
|
||||||
submit: 'enviar',
|
submit: 'enviar',
|
||||||
@@ -282,8 +578,6 @@ const pt = {
|
|||||||
or_drag_and_drop_files: 'ou arraste e solte arquivos',
|
or_drag_and_drop_files: 'ou arraste e solte arquivos',
|
||||||
send_copy_via_email: 'Enviar cópia por e-mail',
|
send_copy_via_email: 'Enviar cópia por e-mail',
|
||||||
download: 'Baixar',
|
download: 'Baixar',
|
||||||
signature: 'Assinatura',
|
|
||||||
initials: 'Iniciais',
|
|
||||||
clear: 'Limpar',
|
clear: 'Limpar',
|
||||||
redraw: 'Redesenhar',
|
redraw: 'Redesenhar',
|
||||||
draw_initials: 'Desenhar iniciais',
|
draw_initials: 'Desenhar iniciais',
|
||||||
@@ -301,14 +595,298 @@ const pt = {
|
|||||||
sending: 'Enviando...',
|
sending: 'Enviando...',
|
||||||
resend_code: 'Reenviar código',
|
resend_code: 'Reenviar código',
|
||||||
verification_code_has_been_resent: 'O código de verificação foi reenviado via SMS',
|
verification_code_has_been_resent: 'O código de verificação foi reenviado via SMS',
|
||||||
email_has_been_sent: 'Email enviado'
|
please_fill_all_required_fields: 'Por favor, preencha todos os campos obrigatórios',
|
||||||
|
set_today: 'Definir Hoje',
|
||||||
|
draw_signature: 'Desenhar assinatura',
|
||||||
|
type_initial: 'Inserir iniciais',
|
||||||
|
draw: 'Desenhar',
|
||||||
|
type: 'Inserir',
|
||||||
|
type_text: 'Inserir texto',
|
||||||
|
toggle_multiline_text: 'Alternar Texto Multilinha',
|
||||||
|
email_has_been_sent: 'Email enviado',
|
||||||
|
processing: 'Processamento',
|
||||||
|
pay_with_strip: 'Pagar com Stripe',
|
||||||
|
reupload: 'Reenviar',
|
||||||
|
upload: 'Carregar',
|
||||||
|
files: 'Arquivos'
|
||||||
}
|
}
|
||||||
|
|
||||||
const i18n = { en, es, it, de, fr, pl, uk, cs, pt }
|
const he = {
|
||||||
|
by_clicking_you_agree_to_the: 'על ידי לחיצה על "{button}", אתה מסכים ל',
|
||||||
|
electronic_signature_disclosure: 'חשיפת חתימה אלקטרונית',
|
||||||
|
esignature_disclosure: 'חשיפת ה-eחתימה',
|
||||||
|
minimize: 'לקטן',
|
||||||
|
text: 'טקסט',
|
||||||
|
signature: 'חתימה',
|
||||||
|
initials: 'ראשי תיבות',
|
||||||
|
date: 'תאריך',
|
||||||
|
number: 'מספר',
|
||||||
|
image: 'תמונה',
|
||||||
|
file: 'קובץ',
|
||||||
|
select: 'בחר',
|
||||||
|
checkbox: 'תיק בחירה',
|
||||||
|
multiple: 'רב ערכים',
|
||||||
|
radio: 'רדיו',
|
||||||
|
cells: 'תאים',
|
||||||
|
stamp: 'חותמת',
|
||||||
|
payment: 'תשלום',
|
||||||
|
phone: 'טלפון',
|
||||||
|
submit_form: 'שלח טופס',
|
||||||
|
sign_now: 'חתום כעת',
|
||||||
|
type_here_: 'הקלד כאן',
|
||||||
|
optional: 'אופציונלי',
|
||||||
|
option: 'אפשרות',
|
||||||
|
appears_on: 'מופיע ב',
|
||||||
|
page: 'עמוד',
|
||||||
|
take_photo: 'צלם תמונה',
|
||||||
|
select_your_option: 'בחר את האפשרות שלך',
|
||||||
|
complete_hightlighted_checkboxes_and_click: 'השלם את תיבות הסימון המודגשות ולחץ',
|
||||||
|
submit: 'שליחה',
|
||||||
|
next: 'הבא',
|
||||||
|
click_to_upload: 'לחץ להעלאה',
|
||||||
|
or_drag_and_drop_files: 'או גרור ושחרר קבצים',
|
||||||
|
send_copy_via_email: 'שלח עותק באמצעות אימייל',
|
||||||
|
download: 'הורדה',
|
||||||
|
clear: 'נקה',
|
||||||
|
redraw: 'צייר מחדש',
|
||||||
|
draw_initials: 'צייר ציוני ראשי תיבות',
|
||||||
|
type_signature_here: 'הקלד חתימה כאן',
|
||||||
|
type_initial_here: 'הקלד ציוני ראשי תיבות כאן',
|
||||||
|
form_has_been_completed: 'הטופס הושלם',
|
||||||
|
create_a_free_account: 'צור חשבון חינם',
|
||||||
|
signed_with: 'חתום באמצעות',
|
||||||
|
please_check_the_box_to_continue: 'אנא סמן את התיבה כדי להמשיך',
|
||||||
|
open_source_documents_software: 'תוכנה פתוחה למסמכים',
|
||||||
|
verified_phone_number: 'אימות מספר טלפון',
|
||||||
|
use_international_format: 'השתמש בפורמט בינלאומי: +1xxx',
|
||||||
|
six_digits_code: 'קוד משתמש שש ספרות',
|
||||||
|
change_phone_number: 'שינוי מספר טלפון',
|
||||||
|
sending: 'שולח',
|
||||||
|
resend_code: 'שלח מחדש קוד',
|
||||||
|
verification_code_has_been_resent: 'קוד האימות נשלח מחדש בSMS',
|
||||||
|
please_fill_all_required_fields: 'אנא מלא את כל השדות הדרושים',
|
||||||
|
set_today: 'קבע היום',
|
||||||
|
toggle_multiline_text: 'שנה בין טקסט במספר שורות לטקסט בשורה אחת',
|
||||||
|
draw_signature: 'צייר חתימה',
|
||||||
|
type_initial: 'הקלד ציוני ראשי תיבות',
|
||||||
|
draw: 'צייר',
|
||||||
|
type: 'הקלד',
|
||||||
|
type_text: 'הקלד טקסט',
|
||||||
|
email_has_been_sent: 'האימייל נשלח',
|
||||||
|
processing: 'מעבד',
|
||||||
|
pay_with_strip: 'שלם עם סטרייפ',
|
||||||
|
reupload: 'העלה שוב',
|
||||||
|
upload: 'העלאה',
|
||||||
|
files: 'קבצים'
|
||||||
|
}
|
||||||
|
|
||||||
const browserLanguage = (navigator.language || navigator.userLanguage || 'en').split('-')[0]
|
const nl = {
|
||||||
|
by_clicking_you_agree_to_the: 'Door op "{button}" te klikken, gaat u akkoord met de',
|
||||||
|
electronic_signature_disclosure: 'Openbaarmaking van Elektronische Handtekening',
|
||||||
|
esignature_disclosure: 'Openbaarmaking van eHandtekening',
|
||||||
|
minimize: 'Minimaliseren',
|
||||||
|
text: 'Tekst',
|
||||||
|
signature: 'Handtekening',
|
||||||
|
initials: 'Initialen',
|
||||||
|
date: 'Datum',
|
||||||
|
number: 'Nummer',
|
||||||
|
image: 'Afbeelding',
|
||||||
|
take_photo: 'Maak een foto',
|
||||||
|
file: 'Bestand',
|
||||||
|
select: 'Selecteer',
|
||||||
|
checkbox: 'Aankruisvakje',
|
||||||
|
multiple: 'Meerdere',
|
||||||
|
radio: 'Radio',
|
||||||
|
cells: 'Cellen',
|
||||||
|
stamp: 'Stempel',
|
||||||
|
payment: 'Betaling',
|
||||||
|
phone: 'Telefoon',
|
||||||
|
submit_form: 'Formulier verzenden',
|
||||||
|
sign_now: 'Nu ondertekenen',
|
||||||
|
type_here_: 'Typ hier...',
|
||||||
|
optional: 'Optioneel',
|
||||||
|
option: 'Optie',
|
||||||
|
appears_on: 'Verschijnt op',
|
||||||
|
page: 'Pagina',
|
||||||
|
select_your_option: 'Selecteer uw optie',
|
||||||
|
complete_hightlighted_checkboxes_and_click: 'Voltooi de gemarkeerde selectievakjes en klik',
|
||||||
|
submit: 'Voltooien',
|
||||||
|
next: 'Volgende',
|
||||||
|
click_to_upload: 'Klik om te uploaden',
|
||||||
|
or_drag_and_drop_files: 'of sleep en plaats bestanden',
|
||||||
|
send_copy_via_email: 'Stuur kopie per e-mail',
|
||||||
|
download: 'Downloaden',
|
||||||
|
clear: 'Wissen',
|
||||||
|
redraw: 'Opnieuw tekenen',
|
||||||
|
draw_initials: 'Initialen tekenen',
|
||||||
|
type_signature_here: 'Typ hier uw handtekening',
|
||||||
|
type_initial_here: 'Typ hier uw initialen',
|
||||||
|
form_has_been_completed: 'Formulier is voltooid!',
|
||||||
|
create_a_free_account: 'Maak een gratis account aan',
|
||||||
|
signed_with: 'Ondertekend met',
|
||||||
|
please_check_the_box_to_continue: 'Vink het vakje aan om door te gaan',
|
||||||
|
open_source_documents_software: 'Open source documenten software',
|
||||||
|
verified_phone_number: 'Geverifieerd telefoonnummer',
|
||||||
|
use_international_format: 'Gebruik internationaal formaat: +1xxx',
|
||||||
|
six_digits_code: '6-cijferige code',
|
||||||
|
change_phone_number: 'Wijzig telefoonnummer',
|
||||||
|
sending: 'Voltooien...',
|
||||||
|
resend_code: 'Code opnieuw verzenden',
|
||||||
|
verification_code_has_been_resent: 'Verificatiecode is opnieuw verzonden via SMS',
|
||||||
|
please_fill_all_required_fields: 'Vul alle verplichte velden in',
|
||||||
|
set_today: 'Vandaag',
|
||||||
|
toggle_multiline_text: 'Schakel meerregelige tekst in/uit',
|
||||||
|
draw_signature: 'Handtekening tekenen',
|
||||||
|
type_initial: 'Typ initialen',
|
||||||
|
draw: 'Tekenen',
|
||||||
|
type: 'Typen',
|
||||||
|
type_text: 'Typ tekst',
|
||||||
|
email_has_been_sent: 'E-mail is verzonden',
|
||||||
|
processing: 'Verwerken',
|
||||||
|
pay_with_strip: 'Betalen met Stripe',
|
||||||
|
reupload: 'Opnieuw uploaden',
|
||||||
|
upload: 'Uploaden',
|
||||||
|
files: 'Bestanden'
|
||||||
|
}
|
||||||
|
|
||||||
const t = (key) => i18n[browserLanguage]?.[key] || i18n.en[key] || key
|
const ar = {
|
||||||
|
by_clicking_you_agree_to_the: 'بالنقر فوق "{button}"، أنت توافق على',
|
||||||
|
electronic_signature_disclosure: 'كشف التوقيع الإلكتروني',
|
||||||
|
esignature_disclosure: 'كشف التوقيع الإلكتروني',
|
||||||
|
text: 'نص',
|
||||||
|
signature: 'توقيع',
|
||||||
|
initials: 'الاختصارات',
|
||||||
|
date: 'تاريخ',
|
||||||
|
number: 'رقم',
|
||||||
|
image: 'صورة',
|
||||||
|
take_photo: 'التقاط صورة',
|
||||||
|
file: 'ملف',
|
||||||
|
select: 'اختيار',
|
||||||
|
checkbox: 'خانة اختيار',
|
||||||
|
multiple: 'متعدد',
|
||||||
|
radio: 'راديو',
|
||||||
|
cells: 'خلايا',
|
||||||
|
stamp: 'ختم',
|
||||||
|
minimize: 'تصغير',
|
||||||
|
payment: 'الدفع',
|
||||||
|
phone: 'هاتف',
|
||||||
|
submit_form: 'إرسال النموذج',
|
||||||
|
sign_now: 'وقع الآن',
|
||||||
|
type_here_: 'اكتب هنا',
|
||||||
|
optional: 'اختياري',
|
||||||
|
option: 'خيار',
|
||||||
|
appears_on: 'يظهر على',
|
||||||
|
page: 'صفحة',
|
||||||
|
select_your_option: 'اختر خيارك',
|
||||||
|
complete_hightlighted_checkboxes_and_click: 'أكمل الخانات المميزة وانقر',
|
||||||
|
submit: 'إرسال',
|
||||||
|
next: 'التالي',
|
||||||
|
click_to_upload: 'انقر للتحميل',
|
||||||
|
or_drag_and_drop_files: 'أو اسحب وأسقط الملفات',
|
||||||
|
send_copy_via_email: 'إرسال نسخة عبر البريد الإلكتروني',
|
||||||
|
download: 'تحميل',
|
||||||
|
clear: 'مسح',
|
||||||
|
redraw: 'إعادة الرسم',
|
||||||
|
draw_initials: 'ارسم الاختصارات',
|
||||||
|
type_signature_here: 'اكتب التوقيع هنا',
|
||||||
|
type_initial_here: 'اكتب الاختصارات هنا',
|
||||||
|
form_has_been_completed: 'تم إكمال النموذج!',
|
||||||
|
create_a_free_account: 'إنشاء حساب مجاني',
|
||||||
|
signed_with: 'تم التوقيع بواسطة',
|
||||||
|
please_check_the_box_to_continue: 'الرجاء التحقق من الخانة للمتابعة',
|
||||||
|
open_source_documents_software: 'برنامج وثائق مفتوح المصدر',
|
||||||
|
verified_phone_number: 'تحقق من رقم الهاتف',
|
||||||
|
use_international_format: 'استخدم الشكل الدولي: +1xxx',
|
||||||
|
six_digits_code: 'رمز مكون من 6 أرقام',
|
||||||
|
change_phone_number: 'تغيير رقم الهاتف',
|
||||||
|
sending: 'جارٍ الإرسال',
|
||||||
|
resend_code: 'إعادة إرسال الرمز',
|
||||||
|
verification_code_has_been_resent: 'تم إعادة إرسال رمز التحقق عبر الرسائل القصيرة',
|
||||||
|
please_fill_all_required_fields: 'الرجاء ملء جميع الحقول المطلوبة',
|
||||||
|
set_today: 'تعيين اليوم',
|
||||||
|
toggle_multiline_text: 'تبديل النصوص متعددة الأسطر',
|
||||||
|
draw_signature: 'ارسم التوقيع',
|
||||||
|
type_initial: 'اكتب الاختصارات',
|
||||||
|
draw: 'ارسم',
|
||||||
|
type: 'اكتب',
|
||||||
|
type_text: 'اكتب نصًا',
|
||||||
|
email_has_been_sent: 'تم إرسال البريد الإلكتروني',
|
||||||
|
processing: 'جارٍ المعالجة',
|
||||||
|
pay_with_strip: 'الدفع بواسطة Stripe',
|
||||||
|
reupload: 'إعادة التحميل',
|
||||||
|
upload: 'تحميل',
|
||||||
|
files: 'الملفات'
|
||||||
|
}
|
||||||
|
|
||||||
|
const ko = {
|
||||||
|
by_clicking_you_agree_to_the: '"{button}"를 클릭함으로써, 다음에 동의하게 됩니다',
|
||||||
|
electronic_signature_disclosure: '전자 서명 공개',
|
||||||
|
esignature_disclosure: '전자 서명 공개',
|
||||||
|
text: '텍스트',
|
||||||
|
signature: '서명',
|
||||||
|
initials: '이니셜',
|
||||||
|
date: '날짜',
|
||||||
|
number: '숫자',
|
||||||
|
image: '이미지',
|
||||||
|
take_photo: '사진 찍기',
|
||||||
|
file: '파일',
|
||||||
|
select: '선택',
|
||||||
|
checkbox: '체크박스',
|
||||||
|
multiple: '다중',
|
||||||
|
radio: '라디오',
|
||||||
|
cells: '셀',
|
||||||
|
stamp: '스탬프',
|
||||||
|
minimize: '최소화',
|
||||||
|
payment: '결제',
|
||||||
|
phone: '전화',
|
||||||
|
submit_form: '양식 제출',
|
||||||
|
sign_now: '지금 서명',
|
||||||
|
type_here_: '여기에 입력...',
|
||||||
|
optional: '선택 사항',
|
||||||
|
option: '옵션',
|
||||||
|
appears_on: '다음에 나타납니다',
|
||||||
|
page: '페이지',
|
||||||
|
select_your_option: '옵션 선택',
|
||||||
|
complete_hightlighted_checkboxes_and_click: '강조된 체크박스를 완료하고 클릭',
|
||||||
|
submit: '제출',
|
||||||
|
next: '다음',
|
||||||
|
click_to_upload: '업로드하려면 클릭',
|
||||||
|
or_drag_and_drop_files: '또는 파일을 드래그하여 놓으십시오',
|
||||||
|
send_copy_via_email: '이메일로 사본 보내기',
|
||||||
|
download: '다운로드',
|
||||||
|
clear: '지우기',
|
||||||
|
redraw: '다시 그리기',
|
||||||
|
draw_initials: '이니셜 그리기',
|
||||||
|
type_signature_here: '여기에 서명 입력',
|
||||||
|
type_initial_here: '여기에 이니셜 입력',
|
||||||
|
form_has_been_completed: '양식이 완료되었습니다!',
|
||||||
|
create_a_free_account: '무료 계정 생성',
|
||||||
|
signed_with: '다음으로 서명:',
|
||||||
|
please_check_the_box_to_continue: '계속하려면 확인란을 선택하십시오',
|
||||||
|
open_source_documents_software: '오픈 소스 문서 소프트웨어',
|
||||||
|
verified_phone_number: '전화번호 확인됨',
|
||||||
|
use_international_format: '국제 포맷 사용: +1xxx',
|
||||||
|
six_digits_code: '6자리 코드',
|
||||||
|
change_phone_number: '전화번호 변경',
|
||||||
|
sending: '전송 중...',
|
||||||
|
resend_code: '코드 다시 보내기',
|
||||||
|
verification_code_has_been_resent: '인증 코드가 SMS로 다시 전송되었습니다',
|
||||||
|
please_fill_all_required_fields: '필수 필드를 모두 입력하십시오',
|
||||||
|
set_today: '오늘 날짜로 설정',
|
||||||
|
toggle_multiline_text: '다중 텍스트 전환',
|
||||||
|
draw_signature: '서명 그리기',
|
||||||
|
type_initial: '이니셜 입력',
|
||||||
|
draw: '그리기',
|
||||||
|
type: '입력',
|
||||||
|
type_text: '텍스트 입력',
|
||||||
|
email_has_been_sent: '이메일이 전송되었습니다',
|
||||||
|
processing: '처리 중',
|
||||||
|
pay_with_strip: '스트라이프로 결제',
|
||||||
|
reupload: '다시 업로드',
|
||||||
|
upload: '업로드',
|
||||||
|
files: '파일'
|
||||||
|
}
|
||||||
|
|
||||||
|
const i18n = { en, es, it, de, fr, pl, uk, cs, pt, he, nl, ar, ko }
|
||||||
|
|
||||||
export default i18n
|
export default i18n
|
||||||
export { t }
|
|
||||||
@@ -3,13 +3,13 @@
|
|||||||
<div class="flex justify-between items-center w-full mb-2">
|
<div class="flex justify-between items-center w-full mb-2">
|
||||||
<label
|
<label
|
||||||
class="label text-2xl"
|
class="label text-2xl"
|
||||||
>{{ field.name || 'Image' }}</label>
|
>{{ showFieldNames && field.name ? field.name : t('image') }}</label>
|
||||||
<button
|
<button
|
||||||
class="btn btn-outline btn-sm"
|
class="btn btn-outline btn-sm"
|
||||||
@click.prevent="remove"
|
@click.prevent="remove"
|
||||||
>
|
>
|
||||||
<IconReload :width="16" />
|
<IconReload :width="16" />
|
||||||
Reupload
|
{{ t('reupload') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -24,13 +24,20 @@
|
|||||||
:name="`values[${field.uuid}]`"
|
:name="`values[${field.uuid}]`"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div
|
||||||
|
v-if="!modelValue"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="field.description"
|
||||||
|
dir="auto"
|
||||||
|
class="mb-3 px-1"
|
||||||
|
>
|
||||||
|
<MarkdownContent :string="field.description" />
|
||||||
|
</div>
|
||||||
<FileDropzone
|
<FileDropzone
|
||||||
v-if="!modelValue"
|
:message="`${t('upload')} ${field.name || t('image')}${field.required ? '' : ` (${t('optional')})`}`"
|
||||||
:message="`Upload ${field.name || 'Image'}${field.required ? '' : ' (optional)'}`"
|
|
||||||
:submitter-slug="submitterSlug"
|
:submitter-slug="submitterSlug"
|
||||||
:accept="'image/*'"
|
:accept="'image/*'"
|
||||||
:is-direct-upload="isDirectUpload"
|
|
||||||
@upload="onImageUpload"
|
@upload="onImageUpload"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -39,22 +46,25 @@
|
|||||||
<script>
|
<script>
|
||||||
import FileDropzone from './dropzone'
|
import FileDropzone from './dropzone'
|
||||||
import { IconReload } from '@tabler/icons-vue'
|
import { IconReload } from '@tabler/icons-vue'
|
||||||
|
import MarkdownContent from './markdown_content'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ImageStep',
|
name: 'ImageStep',
|
||||||
components: {
|
components: {
|
||||||
FileDropzone,
|
FileDropzone,
|
||||||
IconReload
|
IconReload,
|
||||||
|
MarkdownContent
|
||||||
},
|
},
|
||||||
|
inject: ['t'],
|
||||||
props: {
|
props: {
|
||||||
field: {
|
field: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
isDirectUpload: {
|
showFieldNames: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: true,
|
required: false,
|
||||||
default: false
|
default: true
|
||||||
},
|
},
|
||||||
submitterSlug: {
|
submitterSlug: {
|
||||||
type: String,
|
type: String,
|
||||||
|
|||||||
@@ -1,41 +1,76 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div dir="auto">
|
||||||
<div class="flex justify-between items-center w-full mb-2">
|
<div
|
||||||
|
class="flex justify-between items-center w-full"
|
||||||
|
:class="{ 'mb-2': !field.description }"
|
||||||
|
>
|
||||||
<label
|
<label
|
||||||
class="label text-2xl"
|
class="label text-2xl"
|
||||||
>{{ field.name || t('initials') }}</label>
|
>
|
||||||
|
<MarkdownContent
|
||||||
|
v-if="field.title"
|
||||||
|
:string="field.title"
|
||||||
|
/>
|
||||||
|
<template v-else>
|
||||||
|
{{ showFieldNames && field.name ? field.name : t('initials') }}
|
||||||
|
</template>
|
||||||
|
</label>
|
||||||
<div class="space-x-2 flex">
|
<div class="space-x-2 flex">
|
||||||
<span
|
<span
|
||||||
|
v-if="isDrawInitials"
|
||||||
class="tooltip"
|
class="tooltip"
|
||||||
|
:data-tip="t('type_initial')"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
id="type_text_button"
|
||||||
|
href="#"
|
||||||
|
class="btn btn-outline font-medium btn-sm"
|
||||||
|
@click.prevent="toggleTextInput"
|
||||||
|
>
|
||||||
|
<IconTextSize :width="16" />
|
||||||
|
<span class="hidden sm:inline">
|
||||||
|
{{ t('type') }}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
class="tooltip ml-2"
|
||||||
:data-tip="t('draw_initials')"
|
:data-tip="t('draw_initials')"
|
||||||
>
|
>
|
||||||
<button
|
<a
|
||||||
id="type_text_button"
|
id="type_text_button"
|
||||||
class="btn btn-sm btn-circle"
|
href="#"
|
||||||
:class="{ 'btn-neutral': isDrawInitials, 'btn-outline': !isDrawInitials }"
|
class="btn btn-outline font-medium btn-sm"
|
||||||
@click.prevent="toggleTextInput"
|
@click.prevent="toggleTextInput"
|
||||||
>
|
>
|
||||||
<IconSignature :width="16" />
|
<IconSignature :width="16" />
|
||||||
</button>
|
<span class="hidden sm:inline">
|
||||||
|
{{ t('draw') }}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
</span>
|
</span>
|
||||||
<button
|
<a
|
||||||
v-if="modelValue"
|
v-if="modelValue || computedPreviousValue"
|
||||||
class="btn btn-outline btn-sm"
|
href="#"
|
||||||
|
class="btn font-medium btn-outline btn-sm"
|
||||||
@click.prevent="remove"
|
@click.prevent="remove"
|
||||||
>
|
>
|
||||||
<IconReload :width="16" />
|
<IconReload :width="16" />
|
||||||
{{ t('clear') }}
|
{{ t('clear') }}
|
||||||
</button>
|
</a>
|
||||||
<button
|
<a
|
||||||
v-else
|
v-else
|
||||||
class="btn btn-outline btn-sm"
|
href="#"
|
||||||
|
class="btn font-medium btn-outline btn-sm"
|
||||||
@click.prevent="clear"
|
@click.prevent="clear"
|
||||||
>
|
>
|
||||||
<IconReload :width="16" />
|
<IconReload :width="16" />
|
||||||
{{ t('clear') }}
|
{{ t('clear') }}
|
||||||
</button>
|
</a>
|
||||||
<button
|
<a
|
||||||
title="Minimize"
|
:title="t('minimize')"
|
||||||
|
href="#"
|
||||||
class="py-1.5 inline md:hidden"
|
class="py-1.5 inline md:hidden"
|
||||||
@click.prevent="$emit('minimize')"
|
@click.prevent="$emit('minimize')"
|
||||||
>
|
>
|
||||||
@@ -43,26 +78,34 @@
|
|||||||
:width="20"
|
:width="20"
|
||||||
:height="20"
|
:height="20"
|
||||||
/>
|
/>
|
||||||
</button>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="field.description"
|
||||||
|
dir="auto"
|
||||||
|
class="mb-3 px-1"
|
||||||
|
>
|
||||||
|
<MarkdownContent :string="field.description" />
|
||||||
|
</div>
|
||||||
|
<AppearsOn :field="field" />
|
||||||
<input
|
<input
|
||||||
:value="modelValue"
|
:value="modelValue || computedPreviousValue"
|
||||||
type="hidden"
|
type="hidden"
|
||||||
:name="`values[${field.uuid}]`"
|
:name="`values[${field.uuid}]`"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
v-if="modelValue"
|
v-if="modelValue || computedPreviousValue"
|
||||||
:src="attachmentsIndex[modelValue].url"
|
:src="attachmentsIndex[modelValue || computedPreviousValue].url"
|
||||||
class="mx-auto bg-white border border-base-300 rounded max-h-72"
|
class="mx-auto bg-white border border-base-300 rounded max-h-72"
|
||||||
>
|
>
|
||||||
<canvas
|
<canvas
|
||||||
v-show="!modelValue"
|
v-show="!modelValue && !computedPreviousValue"
|
||||||
ref="canvas"
|
ref="canvas"
|
||||||
class="bg-white border border-base-300 rounded"
|
class="bg-white border border-base-300 rounded-2xl w-full"
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
v-if="!isDrawInitials"
|
v-if="!isDrawInitials && !modelValue && !computedPreviousValue"
|
||||||
id="initials_text_input"
|
id="initials_text_input"
|
||||||
ref="textInput"
|
ref="textInput"
|
||||||
class="base-input !text-2xl w-full mt-6 text-center"
|
class="base-input !text-2xl w-full mt-6 text-center"
|
||||||
@@ -77,13 +120,21 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { cropCanvasAndExportToPNG } from './crop_canvas'
|
import { cropCanvasAndExportToPNG } from './crop_canvas'
|
||||||
import { IconReload, IconSignature, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
|
import { IconReload, IconTextSize, IconSignature, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
|
||||||
|
import SignaturePad from 'signature_pad'
|
||||||
|
import AppearsOn from './appears_on'
|
||||||
|
import MarkdownContent from './markdown_content'
|
||||||
|
|
||||||
|
const scale = 3
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'InitialsStep',
|
name: 'InitialsStep',
|
||||||
components: {
|
components: {
|
||||||
|
AppearsOn,
|
||||||
IconReload,
|
IconReload,
|
||||||
|
IconTextSize,
|
||||||
IconSignature,
|
IconSignature,
|
||||||
|
MarkdownContent,
|
||||||
IconArrowsDiagonalMinimize2
|
IconArrowsDiagonalMinimize2
|
||||||
},
|
},
|
||||||
inject: ['baseUrl', 't'],
|
inject: ['baseUrl', 't'],
|
||||||
@@ -96,16 +147,21 @@ export default {
|
|||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
isDirectUpload: {
|
showFieldNames: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: true,
|
required: false,
|
||||||
default: false
|
default: true
|
||||||
},
|
},
|
||||||
attachmentsIndex: {
|
attachmentsIndex: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: false,
|
required: false,
|
||||||
default: () => ({})
|
default: () => ({})
|
||||||
},
|
},
|
||||||
|
previousValue: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -115,26 +171,30 @@ export default {
|
|||||||
emits: ['attached', 'update:model-value', 'start', 'minimize', 'focus'],
|
emits: ['attached', 'update:model-value', 'start', 'minimize', 'focus'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
isInitialsStarted: false,
|
isInitialsStarted: !!this.previousValue,
|
||||||
|
isUsePreviousValue: true,
|
||||||
isDrawInitials: false
|
isDrawInitials: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
computedPreviousValue () {
|
||||||
|
if (this.isUsePreviousValue) {
|
||||||
|
return this.previousValue
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
async mounted () {
|
async mounted () {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
if (this.$refs.canvas) {
|
if (this.$refs.canvas) {
|
||||||
this.$refs.canvas.width = this.$refs.canvas.parentNode.clientWidth
|
this.$refs.canvas.width = this.$refs.canvas.parentNode.clientWidth * scale
|
||||||
this.$refs.canvas.height = this.$refs.canvas.parentNode.clientWidth / 5
|
this.$refs.canvas.height = (this.$refs.canvas.parentNode.clientWidth / 3) * scale
|
||||||
|
|
||||||
|
this.$refs.canvas.getContext('2d').scale(scale, scale)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$refs.textInput?.focus()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if (this.isDirectUpload) {
|
|
||||||
import('@rails/activestorage')
|
|
||||||
}
|
|
||||||
|
|
||||||
const { default: SignaturePad } = await import('signature_pad')
|
|
||||||
|
|
||||||
if (this.$refs.canvas) {
|
if (this.$refs.canvas) {
|
||||||
this.pad = new SignaturePad(this.$refs.canvas)
|
this.pad = new SignaturePad(this.$refs.canvas)
|
||||||
|
|
||||||
@@ -143,11 +203,32 @@ export default {
|
|||||||
|
|
||||||
this.$emit('start')
|
this.$emit('start')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.intersectionObserver = new IntersectionObserver((entries, observer) => {
|
||||||
|
entries.forEach(entry => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
this.$refs.canvas.width = this.$refs.canvas.parentNode.clientWidth * scale
|
||||||
|
this.$refs.canvas.height = (this.$refs.canvas.parentNode.clientWidth / 3) * scale
|
||||||
|
|
||||||
|
this.$refs.canvas.getContext('2d').scale(scale, scale)
|
||||||
|
|
||||||
|
this.intersectionObserver?.disconnect()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
this.intersectionObserver.observe(this.$refs.canvas)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
beforeUnmount () {
|
||||||
|
this.intersectionObserver?.disconnect()
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
remove () {
|
remove () {
|
||||||
this.$emit('update:model-value', '')
|
this.$emit('update:model-value', '')
|
||||||
|
|
||||||
|
this.isUsePreviousValue = false
|
||||||
|
this.isInitialsStarted = false
|
||||||
},
|
},
|
||||||
clear () {
|
clear () {
|
||||||
this.pad.clear()
|
this.pad.clear()
|
||||||
@@ -159,7 +240,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
updateWrittenInitials (e) {
|
updateWrittenInitials (e) {
|
||||||
this.isInitialsStarted = true
|
this.isInitialsStarted = !!e.target.value
|
||||||
|
|
||||||
const canvas = this.$refs.canvas
|
const canvas = this.$refs.canvas
|
||||||
const context = canvas.getContext('2d')
|
const context = canvas.getContext('2d')
|
||||||
@@ -172,8 +253,8 @@ export default {
|
|||||||
context.font = fontStyle + ' ' + fontWeight + ' ' + fontSize + ' ' + fontFamily
|
context.font = fontStyle + ' ' + fontWeight + ' ' + fontSize + ' ' + fontFamily
|
||||||
context.textAlign = 'center'
|
context.textAlign = 'center'
|
||||||
|
|
||||||
context.clearRect(0, 0, canvas.width, canvas.height)
|
context.clearRect(0, 0, canvas.width / scale, canvas.height / scale)
|
||||||
context.fillText(e.target.value, canvas.width / 2, canvas.height / 2 + 11)
|
context.fillText(e.target.value, canvas.width / 2 / scale, canvas.height / 2 / scale + 11)
|
||||||
},
|
},
|
||||||
toggleTextInput () {
|
toggleTextInput () {
|
||||||
this.remove()
|
this.remove()
|
||||||
@@ -189,7 +270,11 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async submit () {
|
async submit () {
|
||||||
if (this.modelValue) {
|
if (this.modelValue || this.computedPreviousValue) {
|
||||||
|
if (this.computedPreviousValue) {
|
||||||
|
this.$emit('update:model-value', this.computedPreviousValue)
|
||||||
|
}
|
||||||
|
|
||||||
return Promise.resolve({})
|
return Promise.resolve({})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,45 +282,21 @@ export default {
|
|||||||
cropCanvasAndExportToPNG(this.$refs.canvas).then(async (blob) => {
|
cropCanvasAndExportToPNG(this.$refs.canvas).then(async (blob) => {
|
||||||
const file = new File([blob], 'initials.png', { type: 'image/png' })
|
const file = new File([blob], 'initials.png', { type: 'image/png' })
|
||||||
|
|
||||||
if (this.isDirectUpload) {
|
const formData = new FormData()
|
||||||
const { DirectUpload } = await import('@rails/activestorage')
|
|
||||||
|
|
||||||
new DirectUpload(
|
formData.append('file', file)
|
||||||
file,
|
formData.append('submitter_slug', this.submitterSlug)
|
||||||
'/direct_uploads'
|
formData.append('name', 'attachments')
|
||||||
).create((_error, data) => {
|
|
||||||
fetch(this.baseUrl + '/api/attachments', {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({
|
|
||||||
submitter_slug: this.submitterSlug,
|
|
||||||
blob_signed_id: data.signed_id,
|
|
||||||
name: 'attachments'
|
|
||||||
}),
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
}).then((resp) => resp.json()).then((attachment) => {
|
|
||||||
this.$emit('update:model-value', attachment.uuid)
|
|
||||||
this.$emit('attached', attachment)
|
|
||||||
|
|
||||||
return resolve(attachment)
|
return fetch(this.baseUrl + '/api/attachments', {
|
||||||
})
|
method: 'POST',
|
||||||
})
|
body: formData
|
||||||
} else {
|
}).then((resp) => resp.json()).then((attachment) => {
|
||||||
const formData = new FormData()
|
this.$emit('attached', attachment)
|
||||||
|
this.$emit('update:model-value', attachment.uuid)
|
||||||
|
|
||||||
formData.append('file', file)
|
return resolve(attachment)
|
||||||
formData.append('submitter_slug', this.submitterSlug)
|
})
|
||||||
formData.append('name', 'attachments')
|
|
||||||
|
|
||||||
return fetch(this.baseUrl + '/api/attachments', {
|
|
||||||
method: 'POST',
|
|
||||||
body: formData
|
|
||||||
}).then((resp) => resp.json()).then((attachment) => {
|
|
||||||
this.$emit('attached', attachment)
|
|
||||||
this.$emit('update:model-value', attachment.uuid)
|
|
||||||
|
|
||||||
return resolve(attachment)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
<template>
|
||||||
|
<span>
|
||||||
|
<template
|
||||||
|
v-for="(item, index) in items"
|
||||||
|
:key="index"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
v-if="item.startsWith('<a') && item.endsWith('</a>')"
|
||||||
|
:href="sanitizeHref(extractAttr(item, 'href'))"
|
||||||
|
rel="noopener noreferrer nofollow"
|
||||||
|
:class="extractAttr(item, 'class') || 'link'"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{{ extractText(item) }}
|
||||||
|
</a>
|
||||||
|
<b
|
||||||
|
v-else-if="item.startsWith('<b>') || item.startsWith('<strong>')"
|
||||||
|
>
|
||||||
|
{{ extractText(item) }}
|
||||||
|
</b>
|
||||||
|
<i
|
||||||
|
v-else-if="item.startsWith('<i>') || item.startsWith('<em>')"
|
||||||
|
>
|
||||||
|
{{ extractText(item) }}
|
||||||
|
</i>
|
||||||
|
<br
|
||||||
|
v-else-if="item === '<br>' || item === '\n'"
|
||||||
|
>
|
||||||
|
<template
|
||||||
|
v-else
|
||||||
|
>
|
||||||
|
{{ item }}
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import snarkdown from 'snarkdown'
|
||||||
|
|
||||||
|
const htmlSplitRegexp = /(<a.+?<\/a>|<i>.+?<\/i>|<b>.+?<\/b>|<em>.+?<\/em>|<strong>.+?<\/strong>|<br>)/
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'MarkdownContent',
|
||||||
|
props: {
|
||||||
|
string: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
items () {
|
||||||
|
return snarkdown(this.string.replace(/\n/g, '<br>')).split(htmlSplitRegexp)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
sanitizeHref (href) {
|
||||||
|
if (href && href.trim().match(/^((?:https?:\/\/)|\/)/)) {
|
||||||
|
return href.replace(/javascript:/g, '')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
extractAttr (text, attr) {
|
||||||
|
if (text.includes(attr)) {
|
||||||
|
return text.split(attr).pop().split('"')[1]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
extractText (text) {
|
||||||
|
if (text) {
|
||||||
|
return text.match(/>(.+?)</)?.[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
Loaded 100 of 399 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user