mirror of
https://github.com/docusealco/docuseal
synced 2026-11-04 14:42:32 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
019222ae40 | ||
|
|
8a10852fe2 | ||
|
|
01232aed5a | ||
|
|
82f3ff4824 | ||
|
|
7be3f5a931 | ||
|
|
9d013a7384 | ||
|
|
0c71d7bff8 | ||
|
|
fdd030652e | ||
|
|
f2bf1461f0 | ||
|
|
fc6f796f30 | ||
|
|
5f44806287 | ||
|
|
318b65af18 | ||
|
|
91b182b04a | ||
|
|
15b886d4ff | ||
|
|
0122a237e3 | ||
|
|
2dca7d8a8e | ||
|
|
87265e2f22 | ||
|
|
3166defa1a | ||
|
|
93ba17f42e | ||
|
|
751be1dae5 | ||
|
|
3f8d2831e2 | ||
|
|
4d8942803e | ||
|
|
46d9bb2c87 | ||
|
|
34433c0575 | ||
|
|
544306eac4 | ||
|
|
0eeebfd3cf | ||
|
|
050224a10d | ||
|
|
7b6fc7d06f | ||
|
|
7fb148c85c | ||
|
|
dffa59fecd | ||
|
|
24fe2a1192 | ||
|
|
97b8ac4444 | ||
|
|
e34a763dd9 | ||
|
|
298b1d3e1e | ||
|
|
d9978519cc | ||
|
|
b4a68cf79e | ||
|
|
5663e60f92 | ||
|
|
19e8835809 | ||
|
|
330fae927f | ||
|
|
c5a5212947 | ||
|
|
21d81e38bf | ||
|
|
5b1a4ebffd | ||
|
|
5f5dabd0e5 | ||
|
|
ff46993de6 | ||
|
|
7676db2196 | ||
|
|
3ef0d28ffe | ||
|
|
231177f057 | ||
|
|
e5f7739952 | ||
|
|
4a8e2a82e5 | ||
|
|
6efe8027f1 | ||
|
|
96bb4eb6e5 | ||
|
|
38e68814cf | ||
|
|
786c80bee7 | ||
|
|
98dcce299e | ||
|
|
cbff242a5d | ||
|
|
1876b3efba | ||
|
|
51091f05f0 | ||
|
|
d76eee2ae4 | ||
|
|
d7bfa96d57 | ||
|
|
be60608211 | ||
|
|
4e5abb3c09 | ||
|
|
7fee82ded2 | ||
|
|
e1cd62ff40 | ||
|
|
b498291b62 | ||
|
|
def3009846 | ||
|
|
9d43fd9aa5 | ||
|
|
22a21fb037 | ||
|
|
2a9c375273 | ||
|
|
847e70ec1b | ||
|
|
7964d6e0f0 | ||
|
|
d0259791bf | ||
|
|
a9dcaeccbc | ||
|
|
501b91a603 | ||
|
|
2b5f772feb | ||
|
|
7cada0af8b | ||
|
|
67be785e69 | ||
|
|
d1cfc64adc | ||
|
|
4e5157d1ee | ||
|
|
5e27cc36c6 | ||
|
|
6e81a3be3f | ||
|
|
858b2f33fd | ||
|
|
6adcc87b2b | ||
|
|
e1d5a5b6a6 | ||
|
|
f090e79002 | ||
|
|
56b32e35a2 | ||
|
|
b40c27cdbf | ||
|
|
c4505e67f0 | ||
|
|
d0799b8cc1 | ||
|
|
889fcad859 | ||
|
|
c133fef99d | ||
|
|
cfdc978a2a | ||
|
|
bff1651966 | ||
|
|
b97854f2fe | ||
|
|
7a4629bf79 | ||
|
|
2573d8cb19 | ||
|
|
dd559e90f1 | ||
|
|
e5db61e358 | ||
|
|
69376c0faf | ||
|
|
1f48be135c | ||
|
|
1cae12b728 | ||
|
|
86d2cd93f8 | ||
|
|
b0110d9340 | ||
|
|
7e70f4fb14 | ||
|
|
28e3d65e63 | ||
|
|
f1a2b71b59 | ||
|
|
47c7270911 | ||
|
|
4e1fa336a2 | ||
|
|
244bc32136 | ||
|
|
42c30bdfd0 | ||
|
|
0abf48ddba | ||
|
|
ef05f030de | ||
|
|
b8c36ff09c | ||
|
|
1ba8a9a374 | ||
|
|
661e9b69fb | ||
|
|
a1aeabccff | ||
|
|
0e5e0e7772 | ||
|
|
2a74fdd095 | ||
|
|
ac426e55b2 | ||
|
|
ad2b35673a | ||
|
|
af1e8fa6f2 | ||
|
|
5800cb6c5a | ||
|
|
64d3360e82 | ||
|
|
3ae98bf03e | ||
|
|
65e81d90f4 | ||
|
|
4fb9637e2d | ||
|
|
6b3726dbd5 | ||
|
|
c5b0f2c76c | ||
|
|
ea18f9e28a | ||
|
|
d229c75978 | ||
|
|
ecbe3c3e4f | ||
|
|
38a5cb968b | ||
|
|
a30ee9e961 | ||
|
|
eba27004dc | ||
|
|
09849acd2f | ||
|
|
19e9047a39 | ||
|
|
82b6bdd9ad | ||
|
|
5764b4dac1 | ||
|
|
3affd67d24 | ||
|
|
d5a98a0f9e | ||
|
|
9629cc7226 | ||
|
|
06415ad869 | ||
|
|
3f872da435 | ||
|
|
bc1d5a9825 | ||
|
|
72c8ed9a00 | ||
|
|
3fec433846 | ||
|
|
3b572702d7 | ||
|
|
febdc0b48d | ||
|
|
8773bb2f62 | ||
|
|
55ad70fabc | ||
|
|
23dcd62672 | ||
|
|
42371d7ce3 | ||
|
|
4116746be3 | ||
|
|
b2b2856ac0 | ||
|
|
b65bca213f | ||
|
|
7a72e2cbb7 | ||
|
|
3a73398a1e | ||
|
|
cce20d9230 | ||
|
|
8b72c91951 | ||
|
|
b0d8e47a75 | ||
|
|
85824faa2b | ||
|
|
8a507339af | ||
|
|
4636849118 | ||
|
|
cc52cae9fc | ||
|
|
b6bb4d2c8b | ||
|
|
5032676bd2 | ||
|
|
146b37f8cd | ||
|
|
47b3a2d09e | ||
|
|
f2306b334b | ||
|
|
3affa65571 | ||
|
|
6f3be16826 | ||
|
|
07ce76537b | ||
|
|
a1fb38b8cb | ||
|
|
1b52a99662 | ||
|
|
f5facfa6b9 | ||
|
|
21c6144dc6 | ||
|
|
2c155f0e42 | ||
|
|
33cc0a8ab5 | ||
|
|
1e65a7e52b | ||
|
|
14e2dbe1aa | ||
|
|
a823a6ac56 | ||
|
|
cfc4e001ba | ||
|
|
496cc855f6 | ||
|
|
bfb2db07a5 | ||
|
|
9f4f868246 | ||
|
|
f91f02f786 | ||
|
|
1da72e3e7a | ||
|
|
2e2c0e087d | ||
|
|
0c72f58ab0 | ||
|
|
d9d969cb79 | ||
|
|
8d685d1f73 | ||
|
|
e406620044 | ||
|
|
a315674421 | ||
|
|
8d953f210f | ||
|
|
7439ef07ad | ||
|
|
fc4b4de6ed | ||
|
|
96e4733f87 | ||
|
|
8b6731a289 | ||
|
|
f93ac68e9c | ||
|
|
439df4879c | ||
|
|
83b6ce8574 | ||
|
|
6ea07bc13b | ||
|
|
a15624690b | ||
|
|
974bb85fb7 | ||
|
|
5dc80601b2 | ||
|
|
df2e53bc40 | ||
|
|
6e685bbc32 | ||
|
|
0b3ca0de96 | ||
|
|
aed3f1f141 | ||
|
|
1a7981e919 | ||
|
|
4add805371 | ||
|
|
66c18317b7 | ||
|
|
9edbb885b4 | ||
|
|
c67c80d6bc | ||
|
|
0399531e0c | ||
|
|
54426556d9 | ||
|
|
fa74ee1755 | ||
|
|
eb53af12c4 | ||
|
|
38efcb99c0 | ||
|
|
a92d914674 | ||
|
|
219a698aa5 | ||
|
|
5bed6448dc | ||
|
|
6d2a8ca3d3 | ||
|
|
bfb2e0043d | ||
|
|
922101b12c | ||
|
|
9ff41a0c4a | ||
|
|
071f187605 | ||
|
|
ab6cd0e89c | ||
|
|
97c0dad7ff | ||
|
|
bc0917a190 | ||
|
|
5718acecff | ||
|
|
395d43dbeb | ||
|
|
37de5e0d0d | ||
|
|
44e848a1f0 | ||
|
|
f6d59d731c | ||
|
|
87b039e709 | ||
|
|
e0c6a4e08b | ||
|
|
80b06550c2 | ||
|
|
ec8e35ed7a | ||
|
|
4dc325a6bb | ||
|
|
d52c57df72 | ||
|
|
0cb2557447 | ||
|
|
cd05c1fd19 | ||
|
|
5458e82b6e | ||
|
|
c59c58624b | ||
|
|
2da059d440 | ||
|
|
b0aabd9e79 | ||
|
|
f1cd60dfad | ||
|
|
a264a8522a | ||
|
|
794e339425 | ||
|
|
9aaf61e658 | ||
|
|
37faa7ece6 | ||
|
|
666e3fde4c | ||
|
|
995e040a4e | ||
|
|
185ac2cbee | ||
|
|
a4e372ff5c | ||
|
|
b4834c7674 | ||
|
|
c24ae1751e | ||
|
|
9bd0ad70c4 | ||
|
|
f210f1d8bf | ||
|
|
92b61992f2 | ||
|
|
e3f3895aac | ||
|
|
c696890cf5 | ||
|
|
288d204d8e | ||
|
|
5a96f4f1eb | ||
|
|
5ff4f9889e | ||
|
|
78fb93fd02 | ||
|
|
3c83d459bd | ||
|
|
6cdb8e7132 | ||
|
|
e2b93cbcab | ||
|
|
a59cc7c44e | ||
|
|
1984b99e04 | ||
|
|
fe3e8fba3e | ||
|
|
97ecdf2ff0 | ||
|
|
e4c62087d0 | ||
|
|
1f9faddfd0 | ||
|
|
6dff6483bf | ||
|
|
2e43eb189f | ||
|
|
cee471bfa5 | ||
|
|
81521dee81 | ||
|
|
3cac62d0fd | ||
|
|
24ed3f2e7c | ||
|
|
02ad620c9f | ||
|
|
9051d5a835 | ||
|
|
0abd270f56 | ||
|
|
fde9affd70 | ||
|
|
4edee2971a | ||
|
|
313d7f8408 | ||
|
|
db60810272 | ||
|
|
b3507c1a3a | ||
|
|
19b5a07bac | ||
|
|
616aced4e4 | ||
|
|
cc218fc0b0 | ||
|
|
c59f2a022a | ||
|
|
2a240cebeb | ||
|
|
b950b1c63e | ||
|
|
4ad0b04b7f | ||
|
|
8edf6405b0 | ||
|
|
8e3caae849 | ||
|
|
ac9a5c9556 | ||
|
|
20d09f6dff | ||
|
|
333a758688 | ||
|
|
2c8b4d5816 | ||
|
|
9ce822bce8 | ||
|
|
0a382a688c | ||
|
|
bd3008a35a | ||
|
|
e9bd11cb2e | ||
|
|
ca429bc445 | ||
|
|
21a0c54843 | ||
|
|
2f41d13009 | ||
|
|
f69dcf77de | ||
|
|
32596d738c | ||
|
|
e7e5d0e776 | ||
|
|
bb3b38e76f | ||
|
|
bc4c21f804 | ||
|
|
2fb5de4fa9 | ||
|
|
2e58b2e37b | ||
|
|
780f5512f4 | ||
|
|
2cf4908d53 | ||
|
|
4a9b898713 | ||
|
|
bb552d13d4 | ||
|
|
15d06f4fb0 | ||
|
|
35a2aba887 | ||
|
|
48a82b1d49 | ||
|
|
b8453e14c4 | ||
|
|
29fbdfbe2f | ||
|
|
5340af0bc6 | ||
|
|
1881b6c50f | ||
|
|
b6a2aae970 | ||
|
|
b317094192 | ||
|
|
ab1f4cf297 | ||
|
|
d3033c072a | ||
|
|
f380200e03 | ||
|
|
a4928268ea | ||
|
|
dd18c51384 | ||
|
|
c2014a1549 | ||
|
|
46edc7dc37 | ||
|
|
d62d83ef8e | ||
|
|
79350c8fba | ||
|
|
6702dd52b6 | ||
|
|
c8b74a2c43 | ||
|
|
1746920eb5 | ||
|
|
bacbc4309c | ||
|
|
4429d742b3 | ||
|
|
38b5eccb65 | ||
|
|
d8126c4a47 | ||
|
|
b1c31672ee | ||
|
|
975f3a95fe | ||
|
|
e3bed5ae67 | ||
|
|
4737b103c0 | ||
|
|
c5178b96d3 | ||
|
|
39a2159196 | ||
|
|
5724159b7b | ||
|
|
05b7cc9fa6 | ||
|
|
18b13b5b1e | ||
|
|
d25555b9b0 | ||
|
|
eec266ee83 | ||
|
|
75683631f0 | ||
|
|
36c64d1de4 | ||
|
|
2552b24f21 | ||
|
|
e6ca6dfd85 | ||
|
|
fa2d86b20a | ||
|
|
8e212e4586 | ||
|
|
a59c18f04b | ||
|
|
1ab0f1c90c | ||
|
|
9beaaa0851 | ||
|
|
b86aac86d1 | ||
|
|
c2eb94b571 | ||
|
|
a377e45a63 | ||
|
|
58574abc6b | ||
|
|
cbae888971 | ||
|
|
944e1f31db | ||
|
|
711b3b3190 | ||
|
|
e7abadcefb | ||
|
|
18d60f5522 | ||
|
|
6d5c6e086d | ||
|
|
5a48ab0360 | ||
|
|
599785b6d1 | ||
|
|
f2fd5fb82b | ||
|
|
f2782bd84e | ||
|
|
e52a721fc9 | ||
|
|
b035123ff9 | ||
|
|
ca8de23082 | ||
|
|
f514654c40 | ||
|
|
99bc528a6c | ||
|
|
f3c77d0177 | ||
|
|
db2b5f35bd | ||
|
|
5728381725 | ||
|
|
a7affb1a49 | ||
|
|
21bf5ed9f7 | ||
|
|
8ddd4ee916 | ||
|
|
0e2fd8ee4f | ||
|
|
27901063ed | ||
|
|
ce31ebde1e | ||
|
|
0c2abf211e | ||
|
|
8ecaa4fd7a | ||
|
|
6759c26d86 | ||
|
|
76f1f140ac | ||
|
|
ca7ab5aab7 | ||
|
|
fd3530ac62 | ||
|
|
4ea863592b | ||
|
|
3f87d0336d | ||
|
|
e251482481 | ||
|
|
c79fca75f3 | ||
|
|
0b0f0b497e | ||
|
|
e9034d8da7 | ||
|
|
8fbb8b8f6e | ||
|
|
4635fdd32b | ||
|
|
6dd4382446 | ||
|
|
50acff2e81 | ||
|
|
cb6d24dfca | ||
|
|
ab4a22d8ba | ||
|
|
bc40a15de1 | ||
|
|
2542f26d96 | ||
|
|
84edb6dbda | ||
|
|
88776e7316 | ||
|
|
f669045362 | ||
|
|
68d3efbdba | ||
|
|
22d4833496 | ||
|
|
ff14caa9f7 | ||
|
|
05647135bc | ||
|
|
ae76c2a4a8 | ||
|
|
fb2b3d44fe | ||
|
|
3868557efd | ||
|
|
9bef28c8ba | ||
|
|
f25fd3b03d | ||
|
|
d1d7571ee4 | ||
|
|
6a298a068c | ||
|
|
d4aef79b68 | ||
|
|
05d578d88f | ||
|
|
8773b30b10 | ||
|
|
1b636c5878 | ||
|
|
46e948ab99 | ||
|
|
889103aa5e | ||
|
|
558a14a5f2 | ||
|
|
72bbdb38bf | ||
|
|
3975a03cb4 | ||
|
|
e398327fb7 | ||
|
|
9266694d65 | ||
|
|
350c4b1c2a | ||
|
|
c3a933d87d | ||
|
|
970e53a5a3 | ||
|
|
520bac2ec7 | ||
|
|
e1eddd31c4 | ||
|
|
3c616a824c | ||
|
|
90d86b58c2 | ||
|
|
4332b4ebb8 | ||
|
|
e2f930f6f7 | ||
|
|
c303159c63 | ||
|
|
c1ce2bec00 | ||
|
|
3d2046f227 | ||
|
|
babaf1aa48 | ||
|
|
3f03d343ca | ||
|
|
110cdb5123 | ||
|
|
440d1114be | ||
|
|
9032517d4e | ||
|
|
7c71fefc47 | ||
|
|
e8bd8ff76f | ||
|
|
fed420cae1 | ||
|
|
910490528d | ||
|
|
ab810be250 | ||
|
|
178ee47093 | ||
|
|
6f90147a11 | ||
|
|
3ebaacbb7a | ||
|
|
64b39b53a2 | ||
|
|
2948910c25 | ||
|
|
1d20c79cf4 | ||
|
|
48f3b13f3e | ||
|
|
1fc1e9af16 | ||
|
|
ce70016603 | ||
|
|
e0e79e343d | ||
|
|
6568f81ee9 | ||
|
|
386f130d7a | ||
|
|
3ee41975fd | ||
|
|
d9f314898b | ||
|
|
4fc7412cac | ||
|
|
9adbfc52c2 | ||
|
|
1ad8d9196a | ||
|
|
e9172392b5 | ||
|
|
d6d94de328 | ||
|
|
77261f457c | ||
|
|
c258915031 | ||
|
|
c1db8bdd3b | ||
|
|
c700484ce8 | ||
|
|
b62bd200ed | ||
|
|
a164fe8dd8 | ||
|
|
bb40e872b3 | ||
|
|
bab80e9488 | ||
|
|
d090d79ebf | ||
|
|
2299ba4e29 | ||
|
|
d6474f0e6e | ||
|
|
e0a297cf81 | ||
|
|
40f5fa4217 | ||
|
|
94e552d170 | ||
|
|
cf1b0a4c16 | ||
|
|
9aa0a8ecbf | ||
|
|
fd4bef1706 | ||
|
|
d32b0e6d20 | ||
|
|
4a846be214 | ||
|
|
3dd3877ea9 | ||
|
|
b4dba0d809 | ||
|
|
dea80a82d8 | ||
|
|
eefa994724 | ||
|
|
35cce24539 | ||
|
|
e6c8203959 | ||
|
|
9479771fc8 | ||
|
|
711427521c | ||
|
|
5dae742361 | ||
|
|
d8ccd4beee | ||
|
|
c532a0115d | ||
|
|
c9c3364764 | ||
|
|
1ff23d7c40 | ||
|
|
95e9b34033 | ||
|
|
6b42dcf91b | ||
|
|
28962c949b | ||
|
|
4ecbbf8410 | ||
|
|
990b35be18 | ||
|
|
8002764855 | ||
|
|
b80982658c | ||
|
|
443ac23c29 | ||
|
|
37bb987e3e | ||
|
|
c7b08e8d30 | ||
|
|
dd502e8cef | ||
|
|
a554182e57 | ||
|
|
0c039ee64f | ||
|
|
c6b004f107 | ||
|
|
2fcb08d27d | ||
|
|
75fe5ce381 | ||
|
|
fd7d0eaf53 | ||
|
|
7e537e4232 | ||
|
|
a892975010 | ||
|
|
05ec92186b |
No files matched your search
@@ -0,0 +1,58 @@
|
|||||||
|
---
|
||||||
|
:position: before
|
||||||
|
:position_in_additional_file_patterns: before
|
||||||
|
:position_in_class: before
|
||||||
|
:position_in_factory: before
|
||||||
|
:position_in_fixture: before
|
||||||
|
:position_in_routes: before
|
||||||
|
:position_in_serializer: before
|
||||||
|
:position_in_test: before
|
||||||
|
:classified_sort: true
|
||||||
|
:exclude_controllers: true
|
||||||
|
:exclude_factories: true
|
||||||
|
:exclude_fixtures: false
|
||||||
|
:exclude_helpers: true
|
||||||
|
:exclude_scaffolds: true
|
||||||
|
:exclude_serializers: false
|
||||||
|
:exclude_sti_subclasses: false
|
||||||
|
:exclude_tests: false
|
||||||
|
:force: false
|
||||||
|
:format_markdown: false
|
||||||
|
:format_rdoc: false
|
||||||
|
:format_yard: false
|
||||||
|
:frozen: false
|
||||||
|
:ignore_model_sub_dir: false
|
||||||
|
:ignore_unknown_models: false
|
||||||
|
:include_version: false
|
||||||
|
:show_check_constraints: false
|
||||||
|
:show_complete_foreign_keys: false
|
||||||
|
:show_foreign_keys: true
|
||||||
|
:show_indexes: true
|
||||||
|
:simple_indexes: false
|
||||||
|
:sort: false
|
||||||
|
:timestamp: false
|
||||||
|
:trace: false
|
||||||
|
:with_comment: true
|
||||||
|
:with_column_comments: true
|
||||||
|
:with_table_comments: true
|
||||||
|
:active_admin: false
|
||||||
|
:command:
|
||||||
|
:debug: false
|
||||||
|
:hide_default_column_types: ''
|
||||||
|
:hide_limit_column_types: ''
|
||||||
|
:ignore_columns:
|
||||||
|
:ignore_routes:
|
||||||
|
:models: true
|
||||||
|
:routes: false
|
||||||
|
:skip_on_db_migrate: false
|
||||||
|
:target_action: :do_annotations
|
||||||
|
:wrapper:
|
||||||
|
:wrapper_close:
|
||||||
|
:wrapper_open:
|
||||||
|
:classes_default_to_s: []
|
||||||
|
:additional_file_patterns: []
|
||||||
|
:model_dir:
|
||||||
|
- app/models
|
||||||
|
:require: []
|
||||||
|
:root_dir:
|
||||||
|
- ''
|
||||||
File renamed without changes.
+40
-13
@@ -7,13 +7,13 @@ jobs:
|
|||||||
name: Rubocop
|
name: Rubocop
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- name: Install Ruby
|
- name: Install Ruby
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
ruby-version: 3.3.4
|
ruby-version: 3.4.2
|
||||||
- name: Cache gems
|
- name: Cache gems
|
||||||
uses: actions/cache@v1
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: vendor/bundle
|
path: vendor/bundle
|
||||||
key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }}
|
key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }}
|
||||||
@@ -31,13 +31,13 @@ jobs:
|
|||||||
name: Erblint
|
name: Erblint
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- name: Install Ruby
|
- name: Install Ruby
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
ruby-version: 3.3.4
|
ruby-version: 3.4.2
|
||||||
- name: Cache gems
|
- name: Cache gems
|
||||||
uses: actions/cache@v1
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: vendor/bundle
|
path: vendor/bundle
|
||||||
key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }}
|
key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }}
|
||||||
@@ -49,13 +49,13 @@ jobs:
|
|||||||
bundle config path vendor/bundle
|
bundle config path vendor/bundle
|
||||||
bundle install --jobs 4 --retry 4
|
bundle install --jobs 4 --retry 4
|
||||||
- name: Run Erblint
|
- name: Run Erblint
|
||||||
run: bundle exec erblint ./app
|
run: bundle exec erb_lint ./app
|
||||||
|
|
||||||
eslint:
|
eslint:
|
||||||
name: ESLint
|
name: ESLint
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- name: Install Node.js
|
- name: Install Node.js
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
@@ -63,7 +63,7 @@ jobs:
|
|||||||
- 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)"
|
||||||
- uses: actions/cache@v1
|
- uses: actions/cache@v4
|
||||||
id: yarn-cache
|
id: yarn-cache
|
||||||
with:
|
with:
|
||||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||||
@@ -77,6 +77,33 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
./node_modules/eslint/bin/eslint.js "app/javascript/**/*.js"
|
./node_modules/eslint/bin/eslint.js "app/javascript/**/*.js"
|
||||||
|
|
||||||
|
brakeman:
|
||||||
|
name: Brakeman
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Install Ruby
|
||||||
|
uses: ruby/setup-ruby@v1
|
||||||
|
with:
|
||||||
|
ruby-version: 3.4.2
|
||||||
|
- name: Cache gems
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: vendor/bundle
|
||||||
|
key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-gem-
|
||||||
|
- name: Install gems
|
||||||
|
run: |
|
||||||
|
gem install bundler
|
||||||
|
bundle config path vendor/bundle
|
||||||
|
bundle install --jobs 4 --retry 4
|
||||||
|
yarn install
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install libvips
|
||||||
|
- name: Run Brakeman
|
||||||
|
run: bundle exec brakeman -q --exit-on-warn
|
||||||
|
|
||||||
rspec:
|
rspec:
|
||||||
name: RSpec
|
name: RSpec
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -96,11 +123,11 @@ jobs:
|
|||||||
--health-retries 5
|
--health-retries 5
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- name: Install Ruby
|
- name: Install Ruby
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
ruby-version: 3.3.4
|
ruby-version: 3.4.2
|
||||||
- name: Set up Node
|
- name: Set up Node
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
@@ -110,12 +137,12 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
chrome-version: 125
|
chrome-version: 125
|
||||||
- name: Cache node_modules
|
- name: Cache node_modules
|
||||||
uses: actions/cache@v1
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: node_modules
|
path: node_modules
|
||||||
key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }}
|
key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }}
|
||||||
- name: Cache gems
|
- name: Cache gems
|
||||||
uses: actions/cache@v1
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: vendor/bundle
|
path: vendor/bundle
|
||||||
key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }}
|
key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-24.04-arm
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
@@ -25,22 +25,22 @@ jobs:
|
|||||||
tags: |
|
tags: |
|
||||||
type=semver,pattern={{version}}
|
type=semver,pattern={{version}}
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v2
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Create .version file
|
- name: Create .version file
|
||||||
run: echo ${{ github.ref_name }} > .version
|
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@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
uses: docker/build-push-action@v4
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: true
|
push: true
|
||||||
|
|||||||
+6
-6
@@ -51,25 +51,25 @@ Metrics/AbcSize:
|
|||||||
- spec/**/*
|
- spec/**/*
|
||||||
|
|
||||||
Metrics/ModuleLength:
|
Metrics/ModuleLength:
|
||||||
Max: 500
|
Max: 1000
|
||||||
|
|
||||||
Metrics/ClassLength:
|
Metrics/ClassLength:
|
||||||
Max: 500
|
Max: 1000
|
||||||
|
|
||||||
RSpec/NestedGroups:
|
RSpec/NestedGroups:
|
||||||
Max: 6
|
Max: 6
|
||||||
|
|
||||||
RSpec/MultipleExpectations:
|
RSpec/MultipleExpectations:
|
||||||
Max: 20
|
Max: 25
|
||||||
|
|
||||||
RSpec/ExampleLength:
|
RSpec/ExampleLength:
|
||||||
Max: 40
|
Max: 50
|
||||||
|
|
||||||
RSpec/MultipleMemoizedHelpers:
|
RSpec/MultipleMemoizedHelpers:
|
||||||
Max: 6
|
Max: 9
|
||||||
|
|
||||||
Metrics/BlockNesting:
|
Metrics/BlockNesting:
|
||||||
Max: 4
|
Max: 5
|
||||||
|
|
||||||
Rails/I18nLocaleTexts:
|
Rails/I18nLocaleTexts:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|||||||
+7
-6
@@ -1,4 +1,4 @@
|
|||||||
FROM ruby:3.3.4-alpine as fonts
|
FROM ruby:3.4.2-alpine AS fonts
|
||||||
|
|
||||||
WORKDIR /fonts
|
WORKDIR /fonts
|
||||||
|
|
||||||
@@ -6,7 +6,7 @@ RUN apk --no-cache add fontforge wget && wget https://github.com/satbyy/go-noto-
|
|||||||
|
|
||||||
RUN fontforge -lang=py -c 'font1 = fontforge.open("FreeSans.ttf"); font2 = fontforge.open("NotoSansSymbols2-Regular.ttf"); font1.mergeFonts(font2); font1.generate("FreeSans.ttf")'
|
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.4-alpine as webpack
|
FROM ruby:3.4.2-alpine AS webpack
|
||||||
|
|
||||||
ENV RAILS_ENV=production
|
ENV RAILS_ENV=production
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
@@ -32,7 +32,7 @@ COPY ./app/views ./app/views
|
|||||||
|
|
||||||
RUN echo "gem 'shakapacker'" > Gemfile && ./bin/shakapacker
|
RUN echo "gem 'shakapacker'" > Gemfile && ./bin/shakapacker
|
||||||
|
|
||||||
FROM ruby:3.3.4-alpine as app
|
FROM ruby:3.4.2-alpine AS app
|
||||||
|
|
||||||
ENV RAILS_ENV=production
|
ENV RAILS_ENV=production
|
||||||
ENV BUNDLE_WITHOUT="development:test"
|
ENV BUNDLE_WITHOUT="development:test"
|
||||||
@@ -41,7 +41,7 @@ ENV OPENSSL_CONF=/app/openssl_legacy.cnf
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN apk add --no-cache sqlite-dev libpq-dev mariadb-dev vips-dev vips-poppler poppler-utils redis vips-heif gcompat ttf-freefont && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf
|
RUN echo '@edge https://dl-cdn.alpinelinux.org/alpine/edge/community' >> /etc/apk/repositories && apk add --no-cache sqlite-dev libpq-dev mariadb-dev vips-dev vips-poppler poppler-utils redis libheif@edge vips-heif gcompat ttf-freefont && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf
|
||||||
|
|
||||||
RUN echo $'.include = /etc/ssl/openssl.cnf\n\
|
RUN echo $'.include = /etc/ssl/openssl.cnf\n\
|
||||||
\n\
|
\n\
|
||||||
@@ -57,17 +57,18 @@ activate = 1' >> /app/openssl_legacy.cnf
|
|||||||
|
|
||||||
COPY ./Gemfile ./Gemfile.lock ./
|
COPY ./Gemfile ./Gemfile.lock ./
|
||||||
|
|
||||||
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
|
RUN apk add --no-cache build-base && bundle install && apk del --no-cache 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
|
||||||
COPY ./config ./config
|
COPY ./config ./config
|
||||||
COPY ./db ./db
|
COPY ./db/migrate ./db/migrate
|
||||||
COPY ./log ./log
|
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 .version ./
|
COPY LICENSE README.md Rakefile config.ru .version ./
|
||||||
|
COPY .version ./public/version
|
||||||
|
|
||||||
COPY --from=fonts /fonts/GoNotoKurrent-Regular.ttf /fonts/GoNotoKurrent-Bold.ttf /fonts/DancingScript-Regular.otf /fonts/OFL.txt /fonts
|
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=fonts /fonts/FreeSans.ttf /usr/share/fonts/freefont
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
source 'https://rubygems.org'
|
source 'https://rubygems.org'
|
||||||
|
|
||||||
ruby '3.3.4'
|
ruby '3.4.2'
|
||||||
|
|
||||||
gem 'arabic-letter-connector', require: 'arabic-letter-connector/logic'
|
gem 'arabic-letter-connector', require: 'arabic-letter-connector/logic'
|
||||||
gem 'aws-sdk-s3', require: false
|
gem 'aws-sdk-s3', require: false
|
||||||
@@ -23,7 +23,6 @@ gem 'image_processing'
|
|||||||
gem 'jwt'
|
gem 'jwt'
|
||||||
gem 'lograge'
|
gem 'lograge'
|
||||||
gem 'mysql2', require: false
|
gem 'mysql2', require: false
|
||||||
gem 'net-smtp', '0.4.0'
|
|
||||||
gem 'oj'
|
gem 'oj'
|
||||||
gem 'pagy'
|
gem 'pagy'
|
||||||
gem 'pg', require: false
|
gem 'pg', require: false
|
||||||
@@ -40,14 +39,13 @@ gem 'ruby-vips'
|
|||||||
gem 'rubyXL'
|
gem 'rubyXL'
|
||||||
gem 'shakapacker'
|
gem 'shakapacker'
|
||||||
gem 'sidekiq'
|
gem 'sidekiq'
|
||||||
gem 'sqlite3', require: false, force_ruby_platform: true
|
gem 'sqlite3', require: false
|
||||||
gem 'strip_attributes'
|
gem 'strip_attributes'
|
||||||
gem 'turbo-rails'
|
gem 'turbo-rails'
|
||||||
gem 'twitter_cldr', require: false
|
gem 'twitter_cldr', require: false
|
||||||
gem 'tzinfo-data'
|
gem 'tzinfo-data'
|
||||||
|
|
||||||
group :development, :test do
|
group :development, :test do
|
||||||
gem 'annotate'
|
|
||||||
gem 'better_html'
|
gem 'better_html'
|
||||||
gem 'bullet'
|
gem 'bullet'
|
||||||
gem 'debug'
|
gem 'debug'
|
||||||
@@ -64,6 +62,9 @@ group :development, :test do
|
|||||||
end
|
end
|
||||||
|
|
||||||
group :development do
|
group :development do
|
||||||
|
gem 'annotaterb'
|
||||||
|
gem 'brakeman', require: false
|
||||||
|
gem 'foreman', require: false
|
||||||
gem 'letter_opener_web'
|
gem 'letter_opener_web'
|
||||||
gem 'web-console'
|
gem 'web-console'
|
||||||
end
|
end
|
||||||
|
|||||||
+252
-225
@@ -1,105 +1,100 @@
|
|||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
actioncable (7.1.3.4)
|
actioncable (8.0.1)
|
||||||
actionpack (= 7.1.3.4)
|
actionpack (= 8.0.1)
|
||||||
activesupport (= 7.1.3.4)
|
activesupport (= 8.0.1)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
websocket-driver (>= 0.6.1)
|
websocket-driver (>= 0.6.1)
|
||||||
zeitwerk (~> 2.6)
|
zeitwerk (~> 2.6)
|
||||||
actionmailbox (7.1.3.4)
|
actionmailbox (8.0.1)
|
||||||
actionpack (= 7.1.3.4)
|
actionpack (= 8.0.1)
|
||||||
activejob (= 7.1.3.4)
|
activejob (= 8.0.1)
|
||||||
activerecord (= 7.1.3.4)
|
activerecord (= 8.0.1)
|
||||||
activestorage (= 7.1.3.4)
|
activestorage (= 8.0.1)
|
||||||
activesupport (= 7.1.3.4)
|
activesupport (= 8.0.1)
|
||||||
mail (>= 2.7.1)
|
mail (>= 2.8.0)
|
||||||
net-imap
|
actionmailer (8.0.1)
|
||||||
net-pop
|
actionpack (= 8.0.1)
|
||||||
net-smtp
|
actionview (= 8.0.1)
|
||||||
actionmailer (7.1.3.4)
|
activejob (= 8.0.1)
|
||||||
actionpack (= 7.1.3.4)
|
activesupport (= 8.0.1)
|
||||||
actionview (= 7.1.3.4)
|
mail (>= 2.8.0)
|
||||||
activejob (= 7.1.3.4)
|
|
||||||
activesupport (= 7.1.3.4)
|
|
||||||
mail (~> 2.5, >= 2.5.4)
|
|
||||||
net-imap
|
|
||||||
net-pop
|
|
||||||
net-smtp
|
|
||||||
rails-dom-testing (~> 2.2)
|
rails-dom-testing (~> 2.2)
|
||||||
actionpack (7.1.3.4)
|
actionpack (8.0.1)
|
||||||
actionview (= 7.1.3.4)
|
actionview (= 8.0.1)
|
||||||
activesupport (= 7.1.3.4)
|
activesupport (= 8.0.1)
|
||||||
nokogiri (>= 1.8.5)
|
nokogiri (>= 1.8.5)
|
||||||
racc
|
|
||||||
rack (>= 2.2.4)
|
rack (>= 2.2.4)
|
||||||
rack-session (>= 1.0.1)
|
rack-session (>= 1.0.1)
|
||||||
rack-test (>= 0.6.3)
|
rack-test (>= 0.6.3)
|
||||||
rails-dom-testing (~> 2.2)
|
rails-dom-testing (~> 2.2)
|
||||||
rails-html-sanitizer (~> 1.6)
|
rails-html-sanitizer (~> 1.6)
|
||||||
actiontext (7.1.3.4)
|
useragent (~> 0.16)
|
||||||
actionpack (= 7.1.3.4)
|
actiontext (8.0.1)
|
||||||
activerecord (= 7.1.3.4)
|
actionpack (= 8.0.1)
|
||||||
activestorage (= 7.1.3.4)
|
activerecord (= 8.0.1)
|
||||||
activesupport (= 7.1.3.4)
|
activestorage (= 8.0.1)
|
||||||
|
activesupport (= 8.0.1)
|
||||||
globalid (>= 0.6.0)
|
globalid (>= 0.6.0)
|
||||||
nokogiri (>= 1.8.5)
|
nokogiri (>= 1.8.5)
|
||||||
actionview (7.1.3.4)
|
actionview (8.0.1)
|
||||||
activesupport (= 7.1.3.4)
|
activesupport (= 8.0.1)
|
||||||
builder (~> 3.1)
|
builder (~> 3.1)
|
||||||
erubi (~> 1.11)
|
erubi (~> 1.11)
|
||||||
rails-dom-testing (~> 2.2)
|
rails-dom-testing (~> 2.2)
|
||||||
rails-html-sanitizer (~> 1.6)
|
rails-html-sanitizer (~> 1.6)
|
||||||
activejob (7.1.3.4)
|
activejob (8.0.1)
|
||||||
activesupport (= 7.1.3.4)
|
activesupport (= 8.0.1)
|
||||||
globalid (>= 0.3.6)
|
globalid (>= 0.3.6)
|
||||||
activemodel (7.1.3.4)
|
activemodel (8.0.1)
|
||||||
activesupport (= 7.1.3.4)
|
activesupport (= 8.0.1)
|
||||||
activerecord (7.1.3.4)
|
activerecord (8.0.1)
|
||||||
activemodel (= 7.1.3.4)
|
activemodel (= 8.0.1)
|
||||||
activesupport (= 7.1.3.4)
|
activesupport (= 8.0.1)
|
||||||
timeout (>= 0.4.0)
|
timeout (>= 0.4.0)
|
||||||
activestorage (7.1.3.4)
|
activestorage (8.0.1)
|
||||||
actionpack (= 7.1.3.4)
|
actionpack (= 8.0.1)
|
||||||
activejob (= 7.1.3.4)
|
activejob (= 8.0.1)
|
||||||
activerecord (= 7.1.3.4)
|
activerecord (= 8.0.1)
|
||||||
activesupport (= 7.1.3.4)
|
activesupport (= 8.0.1)
|
||||||
marcel (~> 1.0)
|
marcel (~> 1.0)
|
||||||
activesupport (7.1.3.4)
|
activesupport (8.0.1)
|
||||||
base64
|
base64
|
||||||
|
benchmark (>= 0.3)
|
||||||
bigdecimal
|
bigdecimal
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
concurrent-ruby (~> 1.0, >= 1.3.1)
|
||||||
connection_pool (>= 2.2.5)
|
connection_pool (>= 2.2.5)
|
||||||
drb
|
drb
|
||||||
i18n (>= 1.6, < 2)
|
i18n (>= 1.6, < 2)
|
||||||
|
logger (>= 1.4.2)
|
||||||
minitest (>= 5.1)
|
minitest (>= 5.1)
|
||||||
mutex_m
|
securerandom (>= 0.3)
|
||||||
tzinfo (~> 2.0)
|
tzinfo (~> 2.0, >= 2.0.5)
|
||||||
addressable (2.8.6)
|
uri (>= 0.13.1)
|
||||||
public_suffix (>= 2.0.2, < 6.0)
|
addressable (2.8.7)
|
||||||
annotate (3.2.0)
|
public_suffix (>= 2.0.2, < 7.0)
|
||||||
activerecord (>= 3.2, < 8.0)
|
annotaterb (4.14.0)
|
||||||
rake (>= 10.4, < 14.0)
|
|
||||||
arabic-letter-connector (0.1.1)
|
arabic-letter-connector (0.1.1)
|
||||||
ast (2.4.2)
|
ast (2.4.2)
|
||||||
aws-eventstream (1.3.0)
|
aws-eventstream (1.3.0)
|
||||||
aws-partitions (1.913.0)
|
aws-partitions (1.1027.0)
|
||||||
aws-sdk-core (3.191.6)
|
aws-sdk-core (3.214.0)
|
||||||
aws-eventstream (~> 1, >= 1.3.0)
|
aws-eventstream (~> 1, >= 1.3.0)
|
||||||
aws-partitions (~> 1, >= 1.651.0)
|
aws-partitions (~> 1, >= 1.992.0)
|
||||||
aws-sigv4 (~> 1.8)
|
aws-sigv4 (~> 1.9)
|
||||||
jmespath (~> 1, >= 1.6.1)
|
jmespath (~> 1, >= 1.6.1)
|
||||||
aws-sdk-kms (1.79.0)
|
aws-sdk-kms (1.96.0)
|
||||||
aws-sdk-core (~> 3, >= 3.191.0)
|
aws-sdk-core (~> 3, >= 3.210.0)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.5)
|
||||||
aws-sdk-s3 (1.146.1)
|
aws-sdk-s3 (1.176.1)
|
||||||
aws-sdk-core (~> 3, >= 3.191.0)
|
aws-sdk-core (~> 3, >= 3.210.0)
|
||||||
aws-sdk-kms (~> 1)
|
aws-sdk-kms (~> 1)
|
||||||
aws-sigv4 (~> 1.8)
|
aws-sigv4 (~> 1.5)
|
||||||
aws-sdk-secretsmanager (1.91.0)
|
aws-sdk-secretsmanager (1.110.0)
|
||||||
aws-sdk-core (~> 3, >= 3.191.0)
|
aws-sdk-core (~> 3, >= 3.210.0)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.5)
|
||||||
aws-sigv4 (1.8.0)
|
aws-sigv4 (1.10.1)
|
||||||
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)
|
||||||
@@ -111,6 +106,7 @@ GEM
|
|||||||
nokogiri (~> 1, >= 1.10.8)
|
nokogiri (~> 1, >= 1.10.8)
|
||||||
base64 (0.2.0)
|
base64 (0.2.0)
|
||||||
bcrypt (3.1.20)
|
bcrypt (3.1.20)
|
||||||
|
benchmark (0.4.0)
|
||||||
better_html (2.1.1)
|
better_html (2.1.1)
|
||||||
actionview (>= 6.0)
|
actionview (>= 6.0)
|
||||||
activesupport (>= 6.0)
|
activesupport (>= 6.0)
|
||||||
@@ -120,14 +116,16 @@ GEM
|
|||||||
smart_properties
|
smart_properties
|
||||||
bigdecimal (3.1.8)
|
bigdecimal (3.1.8)
|
||||||
bindex (0.8.1)
|
bindex (0.8.1)
|
||||||
bootsnap (1.18.3)
|
bootsnap (1.18.4)
|
||||||
msgpack (~> 1.2)
|
msgpack (~> 1.2)
|
||||||
|
brakeman (7.0.0)
|
||||||
|
racc
|
||||||
builder (3.3.0)
|
builder (3.3.0)
|
||||||
bullet (7.1.6)
|
bullet (8.0.0)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
uniform_notifier (~> 1.11)
|
uniform_notifier (~> 1.11)
|
||||||
camertron-eprun (1.1.1)
|
camertron-eprun (1.1.1)
|
||||||
cancancan (3.5.0)
|
cancancan (3.6.1)
|
||||||
capybara (3.40.0)
|
capybara (3.40.0)
|
||||||
addressable
|
addressable
|
||||||
matrix
|
matrix
|
||||||
@@ -137,7 +135,8 @@ GEM
|
|||||||
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)
|
childprocess (5.1.0)
|
||||||
|
logger (~> 1.5)
|
||||||
chunky_png (1.4.0)
|
chunky_png (1.4.0)
|
||||||
cldr-plurals-runtime-rb (1.1.0)
|
cldr-plurals-runtime-rb (1.1.0)
|
||||||
cmdparse (3.0.7)
|
cmdparse (3.0.7)
|
||||||
@@ -148,14 +147,14 @@ GEM
|
|||||||
bigdecimal
|
bigdecimal
|
||||||
rexml
|
rexml
|
||||||
crass (1.0.6)
|
crass (1.0.6)
|
||||||
css_parser (1.17.1)
|
css_parser (1.21.0)
|
||||||
addressable
|
addressable
|
||||||
csv (3.3.0)
|
csv (3.3.2)
|
||||||
cuprite (0.15)
|
cuprite (0.15.1)
|
||||||
capybara (~> 3.0)
|
capybara (~> 3.0)
|
||||||
ferrum (~> 0.14.0)
|
ferrum (~> 0.15.0)
|
||||||
date (3.3.4)
|
date (3.4.1)
|
||||||
debug (1.9.2)
|
debug (1.10.0)
|
||||||
irb (~> 1.10)
|
irb (~> 1.10)
|
||||||
reline (>= 0.3.8)
|
reline (>= 0.3.8)
|
||||||
declarative (0.0.20)
|
declarative (0.0.20)
|
||||||
@@ -165,34 +164,34 @@ GEM
|
|||||||
railties (>= 4.1.0)
|
railties (>= 4.1.0)
|
||||||
responders
|
responders
|
||||||
warden (~> 1.2.3)
|
warden (~> 1.2.3)
|
||||||
devise-two-factor (6.0.0)
|
devise-two-factor (6.1.0)
|
||||||
activesupport (~> 7.0)
|
activesupport (>= 7.0, < 8.1)
|
||||||
devise (~> 4.0)
|
devise (~> 4.0)
|
||||||
railties (~> 7.0)
|
railties (>= 7.0, < 8.1)
|
||||||
rotp (~> 6.0)
|
rotp (~> 6.0)
|
||||||
diff-lcs (1.5.1)
|
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.1)
|
||||||
dotenv (3.1.0)
|
dotenv (3.1.7)
|
||||||
drb (2.2.1)
|
drb (2.2.1)
|
||||||
email_typo (0.2.3)
|
email_typo (0.2.3)
|
||||||
erb_lint (0.5.0)
|
erb_lint (0.7.0)
|
||||||
activesupport
|
activesupport
|
||||||
better_html (>= 2.0.1)
|
better_html (>= 2.0.1)
|
||||||
parser (>= 2.7.1.4)
|
parser (>= 2.7.1.4)
|
||||||
rainbow
|
rainbow
|
||||||
rubocop
|
rubocop (>= 1)
|
||||||
smart_properties
|
smart_properties
|
||||||
erubi (1.13.0)
|
erubi (1.13.1)
|
||||||
factory_bot (6.4.6)
|
factory_bot (6.5.0)
|
||||||
activesupport (>= 5.0.0)
|
activesupport (>= 5.0.0)
|
||||||
factory_bot_rails (6.4.3)
|
factory_bot_rails (6.4.4)
|
||||||
factory_bot (~> 6.4)
|
factory_bot (~> 6.5)
|
||||||
railties (>= 5.0.0)
|
railties (>= 5.0.0)
|
||||||
faker (3.3.1)
|
faker (3.5.1)
|
||||||
i18n (>= 1.8.11, < 2)
|
i18n (>= 1.8.11, < 2)
|
||||||
faraday (1.10.3)
|
faraday (1.10.4)
|
||||||
faraday-em_http (~> 1.0)
|
faraday-em_http (~> 1.0)
|
||||||
faraday-em_synchrony (~> 1.0)
|
faraday-em_synchrony (~> 1.0)
|
||||||
faraday-excon (~> 1.1)
|
faraday-excon (~> 1.1)
|
||||||
@@ -210,60 +209,68 @@ GEM
|
|||||||
faraday-follow_redirects (0.3.0)
|
faraday-follow_redirects (0.3.0)
|
||||||
faraday (>= 1, < 3)
|
faraday (>= 1, < 3)
|
||||||
faraday-httpclient (1.0.1)
|
faraday-httpclient (1.0.1)
|
||||||
faraday-multipart (1.0.4)
|
faraday-multipart (1.1.0)
|
||||||
multipart-post (~> 2)
|
multipart-post (~> 2.0)
|
||||||
faraday-net_http (1.0.1)
|
faraday-net_http (1.0.2)
|
||||||
faraday-net_http_persistent (1.2.0)
|
faraday-net_http_persistent (1.2.0)
|
||||||
faraday-patron (1.0.0)
|
faraday-patron (1.0.0)
|
||||||
faraday-rack (1.0.0)
|
faraday-rack (1.0.0)
|
||||||
faraday-retry (1.0.3)
|
faraday-retry (1.0.3)
|
||||||
faraday_middleware (1.2.0)
|
faraday_middleware (1.2.1)
|
||||||
faraday (~> 1.0)
|
faraday (~> 1.0)
|
||||||
ferrum (0.14)
|
ferrum (0.15)
|
||||||
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.7)
|
||||||
ffi (1.16.3)
|
ffi (1.17.1)
|
||||||
|
ffi (1.17.1-aarch64-linux-gnu)
|
||||||
|
ffi (1.17.1-aarch64-linux-musl)
|
||||||
|
ffi (1.17.1-arm64-darwin)
|
||||||
|
ffi (1.17.1-x86_64-linux-gnu)
|
||||||
|
ffi (1.17.1-x86_64-linux-musl)
|
||||||
|
foreman (0.88.1)
|
||||||
geom2d (0.4.1)
|
geom2d (0.4.1)
|
||||||
globalid (1.2.1)
|
globalid (1.2.1)
|
||||||
activesupport (>= 6.1)
|
activesupport (>= 6.1)
|
||||||
google-apis-core (0.14.1)
|
google-apis-core (0.15.1)
|
||||||
addressable (~> 2.5, >= 2.5.1)
|
addressable (~> 2.5, >= 2.5.1)
|
||||||
googleauth (~> 1.9)
|
googleauth (~> 1.9)
|
||||||
httpclient (>= 2.8.1, < 3.a)
|
httpclient (>= 2.8.3, < 3.a)
|
||||||
mini_mime (~> 1.0)
|
mini_mime (~> 1.0)
|
||||||
|
mutex_m
|
||||||
representable (~> 3.0)
|
representable (~> 3.0)
|
||||||
retriable (>= 2.0, < 4.a)
|
retriable (>= 2.0, < 4.a)
|
||||||
rexml
|
google-apis-iamcredentials_v1 (0.22.0)
|
||||||
google-apis-iamcredentials_v1 (0.20.0)
|
google-apis-core (>= 0.15.0, < 2.a)
|
||||||
google-apis-core (>= 0.14.0, < 2.a)
|
google-apis-storage_v1 (0.49.0)
|
||||||
google-apis-storage_v1 (0.37.0)
|
google-apis-core (>= 0.15.0, < 2.a)
|
||||||
google-apis-core (>= 0.14.0, < 2.a)
|
google-cloud-core (1.7.1)
|
||||||
google-cloud-core (1.7.0)
|
|
||||||
google-cloud-env (>= 1.0, < 3.a)
|
google-cloud-env (>= 1.0, < 3.a)
|
||||||
google-cloud-errors (~> 1.0)
|
google-cloud-errors (~> 1.0)
|
||||||
google-cloud-env (2.1.1)
|
google-cloud-env (2.2.1)
|
||||||
faraday (>= 1.0, < 3.a)
|
faraday (>= 1.0, < 3.a)
|
||||||
google-cloud-errors (1.4.0)
|
google-cloud-errors (1.4.0)
|
||||||
google-cloud-storage (1.49.0)
|
google-cloud-storage (1.54.0)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
digest-crc (~> 0.4)
|
digest-crc (~> 0.4)
|
||||||
google-apis-core (~> 0.13)
|
google-apis-core (~> 0.13)
|
||||||
google-apis-iamcredentials_v1 (~> 0.18)
|
google-apis-iamcredentials_v1 (~> 0.18)
|
||||||
google-apis-storage_v1 (~> 0.33)
|
google-apis-storage_v1 (~> 0.38)
|
||||||
google-cloud-core (~> 1.6)
|
google-cloud-core (~> 1.6)
|
||||||
googleauth (~> 1.9)
|
googleauth (~> 1.9)
|
||||||
mini_mime (~> 1.0)
|
mini_mime (~> 1.0)
|
||||||
googleauth (1.11.0)
|
google-logging-utils (0.1.0)
|
||||||
|
googleauth (1.12.2)
|
||||||
faraday (>= 1.0, < 3.a)
|
faraday (>= 1.0, < 3.a)
|
||||||
google-cloud-env (~> 2.1)
|
google-cloud-env (~> 2.2)
|
||||||
|
google-logging-utils (~> 0.1)
|
||||||
jwt (>= 1.4, < 3.0)
|
jwt (>= 1.4, < 3.0)
|
||||||
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.1.0)
|
hashdiff (1.1.2)
|
||||||
hexapdf (0.42.0)
|
hexapdf (1.0.3)
|
||||||
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)
|
||||||
@@ -271,34 +278,35 @@ GEM
|
|||||||
httpclient (2.8.3)
|
httpclient (2.8.3)
|
||||||
i18n (1.14.6)
|
i18n (1.14.6)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
image_processing (1.12.2)
|
image_processing (1.13.0)
|
||||||
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.7.2)
|
io-console (0.8.0)
|
||||||
irb (1.14.0)
|
irb (1.14.3)
|
||||||
rdoc (>= 4.0.0)
|
rdoc (>= 4.0.0)
|
||||||
reline (>= 0.4.2)
|
reline (>= 0.4.2)
|
||||||
jmespath (1.6.2)
|
jmespath (1.6.2)
|
||||||
json (2.7.2)
|
json (2.9.1)
|
||||||
jwt (2.8.1)
|
jwt (2.9.3)
|
||||||
base64
|
base64
|
||||||
language_server-protocol (3.17.0.3)
|
language_server-protocol (3.17.0.3)
|
||||||
launchy (3.0.0)
|
launchy (3.0.1)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
childprocess (~> 5.0)
|
childprocess (~> 5.0)
|
||||||
letter_opener (1.10.0)
|
letter_opener (1.10.0)
|
||||||
launchy (>= 2.2, < 4)
|
launchy (>= 2.2, < 4)
|
||||||
letter_opener_web (2.0.0)
|
letter_opener_web (3.0.0)
|
||||||
actionmailer (>= 5.2)
|
actionmailer (>= 6.1)
|
||||||
letter_opener (~> 1.7)
|
letter_opener (~> 1.9)
|
||||||
railties (>= 5.2)
|
railties (>= 6.1)
|
||||||
rexml
|
rexml
|
||||||
|
logger (1.6.4)
|
||||||
lograge (0.14.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.22.0)
|
loofah (2.23.1)
|
||||||
crass (~> 1.0.2)
|
crass (~> 1.0.2)
|
||||||
nokogiri (>= 1.12.0)
|
nokogiri (>= 1.12.0)
|
||||||
mail (2.8.1)
|
mail (2.8.1)
|
||||||
@@ -309,49 +317,57 @@ GEM
|
|||||||
marcel (1.0.4)
|
marcel (1.0.4)
|
||||||
matrix (0.4.2)
|
matrix (0.4.2)
|
||||||
method_source (1.1.0)
|
method_source (1.1.0)
|
||||||
mini_magick (4.12.0)
|
mini_magick (4.13.2)
|
||||||
mini_mime (1.1.5)
|
mini_mime (1.1.5)
|
||||||
mini_portile2 (2.8.7)
|
mini_portile2 (2.8.8)
|
||||||
minitest (5.25.1)
|
minitest (5.25.4)
|
||||||
msgpack (1.7.2)
|
msgpack (1.7.5)
|
||||||
multi_json (1.15.0)
|
multi_json (1.15.0)
|
||||||
multipart-post (2.4.0)
|
multipart-post (2.4.1)
|
||||||
mutex_m (0.2.0)
|
mutex_m (0.3.0)
|
||||||
mysql2 (0.5.6)
|
mysql2 (0.5.6)
|
||||||
net-http-persistent (4.0.2)
|
net-http-persistent (4.0.5)
|
||||||
connection_pool (~> 2.2)
|
connection_pool (~> 2.2)
|
||||||
net-imap (0.4.14)
|
net-imap (0.5.6)
|
||||||
date
|
date
|
||||||
net-protocol
|
net-protocol
|
||||||
net-pop (0.1.2)
|
net-pop (0.1.2)
|
||||||
net-protocol
|
net-protocol
|
||||||
net-protocol (0.2.2)
|
net-protocol (0.2.2)
|
||||||
timeout
|
timeout
|
||||||
net-smtp (0.4.0)
|
net-smtp (0.5.0)
|
||||||
net-protocol
|
net-protocol
|
||||||
nio4r (2.7.3)
|
nio4r (2.7.4)
|
||||||
nokogiri (1.16.7)
|
nokogiri (1.18.8)
|
||||||
mini_portile2 (~> 2.8.2)
|
mini_portile2 (~> 2.8.2)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.16.7-aarch64-linux)
|
nokogiri (1.18.8-aarch64-linux-gnu)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.16.7-x86_64-linux)
|
nokogiri (1.18.8-aarch64-linux-musl)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
oj (3.16.3)
|
nokogiri (1.18.8-arm64-darwin)
|
||||||
|
racc (~> 1.4)
|
||||||
|
nokogiri (1.18.8-x86_64-linux-gnu)
|
||||||
|
racc (~> 1.4)
|
||||||
|
nokogiri (1.18.8-x86_64-linux-musl)
|
||||||
|
racc (~> 1.4)
|
||||||
|
oj (3.16.8)
|
||||||
bigdecimal (>= 3.0)
|
bigdecimal (>= 3.0)
|
||||||
openssl (3.2.0)
|
ostruct (>= 0.2)
|
||||||
|
openssl (3.3.0)
|
||||||
orm_adapter (0.5.0)
|
orm_adapter (0.5.0)
|
||||||
os (1.1.4)
|
os (1.1.4)
|
||||||
|
ostruct (0.6.1)
|
||||||
package_json (0.1.0)
|
package_json (0.1.0)
|
||||||
pagy (8.1.2)
|
pagy (9.3.3)
|
||||||
parallel (1.25.1)
|
parallel (1.26.3)
|
||||||
parser (3.3.4.0)
|
parser (3.3.6.0)
|
||||||
ast (~> 2.4.1)
|
ast (~> 2.4.1)
|
||||||
racc
|
racc
|
||||||
pg (1.5.6)
|
pg (1.5.9)
|
||||||
premailer (1.23.0)
|
premailer (1.27.0)
|
||||||
addressable
|
addressable
|
||||||
css_parser (>= 1.12.0)
|
css_parser (>= 1.19.0)
|
||||||
htmlentities (>= 4.0.0)
|
htmlentities (>= 4.0.0)
|
||||||
premailer-rails (1.12.0)
|
premailer-rails (1.12.0)
|
||||||
actionmailer (>= 3)
|
actionmailer (>= 3)
|
||||||
@@ -359,149 +375,149 @@ GEM
|
|||||||
premailer (~> 1.7, >= 1.7.9)
|
premailer (~> 1.7, >= 1.7.9)
|
||||||
pretender (0.5.0)
|
pretender (0.5.0)
|
||||||
actionpack (>= 6.1)
|
actionpack (>= 6.1)
|
||||||
pry (0.14.2)
|
pry (0.15.0)
|
||||||
coderay (~> 1.1)
|
coderay (~> 1.1)
|
||||||
method_source (~> 1.0)
|
method_source (~> 1.0)
|
||||||
pry-rails (0.3.9)
|
pry-rails (0.3.11)
|
||||||
pry (>= 0.10.4)
|
pry (>= 0.13.0)
|
||||||
psych (5.1.2)
|
psych (5.2.2)
|
||||||
|
date
|
||||||
stringio
|
stringio
|
||||||
public_suffix (5.0.5)
|
public_suffix (6.0.1)
|
||||||
puma (6.4.3)
|
puma (6.5.0)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
racc (1.8.1)
|
racc (1.8.1)
|
||||||
rack (3.1.7)
|
rack (3.1.14)
|
||||||
rack-proxy (0.7.7)
|
rack-proxy (0.7.7)
|
||||||
rack
|
rack
|
||||||
rack-session (2.0.0)
|
rack-session (2.0.0)
|
||||||
rack (>= 3.0.0)
|
rack (>= 3.0.0)
|
||||||
rack-test (2.1.0)
|
rack-test (2.1.0)
|
||||||
rack (>= 1.3)
|
rack (>= 1.3)
|
||||||
rackup (2.1.0)
|
rackup (2.2.1)
|
||||||
rack (>= 3)
|
rack (>= 3)
|
||||||
webrick (~> 1.8)
|
rails (8.0.1)
|
||||||
rails (7.1.3.4)
|
actioncable (= 8.0.1)
|
||||||
actioncable (= 7.1.3.4)
|
actionmailbox (= 8.0.1)
|
||||||
actionmailbox (= 7.1.3.4)
|
actionmailer (= 8.0.1)
|
||||||
actionmailer (= 7.1.3.4)
|
actionpack (= 8.0.1)
|
||||||
actionpack (= 7.1.3.4)
|
actiontext (= 8.0.1)
|
||||||
actiontext (= 7.1.3.4)
|
actionview (= 8.0.1)
|
||||||
actionview (= 7.1.3.4)
|
activejob (= 8.0.1)
|
||||||
activejob (= 7.1.3.4)
|
activemodel (= 8.0.1)
|
||||||
activemodel (= 7.1.3.4)
|
activerecord (= 8.0.1)
|
||||||
activerecord (= 7.1.3.4)
|
activestorage (= 8.0.1)
|
||||||
activestorage (= 7.1.3.4)
|
activesupport (= 8.0.1)
|
||||||
activesupport (= 7.1.3.4)
|
|
||||||
bundler (>= 1.15.0)
|
bundler (>= 1.15.0)
|
||||||
railties (= 7.1.3.4)
|
railties (= 8.0.1)
|
||||||
rails-dom-testing (2.2.0)
|
rails-dom-testing (2.2.0)
|
||||||
activesupport (>= 5.0.0)
|
activesupport (>= 5.0.0)
|
||||||
minitest
|
minitest
|
||||||
nokogiri (>= 1.6)
|
nokogiri (>= 1.6)
|
||||||
rails-html-sanitizer (1.6.0)
|
rails-html-sanitizer (1.6.2)
|
||||||
loofah (~> 2.21)
|
loofah (~> 2.21)
|
||||||
nokogiri (~> 1.14)
|
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
|
||||||
rails-i18n (7.0.9)
|
rails-i18n (8.0.1)
|
||||||
i18n (>= 0.7, < 2)
|
i18n (>= 0.7, < 2)
|
||||||
railties (>= 6.0.0, < 8)
|
railties (>= 8.0.0, < 9)
|
||||||
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.1.3.4)
|
railties (8.0.1)
|
||||||
actionpack (= 7.1.3.4)
|
actionpack (= 8.0.1)
|
||||||
activesupport (= 7.1.3.4)
|
activesupport (= 8.0.1)
|
||||||
irb
|
irb (~> 1.13)
|
||||||
rackup (>= 1.0.0)
|
rackup (>= 1.0.0)
|
||||||
rake (>= 12.2)
|
rake (>= 12.2)
|
||||||
thor (~> 1.0, >= 1.2.2)
|
thor (~> 1.0, >= 1.2.2)
|
||||||
zeitwerk (~> 2.6)
|
zeitwerk (~> 2.6)
|
||||||
rainbow (3.1.1)
|
rainbow (3.1.1)
|
||||||
rake (13.2.1)
|
rake (13.2.1)
|
||||||
rdoc (6.7.0)
|
rdoc (6.10.0)
|
||||||
psych (>= 4.0.0)
|
psych (>= 4.0.0)
|
||||||
redis-client (0.22.2)
|
redis-client (0.23.0)
|
||||||
connection_pool
|
connection_pool
|
||||||
regexp_parser (2.9.2)
|
regexp_parser (2.9.3)
|
||||||
reline (0.5.10)
|
reline (0.6.0)
|
||||||
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.6.0)
|
request_store (1.7.0)
|
||||||
rack (>= 1.4)
|
rack (>= 1.4)
|
||||||
responders (3.1.1)
|
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.3.6)
|
rexml (3.4.0)
|
||||||
strscan
|
|
||||||
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.13.0)
|
rspec-core (3.13.2)
|
||||||
rspec-support (~> 3.13.0)
|
rspec-support (~> 3.13.0)
|
||||||
rspec-expectations (3.13.0)
|
rspec-expectations (3.13.3)
|
||||||
diff-lcs (>= 1.2.0, < 2.0)
|
diff-lcs (>= 1.2.0, < 2.0)
|
||||||
rspec-support (~> 3.13.0)
|
rspec-support (~> 3.13.0)
|
||||||
rspec-mocks (3.13.0)
|
rspec-mocks (3.13.2)
|
||||||
diff-lcs (>= 1.2.0, < 2.0)
|
diff-lcs (>= 1.2.0, < 2.0)
|
||||||
rspec-support (~> 3.13.0)
|
rspec-support (~> 3.13.0)
|
||||||
rspec-rails (6.1.2)
|
rspec-rails (7.1.0)
|
||||||
actionpack (>= 6.1)
|
actionpack (>= 7.0)
|
||||||
activesupport (>= 6.1)
|
activesupport (>= 7.0)
|
||||||
railties (>= 6.1)
|
railties (>= 7.0)
|
||||||
rspec-core (~> 3.13)
|
rspec-core (~> 3.13)
|
||||||
rspec-expectations (~> 3.13)
|
rspec-expectations (~> 3.13)
|
||||||
rspec-mocks (~> 3.13)
|
rspec-mocks (~> 3.13)
|
||||||
rspec-support (~> 3.13)
|
rspec-support (~> 3.13)
|
||||||
rspec-support (3.13.1)
|
rspec-support (3.13.2)
|
||||||
rubocop (1.65.0)
|
rubocop (1.69.2)
|
||||||
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.3.0.2)
|
parser (>= 3.3.0.2)
|
||||||
rainbow (>= 2.2.2, < 4.0)
|
rainbow (>= 2.2.2, < 4.0)
|
||||||
regexp_parser (>= 2.4, < 3.0)
|
regexp_parser (>= 2.9.3, < 3.0)
|
||||||
rexml (>= 3.2.5, < 4.0)
|
rubocop-ast (>= 1.36.2, < 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, < 4.0)
|
||||||
rubocop-ast (1.31.3)
|
rubocop-ast (1.37.0)
|
||||||
parser (>= 3.3.1.0)
|
parser (>= 3.3.1.0)
|
||||||
rubocop-performance (1.21.0)
|
rubocop-performance (1.23.0)
|
||||||
rubocop (>= 1.48.1, < 2.0)
|
rubocop (>= 1.48.1, < 2.0)
|
||||||
rubocop-ast (>= 1.31.1, < 2.0)
|
rubocop-ast (>= 1.31.1, < 2.0)
|
||||||
rubocop-rails (2.24.1)
|
rubocop-rails (2.27.0)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
rack (>= 1.1)
|
rack (>= 1.1)
|
||||||
rubocop (>= 1.33.0, < 2.0)
|
rubocop (>= 1.52.0, < 2.0)
|
||||||
rubocop-ast (>= 1.31.1, < 2.0)
|
rubocop-ast (>= 1.31.1, < 2.0)
|
||||||
rubocop-rspec (3.0.3)
|
rubocop-rspec (3.3.0)
|
||||||
rubocop (~> 1.61)
|
rubocop (~> 1.61)
|
||||||
ruby-progressbar (1.13.0)
|
ruby-progressbar (1.13.0)
|
||||||
ruby-vips (2.2.1)
|
ruby-vips (2.2.2)
|
||||||
ffi (~> 1.12)
|
ffi (~> 1.12)
|
||||||
|
logger
|
||||||
ruby2_keywords (0.0.5)
|
ruby2_keywords (0.0.5)
|
||||||
rubyXL (3.4.27)
|
rubyXL (3.4.33)
|
||||||
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)
|
securerandom (0.4.1)
|
||||||
shakapacker (8.0.0)
|
semantic_range (3.1.0)
|
||||||
|
shakapacker (8.0.2)
|
||||||
activesupport (>= 5.2)
|
activesupport (>= 5.2)
|
||||||
package_json
|
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.2.4)
|
sidekiq (7.3.7)
|
||||||
concurrent-ruby (< 2)
|
|
||||||
connection_pool (>= 2.3.0)
|
connection_pool (>= 2.3.0)
|
||||||
|
logger
|
||||||
rack (>= 2.2.4)
|
rack (>= 2.2.4)
|
||||||
redis-client (>= 0.19.0)
|
redis-client (>= 0.22.2)
|
||||||
signet (0.19.0)
|
signet (0.19.0)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
faraday (>= 0.17.5, < 3.a)
|
faraday (>= 0.17.5, < 3.a)
|
||||||
@@ -511,33 +527,40 @@ GEM
|
|||||||
docile (~> 1.1)
|
docile (~> 1.1)
|
||||||
simplecov-html (~> 0.11)
|
simplecov-html (~> 0.11)
|
||||||
simplecov_json_formatter (~> 0.1)
|
simplecov_json_formatter (~> 0.1)
|
||||||
simplecov-html (0.12.3)
|
simplecov-html (0.13.1)
|
||||||
simplecov_json_formatter (0.1.4)
|
simplecov_json_formatter (0.1.4)
|
||||||
smart_properties (1.17.0)
|
smart_properties (1.17.0)
|
||||||
sqlite3 (1.7.3)
|
sqlite3 (2.5.0)
|
||||||
mini_portile2 (~> 2.8.0)
|
mini_portile2 (~> 2.8.0)
|
||||||
stringio (3.1.1)
|
sqlite3 (2.5.0-aarch64-linux-gnu)
|
||||||
strip_attributes (1.13.0)
|
sqlite3 (2.5.0-aarch64-linux-musl)
|
||||||
activemodel (>= 3.0, < 8.0)
|
sqlite3 (2.5.0-arm64-darwin)
|
||||||
strscan (3.1.0)
|
sqlite3 (2.5.0-x86_64-linux-gnu)
|
||||||
|
sqlite3 (2.5.0-x86_64-linux-musl)
|
||||||
|
stringio (3.1.2)
|
||||||
|
strip_attributes (1.14.1)
|
||||||
|
activemodel (>= 3.0, < 9.0)
|
||||||
thor (1.3.2)
|
thor (1.3.2)
|
||||||
timeout (0.4.1)
|
timeout (0.4.3)
|
||||||
trailblazer-option (0.1.2)
|
trailblazer-option (0.1.2)
|
||||||
turbo-rails (2.0.5)
|
turbo-rails (2.0.11)
|
||||||
actionpack (>= 6.0.0)
|
actionpack (>= 6.0.0)
|
||||||
activejob (>= 6.0.0)
|
|
||||||
railties (>= 6.0.0)
|
railties (>= 6.0.0)
|
||||||
twitter_cldr (6.12.0)
|
twitter_cldr (6.12.1)
|
||||||
camertron-eprun
|
camertron-eprun
|
||||||
cldr-plurals-runtime-rb (~> 1.1)
|
cldr-plurals-runtime-rb (~> 1.1)
|
||||||
tzinfo
|
tzinfo
|
||||||
tzinfo (2.0.6)
|
tzinfo (2.0.6)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
tzinfo-data (1.2024.1)
|
tzinfo-data (1.2024.2)
|
||||||
tzinfo (>= 1.0.0)
|
tzinfo (>= 1.0.0)
|
||||||
uber (0.1.0)
|
uber (0.1.0)
|
||||||
unicode-display_width (2.5.0)
|
unicode-display_width (3.1.2)
|
||||||
|
unicode-emoji (~> 4.0, >= 4.0.4)
|
||||||
|
unicode-emoji (4.0.4)
|
||||||
uniform_notifier (1.16.0)
|
uniform_notifier (1.16.0)
|
||||||
|
uri (1.0.3)
|
||||||
|
useragent (0.16.11)
|
||||||
warden (1.2.9)
|
warden (1.2.9)
|
||||||
rack (>= 2.0.9)
|
rack (>= 2.0.9)
|
||||||
web-console (4.2.1)
|
web-console (4.2.1)
|
||||||
@@ -545,31 +568,35 @@ GEM
|
|||||||
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.23.0)
|
webmock (3.24.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)
|
||||||
webrick (1.8.1)
|
webrick (1.9.1)
|
||||||
websocket-driver (0.7.6)
|
websocket-driver (0.7.6)
|
||||||
websocket-extensions (>= 0.1.0)
|
websocket-extensions (>= 0.1.0)
|
||||||
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.18)
|
zeitwerk (2.7.1)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
aarch64-linux
|
aarch64-linux
|
||||||
|
aarch64-linux-musl
|
||||||
|
arm64-darwin
|
||||||
ruby
|
ruby
|
||||||
x86_64-linux
|
x86_64-linux
|
||||||
|
x86_64-linux-musl
|
||||||
|
|
||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
annotate
|
annotaterb
|
||||||
arabic-letter-connector
|
arabic-letter-connector
|
||||||
aws-sdk-s3
|
aws-sdk-s3
|
||||||
aws-sdk-secretsmanager
|
aws-sdk-secretsmanager
|
||||||
azure-storage-blob
|
azure-storage-blob
|
||||||
better_html
|
better_html
|
||||||
bootsnap
|
bootsnap
|
||||||
|
brakeman
|
||||||
bullet
|
bullet
|
||||||
cancancan
|
cancancan
|
||||||
capybara
|
capybara
|
||||||
@@ -585,6 +612,7 @@ DEPENDENCIES
|
|||||||
faker
|
faker
|
||||||
faraday
|
faraday
|
||||||
faraday-follow_redirects
|
faraday-follow_redirects
|
||||||
|
foreman
|
||||||
google-cloud-storage
|
google-cloud-storage
|
||||||
hexapdf
|
hexapdf
|
||||||
image_processing
|
image_processing
|
||||||
@@ -592,7 +620,6 @@ DEPENDENCIES
|
|||||||
letter_opener_web
|
letter_opener_web
|
||||||
lograge
|
lograge
|
||||||
mysql2
|
mysql2
|
||||||
net-smtp (= 0.4.0)
|
|
||||||
oj
|
oj
|
||||||
pagy
|
pagy
|
||||||
pg
|
pg
|
||||||
@@ -625,7 +652,7 @@ DEPENDENCIES
|
|||||||
webmock
|
webmock
|
||||||
|
|
||||||
RUBY VERSION
|
RUBY VERSION
|
||||||
ruby 3.3.4p94
|
ruby 3.4.2p28
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
2.5.3
|
2.5.3
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<h1 align="center" style="border-bottom: none">
|
<h1 align="center" style="border-bottom: none">
|
||||||
<div>
|
<div>
|
||||||
<a href="https://www.docuseal.co">
|
<a href="https://www.docuseal.com">
|
||||||
<img alt="DocuSeal" src="https://github.com/docusealco/docuseal/assets/5418788/c12cd051-81cd-4402-bc3a-92f2cfdc1b06" width="80" />
|
<img alt="DocuSeal" src="https://github.com/docusealco/docuseal/assets/5418788/c12cd051-81cd-4402-bc3a-92f2cfdc1b06" width="80" />
|
||||||
<br>
|
<br>
|
||||||
</a>
|
</a>
|
||||||
@@ -25,12 +25,12 @@
|
|||||||
DocuSeal is an open source platform that provides secure and efficient digital document signing and processing. Create PDF forms to have them filled and signed online on any device with an easy-to-use, mobile-optimized web tool.
|
DocuSeal is an open source platform that provides secure and efficient digital document signing and processing. Create PDF forms to have them filled and signed online on any device with an easy-to-use, mobile-optimized web tool.
|
||||||
</p>
|
</p>
|
||||||
<h2 align="center">
|
<h2 align="center">
|
||||||
<a href="https://demo.docuseal.co">✨ Live Demo</a>
|
<a href="https://demo.docuseal.tech">✨ Live Demo</a>
|
||||||
<span>|</span>
|
<span>|</span>
|
||||||
<a href="https://docuseal.co/sign_up">☁️ Try in Cloud</a>
|
<a href="https://docuseal.com/sign_up">☁️ Try in Cloud</a>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
[](https://demo.docuseal.co)
|
[](https://demo.docuseal.tech)
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
- PDF form fields builder (WYSIWYG)
|
- PDF form fields builder (WYSIWYG)
|
||||||
@@ -42,7 +42,7 @@ DocuSeal is an open source platform that provides secure and efficient digital d
|
|||||||
- PDF signature verification
|
- PDF signature verification
|
||||||
- Users management
|
- Users management
|
||||||
- Mobile-optimized
|
- Mobile-optimized
|
||||||
- Signing available in 13 languages
|
- 6 UI languages with signing available in 14 languages
|
||||||
- API and Webhooks for integrations
|
- API and Webhooks for integrations
|
||||||
- Easy to deploy in minutes
|
- Easy to deploy in minutes
|
||||||
|
|
||||||
@@ -54,11 +54,11 @@ DocuSeal is an open source platform that provides secure and efficient digital d
|
|||||||
- Conditional fields and formulas
|
- Conditional fields and formulas
|
||||||
- Bulk send with CSV, XLSX spreadsheet import
|
- Bulk send with CSV, XLSX spreadsheet import
|
||||||
- SSO / SAML
|
- SSO / SAML
|
||||||
- Template creation with HTML API ([Guide](https://www.docuseal.co/guides/create-pdf-document-fillable-form-with-html-api))
|
- Template creation with HTML API ([Guide](https://www.docuseal.com/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))
|
- Template creation with PDF or DOCX and field tags API ([Guide](https://www.docuseal.com/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 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.com/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))
|
- 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.com/docs/embedded))
|
||||||
- [Learn more](https://www.docuseal.co/pricing)
|
- [Learn more](https://www.docuseal.com/pricing)
|
||||||
|
|
||||||
## Deploy
|
## Deploy
|
||||||
|
|
||||||
@@ -93,7 +93,7 @@ sudo HOST=your-domain-name.com docker compose up
|
|||||||
|
|
||||||
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://www.docuseal.co/contact)
|
[Book a Meeting](https://www.docuseal.com/contact)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
@@ -102,6 +102,6 @@ Unless otherwise noted, all files © 2023 DocuSeal LLC.
|
|||||||
|
|
||||||
## Tools
|
## Tools
|
||||||
|
|
||||||
- [Signature Maker](https://www.docuseal.co/online-signature)
|
- [Signature Maker](https://www.docuseal.com/online-signature)
|
||||||
- [Sign Document Online](https://www.docuseal.co/sign-documents-online)
|
- [Sign Document Online](https://www.docuseal.com/sign-documents-online)
|
||||||
- [Fill PDF Online](https://www.docuseal.co/fill-pdf)
|
- [Fill PDF Online](https://www.docuseal.com/fill-pdf)
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
# Reporting a Vulnerability
|
# 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.
|
If you come across any security concern or vulnarability, please report the information via email to security@docuseal.com 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.**
|
**We have a bug bounty program to reward security researchers.**
|
||||||
|
|
||||||
|
|||||||
@@ -8,15 +8,18 @@ class AccountConfigsController < ApplicationController
|
|||||||
AccountConfig::ALLOW_TYPED_SIGNATURE,
|
AccountConfig::ALLOW_TYPED_SIGNATURE,
|
||||||
AccountConfig::FORCE_MFA,
|
AccountConfig::FORCE_MFA,
|
||||||
AccountConfig::ALLOW_TO_RESUBMIT,
|
AccountConfig::ALLOW_TO_RESUBMIT,
|
||||||
|
AccountConfig::ALLOW_TO_DECLINE_KEY,
|
||||||
AccountConfig::FORM_PREFILL_SIGNATURE_KEY,
|
AccountConfig::FORM_PREFILL_SIGNATURE_KEY,
|
||||||
AccountConfig::ESIGNING_PREFERENCE_KEY,
|
AccountConfig::ESIGNING_PREFERENCE_KEY,
|
||||||
AccountConfig::FORM_WITH_CONFETTI_KEY,
|
AccountConfig::FORM_WITH_CONFETTI_KEY,
|
||||||
AccountConfig::DOWNLOAD_LINKS_AUTH_KEY,
|
AccountConfig::DOWNLOAD_LINKS_AUTH_KEY,
|
||||||
AccountConfig::FORCE_SSO_AUTH_KEY,
|
AccountConfig::FORCE_SSO_AUTH_KEY,
|
||||||
AccountConfig::FLATTEN_RESULT_PDF_KEY,
|
AccountConfig::FLATTEN_RESULT_PDF_KEY,
|
||||||
|
AccountConfig::ENFORCE_SIGNING_ORDER_KEY,
|
||||||
AccountConfig::WITH_SIGNATURE_ID,
|
AccountConfig::WITH_SIGNATURE_ID,
|
||||||
AccountConfig::COMBINE_PDF_RESULT_KEY,
|
AccountConfig::COMBINE_PDF_RESULT_KEY,
|
||||||
AccountConfig::REQUIRE_SIGNING_REASON_KEY
|
AccountConfig::REQUIRE_SIGNING_REASON_KEY,
|
||||||
|
AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY
|
||||||
].freeze
|
].freeze
|
||||||
|
|
||||||
InvalidKey = Class.new(StandardError)
|
InvalidKey = Class.new(StandardError)
|
||||||
@@ -37,7 +40,7 @@ class AccountConfigsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def account_config_params
|
def account_config_params
|
||||||
params.required(:account_config).permit!.tap do |attrs|
|
params.required(:account_config).permit(:key, :value, { value: {} }, { value: [] }).tap do |attrs|
|
||||||
attrs[:value] = attrs[:value] == '1' if attrs[:value].in?(%w[1 0])
|
attrs[:value] = attrs[:value] == '1' if attrs[:value].in?(%w[1 0])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ 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)',
|
'fr-FR' => 'Français',
|
||||||
'es-ES' => 'Spanish (Spain)',
|
'es-ES' => 'Español',
|
||||||
'pt-PT' => 'Portuguese (Portugal)',
|
'pt-PT' => 'Português',
|
||||||
'de-DE' => 'German (Germany)',
|
'de-DE' => 'Deutsch',
|
||||||
'it-IT' => 'Italian (Italy)'
|
'it-IT' => 'Italiano'
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
before_action :load_account
|
before_action :load_account
|
||||||
@@ -19,21 +19,25 @@ class AccountsController < ApplicationController
|
|||||||
def update
|
def update
|
||||||
current_account.update!(account_params)
|
current_account.update!(account_params)
|
||||||
|
|
||||||
@encrypted_config = EncryptedConfig.find_or_initialize_by(account: current_account,
|
unless Docuseal.multitenant?
|
||||||
key: EncryptedConfig::APP_URL_KEY)
|
@encrypted_config = EncryptedConfig.find_or_initialize_by(account: current_account,
|
||||||
@encrypted_config.assign_attributes(app_url_params)
|
key: EncryptedConfig::APP_URL_KEY)
|
||||||
|
@encrypted_config.assign_attributes(app_url_params)
|
||||||
|
|
||||||
unless URI.parse(@encrypted_config.value.to_s).class.in?([URI::HTTP, URI::HTTPS])
|
unless URI.parse(@encrypted_config.value.to_s).class.in?([URI::HTTP, URI::HTTPS])
|
||||||
@encrypted_config.errors.add(:value, I18n.t('should_be_a_valid_url'))
|
@encrypted_config.errors.add(:value, I18n.t('should_be_a_valid_url'))
|
||||||
|
|
||||||
return render :show, status: :unprocessable_entity
|
return render :show, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
|
||||||
|
@encrypted_config.save!
|
||||||
|
|
||||||
|
Docuseal.refresh_default_url_options!
|
||||||
end
|
end
|
||||||
|
|
||||||
@encrypted_config.save!
|
with_locale do
|
||||||
|
redirect_to settings_account_path, notice: I18n.t('account_information_has_been_updated')
|
||||||
Docuseal.refresh_default_url_options!
|
end
|
||||||
|
|
||||||
redirect_to settings_account_path, notice: I18n.t('account_information_has_been_updated')
|
|
||||||
rescue ActiveRecord::RecordInvalid
|
rescue ActiveRecord::RecordInvalid
|
||||||
render :show, status: :unprocessable_entity
|
render :show, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
@@ -41,12 +45,12 @@ class AccountsController < ApplicationController
|
|||||||
def destroy
|
def destroy
|
||||||
authorize!(:manage, current_account)
|
authorize!(:manage, current_account)
|
||||||
|
|
||||||
true_user.update!(locked_at: Time.current)
|
true_user.update!(locked_at: Time.current, email: true_user.email.sub('@', '+removed@'))
|
||||||
|
|
||||||
# rubocop:disable Layout/LineLength
|
# rubocop:disable Layout/LineLength
|
||||||
render turbo_stream: turbo_stream.replace(
|
render turbo_stream: turbo_stream.replace(
|
||||||
:account_delete_button,
|
:account_delete_button,
|
||||||
html: helpers.tag.p(I18n.t('your_account_removal_request_will_be_processed_within_2_weeks_please_contact_us_if_you_want_to_keep_your_account'))
|
html: helpers.tag.p(I18n.t('your_account_removal_request_will_be_processed_within_2_months_please_contact_us_if_you_want_to_keep_your_account'))
|
||||||
)
|
)
|
||||||
# rubocop:enable Layout/LineLength
|
# rubocop:enable Layout/LineLength
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -21,7 +21,11 @@ module Api
|
|||||||
|
|
||||||
blob = ActiveStorage::Blob.find_by!(uuid: blob_uuid)
|
blob = ActiveStorage::Blob.find_by!(uuid: blob_uuid)
|
||||||
|
|
||||||
authorization_check!(blob) if exp.blank?
|
attachment = blob.attachments.take
|
||||||
|
|
||||||
|
@record = attachment.record
|
||||||
|
|
||||||
|
authorization_check!(attachment) if exp.blank?
|
||||||
|
|
||||||
if request.headers['Range'].present?
|
if request.headers['Range'].present?
|
||||||
send_blob_byte_range_data blob, request.headers['Range']
|
send_blob_byte_range_data blob, request.headers['Range']
|
||||||
@@ -37,13 +41,12 @@ module Api
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def authorization_check!(blob)
|
def authorization_check!(attachment)
|
||||||
is_authorized =
|
is_authorized = attachment.name.in?(%w[logo preview_images]) ||
|
||||||
blob.attachments.all? do |a|
|
(current_user && attachment.record.account.id == current_user.account_id) ||
|
||||||
a.name.in?(%w[logo preview_images]) ||
|
(current_user && !Docuseal.multitenant? && current_user.role == 'superadmin') ||
|
||||||
(current_user && a.record.account.id == current_user.account_id) ||
|
!attachment.record.account.account_configs
|
||||||
!a.record.account.account_configs.find_or_initialize_by(key: AccountConfig::DOWNLOAD_LINKS_AUTH_KEY).value
|
.find_or_initialize_by(key: AccountConfig::DOWNLOAD_LINKS_AUTH_KEY).value
|
||||||
end
|
|
||||||
|
|
||||||
return if is_authorized
|
return if is_authorized
|
||||||
|
|
||||||
|
|||||||
@@ -25,9 +25,9 @@ module Api
|
|||||||
render json: { error: 'Too many requests' }, status: :too_many_requests
|
render json: { error: 'Too many requests' }, status: :too_many_requests
|
||||||
end
|
end
|
||||||
|
|
||||||
if Rails.env.production?
|
unless Rails.env.development?
|
||||||
rescue_from CanCan::AccessDenied do |e|
|
rescue_from CanCan::AccessDenied do |e|
|
||||||
render json: { error: e.message }, status: :forbidden
|
render json: { error: access_denied_error_message(e) }, status: :forbidden
|
||||||
end
|
end
|
||||||
|
|
||||||
rescue_from JSON::ParserError do |e|
|
rescue_from JSON::ParserError do |e|
|
||||||
@@ -39,6 +39,33 @@ module Api
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def access_denied_error_message(error)
|
||||||
|
return 'Not authorized' if request.headers['X-Auth-Token'].blank?
|
||||||
|
return 'Not authorized' unless error.subject.is_a?(ActiveRecord::Base)
|
||||||
|
return 'Not authorized' unless error.subject.respond_to?(:account_id)
|
||||||
|
|
||||||
|
linked_account_record_exists =
|
||||||
|
if current_user.account.testing?
|
||||||
|
current_user.account.linked_account_accounts.where(account_type: 'testing')
|
||||||
|
.exists?(account_id: error.subject.account_id)
|
||||||
|
else
|
||||||
|
current_user.account.testing_accounts.exists?(id: error.subject.account_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
return 'Not authorized' unless linked_account_record_exists
|
||||||
|
|
||||||
|
object_name = error.subject.model_name.human
|
||||||
|
id = error.subject.id
|
||||||
|
|
||||||
|
if current_user.account.testing?
|
||||||
|
"#{object_name} #{id} not found using testing API key; Use production API key to " \
|
||||||
|
"access production #{object_name.downcase.pluralize}."
|
||||||
|
else
|
||||||
|
"#{object_name} #{id} not found using production API key; Use testing API key to " \
|
||||||
|
"access testing #{object_name.downcase.pluralize}."
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def paginate(relation, field: :id)
|
def paginate(relation, field: :id)
|
||||||
result = relation.order(field => :desc)
|
result = relation.order(field => :desc)
|
||||||
.limit([params.fetch(:limit, DEFAULT_LIMIT).to_i, MAX_LIMIT].min)
|
.limit([params.fetch(:limit, DEFAULT_LIMIT).to_i, MAX_LIMIT].min)
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Api
|
||||||
|
class SubmissionDocumentsController < ApiBaseController
|
||||||
|
load_and_authorize_resource :submission
|
||||||
|
|
||||||
|
def index
|
||||||
|
documents =
|
||||||
|
if @submission.submitters.all?(&:completed_at?)
|
||||||
|
last_submitter = @submission.submitters.max_by(&:completed_at)
|
||||||
|
|
||||||
|
if last_submitter.documents_attachments.blank?
|
||||||
|
last_submitter.documents_attachments = Submissions::EnsureResultGenerated.call(last_submitter)
|
||||||
|
end
|
||||||
|
|
||||||
|
last_submitter.documents_attachments
|
||||||
|
else
|
||||||
|
values_hash = Submissions::GeneratePreviewAttachments.build_values_hash(@submission)
|
||||||
|
|
||||||
|
if @submission.preview_documents.present? &&
|
||||||
|
@submission.preview_documents.all? { |s| s.metadata['values_hash'] == values_hash }
|
||||||
|
@submission.preview_documents
|
||||||
|
else
|
||||||
|
ApplicationRecord.no_touching do
|
||||||
|
@submission.preview_documents.each(&:destroy)
|
||||||
|
end
|
||||||
|
|
||||||
|
Submissions::GeneratePreviewAttachments.call(@submission, values_hash:)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
ActiveRecord::Associations::Preloader.new(
|
||||||
|
records: documents,
|
||||||
|
associations: [:blob]
|
||||||
|
).call
|
||||||
|
|
||||||
|
render json: {
|
||||||
|
id: @submission.id,
|
||||||
|
documents: documents.map do |attachment|
|
||||||
|
{ name: attachment.filename.base, url: ActiveStorage::Blob.proxy_url(attachment.blob) }
|
||||||
|
end
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -11,11 +11,7 @@ module Api
|
|||||||
|
|
||||||
def index
|
def index
|
||||||
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 = filter_submissions(submissions, params)
|
||||||
|
|
||||||
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,
|
submissions = paginate(submissions.preload(:created_by_user, :submitters,
|
||||||
template: :folder,
|
template: :folder,
|
||||||
@@ -67,32 +63,41 @@ module Api
|
|||||||
|
|
||||||
submissions = create_submissions(@template, params)
|
submissions = create_submissions(@template, params)
|
||||||
|
|
||||||
submissions.each do |submission|
|
WebhookUrls.for_account_id(@template.account_id, 'submission.created').each do |webhook_url|
|
||||||
SendSubmissionCreatedWebhookRequestJob.perform_async({ 'submission_id' => submission.id })
|
submissions.each do |submission|
|
||||||
|
SendSubmissionCreatedWebhookRequestJob.perform_async('submission_id' => submission.id,
|
||||||
|
'webhook_url_id' => webhook_url.id)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
Submissions.send_signature_requests(submissions)
|
Submissions.send_signature_requests(submissions)
|
||||||
|
|
||||||
submissions.each do |submission|
|
submissions.each do |submission|
|
||||||
if submission.submitters.all?(&:completed_at?) && submission.submitters.last
|
submission.submitters.each do |submitter|
|
||||||
ProcessSubmitterCompletionJob.perform_async({ 'submitter_id' => submission.submitters.last.id })
|
next unless submitter.completed_at?
|
||||||
|
|
||||||
|
ProcessSubmitterCompletionJob.perform_async('submitter_id' => submitter.id, 'send_invitation_email' => false)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
render json: build_create_json(submissions)
|
render json: build_create_json(submissions)
|
||||||
rescue Submitters::NormalizeValues::BaseError, DownloadUtils::UnableToDownload => e
|
rescue Submitters::NormalizeValues::BaseError, Submissions::CreateFromSubmitters::BaseError,
|
||||||
|
DownloadUtils::UnableToDownload => e
|
||||||
Rollbar.warning(e) if defined?(Rollbar)
|
Rollbar.warning(e) if defined?(Rollbar)
|
||||||
|
|
||||||
render json: { error: e.message }, status: :unprocessable_entity
|
render json: { error: e.message }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
if params[:permanently] == 'true'
|
if params[:permanently].in?(['true', true])
|
||||||
@submission.destroy!
|
@submission.destroy!
|
||||||
else
|
else
|
||||||
@submission.update!(archived_at: Time.current)
|
@submission.update!(archived_at: Time.current)
|
||||||
|
|
||||||
SendSubmissionArchivedWebhookRequestJob.perform_async('submission_id' => @submission.id)
|
WebhookUrls.for_account_id(@submission.account_id, 'submission.archived').each do |webhook_url|
|
||||||
|
SendSubmissionArchivedWebhookRequestJob.perform_async('submission_id' => @submission.id,
|
||||||
|
'webhook_url_id' => webhook_url.id)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
render json: @submission.as_json(only: %i[id archived_at])
|
render json: @submission.as_json(only: %i[id archived_at])
|
||||||
@@ -100,6 +105,21 @@ module Api
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def filter_submissions(submissions, params)
|
||||||
|
submissions = submissions.where(template_id: params[:template_id]) if params[:template_id].present?
|
||||||
|
submissions = submissions.where(slug: params[:slug]) if params[:slug].present?
|
||||||
|
|
||||||
|
if params[:template_folder].present?
|
||||||
|
submissions = submissions.joins(template: :folder).where(folder: { name: params[:template_folder] })
|
||||||
|
end
|
||||||
|
|
||||||
|
if params.key?(:archived)
|
||||||
|
submissions = params[:archived].in?(['true', true]) ? submissions.archived : submissions.active
|
||||||
|
end
|
||||||
|
|
||||||
|
Submissions::Filter.call(submissions, current_user, params)
|
||||||
|
end
|
||||||
|
|
||||||
def build_create_json(submissions)
|
def build_create_json(submissions)
|
||||||
json = submissions.flat_map do |submission|
|
json = submissions.flat_map do |submission|
|
||||||
submission.submitters.map do |s|
|
submission.submitters.map do |s|
|
||||||
@@ -107,7 +127,19 @@ module Api
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
json = { submitters: json } if request.path.ends_with?('/init')
|
if request.path.ends_with?('/init')
|
||||||
|
json =
|
||||||
|
if submissions.size == 1
|
||||||
|
{
|
||||||
|
id: submissions.first.id,
|
||||||
|
submitters: json,
|
||||||
|
expire_at: submissions.first.expire_at,
|
||||||
|
created_at: submissions.first.created_at
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{ submitters: json }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
json
|
json
|
||||||
end
|
end
|
||||||
@@ -156,7 +188,7 @@ module Api
|
|||||||
message: %i[subject body],
|
message: %i[subject body],
|
||||||
submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role,
|
submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role,
|
||||||
:completed, :phone, :application_key, :external_id, :reply_to, :go_to_last,
|
:completed, :phone, :application_key, :external_id, :reply_to, :go_to_last,
|
||||||
{ metadata: {}, values: {}, readonly_fields: [], message: %i[subject body],
|
{ metadata: {}, values: {}, roles: [], readonly_fields: [], message: %i[subject body],
|
||||||
fields: [:name, :uuid, :default_value, :value, :title, :description,
|
fields: [:name, :uuid, :default_value, :value, :title, :description,
|
||||||
:readonly, :validation_pattern, :invalid_message,
|
:readonly, :validation_pattern, :invalid_message,
|
||||||
{ default_value: [], value: [], preferences: {} }] }]]
|
{ default_value: [], value: [], preferences: {} }] }]]
|
||||||
@@ -169,7 +201,7 @@ module Api
|
|||||||
key = params.key?(:submission) ? :submission : :submissions
|
key = params.key?(:submission) ? :submission : :submissions
|
||||||
|
|
||||||
params.permit(
|
params.permit(
|
||||||
key => [permitted_attrs]
|
{ key => [permitted_attrs] }, { key => permitted_attrs }
|
||||||
).fetch(key, [])
|
).fetch(key, [])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ module Api
|
|||||||
skip_authorization_check
|
skip_authorization_check
|
||||||
|
|
||||||
def create
|
def create
|
||||||
submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
@submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
||||||
|
|
||||||
if params[:t] == SubmissionEvents.build_tracking_param(submitter, 'click_email')
|
if params[:t] == SubmissionEvents.build_tracking_param(@submitter, 'click_email')
|
||||||
SubmissionEvents.create_with_tracking_data(submitter, 'click_email', request)
|
SubmissionEvents.create_with_tracking_data(@submitter, 'click_email', request)
|
||||||
end
|
end
|
||||||
|
|
||||||
render json: {}
|
render json: {}
|
||||||
|
|||||||
@@ -6,14 +6,17 @@ module Api
|
|||||||
skip_authorization_check
|
skip_authorization_check
|
||||||
|
|
||||||
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.opened_at = Time.current
|
||||||
submitter.save
|
@submitter.save
|
||||||
|
|
||||||
SubmissionEvents.create_with_tracking_data(submitter, 'view_form', request)
|
SubmissionEvents.create_with_tracking_data(@submitter, 'view_form', request)
|
||||||
|
|
||||||
SendFormViewedWebhookRequestJob.perform_async({ 'submitter_id' => submitter.id })
|
WebhookUrls.for_account_id(@submitter.account_id, 'form.viewed').each do |webhook_url|
|
||||||
|
SendFormViewedWebhookRequestJob.perform_async('submitter_id' => @submitter.id,
|
||||||
|
'webhook_url_id' => webhook_url.id)
|
||||||
|
end
|
||||||
|
|
||||||
render json: {}
|
render json: {}
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -7,15 +7,7 @@ module Api
|
|||||||
def index
|
def index
|
||||||
submitters = Submitters.search(@submitters, params[:q])
|
submitters = Submitters.search(@submitters, params[:q])
|
||||||
|
|
||||||
submitters = submitters.where(external_id: params[:application_key]) if params[:application_key].present?
|
submitters = filter_submitters(submitters, params)
|
||||||
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 = paginate(
|
||||||
submitters.preload(:template, :submission, :submission_events,
|
submitters.preload(:template, :submission, :submission_events,
|
||||||
@@ -68,7 +60,7 @@ module Api
|
|||||||
end
|
end
|
||||||
|
|
||||||
if @submitter.completed_at?
|
if @submitter.completed_at?
|
||||||
ProcessSubmitterCompletionJob.perform_async({ 'submitter_id' => @submitter.id })
|
ProcessSubmitterCompletionJob.perform_async('submitter_id' => @submitter.id)
|
||||||
elsif normalized_params[:send_email] || normalized_params[:send_sms]
|
elsif normalized_params[:send_email] || normalized_params[:send_sms]
|
||||||
Submitters.send_signature_requests([@submitter])
|
Submitters.send_signature_requests([@submitter])
|
||||||
end
|
end
|
||||||
@@ -120,6 +112,7 @@ module Api
|
|||||||
end&.dig('uuid')
|
end&.dig('uuid')
|
||||||
|
|
||||||
submitter.email = Submissions.normalize_email(attrs[:email]) if attrs.key?(:email)
|
submitter.email = Submissions.normalize_email(attrs[:email]) if attrs.key?(:email)
|
||||||
|
submitter.name = attrs[:name] if attrs.key?(:name)
|
||||||
|
|
||||||
if attrs.key?(:phone)
|
if attrs.key?(:phone)
|
||||||
submitter.phone = attrs[:phone].to_s.gsub(/[^0-9+]/, '')
|
submitter.phone = attrs[:phone].to_s.gsub(/[^0-9+]/, '')
|
||||||
@@ -145,9 +138,15 @@ module Api
|
|||||||
|
|
||||||
if attrs[:completed]
|
if attrs[:completed]
|
||||||
submitter.values = Submitters::SubmitValues.merge_default_values(submitter)
|
submitter.values = Submitters::SubmitValues.merge_default_values(submitter)
|
||||||
submitter.values = Submitters::SubmitValues.merge_formula_values(submitter)
|
|
||||||
submitter.values = Submitters::SubmitValues.maybe_remove_condition_values(submitter)
|
submitter.values = Submitters::SubmitValues.maybe_remove_condition_values(submitter)
|
||||||
|
|
||||||
|
formula_values = Submitters::SubmitValues.build_formula_values(submitter)
|
||||||
|
|
||||||
|
if formula_values.present?
|
||||||
|
submitter.values = submitter.values.merge(formula_values)
|
||||||
|
submitter.values = Submitters::SubmitValues.maybe_remove_condition_values(submitter)
|
||||||
|
end
|
||||||
|
|
||||||
submitter.values = submitter.values.transform_values do |v|
|
submitter.values = submitter.values.transform_values do |v|
|
||||||
v == '{{date}}' ? Time.current.in_time_zone(submitter.account.timezone).to_date.to_s : v
|
v == '{{date}}' ? Time.current.in_time_zone(submitter.account.timezone).to_date.to_s : v
|
||||||
end
|
end
|
||||||
@@ -156,6 +155,19 @@ module Api
|
|||||||
submitter
|
submitter
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def filter_submitters(submitters, params)
|
||||||
|
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(slug: params[:slug]) if params[:slug].present?
|
||||||
|
submitters = submitters.where(submission_id: params[:submission_id]) if params[:submission_id].present?
|
||||||
|
|
||||||
|
if params[:template_id].present?
|
||||||
|
submitters = submitters.joins(:submission).where(submissions: { template_id: params[:template_id] })
|
||||||
|
end
|
||||||
|
|
||||||
|
maybe_filder_by_completed_at(submitters, params)
|
||||||
|
end
|
||||||
|
|
||||||
def assign_external_id(submitter, attrs)
|
def assign_external_id(submitter, attrs)
|
||||||
submitter.external_id = attrs[:application_key] if attrs.key?(:application_key)
|
submitter.external_id = attrs[:application_key] if attrs.key?(:application_key)
|
||||||
submitter.external_id = attrs[:external_id] if attrs.key?(:external_id)
|
submitter.external_id = attrs[:external_id] if attrs.key?(:external_id)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ module Api
|
|||||||
load_and_authorize_resource :template
|
load_and_authorize_resource :template
|
||||||
|
|
||||||
def create
|
def create
|
||||||
authorize!(:manage, @template)
|
authorize!(:create, @template)
|
||||||
|
|
||||||
ActiveRecord::Associations::Preloader.new(
|
ActiveRecord::Associations::Preloader.new(
|
||||||
records: [@template],
|
records: [@template],
|
||||||
@@ -23,9 +23,14 @@ module Api
|
|||||||
cloned_template.source = :api
|
cloned_template.source = :api
|
||||||
cloned_template.save!
|
cloned_template.save!
|
||||||
|
|
||||||
schema_documents = Templates::CloneAttachments.call(template: cloned_template, original_template: @template)
|
schema_documents = Templates::CloneAttachments.call(template: cloned_template,
|
||||||
|
original_template: @template,
|
||||||
|
documents: params[:documents])
|
||||||
|
|
||||||
SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => cloned_template.id)
|
WebhookUrls.for_account_id(cloned_template.account_id, 'template.created').each do |webhook_url|
|
||||||
|
SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => cloned_template.id,
|
||||||
|
'webhook_url_id' => webhook_url.id)
|
||||||
|
end
|
||||||
|
|
||||||
render json: Templates::SerializeForApi.call(cloned_template, schema_documents)
|
render json: Templates::SerializeForApi.call(cloned_template, schema_documents)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -65,13 +65,16 @@ module Api
|
|||||||
|
|
||||||
@template.update!(template_params)
|
@template.update!(template_params)
|
||||||
|
|
||||||
SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => @template.id)
|
WebhookUrls.for_account_id(@template.account_id, 'template.updated').each do |webhook_url|
|
||||||
|
SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => @template.id,
|
||||||
|
'webhook_url_id' => webhook_url.id)
|
||||||
|
end
|
||||||
|
|
||||||
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
|
||||||
if params[:permanently] == 'true'
|
if params[:permanently].in?(['true', true])
|
||||||
@template.destroy!
|
@template.destroy!
|
||||||
else
|
else
|
||||||
@template.update!(archived_at: Time.current)
|
@template.update!(archived_at: Time.current)
|
||||||
@@ -84,9 +87,10 @@ module Api
|
|||||||
|
|
||||||
def filter_templates(templates, params)
|
def filter_templates(templates, params)
|
||||||
templates = Templates.search(templates, params[:q])
|
templates = Templates.search(templates, params[:q])
|
||||||
templates = params[:archived] ? templates.archived : templates.active
|
templates = params[:archived].in?(['true', true]) ? templates.archived : templates.active
|
||||||
templates = templates.where(external_id: params[:application_key]) if params[:application_key].present?
|
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.where(external_id: params[:external_id]) if params[:external_id].present?
|
||||||
|
templates = templates.where(slug: params[:slug]) if params[:slug].present?
|
||||||
templates = templates.joins(:folder).where(folder: { name: params[:folder] }) if params[:folder].present?
|
templates = templates.joins(:folder).where(folder: { name: params[:folder] }) if params[:folder].present?
|
||||||
|
|
||||||
templates
|
templates
|
||||||
@@ -97,12 +101,12 @@ module Api
|
|||||||
:name,
|
:name,
|
||||||
:external_id,
|
:external_id,
|
||||||
{
|
{
|
||||||
submitters: [%i[name uuid is_requester invite_by_uuid linked_to_uuid email]],
|
submitters: [%i[name uuid is_requester invite_by_uuid optional_invite_by_uuid linked_to_uuid email]],
|
||||||
fields: [[:uuid, :submitter_uuid, :name, :type,
|
fields: [[:uuid, :submitter_uuid, :name, :type,
|
||||||
:required, :readonly, :default_value,
|
:required, :readonly, :default_value,
|
||||||
:title, :description,
|
:title, :description,
|
||||||
{ preferences: {},
|
{ preferences: {},
|
||||||
conditions: [%i[field_uuid value action]],
|
conditions: [%i[field_uuid value action operation]],
|
||||||
options: [%i[value uuid]],
|
options: [%i[value uuid]],
|
||||||
validation: %i[message pattern],
|
validation: %i[message pattern],
|
||||||
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]]
|
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]]
|
||||||
|
|||||||
@@ -20,10 +20,7 @@ module Api
|
|||||||
pdf = HexaPDF::Document.new(io: StringIO.new(file))
|
pdf = HexaPDF::Document.new(io: StringIO.new(file))
|
||||||
|
|
||||||
trusted_certs = Accounts.load_trusted_certs(current_account)
|
trusted_certs = Accounts.load_trusted_certs(current_account)
|
||||||
|
is_checksum_found = CompletedDocument.exists?(sha256: Base64.urlsafe_encode64(Digest::SHA256.digest(file)))
|
||||||
is_checksum_found = ActiveStorage::Attachment.joins(:blob)
|
|
||||||
.where(name: 'documents', record_type: 'Submitter')
|
|
||||||
.exists?(blob: { checksum: Digest::MD5.base64digest(file) })
|
|
||||||
|
|
||||||
render json: {
|
render json: {
|
||||||
checksum_status: is_checksum_found ? 'verified' : 'not_found',
|
checksum_status: is_checksum_found ? 'verified' : 'not_found',
|
||||||
@@ -37,6 +34,8 @@ module Api
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
}
|
}
|
||||||
|
rescue HexaPDF::MalformedPDFError
|
||||||
|
render json: { error: 'Malformed PDF' }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -8,12 +8,14 @@ class ApplicationController < ActionController::Base
|
|||||||
|
|
||||||
check_authorization unless: :devise_controller?
|
check_authorization unless: :devise_controller?
|
||||||
|
|
||||||
|
around_action :with_locale
|
||||||
before_action :sign_in_for_demo, if: -> { Docuseal.demo? }
|
before_action :sign_in_for_demo, if: -> { Docuseal.demo? }
|
||||||
before_action :maybe_redirect_to_setup, unless: :signed_in?
|
before_action :maybe_redirect_to_setup, unless: :signed_in?
|
||||||
before_action :authenticate_user!, unless: :devise_controller?
|
before_action :authenticate_user!, unless: :devise_controller?
|
||||||
|
|
||||||
helper_method :button_title,
|
helper_method :button_title,
|
||||||
:current_account,
|
:current_account,
|
||||||
|
:form_link_host,
|
||||||
:svg_icon
|
:svg_icon
|
||||||
|
|
||||||
impersonates :user, with: ->(uuid) { User.find_by(uuid:) }
|
impersonates :user, with: ->(uuid) { User.find_by(uuid:) }
|
||||||
@@ -28,7 +30,7 @@ class ApplicationController < ActionController::Base
|
|||||||
redirect_to request.referer, alert: 'Too many requests', status: :too_many_requests
|
redirect_to request.referer, alert: 'Too many requests', status: :too_many_requests
|
||||||
end
|
end
|
||||||
|
|
||||||
if Rails.env.production?
|
if Rails.env.production? || Rails.env.test?
|
||||||
rescue_from CanCan::AccessDenied do |e|
|
rescue_from CanCan::AccessDenied do |e|
|
||||||
Rollbar.warning(e) if defined?(Rollbar)
|
Rollbar.warning(e) if defined?(Rollbar)
|
||||||
|
|
||||||
@@ -37,6 +39,10 @@ class ApplicationController < ActionController::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
def default_url_options
|
def default_url_options
|
||||||
|
if request.domain == 'docuseal.com'
|
||||||
|
return { host: 'docuseal.com', protocol: ENV['FORCE_SSL'].present? ? 'https' : 'http' }
|
||||||
|
end
|
||||||
|
|
||||||
Docuseal.default_url_options
|
Docuseal.default_url_options
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -51,7 +57,18 @@ class ApplicationController < ActionController::Base
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def with_locale(&)
|
||||||
|
return yield unless current_account
|
||||||
|
|
||||||
|
locale = params[:lang].presence if Rails.env.development?
|
||||||
|
locale ||= current_account.locale
|
||||||
|
|
||||||
|
I18n.with_locale(locale, &)
|
||||||
|
end
|
||||||
|
|
||||||
def with_browser_locale(&)
|
def with_browser_locale(&)
|
||||||
|
return yield if I18n.locale != :'en-US' && I18n.locale != :en
|
||||||
|
|
||||||
locale = params[:lang].presence
|
locale = params[:lang].presence
|
||||||
locale ||= request.env['HTTP_ACCEPT_LANGUAGE'].to_s[BROWSER_LOCALE_REGEXP].to_s
|
locale ||= request.env['HTTP_ACCEPT_LANGUAGE'].to_s[BROWSER_LOCALE_REGEXP].to_s
|
||||||
|
|
||||||
@@ -88,4 +105,14 @@ class ApplicationController < ActionController::Base
|
|||||||
def svg_icon(icon_name, class: '')
|
def svg_icon(icon_name, class: '')
|
||||||
render_to_string(partial: "icons/#{icon_name}", locals: { class: })
|
render_to_string(partial: "icons/#{icon_name}", locals: { class: })
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def form_link_host
|
||||||
|
Docuseal.default_url_options[:host]
|
||||||
|
end
|
||||||
|
|
||||||
|
def maybe_redirect_com
|
||||||
|
return if request.domain != 'docuseal.co'
|
||||||
|
|
||||||
|
redirect_to request.url.gsub('.co/', '.com/'), allow_other_host: true, status: :moved_permanently
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -6,7 +6,7 @@ class ConsoleRedirectController < ApplicationController
|
|||||||
|
|
||||||
def index
|
def index
|
||||||
if request.path == '/upgrade'
|
if request.path == '/upgrade'
|
||||||
params[:redir] = Docuseal.multitenant? ? "#{Docuseal::CONSOLE_URL}/plans" : "#{Docuseal::CONSOLE_URL}/on_premise"
|
params[:redir] = Docuseal.multitenant? ? "#{Docuseal::CONSOLE_URL}/plans" : "#{Docuseal::CONSOLE_URL}/on_premises"
|
||||||
end
|
end
|
||||||
|
|
||||||
params[:redir] = "#{Docuseal::CONSOLE_URL}/manage" if request.path == '/manage'
|
params[:redir] = "#{Docuseal::CONSOLE_URL}/manage" if request.path == '/manage'
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ class EmailSmtpSettingsController < ApplicationController
|
|||||||
|
|
||||||
def create
|
def create
|
||||||
if @encrypted_config.update(email_configs)
|
if @encrypted_config.update(email_configs)
|
||||||
SettingsMailer.smtp_successful_setup(@encrypted_config.value['from_email']).deliver_now!
|
SettingsMailer.smtp_successful_setup(@encrypted_config.value['from_email'] || current_user.email).deliver_now!
|
||||||
|
|
||||||
redirect_to settings_email_index_path, notice: I18n.t('changes_have_been_saved')
|
redirect_to settings_email_index_path, notice: I18n.t('changes_have_been_saved')
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ class EmbedScriptsController < ActionController::Metal
|
|||||||
<h2>Upgrade to Pro</h2>
|
<h2>Upgrade to Pro</h2>
|
||||||
<p>Unlock embedded components by upgrading to Pro</p>
|
<p>Unlock embedded components by upgrading to Pro</p>
|
||||||
<div style="margin-top: 40px;">
|
<div style="margin-top: 40px;">
|
||||||
<a href="#{Docuseal::CONSOLE_URL}/on_premise" target="_blank" style="padding: 15px 25px; background-color: #222; color: white; text-decoration: none; border-radius: 5px; font-size: 16px; cursor: pointer;">
|
<a href="#{Docuseal::CONSOLE_URL}/on_premises" target="_blank" style="padding: 15px 25px; background-color: #222; color: white; text-decoration: none; border-radius: 5px; font-size: 16px; cursor: pointer;">
|
||||||
Learn More
|
Learn More
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
class ErrorsController < ActionController::Base
|
class ErrorsController < ActionController::Base
|
||||||
ENTERPRISE_FEATURE_MESSAGE =
|
ENTERPRISE_FEATURE_MESSAGE =
|
||||||
'This feature is available in Pro Edition: https://www.docuseal.co/pricing'
|
'This feature is available in Pro Edition: https://www.docuseal.com/pricing'
|
||||||
|
|
||||||
ENTERPRISE_PATHS = [
|
ENTERPRISE_PATHS = [
|
||||||
'/templates/html',
|
'/templates/html',
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ class EsignSettingsController < ApplicationController
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
prepend_before_action :maybe_redirect_com, only: %i[show]
|
||||||
|
|
||||||
before_action :load_encrypted_config
|
before_action :load_encrypted_config
|
||||||
authorize_resource :encrypted_config, parent: false, only: %i[new create]
|
authorize_resource :encrypted_config, parent: false, only: %i[new create]
|
||||||
authorize_resource :encrypted_config, only: %i[update destroy show]
|
authorize_resource :encrypted_config, only: %i[update destroy show]
|
||||||
@@ -21,9 +23,9 @@ class EsignSettingsController < ApplicationController
|
|||||||
default_pkcs = GenerateCertificate.load_pkcs(cert_data) if cert_data['cert'].present?
|
default_pkcs = GenerateCertificate.load_pkcs(cert_data) if cert_data['cert'].present?
|
||||||
|
|
||||||
custom_pkcs_list = (cert_data['custom'] || []).map do |e|
|
custom_pkcs_list = (cert_data['custom'] || []).map do |e|
|
||||||
{ 'pkcs' => OpenSSL::PKCS12.new(Base64.urlsafe_decode64(e['data']), e['password'].to_s),
|
pkcs = e['data'].present? ? OpenSSL::PKCS12.new(Base64.urlsafe_decode64(e['data']), e['password'].to_s) : nil
|
||||||
'name' => e['name'],
|
|
||||||
'status' => e['status'] }
|
{ 'pkcs' => pkcs, 'name' => e['name'], 'status' => e['status'] }
|
||||||
end
|
end
|
||||||
|
|
||||||
@pkcs_list = [
|
@pkcs_list = [
|
||||||
@@ -56,7 +58,7 @@ class EsignSettingsController < ApplicationController
|
|||||||
|
|
||||||
save_new_cert!(@encrypted_config, @cert_record)
|
save_new_cert!(@encrypted_config, @cert_record)
|
||||||
|
|
||||||
redirect_to settings_esign_path, notice: I18n.t('certificate_has_been_successfully_added_')
|
redirect_to settings_esign_path, notice: I18n.t('certificate_has_been_successfully_added')
|
||||||
rescue OpenSSL::PKCS12::PKCS12Error => e
|
rescue OpenSSL::PKCS12::PKCS12Error => e
|
||||||
Rollbar.error(e) if defined?(Rollbar)
|
Rollbar.error(e) if defined?(Rollbar)
|
||||||
|
|
||||||
@@ -66,10 +68,16 @@ class EsignSettingsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
@encrypted_config.value['custom'].each { |e| e['status'] = 'validate' }
|
@encrypted_config.value['custom'].to_a.each { |e| e['status'] = 'validate' }
|
||||||
|
|
||||||
custom_cert_data = @encrypted_config.value['custom'].find { |e| e['name'] == params[:name] }
|
custom_cert_data = @encrypted_config.value['custom'].to_a.find { |e| e['name'] == params[:name] }
|
||||||
custom_cert_data['status'] = 'default' if custom_cert_data
|
|
||||||
|
if custom_cert_data
|
||||||
|
custom_cert_data['status'] = 'default'
|
||||||
|
elsif params[:name] == Docuseal::AATL_CERT_NAME
|
||||||
|
@encrypted_config.value['custom'] ||= []
|
||||||
|
@encrypted_config.value['custom'] << { 'name' => params[:name], 'status' => 'default' }
|
||||||
|
end
|
||||||
|
|
||||||
@encrypted_config.save!
|
@encrypted_config.save!
|
||||||
|
|
||||||
@@ -81,7 +89,7 @@ class EsignSettingsController < ApplicationController
|
|||||||
|
|
||||||
@encrypted_config.save!
|
@encrypted_config.save!
|
||||||
|
|
||||||
redirect_to settings_esign_path, notice: I18m.t('certificate_has_been_removed')
|
redirect_to settings_esign_path, notice: I18n.t('certificate_has_been_removed')
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ class NotificationsSettingsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def email_config_params
|
def email_config_params
|
||||||
params.require(:account_config).permit!.tap do |attrs|
|
params.require(:account_config).permit(:key, :value, { value: {} }, { value: [] }).tap do |attrs|
|
||||||
attrs[:key] = nil unless attrs[:key].in?([AccountConfig::BCC_EMAILS, AccountConfig::SUBMITTER_REMINDERS])
|
attrs[:key] = nil unless attrs[:key].in?([AccountConfig::BCC_EMAILS, AccountConfig::SUBMITTER_REMINDERS])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ class PersonalizationSettingsController < ApplicationController
|
|||||||
AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY,
|
AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY,
|
||||||
AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY,
|
AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY,
|
||||||
AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY,
|
AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY,
|
||||||
AccountConfig::FORM_COMPLETED_MESSAGE_KEY
|
AccountConfig::FORM_COMPLETED_MESSAGE_KEY,
|
||||||
|
*(Docuseal.multitenant? ? [] : [AccountConfig::POLICY_LINKS_KEY])
|
||||||
].freeze
|
].freeze
|
||||||
|
|
||||||
InvalidKey = Class.new(StandardError)
|
InvalidKey = Class.new(StandardError)
|
||||||
@@ -49,7 +50,7 @@ class PersonalizationSettingsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def account_config_params
|
def account_config_params
|
||||||
attrs = params.require(:account_config).permit!
|
attrs = params.require(:account_config).permit(:key, :value, { value: {} }, { value: [] })
|
||||||
|
|
||||||
return attrs if attrs[:value].is_a?(String)
|
return attrs if attrs[:value].is_a?(String)
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,10 @@ class PreviewDocumentPageController < ActionController::API
|
|||||||
|
|
||||||
return head :not_found unless attachment
|
return head :not_found unless attachment
|
||||||
|
|
||||||
preview_image = attachment.preview_images.joins(:blob).find_by(blob: { filename: "#{params[:id]}#{FORMAT}" })
|
@template = attachment.record
|
||||||
|
|
||||||
|
preview_image = attachment.preview_images.joins(:blob)
|
||||||
|
.find_by(blob: { filename: ["#{params[:id]}.png", "#{params[:id]}.jpg"] })
|
||||||
|
|
||||||
return redirect_to preview_image.url, allow_other_host: true if preview_image
|
return redirect_to preview_image.url, allow_other_host: true if preview_image
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class PwaController < ActionController::Base
|
||||||
|
end
|
||||||
@@ -9,8 +9,6 @@ class SendSubmissionEmailController < ApplicationController
|
|||||||
|
|
||||||
SEND_DURATION = 30.minutes
|
SEND_DURATION = 30.minutes
|
||||||
|
|
||||||
def success; end
|
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@submitter =
|
@submitter =
|
||||||
if params[:template_slug]
|
if params[:template_slug]
|
||||||
@@ -31,7 +29,7 @@ class SendSubmissionEmailController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
respond_to do |f|
|
respond_to do |f|
|
||||||
f.html { redirect_to success_send_submission_email_index_path }
|
f.html { render :success }
|
||||||
f.json { head :ok }
|
f.json { head :ok }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ class SetupController < ApplicationController
|
|||||||
def account_params
|
def account_params
|
||||||
return {} unless params[:account]
|
return {} unless params[:account]
|
||||||
|
|
||||||
params.require(:account).permit(:name, :timezone)
|
params.require(:account).permit(:name, :timezone, :locale)
|
||||||
end
|
end
|
||||||
|
|
||||||
def encrypted_config_params
|
def encrypted_config_params
|
||||||
|
|||||||
@@ -7,11 +7,15 @@ class StartFormController < ApplicationController
|
|||||||
skip_authorization_check
|
skip_authorization_check
|
||||||
|
|
||||||
around_action :with_browser_locale, only: %i[show completed]
|
around_action :with_browser_locale, only: %i[show completed]
|
||||||
|
before_action :maybe_redirect_com, only: %i[show completed]
|
||||||
before_action :load_template
|
before_action :load_template
|
||||||
|
|
||||||
def show
|
def show
|
||||||
|
raise ActionController::RoutingError, I18n.t('not_found') if @template.preferences['require_phone_2fa'] == true
|
||||||
|
|
||||||
@submitter = @template.submissions.new(account_id: @template.account_id)
|
@submitter = @template.submissions.new(account_id: @template.account_id)
|
||||||
.submitters.new(uuid: (filter_undefined_submitters(@template).first ||
|
.submitters.new(account_id: @template.account_id,
|
||||||
|
uuid: (filter_undefined_submitters(@template).first ||
|
||||||
@template.submitters.first)['uuid'])
|
@template.submitters.first)['uuid'])
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -24,7 +28,7 @@ class StartFormController < ApplicationController
|
|||||||
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
|
||||||
if filter_undefined_submitters(@template).size > 1 && @submitter.new_record?
|
if filter_undefined_submitters(@template).size > 1 && @submitter.new_record?
|
||||||
@error_message = I18n.t('not_found')
|
@error_message = multiple_submitters_error_message
|
||||||
|
|
||||||
return render :show
|
return render :show
|
||||||
end
|
end
|
||||||
@@ -33,11 +37,18 @@ class StartFormController < ApplicationController
|
|||||||
assign_submission_attributes(@submitter, @template)
|
assign_submission_attributes(@submitter, @template)
|
||||||
|
|
||||||
Submissions::AssignDefinedSubmitters.call(@submitter.submission)
|
Submissions::AssignDefinedSubmitters.call(@submitter.submission)
|
||||||
|
else
|
||||||
|
@submitter.assign_attributes(ip: request.remote_ip, ua: request.user_agent)
|
||||||
end
|
end
|
||||||
|
|
||||||
if @submitter.save
|
if @submitter.save
|
||||||
if is_new_record
|
if is_new_record
|
||||||
SendSubmissionCreatedWebhookRequestJob.perform_async({ 'submission_id' => @submitter.submission.id })
|
enqueue_submission_create_webhooks(@submitter)
|
||||||
|
|
||||||
|
if @submitter.submission.expire_at?
|
||||||
|
ProcessSubmissionExpiredJob.perform_at(@submitter.submission.expire_at,
|
||||||
|
'submission_id' => @submitter.submission_id)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
redirect_to submit_form_path(@submitter.slug)
|
redirect_to submit_form_path(@submitter.slug)
|
||||||
@@ -55,13 +66,22 @@ class StartFormController < ApplicationController
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def enqueue_submission_create_webhooks(submitter)
|
||||||
|
WebhookUrls.for_account_id(submitter.account_id, 'submission.created').each do |webhook_url|
|
||||||
|
SendSubmissionCreatedWebhookRequestJob.perform_async('submission_id' => submitter.submission_id,
|
||||||
|
'webhook_url_id' => webhook_url.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def find_or_initialize_submitter(template, submitter_params)
|
def find_or_initialize_submitter(template, submitter_params)
|
||||||
Submitter.where(submission: template.submissions.where(expire_at: Time.current..)
|
Submitter.where(submission: template.submissions.where(expire_at: Time.current..)
|
||||||
.or(template.submissions.where(expire_at: nil)).where(archived_at: nil))
|
.or(template.submissions.where(expire_at: nil)).where(archived_at: nil))
|
||||||
.order(id: :desc)
|
.order(id: :desc)
|
||||||
.where(declined_at: nil)
|
.where(declined_at: nil)
|
||||||
|
.where(external_id: nil)
|
||||||
|
.where(ip: [nil, request.remote_ip])
|
||||||
.then { |rel| params[:resubmit].present? ? rel.where(completed_at: nil) : rel }
|
.then { |rel| params[:resubmit].present? ? rel.where(completed_at: nil) : rel }
|
||||||
.find_or_initialize_by(**submitter_params.compact_blank)
|
.find_or_initialize_by(email: submitter_params[:email], **submitter_params.compact_blank)
|
||||||
end
|
end
|
||||||
|
|
||||||
def assign_submission_attributes(submitter, template)
|
def assign_submission_attributes(submitter, template)
|
||||||
@@ -86,6 +106,7 @@ class StartFormController < ApplicationController
|
|||||||
submitter.submission ||= Submission.new(template:,
|
submitter.submission ||= Submission.new(template:,
|
||||||
account_id: template.account_id,
|
account_id: template.account_id,
|
||||||
template_submitters: template.submitters,
|
template_submitters: template.submitters,
|
||||||
|
expire_at: Templates.build_default_expire_at(template),
|
||||||
submitters: [submitter],
|
submitters: [submitter],
|
||||||
source: :link)
|
source: :link)
|
||||||
|
|
||||||
@@ -109,4 +130,12 @@ class StartFormController < ApplicationController
|
|||||||
|
|
||||||
@template = Template.find_by!(slug:)
|
@template = Template.find_by!(slug:)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def multiple_submitters_error_message
|
||||||
|
if current_user&.account_id == @template.account_id
|
||||||
|
helpers.t('this_submission_has_multiple_signers_which_prevents_the_use_of_a_sharing_link_html')
|
||||||
|
else
|
||||||
|
I18n.t('not_found')
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SubmissionEventsController < ApplicationController
|
||||||
|
SUBMISSION_EVENT_ICONS = {
|
||||||
|
'view_form' => 'eye',
|
||||||
|
'start_form' => 'player_play',
|
||||||
|
'complete_form' => 'check',
|
||||||
|
'send_email' => 'mail_forward',
|
||||||
|
'click_email' => 'hand_click',
|
||||||
|
'api_complete_form' => 'check',
|
||||||
|
'send_reminder_email' => 'mail_forward',
|
||||||
|
'send_2fa_sms' => '2fa',
|
||||||
|
'send_sms' => 'send',
|
||||||
|
'phone_verified' => 'phone_check',
|
||||||
|
'click_sms' => 'hand_click',
|
||||||
|
'decline_form' => 'x',
|
||||||
|
'start_verification' => 'player_play',
|
||||||
|
'complete_verification' => 'check',
|
||||||
|
'invite_party' => 'user_plus'
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
load_and_authorize_resource :submission
|
||||||
|
|
||||||
|
def index; end
|
||||||
|
end
|
||||||
@@ -7,9 +7,17 @@ class SubmissionsArchivedController < ApplicationController
|
|||||||
@submissions = @submissions.joins(:template)
|
@submissions = @submissions.joins(:template)
|
||||||
@submissions = @submissions.where.not(archived_at: nil)
|
@submissions = @submissions.where.not(archived_at: nil)
|
||||||
.or(@submissions.where.not(templates: { archived_at: nil }))
|
.or(@submissions.where.not(templates: { archived_at: nil }))
|
||||||
.preload(:created_by_user, template: :author)
|
.preload(:template_accesses, :created_by_user, template: :author)
|
||||||
@submissions = Submissions.search(@submissions, params[:q], search_template: true)
|
|
||||||
|
|
||||||
@pagy, @submissions = pagy(@submissions.preload(:submitters).order(id: :desc))
|
@submissions = Submissions.search(@submissions, params[:q], search_template: true)
|
||||||
|
@submissions = Submissions::Filter.call(@submissions, current_user, params)
|
||||||
|
|
||||||
|
@submissions = if params[:completed_at_from].present? || params[:completed_at_to].present?
|
||||||
|
@submissions.order(Submitter.arel_table[:completed_at].maximum.desc)
|
||||||
|
else
|
||||||
|
@submissions.order(id: :desc)
|
||||||
|
end
|
||||||
|
|
||||||
|
@pagy, @submissions = pagy(@submissions.preload(submitters: :start_form_submission_events))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -6,9 +6,18 @@ class SubmissionsController < ApplicationController
|
|||||||
|
|
||||||
load_and_authorize_resource :submission, only: %i[show destroy]
|
load_and_authorize_resource :submission, only: %i[show destroy]
|
||||||
|
|
||||||
|
prepend_before_action :maybe_redirect_com, only: %i[show]
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@submission = Submissions.preload_with_pages(@submission)
|
@submission = Submissions.preload_with_pages(@submission)
|
||||||
|
|
||||||
|
unless @submission.submitters.all?(&:completed_at?)
|
||||||
|
ActiveRecord::Associations::Preloader.new(
|
||||||
|
records: [@submission],
|
||||||
|
associations: [submitters: :start_form_submission_events]
|
||||||
|
).call
|
||||||
|
end
|
||||||
|
|
||||||
render :show, layout: 'plain'
|
render :show, layout: 'plain'
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -43,25 +52,31 @@ class SubmissionsController < ApplicationController
|
|||||||
params: params.merge('send_completed_email' => true))
|
params: params.merge('send_completed_email' => true))
|
||||||
end
|
end
|
||||||
|
|
||||||
submissions.each do |submission|
|
enqueue_submission_created_webhooks(@template, submissions)
|
||||||
SendSubmissionCreatedWebhookRequestJob.perform_async({ 'submission_id' => submission.id })
|
|
||||||
end
|
|
||||||
|
|
||||||
Submissions.send_signature_requests(submissions)
|
Submissions.send_signature_requests(submissions)
|
||||||
|
|
||||||
redirect_to template_path(@template), notice: I18n.t('new_recipients_have_been_added')
|
redirect_to template_path(@template), notice: I18n.t('new_recipients_have_been_added')
|
||||||
|
rescue Submissions::CreateFromSubmitters::BaseError => e
|
||||||
|
render turbo_stream: turbo_stream.replace(:submitters_error,
|
||||||
|
partial: 'submissions/error',
|
||||||
|
locals: { error: e.message }),
|
||||||
|
status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
notice =
|
notice =
|
||||||
if params[:permanently].present?
|
if params[:permanently].in?(['true', true])
|
||||||
@submission.destroy!
|
@submission.destroy!
|
||||||
|
|
||||||
I18n.t('submission_has_been_removed')
|
I18n.t('submission_has_been_removed')
|
||||||
else
|
else
|
||||||
@submission.update!(archived_at: Time.current)
|
@submission.update!(archived_at: Time.current)
|
||||||
|
|
||||||
SendSubmissionArchivedWebhookRequestJob.perform_async('submission_id' => @submission.id)
|
WebhookUrls.for_account_id(@submission.account_id, 'submission.archived').each do |webhook_url|
|
||||||
|
SendSubmissionArchivedWebhookRequestJob.perform_async('submission_id' => @submission.id,
|
||||||
|
'webhook_url_id' => webhook_url.id)
|
||||||
|
end
|
||||||
|
|
||||||
I18n.t('submission_has_been_archived')
|
I18n.t('submission_has_been_archived')
|
||||||
end
|
end
|
||||||
@@ -78,6 +93,15 @@ class SubmissionsController < ApplicationController
|
|||||||
template.save!
|
template.save!
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def enqueue_submission_created_webhooks(template, submissions)
|
||||||
|
WebhookUrls.for_account_id(template.account_id, 'submission.created').each do |webhook_url|
|
||||||
|
submissions.each do |submission|
|
||||||
|
SendSubmissionCreatedWebhookRequestJob.perform_async('submission_id' => submission.id,
|
||||||
|
'webhook_url_id' => webhook_url.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
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
|
||||||
|
|||||||
@@ -8,13 +8,17 @@ class SubmissionsDashboardController < ApplicationController
|
|||||||
|
|
||||||
@submissions = @submissions.where(archived_at: nil)
|
@submissions = @submissions.where(archived_at: nil)
|
||||||
.where(templates: { archived_at: nil })
|
.where(templates: { archived_at: nil })
|
||||||
.preload(:created_by_user, template: :author)
|
.preload(:template_accesses, :created_by_user, template: :author)
|
||||||
|
|
||||||
@submissions = Submissions.search(@submissions, params[:q], search_template: true)
|
@submissions = Submissions.search(@submissions, params[:q], search_template: true)
|
||||||
|
@submissions = Submissions::Filter.call(@submissions, current_user, params)
|
||||||
|
|
||||||
@submissions = @submissions.pending if params[:status] == 'pending'
|
@submissions = if params[:completed_at_from].present? || params[:completed_at_to].present?
|
||||||
@submissions = @submissions.completed if params[:status] == 'completed'
|
@submissions.order(Submitter.arel_table[:completed_at].maximum.desc)
|
||||||
|
else
|
||||||
|
@submissions.order(id: :desc)
|
||||||
|
end
|
||||||
|
|
||||||
@pagy, @submissions = pagy(@submissions.preload(:submitters).order(id: :desc))
|
@pagy, @submissions = pagy(@submissions.preload(submitters: :start_form_submission_events))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -8,25 +8,25 @@ class SubmissionsDownloadController < ApplicationController
|
|||||||
FILES_TTL = 5.minutes
|
FILES_TTL = 5.minutes
|
||||||
|
|
||||||
def index
|
def index
|
||||||
submitter = Submitter.find_signed(params[:sig], purpose: :download_completed) if params[:sig].present?
|
@submitter = Submitter.find_signed(params[:sig], purpose: :download_completed) if params[:sig].present?
|
||||||
|
|
||||||
signature_valid =
|
signature_valid =
|
||||||
if submitter&.slug == params[:submitter_slug]
|
if @submitter&.slug == params[:submitter_slug]
|
||||||
true
|
true
|
||||||
else
|
else
|
||||||
submitter = nil
|
@submitter = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
submitter ||= Submitter.find_by!(slug: params[:submitter_slug])
|
@submitter ||= Submitter.find_by!(slug: params[:submitter_slug])
|
||||||
|
|
||||||
Submissions::EnsureResultGenerated.call(submitter)
|
Submissions::EnsureResultGenerated.call(@submitter)
|
||||||
|
|
||||||
last_submitter = submitter.submission.submitters.where.not(completed_at: nil).order(:completed_at).last
|
last_submitter = @submitter.submission.submitters.where.not(completed_at: nil).order(:completed_at).last
|
||||||
|
|
||||||
|
return head :not_found unless last_submitter
|
||||||
|
|
||||||
Submissions::EnsureResultGenerated.call(last_submitter)
|
Submissions::EnsureResultGenerated.call(last_submitter)
|
||||||
|
|
||||||
return head :not_found unless last_submitter.completed_at?
|
|
||||||
|
|
||||||
if last_submitter.completed_at < TTL.ago && !signature_valid && !current_user_submitter?(last_submitter)
|
if last_submitter.completed_at < TTL.ago && !signature_valid && !current_user_submitter?(last_submitter)
|
||||||
Rollbar.info("TTL: #{last_submitter.id}") if defined?(Rollbar)
|
Rollbar.info("TTL: #{last_submitter.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ class SubmissionsDownloadController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
if params[:combined] == 'true'
|
if params[:combined] == 'true'
|
||||||
url = build_combined_url(submitter)
|
url = build_combined_url(@submitter)
|
||||||
|
|
||||||
if url
|
if url
|
||||||
render json: [url]
|
render json: [url]
|
||||||
@@ -53,8 +53,15 @@ class SubmissionsDownloadController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def build_urls(submitter)
|
def build_urls(submitter)
|
||||||
|
filename_format = AccountConfig.find_or_initialize_by(account_id: submitter.account_id,
|
||||||
|
key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value
|
||||||
|
|
||||||
Submitters.select_attachments_for_download(submitter).map do |attachment|
|
Submitters.select_attachments_for_download(submitter).map do |attachment|
|
||||||
ActiveStorage::Blob.proxy_url(attachment.blob, expires_at: FILES_TTL.from_now.to_i)
|
ActiveStorage::Blob.proxy_url(
|
||||||
|
attachment.blob,
|
||||||
|
expires_at: FILES_TTL.from_now.to_i,
|
||||||
|
filename: Submitters.build_document_filename(submitter, attachment.blob, filename_format)
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -65,6 +72,13 @@ class SubmissionsDownloadController < ApplicationController
|
|||||||
attachment = submitter.submission.combined_document_attachment
|
attachment = submitter.submission.combined_document_attachment
|
||||||
attachment ||= Submissions::GenerateCombinedAttachment.call(submitter)
|
attachment ||= Submissions::GenerateCombinedAttachment.call(submitter)
|
||||||
|
|
||||||
ActiveStorage::Blob.proxy_url(attachment.blob, expires_at: FILES_TTL.from_now.to_i)
|
filename_format = AccountConfig.find_or_initialize_by(account_id: submitter.account_id,
|
||||||
|
key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value
|
||||||
|
|
||||||
|
ActiveStorage::Blob.proxy_url(
|
||||||
|
attachment.blob,
|
||||||
|
expires_at: FILES_TTL.from_now.to_i,
|
||||||
|
filename: Submitters.build_document_filename(submitter, attachment.blob, filename_format)
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SubmissionsFiltersController < ApplicationController
|
||||||
|
ALLOWED_NAMES = %w[
|
||||||
|
author
|
||||||
|
completed_at
|
||||||
|
status
|
||||||
|
created_at
|
||||||
|
].freeze
|
||||||
|
|
||||||
|
skip_authorization_check
|
||||||
|
|
||||||
|
def show
|
||||||
|
return head :not_found unless ALLOWED_NAMES.include?(params[:name])
|
||||||
|
|
||||||
|
render params[:name]
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -4,6 +4,8 @@ class SubmissionsPreviewController < ApplicationController
|
|||||||
skip_before_action :authenticate_user!
|
skip_before_action :authenticate_user!
|
||||||
skip_authorization_check
|
skip_authorization_check
|
||||||
|
|
||||||
|
prepend_before_action :maybe_redirect_com, only: %i[show completed]
|
||||||
|
|
||||||
TTL = 40.minutes
|
TTL = 40.minutes
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@@ -18,7 +20,10 @@ class SubmissionsPreviewController < ApplicationController
|
|||||||
|
|
||||||
@submission ||= Submission.find_by!(slug: params[:slug])
|
@submission ||= Submission.find_by!(slug: params[:slug])
|
||||||
|
|
||||||
if !@submission.submitters.all?(&:completed_at?) && current_user.blank?
|
raise ActionController::RoutingError, I18n.t('not_found') if @submission.account.archived_at?
|
||||||
|
|
||||||
|
if !@submission.submitters.all?(&:completed_at?) && !signature_valid &&
|
||||||
|
(!current_user || !current_ability.can?(:read, @submission))
|
||||||
raise ActionController::RoutingError, I18n.t('not_found')
|
raise ActionController::RoutingError, I18n.t('not_found')
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -35,6 +40,7 @@ class SubmissionsPreviewController < ApplicationController
|
|||||||
|
|
||||||
def completed
|
def completed
|
||||||
@submission = Submission.find_by!(slug: params[:submissions_preview_slug])
|
@submission = Submission.find_by!(slug: params[:submissions_preview_slug])
|
||||||
|
@template = @submission.template
|
||||||
|
|
||||||
render :completed, layout: 'form'
|
render :completed, layout: 'form'
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SubmissionsUnarchiveController < ApplicationController
|
||||||
|
load_and_authorize_resource :submission
|
||||||
|
|
||||||
|
def create
|
||||||
|
@submission.update!(archived_at: nil)
|
||||||
|
|
||||||
|
redirect_to submission_path(@submission), notice: I18n.t('submission_has_been_unarchived')
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -7,24 +7,31 @@ class SubmitFormController < ApplicationController
|
|||||||
skip_before_action :authenticate_user!
|
skip_before_action :authenticate_user!
|
||||||
skip_authorization_check
|
skip_authorization_check
|
||||||
|
|
||||||
|
before_action :load_submitter, only: %i[show update completed]
|
||||||
|
|
||||||
CONFIG_KEYS = [].freeze
|
CONFIG_KEYS = [].freeze
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@submitter = Submitter.find_by!(slug: params[:slug])
|
submission = @submitter.submission
|
||||||
|
|
||||||
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?
|
||||||
return render :archived if @submitter.submission.template.archived_at? || @submitter.submission.archived_at?
|
return render :archived if submission.template.archived_at? ||
|
||||||
return render :expired if @submitter.submission.expired?
|
submission.archived_at? ||
|
||||||
|
@submitter.account.archived_at?
|
||||||
|
return render :expired if submission.expired?
|
||||||
return render :declined if @submitter.declined_at?
|
return render :declined if @submitter.declined_at?
|
||||||
|
|
||||||
|
@form_configs = Submitters::FormConfigs.call(@submitter, CONFIG_KEYS)
|
||||||
|
|
||||||
|
return render :awaiting if (@form_configs[:enforce_signing_order] ||
|
||||||
|
submission.template.preferences['submitters_order'] == 'preserved') &&
|
||||||
|
!Submitters.current_submitter_order?(@submitter)
|
||||||
|
|
||||||
Submitters.preload_with_pages(@submitter)
|
Submitters.preload_with_pages(@submitter)
|
||||||
|
|
||||||
Submitters::MaybeUpdateDefaultValues.call(@submitter, current_user)
|
Submitters::MaybeUpdateDefaultValues.call(@submitter, current_user)
|
||||||
|
|
||||||
@attachments_index = ActiveStorage::Attachment.where(record: @submitter.submission.submitters, name: :attachments)
|
@attachments_index = build_attachments_index(submission)
|
||||||
.preload(:blob).index_by(&:uuid)
|
|
||||||
|
|
||||||
@form_configs = Submitters::FormConfigs.call(@submitter, CONFIG_KEYS)
|
|
||||||
|
|
||||||
return unless @form_configs[:prefill_signature]
|
return unless @form_configs[:prefill_signature]
|
||||||
|
|
||||||
@@ -43,35 +50,46 @@ class SubmitFormController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
submitter = Submitter.find_by!(slug: params[:slug])
|
if @submitter.completed_at?
|
||||||
|
|
||||||
if submitter.completed_at?
|
|
||||||
return render json: { error: I18n.t('form_has_been_completed_already') }, status: :unprocessable_entity
|
return render json: { error: I18n.t('form_has_been_completed_already') }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
if submitter.template.archived_at? || submitter.submission.archived_at?
|
if @submitter.template.archived_at? || @submitter.submission.archived_at?
|
||||||
return render json: { error: I18n.t('form_has_been_archived') }, status: :unprocessable_entity
|
return render json: { error: I18n.t('form_has_been_archived') }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
if submitter.submission.expired?
|
if @submitter.submission.expired?
|
||||||
return render json: { error: I18n.t('form_has_been_expired') }, status: :unprocessable_entity
|
return render json: { error: I18n.t('form_has_been_expired') }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
if submitter.declined_at?
|
if @submitter.declined_at?
|
||||||
return render json: { error: I18n.t('form_has_been_declined') },
|
return render json: { error: I18n.t('form_has_been_declined') },
|
||||||
status: :unprocessable_entity
|
status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
Submitters::SubmitValues.call(submitter, params, request)
|
Submitters::SubmitValues.call(@submitter, params, request)
|
||||||
|
|
||||||
head :ok
|
head :ok
|
||||||
|
rescue Submitters::SubmitValues::RequiredFieldError => e
|
||||||
|
Rollbar.warning("Required field #{@submitter.id}: #{e.message}") if defined?(Rollbar)
|
||||||
|
|
||||||
|
render json: { field_uuid: e.message }, status: :unprocessable_entity
|
||||||
rescue Submitters::SubmitValues::ValidationError => e
|
rescue Submitters::SubmitValues::ValidationError => e
|
||||||
render json: { error: e.message }, status: :unprocessable_entity
|
render json: { error: e.message }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
def completed
|
def completed; end
|
||||||
@submitter = Submitter.completed.find_by!(slug: params[:submit_form_slug])
|
|
||||||
end
|
|
||||||
|
|
||||||
def success; end
|
def success; end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def load_submitter
|
||||||
|
@submitter = Submitter.find_by!(slug: params[:slug] || params[:submit_form_slug])
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_attachments_index(submission)
|
||||||
|
ActiveStorage::Attachment.where(record: submission.submitters, name: :attachments)
|
||||||
|
.preload(:blob).index_by(&:uuid)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -21,8 +21,14 @@ class SubmitFormDeclineController < ApplicationController
|
|||||||
|
|
||||||
user = submitter.submission.created_by_user || submitter.template.author
|
user = submitter.submission.created_by_user || submitter.template.author
|
||||||
|
|
||||||
SubmitterMailer.declined_email(submitter, user).deliver_later!
|
if user.user_configs.find_by(key: UserConfig::RECEIVE_DECLINED_EMAIL)&.value != false
|
||||||
SendFormDeclinedWebhookRequestJob.perform_async('submitter_id' => submitter.id)
|
SubmitterMailer.declined_email(submitter, user).deliver_later!
|
||||||
|
end
|
||||||
|
|
||||||
|
WebhookUrls.for_account_id(submitter.account_id, 'form.declined').each do |webhook_url|
|
||||||
|
SendFormDeclinedWebhookRequestJob.perform_async('submitter_id' => submitter.id,
|
||||||
|
'webhook_url_id' => webhook_url.id)
|
||||||
|
end
|
||||||
|
|
||||||
redirect_to submit_form_path(submitter.slug)
|
redirect_to submit_form_path(submitter.slug)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -7,25 +7,25 @@ class SubmitFormDownloadController < ApplicationController
|
|||||||
FILES_TTL = 5.minutes
|
FILES_TTL = 5.minutes
|
||||||
|
|
||||||
def index
|
def index
|
||||||
submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||||
|
|
||||||
return redirect_to submitter_download_index_path(submitter.slug) if submitter.completed_at?
|
return redirect_to submitter_download_index_path(@submitter.slug) if @submitter.completed_at?
|
||||||
|
|
||||||
return head :unprocessable_entity if submitter.declined_at? ||
|
return head :unprocessable_entity if @submitter.declined_at? ||
|
||||||
submitter.submission.archived_at? ||
|
@submitter.submission.archived_at? ||
|
||||||
submitter.submission.expired? ||
|
@submitter.submission.expired? ||
|
||||||
submitter.submission.template.archived_at?
|
@submitter.submission.template.archived_at?
|
||||||
|
|
||||||
last_completed_submitter = submitter.submission.submitters
|
last_completed_submitter = @submitter.submission.submitters
|
||||||
.where.not(id: submitter.id)
|
.where.not(id: @submitter.id)
|
||||||
.where.not(completed_at: nil)
|
.where.not(completed_at: nil)
|
||||||
.max_by(&:completed_at)
|
.max_by(&:completed_at)
|
||||||
|
|
||||||
attachments =
|
attachments =
|
||||||
if last_completed_submitter
|
if last_completed_submitter
|
||||||
Submitters.select_attachments_for_download(last_completed_submitter)
|
Submitters.select_attachments_for_download(last_completed_submitter)
|
||||||
else
|
else
|
||||||
submitter.submission.template.schema_documents.preload(:blob)
|
@submitter.submission.template.schema_documents.preload(:blob)
|
||||||
end
|
end
|
||||||
|
|
||||||
urls = attachments.map do |attachment|
|
urls = attachments.map do |attachment|
|
||||||
|
|||||||
@@ -9,13 +9,15 @@ class SubmitFormInviteController < ApplicationController
|
|||||||
|
|
||||||
return head :unprocessable_entity unless can_invite?(submitter)
|
return head :unprocessable_entity unless can_invite?(submitter)
|
||||||
|
|
||||||
invite_submitters = filter_invite_submitters(submitter)
|
invite_submitters = filter_invite_submitters(submitter, 'invite_by_uuid')
|
||||||
|
optional_invite_submitters = filter_invite_submitters(submitter, 'optional_invite_by_uuid')
|
||||||
|
|
||||||
ApplicationRecord.transaction do
|
ApplicationRecord.transaction do
|
||||||
invite_submitters.each do |item|
|
(invite_submitters + optional_invite_submitters).each do |item|
|
||||||
attrs = submitters_attributes.find { |e| e[:uuid] == item['uuid'] }
|
attrs = submitters_attributes.find { |e| e[:uuid] == item['uuid'] }
|
||||||
|
|
||||||
next unless attrs
|
next unless attrs
|
||||||
|
next if attrs[:email].blank?
|
||||||
|
|
||||||
submitter.submission.submitters.create!(**attrs, account_id: submitter.account_id)
|
submitter.submission.submitters.create!(**attrs, account_id: submitter.account_id)
|
||||||
|
|
||||||
@@ -46,9 +48,9 @@ class SubmitFormInviteController < ApplicationController
|
|||||||
!submitter.submission.template.archived_at?
|
!submitter.submission.template.archived_at?
|
||||||
end
|
end
|
||||||
|
|
||||||
def filter_invite_submitters(submitter)
|
def filter_invite_submitters(submitter, key = 'invite_by_uuid')
|
||||||
(submitter.submission.template_submitters || submitter.submission.template.submitters).select do |s|
|
(submitter.submission.template_submitters || submitter.submission.template.submitters).select do |s|
|
||||||
s['invite_by_uuid'] == submitter.uuid && submitter.submission.submitters.none? { |e| e.uuid == s['uuid'] }
|
s[key] == submitter.uuid && submitter.submission.submitters.none? { |e| e.uuid == s['uuid'] }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ class SubmittersAutocompleteController < ApplicationController
|
|||||||
def index
|
def index
|
||||||
submitters = search_submitters(@submitters)
|
submitters = search_submitters(@submitters)
|
||||||
|
|
||||||
values = submitters.limit(LIMIT).group(SELECT_COLUMNS.join(', ')).pluck(SELECT_COLUMNS.join(', '))
|
arel_columns = SELECT_COLUMNS.map { |col| Submitter.arel_table[col] }
|
||||||
|
values = submitters.limit(LIMIT).group(arel_columns).pluck(arel_columns)
|
||||||
|
|
||||||
attrs = values.map { |row| SELECT_COLUMNS.zip(row).to_h }
|
attrs = values.map { |row| SELECT_COLUMNS.zip(row).to_h }
|
||||||
attrs = attrs.uniq { |e| e[params[:field]] } if params[:field].present?
|
attrs = attrs.uniq { |e| e[params[:field]] } if params[:field].present?
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SubmittersController < ApplicationController
|
||||||
|
load_and_authorize_resource :submitter, only: %i[edit update]
|
||||||
|
|
||||||
|
def edit
|
||||||
|
@submitter_email_message =
|
||||||
|
if @submitter.preferences['email_message_uuid'].present?
|
||||||
|
@submitter.account.email_messages.find_by(uuid: @submitter.preferences['email_message_uuid'])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
submission = @submitter.submission
|
||||||
|
|
||||||
|
if @submitter.submission_events.exists?(event_type: 'start_form') || submission.archived_at? || submission.expired?
|
||||||
|
return redirect_back fallback_location: submission_path(submission), alert: I18n.t('submitter_cannot_be_updated')
|
||||||
|
end
|
||||||
|
|
||||||
|
if submitter_params.values.all?(&:blank?)
|
||||||
|
return redirect_back fallback_location: submission_path(submission),
|
||||||
|
alert: I18n.t('at_least_one_field_must_be_filled')
|
||||||
|
end
|
||||||
|
|
||||||
|
if params[:is_custom_message] != '1'
|
||||||
|
params.delete(:subject)
|
||||||
|
params.delete(:body)
|
||||||
|
end
|
||||||
|
|
||||||
|
submitter_preferences = Submitters.normalize_preferences(@submitter.account, current_user, params)
|
||||||
|
|
||||||
|
if submitter_preferences.key?('email_message_uuid')
|
||||||
|
@submitter.preferences['email_message_uuid'] = submitter_preferences['email_message_uuid']
|
||||||
|
end
|
||||||
|
|
||||||
|
assign_submitter_attrs(@submitter, submitter_params)
|
||||||
|
|
||||||
|
if @submitter.save
|
||||||
|
maybe_resend_email_sms(@submitter, params)
|
||||||
|
|
||||||
|
redirect_back fallback_location: submission_path(submission), notice: I18n.t('changes_have_been_saved')
|
||||||
|
else
|
||||||
|
redirect_back fallback_location: submission_path(submission), alert: I18n.t('unable_to_save')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def maybe_resend_email_sms(submitter, params)
|
||||||
|
if params[:send_email] == '1' && submitter.email.present?
|
||||||
|
is_sent_recently = Docuseal.multitenant? &&
|
||||||
|
EmailEvent.exists?(email: submitter.email,
|
||||||
|
tag: 'submitter_invitation',
|
||||||
|
emailable: submitter,
|
||||||
|
event_type: 'send',
|
||||||
|
created_at: 4.hours.ago..Time.current)
|
||||||
|
|
||||||
|
SendSubmitterInvitationEmailJob.perform_async('submitter_id' => submitter.id) unless is_sent_recently
|
||||||
|
end
|
||||||
|
|
||||||
|
return if submitter.phone.blank?
|
||||||
|
return unless params[:send_sms] == '1'
|
||||||
|
|
||||||
|
SendSubmitterInvitationSmsJob.perform_async('submitter_id' => submitter.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def assign_submitter_attrs(submitter, attrs)
|
||||||
|
submitter.phone = attrs[:phone].to_s.gsub(/[^0-9+]/, '') if attrs.key?(:phone)
|
||||||
|
|
||||||
|
submitter.email = Submissions.normalize_email(attrs[:email]) if attrs.key?(:email)
|
||||||
|
|
||||||
|
submitter.name = attrs[:name] if attrs.key?(:name)
|
||||||
|
|
||||||
|
submitter
|
||||||
|
end
|
||||||
|
|
||||||
|
def submitter_params
|
||||||
|
params.require(:submitter).permit(:email, :name, :phone).transform_values(&:strip)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SubmittersResubmitController < ApplicationController
|
||||||
|
load_and_authorize_resource :submitter, parent: false
|
||||||
|
|
||||||
|
def update
|
||||||
|
return redirect_to submit_form_path(slug: @submitter.slug) if @submitter.email != current_user.email
|
||||||
|
|
||||||
|
submission = @submitter.template.submissions.new(created_by_user: current_user,
|
||||||
|
submitters_order: :preserved,
|
||||||
|
**@submitter.submission.slice(:template_fields,
|
||||||
|
:account_id,
|
||||||
|
:template_schema,
|
||||||
|
:template_submitters,
|
||||||
|
:preferences))
|
||||||
|
|
||||||
|
@submitter.submission.submitters.each do |submitter|
|
||||||
|
new_submitter = submission.submitters.new(submitter.slice(:uuid, :email, :phone, :name,
|
||||||
|
:preferences, :metadata, :account_id))
|
||||||
|
|
||||||
|
next unless submitter.uuid == @submitter.uuid
|
||||||
|
|
||||||
|
assign_submitter_values(new_submitter, submitter)
|
||||||
|
|
||||||
|
@new_submitter ||= new_submitter
|
||||||
|
end
|
||||||
|
|
||||||
|
submission.save!
|
||||||
|
|
||||||
|
redirect_to submit_form_path(slug: @new_submitter.slug)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def assign_submitter_values(new_submitter, submitter)
|
||||||
|
attachments_index = submitter.attachments.index_by(&:uuid)
|
||||||
|
|
||||||
|
submitter.submission.template_fields.each do |field|
|
||||||
|
next if field['submitter_uuid'] != submitter.uuid
|
||||||
|
next if field['default_value'] == '{{date}}'
|
||||||
|
next if field['type'] == 'stamp'
|
||||||
|
next if field['type'] == 'signature'
|
||||||
|
next if field.dig('preferences', 'formula').present?
|
||||||
|
|
||||||
|
value = submitter.values[field['uuid']]
|
||||||
|
|
||||||
|
next if value.blank?
|
||||||
|
|
||||||
|
if field['type'].in?(%w[image file initials])
|
||||||
|
Array.wrap(value).each do |attachment_uuid|
|
||||||
|
new_submitter.attachments << attachments_index[attachment_uuid].dup
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
new_submitter.values[field['uuid']] = value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -6,7 +6,7 @@ class SubmittersSendEmailController < ApplicationController
|
|||||||
def create
|
def create
|
||||||
if Docuseal.multitenant? && SubmissionEvent.exists?(submitter: @submitter,
|
if Docuseal.multitenant? && SubmissionEvent.exists?(submitter: @submitter,
|
||||||
event_type: 'send_email',
|
event_type: 'send_email',
|
||||||
created_at: 24.hours.ago..Time.current)
|
created_at: 10.hours.ago..Time.current)
|
||||||
Rollbar.warning("Already sent: #{@submitter.id}") if defined?(Rollbar)
|
Rollbar.warning("Already sent: #{@submitter.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
return redirect_back(fallback_location: submission_path(@submitter.submission),
|
return redirect_back(fallback_location: submission_path(@submitter.submission),
|
||||||
|
|||||||
@@ -4,10 +4,12 @@ class TemplateFoldersController < ApplicationController
|
|||||||
load_and_authorize_resource :template_folder
|
load_and_authorize_resource :template_folder
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@templates = @template_folder.templates.active.preload(:author).order(id: :desc)
|
@templates = @template_folder.templates.active.accessible_by(current_ability)
|
||||||
|
.preload(:author, :template_accesses)
|
||||||
@templates = Templates.search(@templates, params[:q])
|
@templates = Templates.search(@templates, params[:q])
|
||||||
|
@templates = Templates::Order.call(@templates, current_user, cookies.permanent[:dashboard_templates_order])
|
||||||
|
|
||||||
@pagy, @templates = pagy(@templates, items: 12)
|
@pagy, @templates = pagy(@templates, limit: 12)
|
||||||
end
|
end
|
||||||
|
|
||||||
def edit; end
|
def edit; end
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ 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(archived_at: nil).preload(:author, :folder).order(id: :desc)
|
@templates = @templates.where.not(archived_at: nil).preload(:author, :folder, :template_accesses).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, limit: 12)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -7,8 +7,15 @@ class TemplatesArchivedSubmissionsController < ApplicationController
|
|||||||
def index
|
def index
|
||||||
@submissions = @submissions.where.not(archived_at: nil)
|
@submissions = @submissions.where.not(archived_at: nil)
|
||||||
@submissions = Submissions.search(@submissions, params[:q], search_values: true)
|
@submissions = Submissions.search(@submissions, params[:q], search_values: true)
|
||||||
|
@submissions = Submissions::Filter.call(@submissions, current_user, params)
|
||||||
|
|
||||||
@pagy, @submissions = pagy(@submissions.preload(:submitters).order(id: :desc))
|
@submissions = if params[:completed_at_from].present? || params[:completed_at_to].present?
|
||||||
|
@submissions.order(Submitter.arel_table[:completed_at].maximum.desc)
|
||||||
|
else
|
||||||
|
@submissions.order(id: :desc)
|
||||||
|
end
|
||||||
|
|
||||||
|
@pagy, @submissions = pagy(@submissions.preload(submitters: :start_form_submission_events))
|
||||||
rescue ActiveRecord::RecordNotFound
|
rescue ActiveRecord::RecordNotFound
|
||||||
redirect_to root_path
|
redirect_to root_path
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class TemplatesCloneAndReplaceController < ApplicationController
|
||||||
|
load_and_authorize_resource :template
|
||||||
|
|
||||||
|
def create
|
||||||
|
return head :unprocessable_entity if params[:files].blank?
|
||||||
|
|
||||||
|
ActiveRecord::Associations::Preloader.new(
|
||||||
|
records: [@template],
|
||||||
|
associations: [schema_documents: :preview_images_attachments]
|
||||||
|
).call
|
||||||
|
|
||||||
|
cloned_template = Templates::Clone.call(@template, author: current_user)
|
||||||
|
cloned_template.name = File.basename(params[:files].first.original_filename, '.*')
|
||||||
|
cloned_template.save!
|
||||||
|
|
||||||
|
documents = Templates::ReplaceAttachments.call(cloned_template, params, extract_fields: true)
|
||||||
|
|
||||||
|
cloned_template.save!
|
||||||
|
|
||||||
|
Templates::CloneAttachments.call(template: cloned_template, original_template: @template,
|
||||||
|
excluded_attachment_uuids: documents.map(&:uuid))
|
||||||
|
|
||||||
|
respond_to do |f|
|
||||||
|
f.html { redirect_to edit_template_path(cloned_template) }
|
||||||
|
f.json { render json: { id: cloned_template.id } }
|
||||||
|
end
|
||||||
|
rescue Templates::CreateAttachments::PdfEncrypted
|
||||||
|
respond_to do |f|
|
||||||
|
f.html { render turbo_stream: turbo_stream.append(params[:form_id], html: helpers.tag.prompt_password) }
|
||||||
|
f.json { render json: { error: 'PDF encrypted', status: 'pdf_encrypted' }, status: :unprocessable_entity }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -9,13 +9,19 @@ class TemplatesController < ApplicationController
|
|||||||
submissions = @template.submissions.accessible_by(current_ability)
|
submissions = @template.submissions.accessible_by(current_ability)
|
||||||
submissions = submissions.active if @template.archived_at.blank?
|
submissions = submissions.active if @template.archived_at.blank?
|
||||||
submissions = Submissions.search(submissions, params[:q], search_values: true)
|
submissions = Submissions.search(submissions, params[:q], search_values: true)
|
||||||
|
submissions = Submissions::Filter.call(submissions, current_user, params.except(:status))
|
||||||
|
|
||||||
@base_submissions = submissions
|
@base_submissions = submissions
|
||||||
|
|
||||||
submissions = submissions.pending if params[:status] == 'pending'
|
submissions = Submissions::Filter.filter_by_status(submissions, params)
|
||||||
submissions = submissions.completed if params[:status] == 'completed'
|
|
||||||
|
|
||||||
@pagy, @submissions = pagy(submissions.preload(:submitters).order(id: :desc))
|
submissions = if params[:completed_at_from].present? || params[:completed_at_to].present?
|
||||||
|
submissions.order(Submitter.arel_table[:completed_at].maximum.desc)
|
||||||
|
else
|
||||||
|
submissions.order(id: :desc)
|
||||||
|
end
|
||||||
|
|
||||||
|
@pagy, @submissions = pagy(submissions.preload(:template_accesses, submitters: :start_form_submission_events))
|
||||||
rescue ActiveRecord::RecordNotFound
|
rescue ActiveRecord::RecordNotFound
|
||||||
redirect_to root_path
|
redirect_to root_path
|
||||||
end
|
end
|
||||||
@@ -66,7 +72,7 @@ class TemplatesController < ApplicationController
|
|||||||
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
|
||||||
|
|
||||||
SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => @template.id)
|
enqueue_template_created_webhooks(@template)
|
||||||
|
|
||||||
maybe_redirect_to_template(@template)
|
maybe_redirect_to_template(@template)
|
||||||
else
|
else
|
||||||
@@ -77,14 +83,14 @@ class TemplatesController < ApplicationController
|
|||||||
def update
|
def update
|
||||||
@template.update!(template_params)
|
@template.update!(template_params)
|
||||||
|
|
||||||
SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => @template.id)
|
enqueue_template_updated_webhooks(@template)
|
||||||
|
|
||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
notice =
|
notice =
|
||||||
if params[:permanently].present?
|
if params[:permanently].in?(['true', true])
|
||||||
@template.destroy!
|
@template.destroy!
|
||||||
|
|
||||||
I18n.t('template_has_been_removed')
|
I18n.t('template_has_been_removed')
|
||||||
@@ -102,13 +108,13 @@ class TemplatesController < ApplicationController
|
|||||||
def template_params
|
def template_params
|
||||||
params.require(:template).permit(
|
params.require(:template).permit(
|
||||||
:name,
|
:name,
|
||||||
{ schema: [%i[attachment_uuid name]],
|
{ schema: [[:attachment_uuid, :name, { conditions: [%i[field_uuid value action operation]] }]],
|
||||||
submitters: [%i[name uuid is_requester linked_to_uuid invite_by_uuid email]],
|
submitters: [%i[name uuid is_requester linked_to_uuid invite_by_uuid optional_invite_by_uuid email]],
|
||||||
fields: [[:uuid, :submitter_uuid, :name, :type,
|
fields: [[:uuid, :submitter_uuid, :name, :type,
|
||||||
:required, :readonly, :default_value,
|
:required, :readonly, :default_value,
|
||||||
:title, :description,
|
:title, :description,
|
||||||
{ preferences: {},
|
{ preferences: {},
|
||||||
conditions: [%i[field_uuid value action]],
|
conditions: [%i[field_uuid value action operation]],
|
||||||
options: [%i[value uuid]],
|
options: [%i[value uuid]],
|
||||||
validation: %i[message pattern],
|
validation: %i[message pattern],
|
||||||
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]] }
|
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]] }
|
||||||
@@ -116,7 +122,8 @@ class TemplatesController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def authorized_clone_account_id?(account_id)
|
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)
|
true_user.account_id.to_s == account_id.to_s ||
|
||||||
|
true_user.account.linked_accounts.accessible_by(current_ability).exists?(id: account_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def maybe_redirect_to_template(template)
|
def maybe_redirect_to_template(template)
|
||||||
@@ -127,6 +134,20 @@ class TemplatesController < ApplicationController
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def enqueue_template_created_webhooks(template)
|
||||||
|
WebhookUrls.for_account_id(template.account_id, 'template.created').each do |webhook_url|
|
||||||
|
SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => template.id,
|
||||||
|
'webhook_url_id' => webhook_url.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def enqueue_template_updated_webhooks(template)
|
||||||
|
WebhookUrls.for_account_id(template.account_id, 'template.updated').each do |webhook_url|
|
||||||
|
SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => template.id,
|
||||||
|
'webhook_url_id' => webhook_url.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def load_base_template
|
def load_base_template
|
||||||
return if params[:base_template_id].blank?
|
return if params[:base_template_id].blank?
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,11 @@ class TemplatesDashboardController < ApplicationController
|
|||||||
FOLDERS_PER_PAGE = 18
|
FOLDERS_PER_PAGE = 18
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@template_folders = filter_template_folders(@template_folders)
|
@template_folders = @template_folders.where(id: @templates.active.select(:folder_id))
|
||||||
|
|
||||||
|
@template_folders = TemplateFolders.search(@template_folders, params[:q])
|
||||||
|
@template_folders = sort_template_folders(@template_folders, current_user,
|
||||||
|
cookies.permanent[:dashboard_templates_order])
|
||||||
|
|
||||||
@pagy, @template_folders = pagy(
|
@pagy, @template_folders = pagy(
|
||||||
@template_folders,
|
@template_folders,
|
||||||
@@ -22,42 +26,71 @@ class TemplatesDashboardController < ApplicationController
|
|||||||
else
|
else
|
||||||
@template_folders = @template_folders.reject { |e| e.name == TemplateFolder::DEFAULT_NAME }
|
@template_folders = @template_folders.reject { |e| e.name == TemplateFolder::DEFAULT_NAME }
|
||||||
@templates = filter_templates(@templates)
|
@templates = filter_templates(@templates)
|
||||||
|
@templates = Templates::Order.call(@templates, current_user, cookies.permanent[:dashboard_templates_order])
|
||||||
|
|
||||||
items =
|
limit =
|
||||||
if @template_folders.size < 4
|
if @template_folders.size < 4
|
||||||
TEMPLATES_PER_PAGE
|
TEMPLATES_PER_PAGE
|
||||||
else
|
else
|
||||||
(@template_folders.size < 7 ? 9 : 6)
|
(@template_folders.size < 7 ? 9 : 6)
|
||||||
end
|
end
|
||||||
|
|
||||||
@pagy, @templates = pagy(@templates, items:)
|
@pagy, @templates = pagy(@templates, limit:)
|
||||||
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)
|
def filter_templates(templates)
|
||||||
rel = templates.active.preload(:author).order(id: :desc)
|
rel = templates.active.preload(:author, :template_accesses)
|
||||||
|
|
||||||
if params[:q].blank?
|
if params[:q].blank?
|
||||||
if Docuseal.multitenant? && !current_account.testing?
|
if Docuseal.multitenant? ? current_account.testing? : current_account.linked_account_account
|
||||||
rel = rel.where(folder_id: current_account.default_template_folder.id)
|
shared_account_ids = [current_user.account_id]
|
||||||
else
|
shared_account_ids << TemplateSharing::ALL_ID if !Docuseal.multitenant? && !current_account.testing?
|
||||||
shared_template_ids =
|
|
||||||
TemplateSharing.where(account_id: [current_account.id, TemplateSharing::ALL_ID]).select(:template_id)
|
shared_template_ids = TemplateSharing.where(account_id: shared_account_ids).select(:template_id)
|
||||||
|
|
||||||
rel = rel.where(folder_id: current_account.default_template_folder.id).or(rel.where(id: shared_template_ids))
|
rel = rel.where(folder_id: current_account.default_template_folder.id).or(rel.where(id: shared_template_ids))
|
||||||
|
else
|
||||||
|
rel = rel.where(folder_id: current_account.default_template_folder.id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
Templates.search(rel, params[:q])
|
Templates.search(rel, params[:q])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def sort_template_folders(template_folders, current_user, order)
|
||||||
|
case order
|
||||||
|
when 'used_at'
|
||||||
|
subquery =
|
||||||
|
Template.left_joins(:submissions)
|
||||||
|
.group(:folder_id)
|
||||||
|
.where(account_id: current_user.account_id)
|
||||||
|
.select(
|
||||||
|
:folder_id,
|
||||||
|
Template.arel_table[:updated_at].maximum.as('updated_at_max'),
|
||||||
|
Submission.arel_table[:created_at].maximum.as('submission_created_at_max')
|
||||||
|
)
|
||||||
|
|
||||||
|
template_folders = template_folders.joins(
|
||||||
|
Template.arel_table
|
||||||
|
.join(subquery.arel.as('templates'), Arel::Nodes::OuterJoin)
|
||||||
|
.on(TemplateFolder.arel_table[:id].eq(Template.arel_table[:folder_id]))
|
||||||
|
.join_sources
|
||||||
|
)
|
||||||
|
|
||||||
|
template_folders.order(
|
||||||
|
Arel::Nodes::Case.new
|
||||||
|
.when(Template.arel_table[:submission_created_at_max].gt(Template.arel_table[:updated_at_max]))
|
||||||
|
.then(Template.arel_table[:submission_created_at_max])
|
||||||
|
.else(Template.arel_table[:updated_at_max])
|
||||||
|
.desc
|
||||||
|
)
|
||||||
|
when 'name'
|
||||||
|
template_folders.order(name: :asc)
|
||||||
|
else
|
||||||
|
template_folders.order(id: :desc)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -6,9 +6,10 @@ class TemplatesDebugController < ApplicationController
|
|||||||
def show
|
def show
|
||||||
attachment = @template.documents.first
|
attachment = @template.documents.first
|
||||||
|
|
||||||
pdf = HexaPDF::Document.new(io: StringIO.new(attachment.download))
|
data = attachment.download
|
||||||
|
pdf = HexaPDF::Document.new(io: StringIO.new(data))
|
||||||
|
|
||||||
fields = Templates::FindAcroFields.call(pdf, attachment)
|
fields = Templates::FindAcroFields.call(pdf, attachment, data)
|
||||||
|
|
||||||
attachment.metadata['pdf'] ||= {}
|
attachment.metadata['pdf'] ||= {}
|
||||||
attachment.metadata['pdf']['fields'] = fields
|
attachment.metadata['pdf']['fields'] = fields
|
||||||
|
|||||||
@@ -22,12 +22,24 @@ class TemplatesPreferencesController < ApplicationController
|
|||||||
preferences: %i[bcc_completed request_email_subject request_email_body
|
preferences: %i[bcc_completed request_email_subject request_email_body
|
||||||
documents_copy_email_subject documents_copy_email_body
|
documents_copy_email_subject documents_copy_email_body
|
||||||
documents_copy_email_enabled documents_copy_email_attach_audit
|
documents_copy_email_enabled documents_copy_email_attach_audit
|
||||||
|
documents_copy_email_attach_documents documents_copy_email_reply_to
|
||||||
completed_notification_email_attach_documents
|
completed_notification_email_attach_documents
|
||||||
completed_redirect_url
|
completed_redirect_url validate_unique_submitters
|
||||||
|
submitters_order require_phone_2fa
|
||||||
|
default_expire_at_duration
|
||||||
|
default_expire_at
|
||||||
completed_notification_email_subject completed_notification_email_body
|
completed_notification_email_subject completed_notification_email_body
|
||||||
completed_notification_email_enabled completed_notification_email_attach_audit] +
|
completed_notification_email_enabled completed_notification_email_attach_audit] +
|
||||||
[completed_message: %i[title body]]
|
[completed_message: %i[title body],
|
||||||
|
submitters: [%i[uuid request_email_subject request_email_body]]]
|
||||||
).tap do |attrs|
|
).tap do |attrs|
|
||||||
|
attrs[:preferences].delete(:submitters) if params[:request_email_per_submitter] != '1'
|
||||||
|
|
||||||
|
if (default_expire_at = attrs.dig(:preferences, :default_expire_at).presence)
|
||||||
|
attrs[:preferences][:default_expire_at] =
|
||||||
|
(ActiveSupport::TimeZone[current_account.timezone] || Time.zone).parse(default_expire_at).utc
|
||||||
|
end
|
||||||
|
|
||||||
attrs[:preferences] = attrs[:preferences].transform_values do |value|
|
attrs[:preferences] = attrs[:preferences].transform_values do |value|
|
||||||
if %w[true false].include?(value)
|
if %w[true false].include?(value)
|
||||||
value == 'true'
|
value == 'true'
|
||||||
|
|||||||
@@ -17,38 +17,47 @@ class TemplatesRecipientsController < ApplicationController
|
|||||||
private
|
private
|
||||||
|
|
||||||
def submitters_params
|
def submitters_params
|
||||||
params.require(:template).permit(
|
permit_params = { submitters: [%i[name uuid is_requester optional_invite_by_uuid
|
||||||
submitters: [%i[name uuid is_requester invite_by_uuid linked_to_uuid email option]]
|
invite_by_uuid linked_to_uuid email option]] }
|
||||||
).fetch(:submitters, {}).values.filter_map do |s|
|
|
||||||
|
params.require(:template).permit(permit_params).fetch(:submitters, {}).values.filter_map do |s|
|
||||||
next if s[:uuid].blank?
|
next if s[:uuid].blank?
|
||||||
|
|
||||||
if s[:is_requester] == '1' && s[:invite_by_uuid].blank?
|
if s[:is_requester] == '1' && s[:invite_by_uuid].blank? && s[:optional_invite_by_uuid].blank?
|
||||||
s[:is_requester] = true
|
s[:is_requester] = true
|
||||||
else
|
else
|
||||||
s.delete(:is_requester)
|
s.delete(:is_requester)
|
||||||
end
|
end
|
||||||
|
|
||||||
s.delete(:invite_by_uuid) if s[:invite_by_uuid].blank?
|
s.delete(:invite_by_uuid) if s[:invite_by_uuid].blank?
|
||||||
|
s.delete(:optional_invite_by_uuid) if s[:optional_invite_by_uuid].blank?
|
||||||
|
|
||||||
option = s.delete(:option)
|
normalize_option_value(s)
|
||||||
|
|
||||||
if option.present?
|
|
||||||
case option
|
|
||||||
when 'is_requester'
|
|
||||||
s[:is_requester] = true
|
|
||||||
when 'not_set'
|
|
||||||
s.delete(:is_requester)
|
|
||||||
s.delete(:email)
|
|
||||||
s.delete(:linked_to_uuid)
|
|
||||||
s.delete(:invite_by_uuid)
|
|
||||||
when /\Alinked_to_(.*)\z/
|
|
||||||
s[:linked_to_uuid] = ::Regexp.last_match(-1)
|
|
||||||
when /\Ainvite_by_(.*)\z/
|
|
||||||
s[:invite_by_uuid] = ::Regexp.last_match(-1)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
s
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def normalize_option_value(attrs)
|
||||||
|
option = attrs.delete(:option)
|
||||||
|
|
||||||
|
if option.present?
|
||||||
|
case option
|
||||||
|
when 'is_requester'
|
||||||
|
attrs[:is_requester] = true
|
||||||
|
when 'not_set'
|
||||||
|
attrs.delete(:is_requester)
|
||||||
|
attrs.delete(:email)
|
||||||
|
attrs.delete(:linked_to_uuid)
|
||||||
|
attrs.delete(:invite_by_uuid)
|
||||||
|
attrs.delete(:optional_invite_by_uuid)
|
||||||
|
when /\Alinked_to_(.*)\z/
|
||||||
|
attrs[:linked_to_uuid] = ::Regexp.last_match(-1)
|
||||||
|
when /\Aoptional_invite_by_(.*)\z/
|
||||||
|
attrs[:optional_invite_by_uuid] = ::Regexp.last_match(-1)
|
||||||
|
when /\Ainvite_by_(.*)\z/
|
||||||
|
attrs[:invite_by_uuid] = ::Regexp.last_match(-1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
attrs
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -23,7 +23,7 @@ class TemplatesUploadsController < ApplicationController
|
|||||||
|
|
||||||
@template.update!(schema:)
|
@template.update!(schema:)
|
||||||
|
|
||||||
SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => @template.id)
|
enqueue_template_created_webhooks(@template)
|
||||||
|
|
||||||
redirect_to edit_template_path(@template)
|
redirect_to edit_template_path(@template)
|
||||||
rescue Templates::CreateAttachments::PdfEncrypted
|
rescue Templates::CreateAttachments::PdfEncrypted
|
||||||
@@ -55,14 +55,22 @@ class TemplatesUploadsController < ApplicationController
|
|||||||
tempfile.write(DownloadUtils.call(params[:url]).body)
|
tempfile.write(DownloadUtils.call(params[:url]).body)
|
||||||
tempfile.rewind
|
tempfile.rewind
|
||||||
|
|
||||||
|
filename = URI.decode_www_form_component(params[:filename]) if params[:filename].present?
|
||||||
|
filename ||= File.basename(URI.decode_www_form_component(params[:url]))
|
||||||
|
|
||||||
file = ActionDispatch::Http::UploadedFile.new(
|
file = ActionDispatch::Http::UploadedFile.new(
|
||||||
tempfile:,
|
tempfile:,
|
||||||
filename: File.basename(
|
filename:,
|
||||||
URI.decode_www_form_component(params[:filename].presence || params[:url]), '.*'
|
|
||||||
),
|
|
||||||
type: Marcel::MimeType.for(tempfile)
|
type: Marcel::MimeType.for(tempfile)
|
||||||
)
|
)
|
||||||
|
|
||||||
{ files: [file] }
|
{ files: [file] }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def enqueue_template_created_webhooks(template)
|
||||||
|
WebhookUrls.for_account_id(template.account_id, 'template.created').each do |webhook_url|
|
||||||
|
SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => template.id,
|
||||||
|
'webhook_url_id' => webhook_url.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -4,9 +4,8 @@ class TestingApiSettingsController < ApplicationController
|
|||||||
def index
|
def index
|
||||||
authorize!(:manage, current_user.access_token)
|
authorize!(:manage, current_user.access_token)
|
||||||
|
|
||||||
@webhook_config =
|
@webhook_url = current_account.webhook_urls.first_or_initialize
|
||||||
current_account.encrypted_configs.find_or_initialize_by(key: EncryptedConfig::WEBHOOK_URL_KEY)
|
|
||||||
|
|
||||||
authorize!(:manage, @webhook_config)
|
authorize!(:manage, @webhook_url)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -5,7 +5,8 @@ class UserConfigsController < ApplicationController
|
|||||||
authorize_resource :user_config
|
authorize_resource :user_config
|
||||||
|
|
||||||
ALLOWED_KEYS = [
|
ALLOWED_KEYS = [
|
||||||
UserConfig::RECEIVE_COMPLETED_EMAIL
|
UserConfig::RECEIVE_COMPLETED_EMAIL,
|
||||||
|
UserConfig::SHOW_APP_TOUR
|
||||||
].freeze
|
].freeze
|
||||||
|
|
||||||
InvalidKey = Class.new(StandardError)
|
InvalidKey = Class.new(StandardError)
|
||||||
@@ -26,8 +27,9 @@ class UserConfigsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def user_config_params
|
def user_config_params
|
||||||
params.required(:user_config).permit!.tap do |attrs|
|
params.required(:user_config).permit(:key, :value, { value: {} }, { value: [] }).tap do |attrs|
|
||||||
attrs[:value] = attrs[:value] == '1' if attrs[:value].in?(%w[1 0])
|
attrs[:value] = attrs[:value] == '1' if attrs[:value].in?(%w[1 0])
|
||||||
|
attrs[:value] = attrs[:value] == 'true' if attrs[:value].in?(%w[true false])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -9,7 +9,7 @@ class UserSignaturesController < ApplicationController
|
|||||||
def update
|
def update
|
||||||
file = params[:file]
|
file = params[:file]
|
||||||
|
|
||||||
return redirect_to settings_profile_index_path, notice: I18n.t('Unable to save signature') if file.blank?
|
return redirect_to settings_profile_index_path, notice: I18n.t('unable_to_save_signature') if file.blank?
|
||||||
|
|
||||||
blob = ActiveStorage::Blob.create_and_upload!(io: file.open,
|
blob = ActiveStorage::Blob.create_and_upload!(io: file.open,
|
||||||
filename: file.original_filename,
|
filename: file.original_filename,
|
||||||
@@ -24,7 +24,7 @@ class UserSignaturesController < ApplicationController
|
|||||||
if @user_config.update(value: attachment.uuid)
|
if @user_config.update(value: attachment.uuid)
|
||||||
redirect_to settings_profile_index_path, notice: I18n.t('signature_has_been_saved')
|
redirect_to settings_profile_index_path, notice: I18n.t('signature_has_been_saved')
|
||||||
else
|
else
|
||||||
redirect_to settings_profile_index_path, notice: I18n.t('Unable to save signature')
|
redirect_to settings_profile_index_path, notice: I18n.t('unable_to_save_signature')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -9,12 +9,14 @@ class UsersController < ApplicationController
|
|||||||
def index
|
def index
|
||||||
@users =
|
@users =
|
||||||
if params[:status] == 'archived'
|
if params[:status] == 'archived'
|
||||||
@users.archived
|
@users.archived.where.not(role: 'integration')
|
||||||
|
elsif params[:status] == 'integration'
|
||||||
|
@users.active.where(role: 'integration')
|
||||||
else
|
else
|
||||||
@users.active
|
@users.active.where.not(role: 'integration')
|
||||||
end
|
end
|
||||||
|
|
||||||
@pagy, @users = pagy(@users.where(account: current_account).order(id: :desc))
|
@pagy, @users = pagy(@users.preload(account: :account_accesses).where(account: current_account).order(id: :desc))
|
||||||
end
|
end
|
||||||
|
|
||||||
def new; end
|
def new; end
|
||||||
@@ -22,16 +24,14 @@ 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 User.accessible_by(current_ability).exists?(email: @user.email)
|
||||||
|
@user.errors.add(:email, I18n.t('already_exists'))
|
||||||
|
|
||||||
if existing_user
|
return render turbo_stream: turbo_stream.replace(:modal, template: 'users/new'), status: :unprocessable_entity
|
||||||
existing_user.archived_at = nil
|
|
||||||
existing_user.assign_attributes(user_params)
|
|
||||||
existing_user.account = current_account
|
|
||||||
|
|
||||||
@user = existing_user
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@user.role = User::ADMIN_ROLE unless role_valid?(@user.role)
|
||||||
|
|
||||||
if @user.save
|
if @user.save
|
||||||
UserMailer.invitation_email(@user).deliver_later!
|
UserMailer.invitation_email(@user).deliver_later!
|
||||||
|
|
||||||
@@ -45,17 +45,16 @@ class UsersController < ApplicationController
|
|||||||
return redirect_to settings_users_path, notice: I18n.t('unable_to_update_user') if Docuseal.demo?
|
return redirect_to settings_users_path, notice: I18n.t('unable_to_update_user') if Docuseal.demo?
|
||||||
|
|
||||||
attrs = user_params.compact_blank.merge(user_params.slice(:archived_at))
|
attrs = user_params.compact_blank.merge(user_params.slice(:archived_at))
|
||||||
attrs.delete(:role) if !role_valid?(attrs[:role]) || current_user == @user
|
|
||||||
|
|
||||||
if params.dig(:user, :account_id).present?
|
if params.dig(:user, :account_id).present?
|
||||||
account = Account.accessible_by(current_ability).find(params[:user][:account_id])
|
account = Account.accessible_by(current_ability).find(params.dig(:user, :account_id))
|
||||||
|
|
||||||
authorize!(:manage, account)
|
authorize!(:manage, account)
|
||||||
|
|
||||||
@user.account = account
|
@user.account = account
|
||||||
end
|
end
|
||||||
|
|
||||||
if @user.update(attrs)
|
if @user.update(attrs.except(current_user == @user ? :role : nil))
|
||||||
redirect_back fallback_location: settings_users_path, notice: I18n.t('user_has_been_updated')
|
redirect_back fallback_location: settings_users_path, notice: I18n.t('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
|
||||||
@@ -79,20 +78,16 @@ class UsersController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def build_user
|
def build_user
|
||||||
@user = current_account.users.find_by(email: user_params[:email])&.tap do |user|
|
@user = current_account.users.new(user_params)
|
||||||
user.assign_attributes(user_params)
|
|
||||||
user.archived_at = nil
|
|
||||||
end
|
|
||||||
|
|
||||||
@user ||= current_account.users.new(user_params)
|
|
||||||
|
|
||||||
@user
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def user_params
|
def user_params
|
||||||
if params.key?(:user)
|
if params.key?(:user)
|
||||||
params.require(:user).permit(:email, :first_name, :last_name, :password,
|
permitted_params = %i[email first_name last_name password archived_at]
|
||||||
:role, :archived_at, :account_id)
|
|
||||||
|
permitted_params << :role if role_valid?(params.dig(:user, :role))
|
||||||
|
|
||||||
|
params.require(:user).permit(permitted_params)
|
||||||
else
|
else
|
||||||
{}
|
{}
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,37 +1,22 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class WebhookPreferencesController < ApplicationController
|
class WebhookPreferencesController < ApplicationController
|
||||||
EVENTS = %w[
|
load_and_authorize_resource :webhook_url, parent: false
|
||||||
form.viewed
|
|
||||||
form.started
|
|
||||||
form.completed
|
|
||||||
form.declined
|
|
||||||
template.created
|
|
||||||
template.updated
|
|
||||||
submission.created
|
|
||||||
submission.archived
|
|
||||||
].freeze
|
|
||||||
|
|
||||||
before_action :load_account_config
|
def update
|
||||||
authorize_resource :account_config, parent: false
|
webhook_preferences_params[:events].each do |event, val|
|
||||||
|
@webhook_url.events.delete(event) if val == '0'
|
||||||
|
@webhook_url.events.push(event) if val == '1' && @webhook_url.events.exclude?(event)
|
||||||
|
end
|
||||||
|
|
||||||
def create
|
@webhook_url.save!
|
||||||
@account_config.value[account_config_params[:event]] = account_config_params[:value] == '1'
|
|
||||||
|
|
||||||
@account_config.save!
|
|
||||||
|
|
||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def load_account_config
|
def webhook_preferences_params
|
||||||
@account_config =
|
params.require(:webhook_url).permit(events: {})
|
||||||
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
|
||||||
end
|
end
|
||||||
@@ -1,29 +1,22 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class WebhookSecretController < ApplicationController
|
class WebhookSecretController < ApplicationController
|
||||||
before_action :load_encrypted_config
|
load_and_authorize_resource :webhook_url, parent: false
|
||||||
authorize_resource :encrypted_config, parent: false
|
|
||||||
|
|
||||||
def index; end
|
def show; end
|
||||||
|
|
||||||
def create
|
def update
|
||||||
@encrypted_config.assign_attributes(value: {
|
@webhook_url.update!(secret: {
|
||||||
encrypted_config_params[:key] => encrypted_config_params[:value]
|
webhook_secret_params[:key] => webhook_secret_params[:value]
|
||||||
}.compact_blank)
|
}.compact_blank)
|
||||||
|
|
||||||
@encrypted_config.value.present? ? @encrypted_config.save! : @encrypted_config.delete
|
redirect_back(fallback_location: settings_webhook_path(@webhook_url),
|
||||||
|
notice: I18n.t('webhook_secret_has_been_saved'))
|
||||||
redirect_back(fallback_location: settings_webhooks_path, notice: I18n.t('webhook_secret_has_been_saved'))
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def load_encrypted_config
|
def webhook_secret_params
|
||||||
@encrypted_config =
|
params.require(:webhook_url).permit(secret: %i[key value]).fetch(:secret, {})
|
||||||
current_account.encrypted_configs.find_or_initialize_by(key: EncryptedConfig::WEBHOOK_SECRET_KEY)
|
|
||||||
end
|
|
||||||
|
|
||||||
def encrypted_config_params
|
|
||||||
params.require(:encrypted_config).permit(value: %i[key value]).fetch(:value, {})
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -1,36 +1,60 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class WebhookSettingsController < ApplicationController
|
class WebhookSettingsController < ApplicationController
|
||||||
before_action :load_encrypted_config
|
load_and_authorize_resource :webhook_url, parent: false, only: %i[index show new create update destroy]
|
||||||
authorize_resource :encrypted_config, parent: false
|
load_and_authorize_resource :webhook_url, only: %i[resend], id_param: :webhook_id
|
||||||
|
|
||||||
|
def index
|
||||||
|
@webhook_urls = @webhook_urls.order(id: :desc)
|
||||||
|
@webhook_url = @webhook_urls.first_or_initialize
|
||||||
|
|
||||||
|
render @webhook_urls.size > 1 ? 'index' : 'show'
|
||||||
|
end
|
||||||
|
|
||||||
def show; end
|
def show; end
|
||||||
|
|
||||||
|
def new; end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@encrypted_config.assign_attributes(encrypted_config_params)
|
@webhook_url.save!
|
||||||
|
|
||||||
@encrypted_config.value.present? ? @encrypted_config.save! : @encrypted_config.delete
|
redirect_to settings_webhooks_path, notice: I18n.t('webhook_url_has_been_saved')
|
||||||
|
|
||||||
redirect_back(fallback_location: settings_webhooks_path, notice: I18n.t('webhook_url_has_been_saved'))
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
|
@webhook_url.update!(update_params)
|
||||||
|
|
||||||
|
redirect_back(fallback_location: settings_webhook_path(@webhook_url),
|
||||||
|
notice: I18n.t('webhook_url_has_been_updated'))
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
@webhook_url.destroy!
|
||||||
|
|
||||||
|
redirect_to settings_webhooks_path, notice: I18n.t('webhook_url_has_been_deleted')
|
||||||
|
end
|
||||||
|
|
||||||
|
def resend
|
||||||
submitter = current_account.submitters.where.not(completed_at: nil).order(:id).last
|
submitter = current_account.submitters.where.not(completed_at: nil).order(:id).last
|
||||||
|
|
||||||
SendFormCompletedWebhookRequestJob.perform_async({ 'submitter_id' => submitter.id,
|
if submitter.blank? || @webhook_url.blank?
|
||||||
'encrypted_config_id' => @encrypted_config.id })
|
return redirect_back(fallback_location: settings_webhooks_path,
|
||||||
|
alert: I18n.t('unable_to_resend_webhook_request'))
|
||||||
|
end
|
||||||
|
|
||||||
|
SendFormCompletedWebhookRequestJob.perform_async('submitter_id' => submitter.id,
|
||||||
|
'webhook_url_id' => @webhook_url.id)
|
||||||
|
|
||||||
redirect_back(fallback_location: settings_webhooks_path, notice: I18n.t('webhook_request_has_been_sent'))
|
redirect_back(fallback_location: settings_webhooks_path, notice: I18n.t('webhook_request_has_been_sent'))
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def load_encrypted_config
|
def create_params
|
||||||
@encrypted_config =
|
params.require(:webhook_url).permit(:url, events: []).reverse_merge(events: [])
|
||||||
current_account.encrypted_configs.find_or_initialize_by(key: EncryptedConfig::WEBHOOK_URL_KEY)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def encrypted_config_params
|
def update_params
|
||||||
params.require(:encrypted_config).permit(:value)
|
params.require(:webhook_url).permit(:url)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -29,11 +29,14 @@ import SearchInput from './elements/search_input'
|
|||||||
import ToggleAttribute from './elements/toggle_attribute'
|
import ToggleAttribute from './elements/toggle_attribute'
|
||||||
import LinkedInput from './elements/linked_input'
|
import LinkedInput from './elements/linked_input'
|
||||||
import CheckboxGroup from './elements/checkbox_group'
|
import CheckboxGroup from './elements/checkbox_group'
|
||||||
|
import MaskedInput from './elements/masked_input'
|
||||||
|
import SetDateButton from './elements/set_date_button'
|
||||||
|
import IndeterminateCheckbox from './elements/indeterminate_checkbox'
|
||||||
|
import AppTour from './elements/app_tour'
|
||||||
|
import DashboardDropzone from './elements/dashboard_dropzone'
|
||||||
|
|
||||||
import * as TurboInstantClick from './lib/turbo_instant_click'
|
import * as TurboInstantClick from './lib/turbo_instant_click'
|
||||||
|
|
||||||
import './images/preview.png'
|
|
||||||
|
|
||||||
TurboInstantClick.start()
|
TurboInstantClick.start()
|
||||||
|
|
||||||
document.addEventListener('turbo:before-cache', () => {
|
document.addEventListener('turbo:before-cache', () => {
|
||||||
@@ -95,6 +98,11 @@ safeRegisterElement('search-input', SearchInput)
|
|||||||
safeRegisterElement('toggle-attribute', ToggleAttribute)
|
safeRegisterElement('toggle-attribute', ToggleAttribute)
|
||||||
safeRegisterElement('linked-input', LinkedInput)
|
safeRegisterElement('linked-input', LinkedInput)
|
||||||
safeRegisterElement('checkbox-group', CheckboxGroup)
|
safeRegisterElement('checkbox-group', CheckboxGroup)
|
||||||
|
safeRegisterElement('masked-input', MaskedInput)
|
||||||
|
safeRegisterElement('set-date-button', SetDateButton)
|
||||||
|
safeRegisterElement('indeterminate-checkbox', IndeterminateCheckbox)
|
||||||
|
safeRegisterElement('app-tour', AppTour)
|
||||||
|
safeRegisterElement('dashboard-dropzone', DashboardDropzone)
|
||||||
|
|
||||||
safeRegisterElement('template-builder', class extends HTMLElement {
|
safeRegisterElement('template-builder', class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
@@ -109,6 +117,7 @@ safeRegisterElement('template-builder', class extends HTMLElement {
|
|||||||
backgroundColor: '#faf7f5',
|
backgroundColor: '#faf7f5',
|
||||||
locale: this.dataset.locale,
|
locale: this.dataset.locale,
|
||||||
withPhone: this.dataset.withPhone === 'true',
|
withPhone: this.dataset.withPhone === 'true',
|
||||||
|
withVerification: ['true', 'false'].includes(this.dataset.withVerification) ? this.dataset.withVerification === 'true' : null,
|
||||||
withLogo: this.dataset.withLogo !== 'false',
|
withLogo: this.dataset.withLogo !== 'false',
|
||||||
editable: this.dataset.editable !== 'false',
|
editable: this.dataset.editable !== 'false',
|
||||||
authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content,
|
authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content,
|
||||||
@@ -118,8 +127,10 @@ safeRegisterElement('template-builder', class extends HTMLElement {
|
|||||||
withSendButton: this.dataset.withSendButton !== 'false',
|
withSendButton: this.dataset.withSendButton !== 'false',
|
||||||
withSignYourselfButton: this.dataset.withSignYourselfButton !== 'false',
|
withSignYourselfButton: this.dataset.withSignYourselfButton !== 'false',
|
||||||
withConditions: this.dataset.withConditions === 'true',
|
withConditions: this.dataset.withConditions === 'true',
|
||||||
|
withReplaceAndCloneUpload: true,
|
||||||
currencies: (this.dataset.currencies || '').split(',').filter(Boolean),
|
currencies: (this.dataset.currencies || '').split(',').filter(Boolean),
|
||||||
acceptFileTypes: this.dataset.acceptFileTypes
|
acceptFileTypes: this.dataset.acceptFileTypes,
|
||||||
|
showTourStartForm: this.dataset.showTourStartForm === 'true'
|
||||||
})
|
})
|
||||||
|
|
||||||
this.component = this.app.mount(this.appElem)
|
this.component = this.app.mount(this.appElem)
|
||||||
@@ -150,7 +161,8 @@ safeRegisterElement('import-list', class extends HTMLElement {
|
|||||||
this.app = createApp(ImportList, {
|
this.app = createApp(ImportList, {
|
||||||
template: JSON.parse(this.dataset.template),
|
template: JSON.parse(this.dataset.template),
|
||||||
multitenant: this.dataset.multitenant === 'true',
|
multitenant: this.dataset.multitenant === 'true',
|
||||||
authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content
|
authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content,
|
||||||
|
i18n: JSON.parse(this.dataset.i18n || '{}')
|
||||||
})
|
})
|
||||||
|
|
||||||
this.app.mount(this.appElem)
|
this.app.mount(this.appElem)
|
||||||
|
|||||||
@@ -51,10 +51,6 @@ button[disabled] .enabled {
|
|||||||
@apply input input-bordered bg-white;
|
@apply input input-bordered bg-white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.base-input-slim {
|
|
||||||
@apply input input-bordered bg-white h-10;
|
|
||||||
}
|
|
||||||
|
|
||||||
.base-textarea {
|
.base-textarea {
|
||||||
@apply textarea textarea-bordered bg-white rounded-3xl;
|
@apply textarea textarea-bordered bg-white rounded-3xl;
|
||||||
}
|
}
|
||||||
@@ -101,6 +97,23 @@ button[disabled] .enabled {
|
|||||||
bottom: auto;
|
bottom: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tooltip-bottom-start:before {
|
||||||
|
transform: translateX(-31%);
|
||||||
|
top: var(--tooltip-offset);
|
||||||
|
left: 100%;
|
||||||
|
right: auto;
|
||||||
|
bottom: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-bottom-start:after {
|
||||||
|
transform: translateX(-25%);
|
||||||
|
border-color: transparent transparent var(--tooltip-color) transparent;
|
||||||
|
top: var(--tooltip-tail-offset);
|
||||||
|
left: 50%;
|
||||||
|
right: auto;
|
||||||
|
bottom: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.autocomplete {
|
.autocomplete {
|
||||||
background: white;
|
background: white;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
@@ -134,7 +147,3 @@ 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;
|
|
||||||
}
|
|
||||||
+24
-17
@@ -1,5 +1,6 @@
|
|||||||
import SignaturePad from 'signature_pad'
|
import SignaturePad from 'signature_pad'
|
||||||
import { cropCanvasAndExportToPNG } from './submission_form/crop_canvas'
|
import { cropCanvasAndExportToPNG } from './submission_form/crop_canvas'
|
||||||
|
import { isValidSignatureCanvas } from './submission_form/validate_signature'
|
||||||
|
|
||||||
window.customElements.define('draw-signature', class extends HTMLElement {
|
window.customElements.define('draw-signature', class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
@@ -12,6 +13,10 @@ window.customElements.define('draw-signature', class extends HTMLElement {
|
|||||||
|
|
||||||
this.pad = new SignaturePad(this.canvas)
|
this.pad = new SignaturePad(this.canvas)
|
||||||
|
|
||||||
|
if (this.dataset.color) {
|
||||||
|
this.pad.penColor = this.dataset.color
|
||||||
|
}
|
||||||
|
|
||||||
this.pad.addEventListener('endStroke', () => {
|
this.pad.addEventListener('endStroke', () => {
|
||||||
this.updateSubmitButtonVisibility()
|
this.updateSubmitButtonVisibility()
|
||||||
})
|
})
|
||||||
@@ -39,6 +44,8 @@ window.customElements.define('draw-signature', class extends HTMLElement {
|
|||||||
|
|
||||||
return response
|
return response
|
||||||
})
|
})
|
||||||
|
}).catch(error => {
|
||||||
|
console.log(error)
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
this.submitButton.disabled = false
|
this.submitButton.disabled = false
|
||||||
})
|
})
|
||||||
@@ -61,26 +68,26 @@ window.customElements.define('draw-signature', class extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async submitImage () {
|
async submitImage () {
|
||||||
return new Promise((resolve, reject) => {
|
if (!isValidSignatureCanvas(this.pad.toData())) {
|
||||||
cropCanvasAndExportToPNG(this.canvas, { errorOnTooSmall: true }).then(async (blob) => {
|
alert('Signature is too small or simple. Please redraw.')
|
||||||
const file = new File([blob], 'signature.png', { type: 'image/png' })
|
|
||||||
|
|
||||||
const formData = new FormData()
|
return Promise.reject(new Error('Image too small or simple'))
|
||||||
|
}
|
||||||
|
|
||||||
formData.append('file', file)
|
return cropCanvasAndExportToPNG(this.canvas).then(async (blob) => {
|
||||||
formData.append('submitter_slug', this.dataset.slug)
|
const file = new File([blob], 'signature.png', { type: 'image/png' })
|
||||||
formData.append('name', 'attachments')
|
|
||||||
formData.append('remember_signature', 'true')
|
|
||||||
|
|
||||||
return fetch('/api/attachments', {
|
const formData = new FormData()
|
||||||
method: 'POST',
|
|
||||||
body: formData
|
formData.append('file', file)
|
||||||
}).then((resp) => resp.json()).then((attachment) => {
|
formData.append('submitter_slug', this.dataset.slug)
|
||||||
return resolve(attachment)
|
formData.append('name', 'attachments')
|
||||||
})
|
formData.append('remember_signature', 'true')
|
||||||
}).catch((error) => {
|
|
||||||
return reject(error)
|
return fetch('/api/attachments', {
|
||||||
})
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
}).then(resp => resp.json())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,338 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
async connectedCallback () {
|
||||||
|
this.tourType = this.dataset.type
|
||||||
|
this.nextPagePath = this.dataset.nextPagePath
|
||||||
|
this.I18n = JSON.parse(this.dataset.i18n || '{}')
|
||||||
|
|
||||||
|
if (this.dataset.showTour === 'true') this.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
async start () {
|
||||||
|
if (window.innerWidth < 768) return
|
||||||
|
|
||||||
|
const [{ driver }] = await Promise.all([
|
||||||
|
import('driver.js'),
|
||||||
|
import('driver.js/dist/driver.css')
|
||||||
|
])
|
||||||
|
|
||||||
|
this.driverObj = driver({
|
||||||
|
showProgress: true,
|
||||||
|
nextBtnText: this.I18n.next,
|
||||||
|
prevBtnText: this.I18n.previous,
|
||||||
|
doneBtnText: this.I18n.done,
|
||||||
|
onDestroyStarted: () => {
|
||||||
|
this.disableAppGuide().finally(() => { this.destroy() })
|
||||||
|
},
|
||||||
|
onHighlightStarted: (element) => {
|
||||||
|
if (element) {
|
||||||
|
const clickHandler = () => {
|
||||||
|
this.disableAppGuide().finally(() => { this.destroy() })
|
||||||
|
element.removeEventListener('click', clickHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
element.addEventListener('click', clickHandler)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (this.tourType === 'dashboard') {
|
||||||
|
this.showDashboardTour()
|
||||||
|
} else if (this.tourType === 'builder') {
|
||||||
|
this.showTemplateBuilderTour()
|
||||||
|
} else if (this.tourType === 'account') {
|
||||||
|
this.showAccountTour()
|
||||||
|
} else if (this.tourType === 'template') {
|
||||||
|
this.showTemplateTour()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback () {
|
||||||
|
if (this.driverObj) this.destroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy () {
|
||||||
|
if (this.builderTemplate) this.builderTemplate.fields.shift()
|
||||||
|
if (this.driverObj) this.driverObj.destroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
showTemplateTour () {
|
||||||
|
const steps = [
|
||||||
|
{
|
||||||
|
element: '#share_link_clipboard',
|
||||||
|
popover: {
|
||||||
|
title: this.I18n.copy_and_share_link,
|
||||||
|
description: this.I18n.copy_and_share_link_description,
|
||||||
|
side: 'bottom',
|
||||||
|
align: 'end'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: '#sign_yourself_button',
|
||||||
|
popover: {
|
||||||
|
title: this.I18n.sign_the_document,
|
||||||
|
description: this.I18n.sign_the_document_description,
|
||||||
|
side: 'top',
|
||||||
|
align: 'center'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: '#send_to_recipients_button',
|
||||||
|
popover: {
|
||||||
|
title: this.I18n.send_for_signing,
|
||||||
|
description: this.I18n.add_recipients_description,
|
||||||
|
side: 'top',
|
||||||
|
align: 'center'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: '#add_recipients_button',
|
||||||
|
popover: {
|
||||||
|
title: this.I18n.add_recipients,
|
||||||
|
description: this.I18n.add_recipients_description,
|
||||||
|
side: 'bottom',
|
||||||
|
align: 'end'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: '#account_settings_button',
|
||||||
|
popover: {
|
||||||
|
title: this.I18n.settings,
|
||||||
|
description: this.I18n.settings_template_description,
|
||||||
|
side: 'right',
|
||||||
|
align: 'start',
|
||||||
|
showButtons: this.nextPagePath ? ['next', 'previous', 'close'] : ['previous', 'close'],
|
||||||
|
onNextClick: () => {
|
||||||
|
if (this.nextPagePath) {
|
||||||
|
window.Turbo.visit(this.nextPagePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
].filter((step) => document.querySelector(step.element))
|
||||||
|
|
||||||
|
this.driverObj.setSteps(steps)
|
||||||
|
this.driverObj.drive()
|
||||||
|
}
|
||||||
|
|
||||||
|
showDashboardTour () {
|
||||||
|
this.driverObj.setSteps([
|
||||||
|
{
|
||||||
|
element: '#templates_submissions_toggle',
|
||||||
|
popover: {
|
||||||
|
title: this.I18n.template_and_submissions,
|
||||||
|
description: this.I18n.template_and_submissions_description,
|
||||||
|
side: 'right',
|
||||||
|
align: 'start'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: '#templates_upload_button',
|
||||||
|
popover: {
|
||||||
|
title: this.I18n.upload_a_pdf_file,
|
||||||
|
description: this.I18n.upload_a_pdf_file_description,
|
||||||
|
side: 'left',
|
||||||
|
align: 'start',
|
||||||
|
showButtons: this.nextPagePath ? ['next', 'previous', 'close'] : ['previous', 'close'],
|
||||||
|
onNextClick: () => {
|
||||||
|
if (this.nextPagePath) {
|
||||||
|
window.Turbo.visit(this.nextPagePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onHighlightStarted: () => {}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
this.driverObj.drive()
|
||||||
|
}
|
||||||
|
|
||||||
|
showAccountTour () {
|
||||||
|
this.driverObj.setSteps([
|
||||||
|
{
|
||||||
|
element: '#account_settings_menu',
|
||||||
|
popover: {
|
||||||
|
title: this.I18n.settings,
|
||||||
|
description: this.I18n.settings_account_description,
|
||||||
|
side: 'right',
|
||||||
|
align: 'start'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: '#support_channels',
|
||||||
|
popover: {
|
||||||
|
title: this.I18n.support,
|
||||||
|
description: this.I18n.support_description,
|
||||||
|
side: 'left',
|
||||||
|
align: 'start'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
].filter((step) => document.querySelector(step.element)))
|
||||||
|
|
||||||
|
this.driverObj.drive()
|
||||||
|
}
|
||||||
|
|
||||||
|
showTemplateBuilderTour () {
|
||||||
|
const builderComponent = document.querySelector('template-builder')?.component
|
||||||
|
|
||||||
|
this.builderTemplate = builderComponent?.template
|
||||||
|
|
||||||
|
if (this.builderTemplate) {
|
||||||
|
this.builderTemplate.fields.unshift({
|
||||||
|
uuid: 'b387399b-88dc-4345-9d37-743e97a9b2b3',
|
||||||
|
submitter_uuid: this.builderTemplate.submitters[0].uuid,
|
||||||
|
name: 'First Name',
|
||||||
|
type: 'text'
|
||||||
|
})
|
||||||
|
|
||||||
|
builderComponent.$nextTick(() => {
|
||||||
|
this.driverObj.setSteps([
|
||||||
|
{
|
||||||
|
element: '.roles-dropdown',
|
||||||
|
popover: {
|
||||||
|
title: this.I18n.select_a_signer_party,
|
||||||
|
description: this.I18n.select_a_signer_party_description,
|
||||||
|
side: 'left',
|
||||||
|
align: 'start',
|
||||||
|
onPopoverRender: () => {
|
||||||
|
const rolesDropdown = document.querySelector('.roles-dropdown')
|
||||||
|
|
||||||
|
rolesDropdown.dispatchEvent(new Event('mouseenter', { bubbles: true, cancelable: true }))
|
||||||
|
rolesDropdown.classList.add('dropdown-open')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: '.roles-dropdown .dropdown-content',
|
||||||
|
popover: {
|
||||||
|
title: this.I18n.available_parties,
|
||||||
|
description: this.I18n.available_parties_description,
|
||||||
|
side: 'left',
|
||||||
|
align: 'start',
|
||||||
|
onPopoverRender: () => {
|
||||||
|
document.querySelector('.roles-dropdown .dropdown-content').classList.remove('driver-active-element')
|
||||||
|
},
|
||||||
|
onNextClick: () => {
|
||||||
|
document.querySelector('.roles-dropdown').classList.remove('dropdown-open')
|
||||||
|
this.driverObj.moveNext()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: '#field-types-grid',
|
||||||
|
popover: {
|
||||||
|
title: this.I18n.available_field_types,
|
||||||
|
description: this.I18n.available_field_types_description,
|
||||||
|
side: 'right',
|
||||||
|
align: 'start',
|
||||||
|
onPrevClick: () => {
|
||||||
|
document.querySelector('.roles-dropdown').classList.add('dropdown-open')
|
||||||
|
this.driverObj.movePrevious()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: '#text_type_field_button',
|
||||||
|
popover: {
|
||||||
|
title: this.I18n.text_input_field,
|
||||||
|
description: this.I18n.text_input_field_description,
|
||||||
|
side: 'left',
|
||||||
|
align: 'start'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: '#signature_type_field_button',
|
||||||
|
popover: {
|
||||||
|
title: this.I18n.signature_field,
|
||||||
|
description: this.I18n.signature_field_description,
|
||||||
|
side: 'left',
|
||||||
|
align: 'start'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: '.fields',
|
||||||
|
popover: {
|
||||||
|
title: this.I18n.added_fields,
|
||||||
|
description: this.I18n.added_fields_description,
|
||||||
|
side: 'right',
|
||||||
|
align: 'start'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: '.list-field label:has(svg.tabler-icon-settings)',
|
||||||
|
popover: {
|
||||||
|
title: this.I18n.open_field_settings,
|
||||||
|
description: this.I18n.open_field_settings_description,
|
||||||
|
side: 'bottom',
|
||||||
|
align: 'end',
|
||||||
|
onPopoverRender: () => {
|
||||||
|
const settingsDropdown = document.querySelector('.list-field div:first-child span:has(svg.tabler-icon-settings)')
|
||||||
|
|
||||||
|
document.querySelectorAll('.list-field div:first-child .text-transparent').forEach((e) => e.classList.remove('text-transparent'))
|
||||||
|
settingsDropdown.dispatchEvent(new Event('mouseenter', { bubbles: true, cancelable: true }))
|
||||||
|
settingsDropdown.classList.add('dropdown-open')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: '.list-field div:first-child span:has(svg.tabler-icon-settings) .dropdown-content',
|
||||||
|
popover: {
|
||||||
|
title: this.I18n.field_settings,
|
||||||
|
description: this.I18n.field_settings_description,
|
||||||
|
side: 'left',
|
||||||
|
align: 'start',
|
||||||
|
onPopoverRender: () => {
|
||||||
|
document.querySelector('.list-field div:first-child span:has(svg.tabler-icon-settings) .dropdown-content').classList.remove('driver-active-element')
|
||||||
|
},
|
||||||
|
onNextClick: () => {
|
||||||
|
document.querySelector('.list-field div:first-child span:has(svg.tabler-icon-settings)').classList.remove('dropdown-open')
|
||||||
|
this.driverObj.moveNext()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: '#send_button',
|
||||||
|
popover: {
|
||||||
|
title: this.I18n.send_document,
|
||||||
|
description: this.I18n.send_document_description,
|
||||||
|
side: 'bottom',
|
||||||
|
align: 'end',
|
||||||
|
onPrevClick: () => {
|
||||||
|
document.querySelector('.list-field div:first-child span:has(svg.tabler-icon-settings)').classList.add('dropdown-open')
|
||||||
|
this.driverObj.movePrevious()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: '#sign_yourself_button',
|
||||||
|
popover: {
|
||||||
|
title: this.I18n.sign_yourself,
|
||||||
|
description: this.I18n.sign_yourself_description,
|
||||||
|
side: 'bottom',
|
||||||
|
align: 'end',
|
||||||
|
onNextClick: () => {
|
||||||
|
if (this.nextPagePath) {
|
||||||
|
window.Turbo.visit(this.nextPagePath)
|
||||||
|
} else {
|
||||||
|
this.destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
this.driverObj.drive()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async disableAppGuide () {
|
||||||
|
return fetch('/user_configs', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ key: 'show_app_tour', value: false })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,8 @@ export default class extends HTMLElement {
|
|||||||
this.resize()
|
this.resize()
|
||||||
|
|
||||||
this.textarea.addEventListener('input', () => this.resize())
|
this.textarea.addEventListener('input', () => this.resize())
|
||||||
|
|
||||||
|
this.observeVisibility()
|
||||||
}
|
}
|
||||||
|
|
||||||
resize () {
|
resize () {
|
||||||
@@ -11,6 +13,28 @@ export default class extends HTMLElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
observeVisibility () {
|
||||||
|
this.observer = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
entries.forEach((entry) => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
this.resize()
|
||||||
|
this.observer.unobserve(this.textarea)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
threshold: 0.1
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
this.observer.observe(this.textarea)
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback () {
|
||||||
|
this.observer.unobserve(this.textarea)
|
||||||
|
}
|
||||||
|
|
||||||
get textarea () {
|
get textarea () {
|
||||||
return this.querySelector('textarea')
|
return this.querySelector('textarea')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,222 @@
|
|||||||
|
import { target, targets, targetable } from '@github/catalyst/lib/targetable'
|
||||||
|
|
||||||
|
const loadingIconHtml = `<svg xmlns="http://www.w3.org/2000/svg" class="animate-spin" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<path d="M12 3a9 9 0 1 0 9 9" />
|
||||||
|
</svg>`
|
||||||
|
|
||||||
|
export default targetable(class extends HTMLElement {
|
||||||
|
static [targets.static] = [
|
||||||
|
'hiddenOnDrag',
|
||||||
|
'folderCards',
|
||||||
|
'templateCards'
|
||||||
|
]
|
||||||
|
|
||||||
|
static [target.static] = [
|
||||||
|
'form',
|
||||||
|
'fileDropzone',
|
||||||
|
'fileDropzoneLoading'
|
||||||
|
]
|
||||||
|
|
||||||
|
connectedCallback () {
|
||||||
|
document.addEventListener('drop', this.onWindowDragdrop)
|
||||||
|
document.addEventListener('dragover', this.onWindowDropover)
|
||||||
|
|
||||||
|
window.addEventListener('dragleave', this.onWindowDragleave)
|
||||||
|
|
||||||
|
this.fileDropzone?.addEventListener('drop', this.onDropFile)
|
||||||
|
|
||||||
|
this.folderCards.forEach((el) => el.addEventListener('drop', (e) => this.onDropFolder(e, el)))
|
||||||
|
this.templateCards.forEach((el) => el.addEventListener('drop', this.onDropTemplate))
|
||||||
|
this.templateCards.forEach((el) => el.addEventListener('dragstart', this.onTemplateDragStart))
|
||||||
|
|
||||||
|
return [this.fileDropzone, ...this.folderCards, ...this.templateCards].forEach((el) => {
|
||||||
|
el?.addEventListener('dragover', this.onDragover)
|
||||||
|
el?.addEventListener('dragleave', this.onDragleave)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback () {
|
||||||
|
document.removeEventListener('drop', this.onWindowDragdrop)
|
||||||
|
document.removeEventListener('dragover', this.onWindowDropover)
|
||||||
|
|
||||||
|
window.removeEventListener('dragleave', this.onWindowDragleave)
|
||||||
|
}
|
||||||
|
|
||||||
|
onTemplateDragStart = (e) => {
|
||||||
|
const id = e.target.href.split('/').pop()
|
||||||
|
|
||||||
|
e.dataTransfer.effectAllowed = 'move'
|
||||||
|
|
||||||
|
if (id) {
|
||||||
|
e.dataTransfer.setData('template_id', id)
|
||||||
|
|
||||||
|
const dragPreview = e.target.cloneNode(true)
|
||||||
|
const rect = e.target.getBoundingClientRect()
|
||||||
|
|
||||||
|
const height = e.target.children[0].getBoundingClientRect().height + 50
|
||||||
|
|
||||||
|
dragPreview.children[1].remove()
|
||||||
|
dragPreview.style.width = `${rect.width}px`
|
||||||
|
dragPreview.style.height = `${height}px`
|
||||||
|
dragPreview.style.position = 'absolute'
|
||||||
|
dragPreview.style.top = '-1000px'
|
||||||
|
dragPreview.style.pointerEvents = 'none'
|
||||||
|
dragPreview.style.opacity = '0.9'
|
||||||
|
|
||||||
|
document.body.appendChild(dragPreview)
|
||||||
|
|
||||||
|
e.dataTransfer.setDragImage(dragPreview, rect.width / 2, height / 2)
|
||||||
|
|
||||||
|
setTimeout(() => document.body.removeChild(dragPreview), 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onDropFile = (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
this.fileDropzoneLoading.classList.remove('hidden')
|
||||||
|
this.fileDropzoneLoading.previousElementSibling.classList.add('hidden')
|
||||||
|
this.fileDropzoneLoading.classList.add('opacity-50')
|
||||||
|
|
||||||
|
this.uploadFiles(e.dataTransfer.files, '/templates_upload')
|
||||||
|
}
|
||||||
|
|
||||||
|
onDropFolder = (e, el) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
const templateId = e.dataTransfer.getData('template_id')
|
||||||
|
|
||||||
|
if (e.dataTransfer.files.length || templateId) {
|
||||||
|
const loading = document.createElement('div')
|
||||||
|
const svg = el.querySelector('svg')
|
||||||
|
|
||||||
|
loading.innerHTML = loadingIconHtml
|
||||||
|
loading.children[0].classList.add(...svg.classList)
|
||||||
|
|
||||||
|
el.replaceChild(loading.children[0], svg)
|
||||||
|
el.classList.add('opacity-50')
|
||||||
|
|
||||||
|
if (e.dataTransfer.files.length) {
|
||||||
|
const params = new URLSearchParams({ folder_name: el.innerText }).toString()
|
||||||
|
|
||||||
|
this.uploadFiles(e.dataTransfer.files, `/templates_upload?${params}`)
|
||||||
|
} else {
|
||||||
|
const formData = new FormData()
|
||||||
|
|
||||||
|
formData.append('name', el.innerText)
|
||||||
|
|
||||||
|
fetch(`/templates/${templateId}/folder`, {
|
||||||
|
method: 'PUT',
|
||||||
|
redirect: 'manual',
|
||||||
|
body: formData,
|
||||||
|
headers: {
|
||||||
|
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
|
||||||
|
}
|
||||||
|
}).finally(() => {
|
||||||
|
window.Turbo.cache.clear()
|
||||||
|
window.Turbo.visit(location.href)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onDropTemplate = (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
if (e.dataTransfer.files.length) {
|
||||||
|
const loading = document.createElement('div')
|
||||||
|
loading.classList.add('bottom-5', 'left-0', 'flex', 'justify-center', 'w-full', 'absolute')
|
||||||
|
loading.innerHTML = loadingIconHtml
|
||||||
|
|
||||||
|
e.target.appendChild(loading)
|
||||||
|
e.target.classList.add('opacity-50')
|
||||||
|
|
||||||
|
const id = e.target.href.split('/').pop()
|
||||||
|
|
||||||
|
this.uploadFiles(e.dataTransfer.files, `/templates/${id}/clone_and_replace`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onWindowDragdrop = (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
if (!this.isLoading) this.hideDraghover()
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadFiles (files, url) {
|
||||||
|
this.isLoading = true
|
||||||
|
|
||||||
|
this.form.action = url
|
||||||
|
|
||||||
|
this.form.querySelector('[type="file"]').files = files
|
||||||
|
|
||||||
|
this.form.querySelector('[type="submit"]').click()
|
||||||
|
}
|
||||||
|
|
||||||
|
onWindowDropover = (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
if (e.dataTransfer?.types?.includes('Files')) {
|
||||||
|
this.showDraghover()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onDragover (e) {
|
||||||
|
if (e.dataTransfer?.types?.includes('Files') || this.dataset.targets !== 'dashboard-dropzone.templateCards') {
|
||||||
|
this.style.backgroundColor = '#F7F3F0'
|
||||||
|
|
||||||
|
if (this.classList.contains('before:border-base-300')) {
|
||||||
|
this.classList.remove('before:border-base-300')
|
||||||
|
this.classList.add('before:border-base-content/30')
|
||||||
|
} else if (this.classList.contains('border-base-300')) {
|
||||||
|
this.classList.remove('border-base-300')
|
||||||
|
this.classList.add('border-base-content/30')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onDragleave () {
|
||||||
|
this.style.backgroundColor = null
|
||||||
|
|
||||||
|
if (this.classList.contains('before:border-base-content/30')) {
|
||||||
|
this.classList.remove('before:border-base-content/30')
|
||||||
|
this.classList.add('before:border-base-300')
|
||||||
|
} else if (this.classList.contains('border-base-content/30')) {
|
||||||
|
this.classList.remove('border-base-content/30')
|
||||||
|
this.classList.add('border-base-300')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onWindowDragleave = (e) => {
|
||||||
|
if (e.clientX <= 0 || e.clientY <= 0 || e.clientX >= window.innerWidth || e.clientY >= window.innerHeight) {
|
||||||
|
this.hideDraghover()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showDraghover = () => {
|
||||||
|
if (this.isDrag) return
|
||||||
|
|
||||||
|
this.isDrag = true
|
||||||
|
|
||||||
|
this.fileDropzone?.classList?.remove('hidden')
|
||||||
|
|
||||||
|
this.hiddenOnDrag.forEach((el) => { el.style.display = 'none' })
|
||||||
|
|
||||||
|
return [...this.folderCards, ...this.templateCards].forEach((el) => {
|
||||||
|
el.classList.remove('bg-base-200', 'before:hidden')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
hideDraghover = () => {
|
||||||
|
this.isDrag = false
|
||||||
|
|
||||||
|
this.fileDropzone?.classList?.add('hidden')
|
||||||
|
|
||||||
|
this.hiddenOnDrag.forEach((el) => { el.style.display = null })
|
||||||
|
|
||||||
|
return [...this.folderCards, ...this.templateCards].forEach((el) => {
|
||||||
|
el.classList.add('bg-base-200', 'before:hidden')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -20,7 +20,8 @@ export default targetable(class extends HTMLElement {
|
|||||||
fetch(this.dataset.src).then(async (response) => {
|
fetch(this.dataset.src).then(async (response) => {
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const urls = await response.json()
|
const urls = await response.json()
|
||||||
const isSafariIos = /iPhone|iPad|iPod/i.test(navigator.userAgent)
|
const isMobileSafariIos = 'ontouchstart' in window && navigator.maxTouchPoints > 0 && /AppleWebKit/i.test(navigator.userAgent)
|
||||||
|
const isSafariIos = isMobileSafariIos || /iPhone|iPad|iPod/i.test(navigator.userAgent)
|
||||||
|
|
||||||
if (isSafariIos && urls.length > 1) {
|
if (isSafariIos && urls.length > 1) {
|
||||||
this.downloadSafariIos(urls)
|
this.downloadSafariIos(urls)
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
this.form.addEventListener('submit', (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
this.submit()
|
||||||
|
})
|
||||||
|
|
||||||
|
if (this.dataset.onload === 'true') {
|
||||||
|
this.form.querySelector('button').click()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
submit () {
|
||||||
|
fetch(this.form.action, {
|
||||||
|
method: this.form.method,
|
||||||
|
body: new FormData(this.form)
|
||||||
|
}).then(async (resp) => {
|
||||||
|
if (!resp.ok) {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(await resp.text())
|
||||||
|
|
||||||
|
if (data.error) {
|
||||||
|
alert(data.error)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
get form () {
|
||||||
|
return this.querySelector('form')
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,19 +5,37 @@ export default actionable(targetable(class extends HTMLElement {
|
|||||||
static [target.static] = [
|
static [target.static] = [
|
||||||
'loading',
|
'loading',
|
||||||
'icon',
|
'icon',
|
||||||
'input'
|
'input',
|
||||||
|
'area'
|
||||||
]
|
]
|
||||||
|
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
this.addEventListener('drop', this.onDrop)
|
|
||||||
|
|
||||||
this.addEventListener('dragover', (e) => e.preventDefault())
|
this.addEventListener('dragover', (e) => e.preventDefault())
|
||||||
|
this.addEventListener('drop', this.onDrop)
|
||||||
document.addEventListener('turbo:submit-end', this.toggleLoading)
|
document.addEventListener('turbo:submit-end', this.toggleLoading)
|
||||||
|
this.area?.addEventListener('dragover', this.onDragover)
|
||||||
|
this.area?.addEventListener('dragleave', this.onDragleave)
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback () {
|
disconnectedCallback () {
|
||||||
|
this.removeEventListener('drop', this.onDrop)
|
||||||
document.removeEventListener('turbo:submit-end', this.toggleLoading)
|
document.removeEventListener('turbo:submit-end', this.toggleLoading)
|
||||||
|
this.area?.removeEventListener('dragover', this.onDragover)
|
||||||
|
this.area?.removeEventListener('dragleave', this.onDragleave)
|
||||||
|
}
|
||||||
|
|
||||||
|
onDragover (e) {
|
||||||
|
if (e.dataTransfer?.types?.includes('Files')) {
|
||||||
|
this.style.backgroundColor = '#F7F3F0'
|
||||||
|
this.classList.remove('border-base-300', 'hover:bg-base-200/30')
|
||||||
|
this.classList.add('border-base-content/30')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onDragleave () {
|
||||||
|
this.style.backgroundColor = null
|
||||||
|
this.classList.remove('border-base-content/30')
|
||||||
|
this.classList.add('border-base-300', 'hover:bg-base-200/30')
|
||||||
}
|
}
|
||||||
|
|
||||||
onDrop (e) {
|
onDrop (e) {
|
||||||
@@ -35,7 +53,7 @@ export default actionable(targetable(class extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toggleLoading = (e) => {
|
toggleLoading = (e) => {
|
||||||
if (e && e.target && !e.target.contains(this)) {
|
if (e && e.target && (!e.target.contains(this) || !e.detail?.formSubmission?.formElement?.contains(this))) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
if (this.dataset.indeterminate === 'true') {
|
||||||
|
this.checkbox.indeterminate = true
|
||||||
|
this.checkbox.readOnly = true
|
||||||
|
}
|
||||||
|
|
||||||
|
this.checkbox.addEventListener('click', () => {
|
||||||
|
this.checkbox.setAttribute('name', this.dataset.name)
|
||||||
|
|
||||||
|
if (this.showIndeterminateEl) {
|
||||||
|
this.showIndeterminateEl.classList.add('hidden')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.checkbox.readOnly) {
|
||||||
|
this.checkbox.checked = this.checkbox.readOnly = false
|
||||||
|
} else if (!this.checkbox.checked) {
|
||||||
|
if (this.showIndeterminateEl) {
|
||||||
|
this.showIndeterminateEl.classList.remove('hidden')
|
||||||
|
}
|
||||||
|
|
||||||
|
this.checkbox.setAttribute('name', this.dataset.indeterminateName)
|
||||||
|
this.checkbox.checked = this.checkbox.readOnly = this.checkbox.indeterminate = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
get checkbox () {
|
||||||
|
return this.querySelector('input[type="checkbox"]')
|
||||||
|
}
|
||||||
|
|
||||||
|
get showIndeterminateEl () {
|
||||||
|
return document.getElementById(this.dataset.showIndeterminateId)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
const maskedToken = this.input.value
|
||||||
|
|
||||||
|
this.input.addEventListener('focus', () => {
|
||||||
|
this.input.value = this.dataset.token
|
||||||
|
this.input.select()
|
||||||
|
})
|
||||||
|
|
||||||
|
this.input.addEventListener('focusout', () => {
|
||||||
|
this.input.value = maskedToken
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
get input () {
|
||||||
|
return this.querySelector('input')
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
this.header = document.querySelector('#signing_form_header')
|
||||||
|
|
||||||
|
window.addEventListener('scroll', this.onScroll)
|
||||||
|
window.addEventListener('resize', this.onResize)
|
||||||
|
|
||||||
|
if (!this.isNarrow() && this.isHeaderNotVisible()) {
|
||||||
|
this.showButtons({ animate: false })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback () {
|
||||||
|
window.removeEventListener('scroll', this.onScroll)
|
||||||
|
window.removeEventListener('resize', this.onResize)
|
||||||
|
}
|
||||||
|
|
||||||
|
onResize = () => {
|
||||||
|
if (this.isNarrow()) {
|
||||||
|
this.hideButtons(true)
|
||||||
|
} else if (this.isHeaderNotVisible()) {
|
||||||
|
this.showButtons()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isNarrow () {
|
||||||
|
return window.innerWidth < 1230
|
||||||
|
}
|
||||||
|
|
||||||
|
onScroll = () => {
|
||||||
|
if (this.isHeaderNotVisible() && !this.isNarrow()) {
|
||||||
|
this.showButtons()
|
||||||
|
} else {
|
||||||
|
this.hideButtons()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isHeaderNotVisible () {
|
||||||
|
const rect = this.header.getBoundingClientRect()
|
||||||
|
return rect.bottom <= 0 || rect.top >= window.innerHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
showButtons ({ animate } = { animate: true }) {
|
||||||
|
if (animate) {
|
||||||
|
this.classList.add('transition-transform', 'duration-300')
|
||||||
|
}
|
||||||
|
|
||||||
|
this.classList.remove('hidden', '-translate-y-10', 'opacity-0')
|
||||||
|
this.classList.add('translate-y-0', 'opacity-100')
|
||||||
|
}
|
||||||
|
|
||||||
|
hideButtons () {
|
||||||
|
this.classList.remove('translate-y-0', 'opacity-100')
|
||||||
|
this.classList.add('-translate-y-10', 'opacity-0')
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this.classList.contains('-translate-y-10')) {
|
||||||
|
this.classList.add('hidden')
|
||||||
|
}
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
this.button.addEventListener('click', () => {
|
||||||
|
this.fromInput.value = this.dataset.fromValue || ''
|
||||||
|
this.toInput.value = this.dataset.toValue || ''
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
get button () {
|
||||||
|
return this.querySelector('button')
|
||||||
|
}
|
||||||
|
|
||||||
|
get fromInput () {
|
||||||
|
return document.getElementById(this.dataset.fromId)
|
||||||
|
}
|
||||||
|
|
||||||
|
get toInput () {
|
||||||
|
return document.getElementById(this.dataset.toId)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ export default actionable(class extends HTMLElement {
|
|||||||
const elementIds = JSON.parse(this.dataset.elementIds)
|
const elementIds = JSON.parse(this.dataset.elementIds)
|
||||||
|
|
||||||
elementIds.forEach((elementId) => {
|
elementIds.forEach((elementId) => {
|
||||||
document.getElementById(elementId).classList.toggle('hidden', event.target.value !== elementId)
|
document.getElementById(elementId).classList.toggle('hidden', (event.target.dataset.toggleId || event.target.value) !== elementId)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -3,11 +3,15 @@ import { createApp, reactive } from 'vue'
|
|||||||
import Form from './submission_form/form'
|
import Form from './submission_form/form'
|
||||||
import DownloadButton from './elements/download_button'
|
import DownloadButton from './elements/download_button'
|
||||||
import ToggleSubmit from './elements/toggle_submit'
|
import ToggleSubmit from './elements/toggle_submit'
|
||||||
|
import FetchForm from './elements/fetch_form'
|
||||||
|
import ScrollButtons from './elements/scroll_buttons'
|
||||||
|
|
||||||
const safeRegisterElement = (name, element, options = {}) => !window.customElements.get(name) && window.customElements.define(name, element, options)
|
const safeRegisterElement = (name, element, options = {}) => !window.customElements.get(name) && window.customElements.define(name, element, options)
|
||||||
|
|
||||||
safeRegisterElement('download-button', DownloadButton)
|
safeRegisterElement('download-button', DownloadButton)
|
||||||
safeRegisterElement('toggle-submit', ToggleSubmit)
|
safeRegisterElement('toggle-submit', ToggleSubmit)
|
||||||
|
safeRegisterElement('fetch-form', FetchForm)
|
||||||
|
safeRegisterElement('scroll-buttons', ScrollButtons)
|
||||||
safeRegisterElement('submission-form', class extends HTMLElement {
|
safeRegisterElement('submission-form', class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
this.appElem = document.createElement('div')
|
this.appElem = document.createElement('div')
|
||||||
@@ -15,18 +19,22 @@ safeRegisterElement('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),
|
||||||
inviteSubmitters: JSON.parse(this.dataset.inviteSubmitters),
|
inviteSubmitters: JSON.parse(this.dataset.inviteSubmitters),
|
||||||
|
optionalInviteSubmitters: JSON.parse(this.dataset.optionalInviteSubmitters),
|
||||||
|
schema: JSON.parse(this.dataset.schema),
|
||||||
canSendEmail: this.dataset.canSendEmail === 'true',
|
canSendEmail: this.dataset.canSendEmail === 'true',
|
||||||
previousSignatureValue: this.dataset.previousSignatureValue,
|
previousSignatureValue: this.dataset.previousSignatureValue,
|
||||||
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',
|
||||||
scrollPadding: this.dataset.scrollPadding || '-80px',
|
scrollPadding: this.dataset.scrollPadding || '-80px',
|
||||||
|
language: this.dataset.language,
|
||||||
dryRun: this.dataset.dryRun === 'true',
|
dryRun: this.dataset.dryRun === 'true',
|
||||||
expand: ['true', 'false'].includes(this.dataset.expand) ? this.dataset.expand === 'true' : null,
|
expand: ['true', 'false'].includes(this.dataset.expand) ? this.dataset.expand === 'true' : null,
|
||||||
withSignatureId: this.dataset.withSignatureId === 'true',
|
withSignatureId: this.dataset.withSignatureId === 'true',
|
||||||
requireSigningReason: this.dataset.requireSigningReason === 'true',
|
requireSigningReason: this.dataset.requireSigningReason === 'true',
|
||||||
withConfetti: this.dataset.withConfetti !== 'false',
|
withConfetti: this.dataset.withConfetti !== 'false',
|
||||||
withDisclosure: this.dataset.withDisclosure === 'true',
|
withDisclosure: this.dataset.withDisclosure === 'true',
|
||||||
|
reuseSignature: this.dataset.reuseSignature !== 'false',
|
||||||
withTypedSignature: this.dataset.withTypedSignature !== 'false',
|
withTypedSignature: this.dataset.withTypedSignature !== 'false',
|
||||||
authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content,
|
authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content,
|
||||||
rememberSignature: this.dataset.rememberSignature === 'true',
|
rememberSignature: this.dataset.rememberSignature === 'true',
|
||||||
|
|||||||
@@ -31,10 +31,6 @@ button[disabled] .enabled {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
select:required:invalid {
|
|
||||||
@apply text-gray-300;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-bordered {
|
.input-bordered {
|
||||||
@apply border-base-content/20;
|
@apply border-base-content/20;
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 41 KiB |
@@ -1,9 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="field-area flex absolute lg:text-base -outline-offset-1"
|
class="flex absolute lg:text-base -outline-offset-1 field-area"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
:style="computedStyle"
|
:style="computedStyle"
|
||||||
:class="{ 'font-serif': field.preferences?.font === 'Times', 'text-[1.6vw] 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 }"
|
:class="{ 'text-[1.6vw] 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, 'outline-red-500 outline-dashed outline-2 z-10 field-area-active': isActive && submittable, 'bg-opacity-40': (isActive || isValueSet) && submittable }"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="(!withFieldPlaceholder || !field.name || field.type === 'cells') && !isActive && !isValueSet && field.type !== 'checkbox' && submittable && !area.option_uuid"
|
v-if="(!withFieldPlaceholder || !field.name || field.type === 'cells') && !isActive && !isValueSet && field.type !== 'checkbox' && submittable && !area.option_uuid"
|
||||||
@@ -39,7 +39,6 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="isActive"
|
|
||||||
ref="scrollToElem"
|
ref="scrollToElem"
|
||||||
class="absolute"
|
class="absolute"
|
||||||
:style="{ top: scrollPadding }"
|
:style="{ top: scrollPadding }"
|
||||||
@@ -56,10 +55,12 @@
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-else-if="field.type === 'signature' && signature"
|
v-else-if="field.type === 'signature' && signature"
|
||||||
class="flex flex-col justify-between h-full overflow-hidden"
|
class="flex justify-between h-full gap-1 overflow-hidden"
|
||||||
|
:class="isNarrow ? 'flex-row' : 'flex-col'"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex-grow flex overflow-hidden"
|
class="flex overflow-hidden"
|
||||||
|
:class="isNarrow ? 'w-1/2' : 'flex-grow'"
|
||||||
style="min-height: 50%"
|
style="min-height: 50%"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
@@ -69,13 +70,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="withSignatureId"
|
v-if="withSignatureId"
|
||||||
class="w-full mt-1 text-[1vw] lg:text-[0.55rem] lg:leading-[0.65rem]"
|
class="text-[1vw] lg:text-[0.55rem] lg:leading-[0.65rem]"
|
||||||
|
:class="isNarrow ? 'w-1/2' : 'w-full'"
|
||||||
>
|
>
|
||||||
<div class="truncate uppercase">
|
<div class="truncate uppercase">
|
||||||
ID: {{ signature.uuid }}
|
ID: {{ signature.uuid }}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{{ t('reason') }}: {{ values[field.preferences?.reason_field_uuid] || t('digitally_signed_by') }} {{ submitter.name }}
|
<span v-if="values[field.preferences?.reason_field_uuid]">{{ t('reason') }}: </span>{{ values[field.preferences?.reason_field_uuid] || t('digitally_signed_by') }} {{ submitter.name }}
|
||||||
<template v-if="submitter.email">
|
<template v-if="submitter.email">
|
||||||
<{{ submitter.email }}>
|
<{{ submitter.email }}>
|
||||||
</template>
|
</template>
|
||||||
@@ -169,6 +171,7 @@
|
|||||||
<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"
|
||||||
|
:class="{ 'justify-end': field.preferences?.align === 'right', ...fontClasses }"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="(char, index) in modelValue"
|
v-for="(char, index) in modelValue"
|
||||||
@@ -183,21 +186,30 @@
|
|||||||
v-else
|
v-else
|
||||||
ref="textContainer"
|
ref="textContainer"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
class="flex items-center px-0.5 w-full"
|
class="flex px-0.5 w-full"
|
||||||
:class="alignClasses[field.preferences?.align]"
|
:class="{ ...alignClasses, ...fontClasses }"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
v-if="field && field.name && withFieldPlaceholder && !modelValue && modelValue !== 0"
|
v-if="field && field.name && withFieldPlaceholder && !modelValue && modelValue !== 0"
|
||||||
class="whitespace-pre-wrap text-gray-400"
|
class="whitespace-pre-wrap text-gray-400"
|
||||||
:class="{ 'w-full': field.preferences?.align }"
|
:class="{ 'w-full': field.preferences?.align }"
|
||||||
>{{ field.name }}</span>
|
>{{ field.name }}</span>
|
||||||
<span v-else-if="Array.isArray(modelValue)">
|
<span
|
||||||
|
v-else-if="Array.isArray(modelValue)"
|
||||||
|
:class="{ 'w-full': field.preferences?.align }"
|
||||||
|
>
|
||||||
{{ modelValue.join(', ') }}
|
{{ modelValue.join(', ') }}
|
||||||
</span>
|
</span>
|
||||||
<span v-else-if="field.type === 'date'">
|
<span
|
||||||
|
v-else-if="field.type === 'date'"
|
||||||
|
:class="{ 'w-full': field.preferences?.align }"
|
||||||
|
>
|
||||||
{{ formattedDate }}
|
{{ formattedDate }}
|
||||||
</span>
|
</span>
|
||||||
<span v-else-if="field.type === 'number'">
|
<span
|
||||||
|
v-else-if="field.type === 'number'"
|
||||||
|
class="w-full"
|
||||||
|
>
|
||||||
{{ formatNumber(modelValue, field.preferences?.format) }}
|
{{ formatNumber(modelValue, field.preferences?.format) }}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
@@ -210,7 +222,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconCheck, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp, IconSquareNumber1 } from '@tabler/icons-vue'
|
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconCheck, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp, IconSquareNumber1, IconId } from '@tabler/icons-vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FieldArea',
|
name: 'FieldArea',
|
||||||
@@ -312,14 +324,34 @@ export default {
|
|||||||
cells: this.t('cells'),
|
cells: this.t('cells'),
|
||||||
stamp: this.t('stamp'),
|
stamp: this.t('stamp'),
|
||||||
payment: this.t('payment'),
|
payment: this.t('payment'),
|
||||||
phone: this.t('phone')
|
phone: this.t('phone'),
|
||||||
|
verification: this.t('verify_id')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
alignClasses () {
|
alignClasses () {
|
||||||
|
if (!this.field.preferences) {
|
||||||
|
return { 'items-center': true }
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
center: 'text-center',
|
'text-center': this.field.preferences.align === 'center',
|
||||||
left: 'text-left',
|
'text-left': this.field.preferences.align === 'left',
|
||||||
right: 'text-right'
|
'text-right': this.field.preferences.align === 'right',
|
||||||
|
'items-center': !this.field.preferences.valign || this.field.preferences.valign === 'center',
|
||||||
|
'items-start': this.field.preferences.valign === 'top',
|
||||||
|
'items-end': this.field.preferences.valign === 'bottom'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fontClasses () {
|
||||||
|
if (!this.field.preferences) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
'font-mono': this.field.preferences.font === 'Courier',
|
||||||
|
'font-serif': this.field.preferences.font === 'Times',
|
||||||
|
'font-bold': ['bold_italic', 'bold'].includes(this.field.preferences.font_type),
|
||||||
|
italic: ['bold_italic', 'italic'].includes(this.field.preferences.font_type)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
option () {
|
option () {
|
||||||
@@ -341,7 +373,8 @@ export default {
|
|||||||
cells: IconColumns3,
|
cells: IconColumns3,
|
||||||
multiple: IconChecks,
|
multiple: IconChecks,
|
||||||
phone: IconPhoneCheck,
|
phone: IconPhoneCheck,
|
||||||
payment: IconCreditCard
|
payment: IconCreditCard,
|
||||||
|
verification: IconId
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
image () {
|
image () {
|
||||||
@@ -405,10 +438,18 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.field.preferences?.font_size) {
|
if (this.field.preferences?.font_size) {
|
||||||
style.fontSize = this.field.preferences.font_size + 'pt'
|
style.fontSize = `clamp(4pt, 1.6vw, ${parseInt(this.field.preferences.font_size) * 1.23}pt)`
|
||||||
|
style.lineHeight = `clamp(6pt, 2.0vw, ${parseInt(this.field.preferences.font_size) * 1.23 + 3}pt)`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.field.preferences?.color) {
|
||||||
|
style.color = this.field.preferences.color
|
||||||
}
|
}
|
||||||
|
|
||||||
return style
|
return style
|
||||||
|
},
|
||||||
|
isNarrow () {
|
||||||
|
return this.area.h > 0 && (this.area.w / this.area.h) > 6
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -440,8 +481,18 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
formatNumber (number, format) {
|
formatNumber (number, format) {
|
||||||
|
if (!number && number !== 0) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
if (format === 'comma') {
|
if (format === 'comma') {
|
||||||
return new Intl.NumberFormat('en-US').format(number)
|
return new Intl.NumberFormat('en-US').format(number)
|
||||||
|
} else if (format === 'usd') {
|
||||||
|
return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(number)
|
||||||
|
} else if (format === 'gbp') {
|
||||||
|
return new Intl.NumberFormat('en-GB', { style: 'currency', currency: 'GBP', minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(number)
|
||||||
|
} else if (format === 'eur') {
|
||||||
|
return new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR', minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(number)
|
||||||
} else if (format === 'dot') {
|
} else if (format === 'dot') {
|
||||||
return new Intl.NumberFormat('de-DE').format(number)
|
return new Intl.NumberFormat('de-DE').format(number)
|
||||||
} else if (format === 'space') {
|
} else if (format === 'space') {
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
:with-label="withLabel && !withFieldPlaceholder && step.length < 2"
|
:with-label="withLabel && !withFieldPlaceholder && step.length < 2"
|
||||||
:is-value-set="step.some((f) => f.uuid in values)"
|
:is-value-set="step.some((f) => f.uuid in values)"
|
||||||
:attachments-index="attachmentsIndex"
|
:attachments-index="attachmentsIndex"
|
||||||
@click="$emit('focus-step', stepIndex)"
|
@click="[$emit('focus-step', stepIndex), maybeScrollOnClick(field, area)]"
|
||||||
/>
|
/>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
</template>
|
</template>
|
||||||
@@ -109,6 +109,14 @@ export default {
|
|||||||
areaRefs: []
|
areaRefs: []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
isMobileContainer () {
|
||||||
|
const root = this.$root.$el.parentNode.getRootNode()
|
||||||
|
const container = root.body || root.querySelector('div')
|
||||||
|
|
||||||
|
return container.style.overflow === 'hidden'
|
||||||
|
}
|
||||||
|
},
|
||||||
beforeUpdate () {
|
beforeUpdate () {
|
||||||
this.areaRefs = []
|
this.areaRefs = []
|
||||||
},
|
},
|
||||||
@@ -121,14 +129,16 @@ export default {
|
|||||||
this.scrollIntoArea(field.areas[0])
|
this.scrollIntoArea(field.areas[0])
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
maybeScrollOnClick (field, area) {
|
||||||
|
if (['text', 'number', 'cells'].includes(field.type) && this.isMobileContainer) {
|
||||||
|
this.scrollIntoArea(area)
|
||||||
|
}
|
||||||
|
},
|
||||||
scrollIntoArea (area) {
|
scrollIntoArea (area) {
|
||||||
const areaRef = this.areaRefs.find((a) => a.area === area)
|
const areaRef = this.areaRefs.find((a) => a.area === area)
|
||||||
|
|
||||||
if (areaRef) {
|
if (areaRef) {
|
||||||
const root = this.$root.$el.parentNode.getRootNode()
|
if (this.isMobileContainer) {
|
||||||
const container = root.body || root.querySelector('div')
|
|
||||||
|
|
||||||
if (container.style.overflow === 'hidden') {
|
|
||||||
this.scrollInContainer(areaRef.$el)
|
this.scrollInContainer(areaRef.$el)
|
||||||
} else {
|
} else {
|
||||||
const targetRect = areaRef.$refs.scrollToElem.getBoundingClientRect()
|
const targetRect = areaRef.$refs.scrollToElem.getBoundingClientRect()
|
||||||
@@ -157,13 +167,15 @@ export default {
|
|||||||
const formContainer = root.getElementById('form_container')
|
const formContainer = root.getElementById('form_container')
|
||||||
const container = root.body || root.querySelector('div')
|
const container = root.body || root.querySelector('div')
|
||||||
|
|
||||||
const padding = 64
|
const isAndroid = /android/i.test(navigator.userAgent)
|
||||||
|
const padding = isAndroid ? 128 : 64
|
||||||
|
const scrollboxTop = isAndroid ? scrollbox.getBoundingClientRect().top : 0
|
||||||
const boxRect = scrollbox.children[0].getBoundingClientRect()
|
const boxRect = scrollbox.children[0].getBoundingClientRect()
|
||||||
const targetRect = target.getBoundingClientRect()
|
const targetRect = target.getBoundingClientRect()
|
||||||
|
|
||||||
const targetTopRelativeToBox = targetRect.top - boxRect.top
|
const targetTopRelativeToBox = targetRect.top - boxRect.top
|
||||||
|
|
||||||
scrollbox.scrollTo({ top: targetTopRelativeToBox - container.offsetHeight + formContainer.offsetHeight + target.offsetHeight + padding, behavior: 'smooth' })
|
scrollbox.scrollTo({ top: targetTopRelativeToBox + scrollboxTop - container.offsetHeight + formContainer.offsetHeight + target.offsetHeight + padding, behavior: 'smooth' })
|
||||||
},
|
},
|
||||||
setAreaRef (el) {
|
setAreaRef (el) {
|
||||||
if (el) {
|
if (el) {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
v-if="val"
|
v-if="val"
|
||||||
class="flex items-center space-x-1.5 w-full"
|
class="flex items-center space-x-1.5 w-full attachment-file-name"
|
||||||
:href="attachmentsIndex[val].url"
|
:href="attachmentsIndex[val].url"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
@@ -26,7 +26,10 @@
|
|||||||
{{ attachmentsIndex[val].filename }}
|
{{ attachmentsIndex[val].filename }}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
<button @click.prevent="removeAttachment(val)">
|
<button
|
||||||
|
class="remove-attachment-button"
|
||||||
|
@click.prevent="removeAttachment(val)"
|
||||||
|
>
|
||||||
<IconTrashX
|
<IconTrashX
|
||||||
:width="18"
|
:width="18"
|
||||||
:heigh="19"
|
:heigh="19"
|
||||||
@@ -44,12 +47,12 @@
|
|||||||
<div
|
<div
|
||||||
v-if="field.description && !modelValue.length"
|
v-if="field.description && !modelValue.length"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
class="mb-3 px-1"
|
class="mb-3 px-1 field-description-text"
|
||||||
>
|
>
|
||||||
<MarkdownContent :string="field.description" />
|
<MarkdownContent :string="field.description" />
|
||||||
</div>
|
</div>
|
||||||
<FileDropzone
|
<FileDropzone
|
||||||
:message="`${t('upload')} ${field.name || t('files')}${field.required ? '' : ` (${t('optional')})`}`"
|
:message="`${t('upload')} ${(field.title || field.name) || t('files')}${field.required ? '' : ` (${t('optional')})`}`"
|
||||||
:submitter-slug="submitterSlug"
|
:submitter-slug="submitterSlug"
|
||||||
:multiple="true"
|
:multiple="true"
|
||||||
:dry-run="dryRun"
|
:dry-run="dryRun"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="mx-auto max-w-md flex flex-col"
|
id="form_completed"
|
||||||
|
class="mx-auto max-w-md flex flex-col completed-form"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
>
|
>
|
||||||
<div class="font-medium text-2xl flex items-center space-x-1.5 mx-auto">
|
<div class="font-medium text-2xl flex items-center space-x-1.5 mx-auto">
|
||||||
@@ -9,13 +10,13 @@
|
|||||||
:width="30"
|
:width="30"
|
||||||
:height="30"
|
:height="30"
|
||||||
/>
|
/>
|
||||||
<span>
|
<span class="completed-form-message-title">
|
||||||
{{ completedMessage.title || (hasSignatureFields ? (hasMultipleDocuments ? t('documents_have_been_signed') : t('document_has_been_signed')) : t('form_has_been_completed')) }}
|
{{ completedMessage.title || (hasSignatureFields ? (hasMultipleDocuments ? t('documents_have_been_signed') : t('document_has_been_signed')) : t('form_has_been_completed')) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="completedMessage.body"
|
v-if="completedMessage.body"
|
||||||
class="mt-2"
|
class="mt-2 completed-form-message-body"
|
||||||
>
|
>
|
||||||
<MarkdownContent
|
<MarkdownContent
|
||||||
:string="completedMessage.body"
|
:string="completedMessage.body"
|
||||||
@@ -26,7 +27,7 @@
|
|||||||
v-if="completedButton.url"
|
v-if="completedButton.url"
|
||||||
:href="sanitizeHref(completedButton.url)"
|
:href="sanitizeHref(completedButton.url)"
|
||||||
rel="noopener noreferrer nofollow"
|
rel="noopener noreferrer nofollow"
|
||||||
class="white-button flex items-center w-full"
|
class="white-button flex items-center w-full completed-form-completed-button"
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
{{ completedButton.title || 'Back to Website' }}
|
{{ completedButton.title || 'Back to Website' }}
|
||||||
@@ -34,7 +35,7 @@
|
|||||||
</a>
|
</a>
|
||||||
<button
|
<button
|
||||||
v-if="canSendEmail && !isDemo && withSendCopyButton"
|
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 completed-form-send-copy-button"
|
||||||
:disabled="isSendingCopy"
|
:disabled="isSendingCopy"
|
||||||
@click.prevent="sendCopyToEmail"
|
@click.prevent="sendCopyToEmail"
|
||||||
>
|
>
|
||||||
@@ -49,7 +50,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="!isWebView && withDownloadButton"
|
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 completed-form-download-button"
|
||||||
:disabled="isDownloading"
|
:disabled="isDownloading"
|
||||||
@click.prevent="download"
|
@click.prevent="download"
|
||||||
>
|
>
|
||||||
@@ -75,7 +76,7 @@
|
|||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
v-if="isDemo"
|
v-if="isDemo"
|
||||||
href="https://docuseal.co/sign_up"
|
href="https://docuseal.com/sign_up"
|
||||||
class="white-button flex items-center space-x-1 w-full"
|
class="white-button flex items-center space-x-1 w-full"
|
||||||
>
|
>
|
||||||
<IconLogin />
|
<IconLogin />
|
||||||
@@ -90,7 +91,7 @@
|
|||||||
>
|
>
|
||||||
{{ t('powered_by') }}
|
{{ t('powered_by') }}
|
||||||
<a
|
<a
|
||||||
href="https://www.docuseal.co/start"
|
href="https://www.docuseal.com/start"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="underline"
|
class="underline"
|
||||||
>DocuSeal</a> - {{ t('open_source_documents_software') }}
|
>DocuSeal</a> - {{ t('open_source_documents_software') }}
|
||||||
@@ -192,9 +193,9 @@ export default {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (window.decline_button) {
|
document.querySelectorAll('#decline_button').forEach((button) => {
|
||||||
window.decline_button.setAttribute('disabled', 'true')
|
button.setAttribute('disabled', 'true')
|
||||||
}
|
})
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
sendCopyToEmail () {
|
sendCopyToEmail () {
|
||||||
@@ -214,7 +215,8 @@ export default {
|
|||||||
fetch(this.baseUrl + `/submitters/${this.submitterSlug}/download`).then(async (response) => {
|
fetch(this.baseUrl + `/submitters/${this.submitterSlug}/download`).then(async (response) => {
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const urls = await response.json()
|
const urls = await response.json()
|
||||||
const isSafariIos = /iPhone|iPad|iPod/i.test(navigator.userAgent)
|
const isMobileSafariIos = 'ontouchstart' in window && navigator.maxTouchPoints > 0 && /AppleWebKit/i.test(navigator.userAgent)
|
||||||
|
const isSafariIos = isMobileSafariIos || /iPhone|iPad|iPod/i.test(navigator.userAgent)
|
||||||
|
|
||||||
if (isSafariIos && urls.length > 1) {
|
if (isSafariIos && urls.length > 1) {
|
||||||
this.downloadSafariIos(urls)
|
this.downloadSafariIos(urls)
|
||||||
@@ -222,7 +224,7 @@ export default {
|
|||||||
this.downloadUrls(urls)
|
this.downloadUrls(urls)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
alert('Failed to download files')
|
alert(this.t('failed_to_download_files'))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,26 +1,32 @@
|
|||||||
<template>
|
<template>
|
||||||
<div dir="auto">
|
<div dir="auto">
|
||||||
<div
|
<div
|
||||||
class="flex justify-between items-center w-full"
|
class="flex justify-between items-end w-full mb-3.5 sm:mb-4"
|
||||||
:class="{ 'mb-2': !field.description }"
|
:class="{ 'mb-2': !field.description }"
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
|
v-if="showFieldNames"
|
||||||
:for="field.uuid"
|
:for="field.uuid"
|
||||||
class="label text-2xl"
|
class="label text-xl sm:text-2xl py-0 field-name-label"
|
||||||
>
|
>
|
||||||
<MarkdownContent
|
<MarkdownContent
|
||||||
v-if="field.title"
|
v-if="field.title"
|
||||||
:string="field.title"
|
:string="field.title"
|
||||||
/>
|
/>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
{{ field.name && showFieldNames ? field.name : t('date') }}
|
{{ field.name || t('date') }}
|
||||||
<template v-if="!field.required">
|
</template>
|
||||||
|
<template v-if="!field.required">
|
||||||
|
<span
|
||||||
|
class="ml-1"
|
||||||
|
:class="{ 'hidden sm:inline': (field.title || field.name || t('date') ).length > 10 }"
|
||||||
|
>
|
||||||
({{ t('optional') }})
|
({{ t('optional') }})
|
||||||
</template>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</label>
|
</label>
|
||||||
<button
|
<button
|
||||||
class="btn btn-outline btn-sm !normal-case font-normal"
|
class="btn btn-outline btn-sm !normal-case font-normal set-current-date-button"
|
||||||
@click.prevent="[setCurrentDate(), $emit('focus')]"
|
@click.prevent="[setCurrentDate(), $emit('focus')]"
|
||||||
>
|
>
|
||||||
<IconCalendarCheck :width="16" />
|
<IconCalendarCheck :width="16" />
|
||||||
@@ -29,7 +35,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="field.description"
|
v-if="field.description"
|
||||||
class="mb-3 px-1"
|
class="mb-3 px-1 field-description-text"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
>
|
>
|
||||||
<MarkdownContent :string="field.description" />
|
<MarkdownContent :string="field.description" />
|
||||||
@@ -37,6 +43,7 @@
|
|||||||
<AppearsOn :field="field" />
|
<AppearsOn :field="field" />
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<input
|
<input
|
||||||
|
:id="field.uuid"
|
||||||
ref="input"
|
ref="input"
|
||||||
v-model="value"
|
v-model="value"
|
||||||
class="base-input !text-2xl text-center w-full"
|
class="base-input !text-2xl text-center w-full"
|
||||||
@@ -45,6 +52,7 @@
|
|||||||
:name="`values[${field.uuid}]`"
|
:name="`values[${field.uuid}]`"
|
||||||
@keydown.enter="onEnter"
|
@keydown.enter="onEnter"
|
||||||
@focus="$emit('focus')"
|
@focus="$emit('focus')"
|
||||||
|
@paste="onPaste"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -98,6 +106,25 @@ export default {
|
|||||||
this.$emit('submit')
|
this.$emit('submit')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onPaste (e) {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
let pasteData = e.clipboardData.getData('text').trim()
|
||||||
|
|
||||||
|
if (pasteData.match(/^\d{2}\.\d{2}\.\d{4}$/)) {
|
||||||
|
pasteData = pasteData.split('.').reverse().join('-')
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedDate = new Date(pasteData)
|
||||||
|
|
||||||
|
if (!isNaN(parsedDate)) {
|
||||||
|
const inputEl = this.$refs.input
|
||||||
|
|
||||||
|
inputEl.valueAsDate = new Date(parsedDate.getTime() - parsedDate.getTimezoneOffset() * 60000)
|
||||||
|
|
||||||
|
inputEl.dispatchEvent(new Event('input', { bubbles: true }))
|
||||||
|
}
|
||||||
|
},
|
||||||
setCurrentDate () {
|
setCurrentDate () {
|
||||||
const inputEl = this.$refs.input
|
const inputEl = this.$refs.input
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
|
id="dropzone"
|
||||||
class="flex h-32 w-full"
|
class="flex h-32 w-full"
|
||||||
@dragover.prevent
|
@dragover.prevent
|
||||||
@drop.prevent="onDropFiles"
|
@drop.prevent="onDropFiles"
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
:for="inputId"
|
:for="inputId"
|
||||||
class="w-full relative bg-base-300 hover:bg-base-200 rounded-md border border-base-content border-dashed"
|
class="w-full relative bg-base-300 hover:bg-base-200 rounded-md border border-base-content border-dashed file-dropzone"
|
||||||
:class="{ 'opacity-50': isLoading }"
|
:class="{ 'opacity-50': isLoading }"
|
||||||
>
|
>
|
||||||
<div class="absolute top-0 right-0 left-0 bottom-0 flex items-center justify-center">
|
<div class="absolute top-0 right-0 left-0 bottom-0 flex items-center justify-center">
|
||||||
@@ -127,7 +128,7 @@ export default {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
if (file.type === 'image/bmp') {
|
if (file.type === 'image/bmp' || file.type === 'image/vnd.microsoft.icon') {
|
||||||
file = await this.convertBmpToPng(file)
|
file = await this.convertBmpToPng(file)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,19 +15,59 @@
|
|||||||
/>
|
/>
|
||||||
<FieldAreas
|
<FieldAreas
|
||||||
:steps="readonlyConditionalFields.map((e) => [e])"
|
:steps="readonlyConditionalFields.map((e) => [e])"
|
||||||
:values="readonlyConditionalFields.reduce((acc, f) => { acc[f.uuid] = f.default_value; return acc }, {})"
|
:values="readonlyConditionalFieldValues"
|
||||||
:submitter="submitter"
|
:submitter="submitter"
|
||||||
|
:attachments-index="attachmentsIndex"
|
||||||
:submittable="false"
|
:submittable="false"
|
||||||
/>
|
/>
|
||||||
<FormulaFieldAreas
|
<FormulaFieldAreas
|
||||||
v-if="formulaFields.length"
|
v-if="formulaFields.length"
|
||||||
:fields="formulaFields"
|
:fields="formulaFields"
|
||||||
|
:readonly-values="readonlyConditionalFieldValues"
|
||||||
:values="values"
|
:values="values"
|
||||||
/>
|
/>
|
||||||
|
<Teleport
|
||||||
|
v-if="completeButtonToRef"
|
||||||
|
:to="completeButtonToRef"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="(emptyValueRequiredStep && ((stepFields.length - 1) !== currentStep || currentStepFields !== emptyValueRequiredStep)) || isCompleted"
|
||||||
|
class="tooltip-left"
|
||||||
|
:class="{ tooltip: !isCompleted }"
|
||||||
|
:data-tip="t('fill_all_required_fields_to_complete')"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="btn btn-sm btn-neutral text-white px-4 w-full flex justify-center btn-disabled pointer-events-auto"
|
||||||
|
@click="[isFormVisible = true, !isCompleted && goToStep(stepFields.indexOf(emptyValueRequiredStep), true, false)]"
|
||||||
|
>
|
||||||
|
{{ t('complete') }}
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
v-else
|
||||||
|
id="complete_form_button"
|
||||||
|
class="btn btn-sm btn-neutral text-white px-4 w-full flex justify-center"
|
||||||
|
form="steps_form"
|
||||||
|
type="submit"
|
||||||
|
name="completed"
|
||||||
|
value="true"
|
||||||
|
:disabled="isSubmittingComplete"
|
||||||
|
>
|
||||||
|
<span class="flex items-center">
|
||||||
|
<IconInnerShadowTop
|
||||||
|
v-if="isSubmittingComplete"
|
||||||
|
class="mr-1 animate-spin w-5 h-5"
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
{{ t('complete') }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</Teleport>
|
||||||
<button
|
<button
|
||||||
v-if="!isFormVisible"
|
v-if="!isFormVisible"
|
||||||
id="expand_form_button"
|
id="expand_form_button"
|
||||||
class="btn btn-neutral flex text-white absolute bottom-0 w-full mb-3"
|
class="btn btn-neutral flex text-white absolute bottom-0 w-full mb-3 expand-form-button"
|
||||||
style="width: 96%; margin-left: 2%"
|
style="width: 96%; margin-left: 2%"
|
||||||
@click.prevent="[isFormVisible = true, scrollIntoField(currentField)]"
|
@click.prevent="[isFormVisible = true, scrollIntoField(currentField)]"
|
||||||
>
|
>
|
||||||
@@ -53,14 +93,15 @@
|
|||||||
<div
|
<div
|
||||||
v-show="isFormVisible"
|
v-show="isFormVisible"
|
||||||
id="form_container"
|
id="form_container"
|
||||||
class="shadow-md bg-base-100 absolute bottom-0 w-full border-base-200 border p-4 rounded"
|
class="shadow-md bg-base-100 absolute bottom-0 w-full border-base-200 border p-4 rounded form-container overflow-hidden"
|
||||||
:class="{ 'md:bottom-4': isBreakpointMd }"
|
:class="{ 'md:bottom-4': isBreakpointMd }"
|
||||||
:style="{ backgroundColor: backgroundColor }"
|
:style="{ backgroundColor: backgroundColor }"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
v-if="!isCompleted"
|
v-if="!isCompleted"
|
||||||
id="minimize_form_button"
|
id="minimize_form_button"
|
||||||
class="absolute right-0 mr-2 mt-2 top-0 hidden md:block"
|
class="absolute right-0 top-0 minimize-form-button"
|
||||||
|
:class="currentField?.description?.length > 100 ? 'mr-1 mt-1 md:mr-2 md:mt-2': 'mr-2 mt-2 hidden md:block'"
|
||||||
:title="t('minimize')"
|
:title="t('minimize')"
|
||||||
@click.prevent="minimizeForm"
|
@click.prevent="minimizeForm"
|
||||||
>
|
>
|
||||||
@@ -74,10 +115,11 @@
|
|||||||
>
|
>
|
||||||
<form
|
<form
|
||||||
v-if="!isCompleted && !isInvite"
|
v-if="!isCompleted && !isInvite"
|
||||||
|
id="steps_form"
|
||||||
ref="form"
|
ref="form"
|
||||||
:action="submitPath"
|
:action="submitPath"
|
||||||
method="post"
|
method="post"
|
||||||
class="mx-auto"
|
class="mx-auto steps-form"
|
||||||
:style="{ maxWidth: isBreakpointMd ? '582px' : '' }"
|
:style="{ maxWidth: isBreakpointMd ? '582px' : '' }"
|
||||||
@submit.prevent="submitStep"
|
@submit.prevent="submitStep"
|
||||||
>
|
>
|
||||||
@@ -115,7 +157,7 @@
|
|||||||
v-model="values[currentField.uuid]"
|
v-model="values[currentField.uuid]"
|
||||||
:show-field-names="showFieldNames"
|
:show-field-names="showFieldNames"
|
||||||
:field="currentField"
|
:field="currentField"
|
||||||
@submit="submitStep"
|
@submit="!isSubmitting && submitStep()"
|
||||||
@focus="scrollIntoField(currentField)"
|
@focus="scrollIntoField(currentField)"
|
||||||
/>
|
/>
|
||||||
<div v-else-if="currentField.type === 'select'">
|
<div v-else-if="currentField.type === 'select'">
|
||||||
@@ -123,7 +165,7 @@
|
|||||||
v-if="showFieldNames && (currentField.name || currentField.title)"
|
v-if="showFieldNames && (currentField.name || currentField.title)"
|
||||||
:for="currentField.uuid"
|
:for="currentField.uuid"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
class="label text-2xl"
|
class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5 field-name-label"
|
||||||
:class="{ 'mb-2': !currentField.description }"
|
:class="{ 'mb-2': !currentField.description }"
|
||||||
>
|
>
|
||||||
<MarkdownContent
|
<MarkdownContent
|
||||||
@@ -132,7 +174,11 @@
|
|||||||
/>
|
/>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
{{ currentField.name }}
|
{{ currentField.name }}
|
||||||
<template v-if="!currentField.required">({{ t('optional') }})</template>
|
</template>
|
||||||
|
<template v-if="!currentField.required">
|
||||||
|
<span :class="{ 'hidden sm:inline': (currentField.title || currentField.name).length > 20 }">
|
||||||
|
({{ t('optional') }})
|
||||||
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</label>
|
</label>
|
||||||
<div
|
<div
|
||||||
@@ -142,7 +188,7 @@
|
|||||||
<div
|
<div
|
||||||
v-if="currentField.description"
|
v-if="currentField.description"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
class="mb-3 px-1"
|
class="mb-3 px-1 field-description-text"
|
||||||
>
|
>
|
||||||
<MarkdownContent :string="currentField.description" />
|
<MarkdownContent :string="currentField.description" />
|
||||||
</div>
|
</div>
|
||||||
@@ -152,6 +198,7 @@
|
|||||||
dir="auto"
|
dir="auto"
|
||||||
:required="currentField.required"
|
:required="currentField.required"
|
||||||
class="select base-input !text-2xl w-full text-center font-normal"
|
class="select base-input !text-2xl w-full text-center font-normal"
|
||||||
|
:class="{ 'text-gray-300': !values[currentField.uuid] }"
|
||||||
:name="`values[${currentField.uuid}]`"
|
:name="`values[${currentField.uuid}]`"
|
||||||
@change="values[currentField.uuid] = $event.target.value"
|
@change="values[currentField.uuid] = $event.target.value"
|
||||||
@focus="scrollIntoField(currentField)"
|
@focus="scrollIntoField(currentField)"
|
||||||
@@ -159,6 +206,7 @@
|
|||||||
<option
|
<option
|
||||||
value=""
|
value=""
|
||||||
:selected="!values[currentField.uuid]"
|
:selected="!values[currentField.uuid]"
|
||||||
|
class="text-gray-300"
|
||||||
>
|
>
|
||||||
{{ t('select_your_option') }}
|
{{ t('select_your_option') }}
|
||||||
</option>
|
</option>
|
||||||
@@ -167,6 +215,7 @@
|
|||||||
:key="option.uuid"
|
:key="option.uuid"
|
||||||
:selected="values[currentField.uuid] == option.value"
|
:selected="values[currentField.uuid] == option.value"
|
||||||
:value="option.value"
|
:value="option.value"
|
||||||
|
class="text-base-content"
|
||||||
>
|
>
|
||||||
{{ option.value }}
|
{{ option.value }}
|
||||||
</option>
|
</option>
|
||||||
@@ -177,7 +226,7 @@
|
|||||||
v-if="showFieldNames && (currentField.name || currentField.title)"
|
v-if="showFieldNames && (currentField.name || currentField.title)"
|
||||||
:for="currentField.uuid"
|
:for="currentField.uuid"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
class="label text-2xl"
|
class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5 field-name-label"
|
||||||
:class="{ 'mb-2': !currentField.description }"
|
:class="{ 'mb-2': !currentField.description }"
|
||||||
>
|
>
|
||||||
<MarkdownContent
|
<MarkdownContent
|
||||||
@@ -186,13 +235,17 @@
|
|||||||
/>
|
/>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
{{ currentField.name }}
|
{{ currentField.name }}
|
||||||
<template v-if="!currentField.required">({{ t('optional') }})</template>
|
</template>
|
||||||
|
<template v-if="!currentField.required">
|
||||||
|
<span :class="{ 'hidden sm:inline': (currentField.title || currentField.name).length > 20 }">
|
||||||
|
({{ t('optional') }})
|
||||||
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</label>
|
</label>
|
||||||
<div
|
<div
|
||||||
v-if="currentField.description"
|
v-if="currentField.description"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
class="mb-3 px-1"
|
class="mb-3 px-1 field-description-text"
|
||||||
>
|
>
|
||||||
<MarkdownContent :string="currentField.description" />
|
<MarkdownContent :string="currentField.description" />
|
||||||
</div>
|
</div>
|
||||||
@@ -217,7 +270,7 @@
|
|||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
:for="option.uuid"
|
:for="option.uuid"
|
||||||
class="flex items-center space-x-3"
|
class="flex items-center space-x-3 radio-label"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
:id="option.uuid"
|
:id="option.uuid"
|
||||||
@@ -251,7 +304,7 @@
|
|||||||
<div
|
<div
|
||||||
v-if="currentField.description"
|
v-if="currentField.description"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
class="mb-3 px-1"
|
class="mb-3 px-1 field-description-text"
|
||||||
>
|
>
|
||||||
<MarkdownContent :string="currentField.description" />
|
<MarkdownContent :string="currentField.description" />
|
||||||
</div>
|
</div>
|
||||||
@@ -285,7 +338,7 @@
|
|||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
:for="field.uuid"
|
:for="field.uuid"
|
||||||
class="flex items-center space-x-3"
|
class="flex items-center space-x-3 checkbox-label"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="hidden"
|
type="hidden"
|
||||||
@@ -392,7 +445,7 @@
|
|||||||
:default-value="submitter.phone"
|
:default-value="submitter.phone"
|
||||||
:submitter-slug="submitterSlug"
|
:submitter-slug="submitterSlug"
|
||||||
@focus="scrollIntoField(currentField)"
|
@focus="scrollIntoField(currentField)"
|
||||||
@submit="submitStep"
|
@submit="!isSubmitting && submitStep()"
|
||||||
/>
|
/>
|
||||||
<PaymentStep
|
<PaymentStep
|
||||||
v-else-if="currentField.type === 'payment'"
|
v-else-if="currentField.type === 'payment'"
|
||||||
@@ -404,18 +457,31 @@
|
|||||||
:values="values"
|
:values="values"
|
||||||
@attached="attachments.push($event)"
|
@attached="attachments.push($event)"
|
||||||
@focus="scrollIntoField(currentField)"
|
@focus="scrollIntoField(currentField)"
|
||||||
@submit="submitStep"
|
@submit="!isSubmitting && submitStep()"
|
||||||
|
/>
|
||||||
|
<VerificationStep
|
||||||
|
v-else-if="currentField.type === 'verification'"
|
||||||
|
ref="currentStep"
|
||||||
|
:key="currentField.uuid"
|
||||||
|
:locale="language?.toLowerCase() || browserLanguage"
|
||||||
|
:submitter="submitter"
|
||||||
|
:empty-value-required-step="emptyValueRequiredStep"
|
||||||
|
:field="currentField"
|
||||||
|
:submitter-slug="submitterSlug"
|
||||||
|
:values="values"
|
||||||
|
@focus="scrollIntoField(currentField)"
|
||||||
|
@submit="!isSubmitting && submitStep()"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="currentField.type !== 'payment' || submittedValues[currentField.uuid]"
|
v-if="(currentField.type !== 'payment' && currentField.type !== 'verification') || submittedValues[currentField.uuid]"
|
||||||
:class="currentField.type === 'signature' ? 'mt-2' : 'mt-6 md:mt-8'"
|
:class="currentField.type === 'signature' ? 'mt-2' : 'mt-4 md:mt-6'"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
id="submit_form_button"
|
id="submit_form_button"
|
||||||
ref="submitButton"
|
ref="submitButton"
|
||||||
type="submit"
|
type="submit"
|
||||||
class="base-button w-full flex justify-center"
|
class="base-button w-full flex justify-center submit-form-button"
|
||||||
:disabled="isButtonDisabled"
|
:disabled="isButtonDisabled"
|
||||||
>
|
>
|
||||||
<span class="flex">
|
<span class="flex">
|
||||||
@@ -442,6 +508,7 @@
|
|||||||
<InviteForm
|
<InviteForm
|
||||||
v-else-if="isInvite"
|
v-else-if="isInvite"
|
||||||
:submitters="inviteSubmitters"
|
:submitters="inviteSubmitters"
|
||||||
|
:optional-submitters="optionalInviteSubmitters"
|
||||||
:submitter-slug="submitterSlug"
|
:submitter-slug="submitterSlug"
|
||||||
:authenticity-token="authenticityToken"
|
:authenticity-token="authenticityToken"
|
||||||
:url="baseUrl + submitPath + '/invite'"
|
:url="baseUrl + submitPath + '/invite'"
|
||||||
@@ -464,15 +531,15 @@
|
|||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="stepFields.length < 80"
|
v-if="stepFields.length < 80"
|
||||||
class="flex justify-center"
|
class="flex justify-center mt-3 sm:mt-4 mb-0 sm:mb-1"
|
||||||
>
|
>
|
||||||
<div class="flex items-center mt-4 mb-1 flex-wrap">
|
<div class="flex items-center flex-wrap steps-progress">
|
||||||
<a
|
<a
|
||||||
v-for="(step, index) in stepFields"
|
v-for="(step, index) in stepFields"
|
||||||
:key="step[0].uuid"
|
:key="step[0].uuid"
|
||||||
href="#"
|
href="#"
|
||||||
class="inline border border-base-300 h-3 w-3 rounded-full mx-1 mt-1"
|
class="inline border border-base-300 h-3 w-3 rounded-full mx-1 mt-1"
|
||||||
:class="{ 'bg-base-300': index === currentStep, 'bg-base-content': (index < currentStep && stepFields[index].every((f) => !f.required || ![null, undefined, ''].includes(values[f.uuid]))) || isCompleted, 'bg-white': index > currentStep }"
|
:class="{ 'bg-base-300 steps-progress-current': index === currentStep, 'bg-base-content': (index < currentStep && stepFields[index].every((f) => !f.required || ![null, undefined, ''].includes(values[f.uuid]))) || isCompleted, 'bg-white': index > currentStep }"
|
||||||
@click.prevent="isCompleted ? '' : [saveStep(), goToStep(index, true)]"
|
@click.prevent="isCompleted ? '' : [saveStep(), goToStep(index, true)]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -495,6 +562,7 @@ import AttachmentStep from './attachment_step'
|
|||||||
import MultiSelectStep from './multi_select_step'
|
import MultiSelectStep from './multi_select_step'
|
||||||
import PhoneStep from './phone_step'
|
import PhoneStep from './phone_step'
|
||||||
import PaymentStep from './payment_step'
|
import PaymentStep from './payment_step'
|
||||||
|
import VerificationStep from './verification_step'
|
||||||
import TextStep from './text_step'
|
import TextStep from './text_step'
|
||||||
import NumberStep from './number_step'
|
import NumberStep from './number_step'
|
||||||
import DateStep from './date_step'
|
import DateStep from './date_step'
|
||||||
@@ -537,6 +605,7 @@ export default {
|
|||||||
IconWritingSign,
|
IconWritingSign,
|
||||||
AttachmentStep,
|
AttachmentStep,
|
||||||
InitialsStep,
|
InitialsStep,
|
||||||
|
VerificationStep,
|
||||||
InviteForm,
|
InviteForm,
|
||||||
MultiSelectStep,
|
MultiSelectStep,
|
||||||
IconInnerShadowTop,
|
IconInnerShadowTop,
|
||||||
@@ -569,6 +638,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
|
optionalInviteSubmitters: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
withSignatureId: {
|
withSignatureId: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -579,6 +653,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: '-80px'
|
default: '-80px'
|
||||||
},
|
},
|
||||||
|
orderAsOnPage: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
requireSigningReason: {
|
requireSigningReason: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -589,6 +668,16 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
completeButtonToRef: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
attachments: {
|
attachments: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -769,13 +858,45 @@ export default {
|
|||||||
phoneVerifiedValues: {},
|
phoneVerifiedValues: {},
|
||||||
orientation: screen?.orientation?.type,
|
orientation: screen?.orientation?.type,
|
||||||
isSubmitting: false,
|
isSubmitting: false,
|
||||||
|
isSubmittingComplete: false,
|
||||||
submittedValues: {},
|
submittedValues: {},
|
||||||
recalculateButtonDisabledKey: ''
|
recalculateButtonDisabledKey: ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
isMobile () {
|
isMobile () {
|
||||||
return /android|iphone|ipad/i.test(navigator.userAgent)
|
const isMobileSafariIos = 'ontouchstart' in window && navigator.maxTouchPoints > 0 && /AppleWebKit/i.test(navigator.userAgent)
|
||||||
|
|
||||||
|
return isMobileSafariIos || /android|iphone|ipad/i.test(navigator.userAgent)
|
||||||
|
},
|
||||||
|
readonlyConditionalFieldValues () {
|
||||||
|
return this.readonlyConditionalFields.reduce((acc, f) => {
|
||||||
|
acc[f.uuid] = (this.values[f.uuid] || f.default_value)
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
},
|
||||||
|
attachmentConditionsIndex () {
|
||||||
|
return this.schema.reduce((acc, item) => {
|
||||||
|
if (item.conditions?.length) {
|
||||||
|
if (item.conditions.every((c) => this.fieldsUuidIndex[c.field_uuid])) {
|
||||||
|
acc[item.attachment_uuid] = this.checkFieldConditions(item)
|
||||||
|
} else {
|
||||||
|
acc[item.attachment_uuid] = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
acc[item.attachment_uuid] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
},
|
||||||
|
emptyValueRequiredStep () {
|
||||||
|
return this.stepFields.find((fields, index) => {
|
||||||
|
return fields.some((f) => {
|
||||||
|
return f.required && isEmpty(this.values[f.uuid])
|
||||||
|
})
|
||||||
|
})
|
||||||
},
|
},
|
||||||
submitButtonText () {
|
submitButtonText () {
|
||||||
if (this.alwaysMinimize) {
|
if (this.alwaysMinimize) {
|
||||||
@@ -850,13 +971,61 @@ export default {
|
|||||||
return this.currentStepFields[0]
|
return this.currentStepFields[0]
|
||||||
},
|
},
|
||||||
readonlyConditionalFields () {
|
readonlyConditionalFields () {
|
||||||
return this.fields.filter((f) => f.readonly && f.conditions?.length && this.checkFieldConditions(f))
|
return this.fields.filter((f) => f.readonly && f.conditions?.length && this.checkFieldConditions(f) && this.checkFieldDocumentsConditions(f))
|
||||||
},
|
},
|
||||||
stepFields () {
|
stepFields () {
|
||||||
return this.fields.filter((f) => !f.readonly).reduce((acc, f) => {
|
const verificationFields = []
|
||||||
|
|
||||||
|
const sortedFields = this.fields.reduce((acc, f) => {
|
||||||
|
if (f.type === 'verification') {
|
||||||
|
verificationFields.push(f)
|
||||||
|
} else if (!f.readonly) {
|
||||||
|
acc.push(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
if (this.orderAsOnPage) {
|
||||||
|
const fieldAreasIndex = {}
|
||||||
|
const attachmentUuids = Object.keys(this.attachmentConditionsIndex)
|
||||||
|
|
||||||
|
const sortArea = (aArea, bArea) => {
|
||||||
|
if (aArea.attachment_uuid === bArea.attachment_uuid) {
|
||||||
|
if (aArea.page === bArea.page) {
|
||||||
|
if (Math.abs(aArea.y - bArea.y) < 0.01) {
|
||||||
|
if (aArea.x === bArea.x) {
|
||||||
|
return 0
|
||||||
|
} else {
|
||||||
|
return aArea.x - bArea.x
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return aArea.y - bArea.y
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return aArea.page - bArea.page
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return attachmentUuids.indexOf(aArea.attachment_uuid) - attachmentUuids.indexOf(bArea.attachment_uuid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sortedFields.sort((aField, bField) => {
|
||||||
|
const aArea = (fieldAreasIndex[aField.uuid] ||= [...(aField.areas || [])].sort(sortArea)[0])
|
||||||
|
const bArea = (fieldAreasIndex[bField.uuid] ||= [...(bField.areas || [])].sort(sortArea)[0])
|
||||||
|
|
||||||
|
return sortArea(aArea, bArea)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (verificationFields.length) {
|
||||||
|
sortedFields.push(verificationFields.pop())
|
||||||
|
}
|
||||||
|
|
||||||
|
return sortedFields.reduce((acc, f) => {
|
||||||
const prevStep = acc[acc.length - 1]
|
const prevStep = acc[acc.length - 1]
|
||||||
|
|
||||||
if (this.checkFieldConditions(f)) {
|
if (this.checkFieldConditions(f) && this.checkFieldDocumentsConditions(f)) {
|
||||||
if (f.type === 'checkbox' && Array.isArray(prevStep) && prevStep[0].type === 'checkbox' && !f.description) {
|
if (f.type === 'checkbox' && Array.isArray(prevStep) && prevStep[0].type === 'checkbox' && !f.description) {
|
||||||
prevStep.push(f)
|
prevStep.push(f)
|
||||||
} else {
|
} else {
|
||||||
@@ -868,7 +1037,7 @@ export default {
|
|||||||
}, [])
|
}, [])
|
||||||
},
|
},
|
||||||
formulaFields () {
|
formulaFields () {
|
||||||
return this.fields.filter((f) => f.preferences?.formula && f.type !== 'payment')
|
return this.fields.filter((f) => f.preferences?.formula && f.type !== 'payment' && this.checkFieldConditions(f) && this.checkFieldDocumentsConditions(f))
|
||||||
},
|
},
|
||||||
attachmentsIndex () {
|
attachmentsIndex () {
|
||||||
return this.attachments.reduce((acc, a) => {
|
return this.attachments.reduce((acc, a) => {
|
||||||
@@ -889,6 +1058,23 @@ export default {
|
|||||||
if (isEmpty(value) && this.currentStep > 0) {
|
if (isEmpty(value) && this.currentStep > 0) {
|
||||||
this.currentStep -= 1
|
this.currentStep -= 1
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
attachmentConditionsIndex: {
|
||||||
|
deep: true,
|
||||||
|
immediate: true,
|
||||||
|
handler (value) {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
const root = this.$root.$el.parentNode.getRootNode()
|
||||||
|
|
||||||
|
for (const key in value) {
|
||||||
|
const doc = root.querySelector(`[id="document-${key}"`)
|
||||||
|
|
||||||
|
if (doc) {
|
||||||
|
doc.classList.toggle('hidden', !value[key])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeUnmount () {
|
beforeUnmount () {
|
||||||
@@ -897,7 +1083,7 @@ export default {
|
|||||||
mounted () {
|
mounted () {
|
||||||
this.submittedValues = JSON.parse(JSON.stringify(this.values))
|
this.submittedValues = JSON.parse(JSON.stringify(this.values))
|
||||||
|
|
||||||
screen?.orientation.addEventListener('change', this.onOrientationChange)
|
screen?.orientation?.addEventListener('change', this.onOrientationChange)
|
||||||
|
|
||||||
this.fields.forEach((field) => {
|
this.fields.forEach((field) => {
|
||||||
if (field.default_value && !field.readonly) {
|
if (field.default_value && !field.readonly) {
|
||||||
@@ -936,7 +1122,9 @@ export default {
|
|||||||
this.minimizeForm()
|
this.minimizeForm()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (/iPhone|iPad|iPod/i.test(navigator.userAgent)) {
|
const isMobile = 'ontouchstart' in window && navigator.maxTouchPoints > 0 && /AppleWebKit|android/i.test(navigator.userAgent)
|
||||||
|
|
||||||
|
if (isMobile || /iPhone|iPad|iPod/i.test(navigator.userAgent)) {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
const root = this.$root.$el.parentNode.getRootNode()
|
const root = this.$root.$el.parentNode.getRootNode()
|
||||||
const scrollbox = root.getElementById('scrollbox')
|
const scrollbox = root.getElementById('scrollbox')
|
||||||
@@ -945,7 +1133,7 @@ export default {
|
|||||||
parent.style.overflow = 'hidden'
|
parent.style.overflow = 'hidden'
|
||||||
|
|
||||||
scrollbox.classList.add('h-full', 'overflow-y-auto')
|
scrollbox.classList.add('h-full', 'overflow-y-auto')
|
||||||
scrollbox.parentNode.classList.add('h-screen', 'overflow-y-auto')
|
scrollbox.parentNode.classList.add('h-screen', 'h-[100dvh]', 'overflow-y-auto')
|
||||||
scrollbox.parentNode.style.maxHeight = '-webkit-fill-available'
|
scrollbox.parentNode.style.maxHeight = '-webkit-fill-available'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -970,33 +1158,59 @@ export default {
|
|||||||
onOrientationChange (event) {
|
onOrientationChange (event) {
|
||||||
this.orientation = event.target.type
|
this.orientation = event.target.type
|
||||||
},
|
},
|
||||||
|
checkFieldDocumentsConditions (field) {
|
||||||
|
if (field.areas?.length) {
|
||||||
|
return field.areas.some((area) => {
|
||||||
|
return this.attachmentConditionsIndex[area.attachment_uuid]
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
},
|
||||||
checkFieldConditions (field) {
|
checkFieldConditions (field) {
|
||||||
if (field.conditions?.length) {
|
if (field.conditions?.length) {
|
||||||
return field.conditions.reduce((acc, c) => {
|
const result = field.conditions.reduce((acc, cond) => {
|
||||||
const field = this.fieldsUuidIndex[c.field_uuid]
|
if (cond.operation === 'or') {
|
||||||
|
acc.push(acc.pop() || this.checkFieldCondition(cond))
|
||||||
if (['not_empty', 'checked', 'equal', 'contains'].includes(c.action) && field && !this.checkFieldConditions(field)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (['empty', 'unchecked'].includes(c.action)) {
|
|
||||||
return acc && isEmpty(this.values[c.field_uuid])
|
|
||||||
} else if (['not_empty', 'checked'].includes(c.action)) {
|
|
||||||
return acc && !isEmpty(this.values[c.field_uuid])
|
|
||||||
} else if (['equal', 'contains'].includes(c.action) && field) {
|
|
||||||
const option = field.options.find((o) => o.uuid === c.value)
|
|
||||||
const values = [this.values[c.field_uuid]].flat()
|
|
||||||
|
|
||||||
return acc && values.includes(this.optionValue(option, field.options.indexOf(option)))
|
|
||||||
} else if (['not_equal', 'does_not_contain'].includes(c.action) && field) {
|
|
||||||
const option = field.options.find((o) => o.uuid === c.value)
|
|
||||||
const values = [this.values[c.field_uuid]].flat()
|
|
||||||
|
|
||||||
return acc && !values.includes(this.optionValue(option, field.options.indexOf(option)))
|
|
||||||
} else {
|
} else {
|
||||||
return acc
|
acc.push(this.checkFieldCondition(cond))
|
||||||
}
|
}
|
||||||
}, true)
|
|
||||||
|
return acc
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return !result.includes(false)
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
checkFieldCondition (condition) {
|
||||||
|
const field = this.fieldsUuidIndex[condition.field_uuid]
|
||||||
|
|
||||||
|
if (['not_empty', 'checked', 'equal', 'contains'].includes(condition.action) && field && !this.checkFieldConditions(field)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultValue = !field || isEmpty(field.default_value) ? null : field.default_value
|
||||||
|
|
||||||
|
if (['empty', 'unchecked'].includes(condition.action)) {
|
||||||
|
return isEmpty(this.values[condition.field_uuid] ?? defaultValue)
|
||||||
|
} else if (['not_empty', 'checked'].includes(condition.action)) {
|
||||||
|
return !isEmpty(this.values[condition.field_uuid] ?? defaultValue)
|
||||||
|
} else if (['equal', 'contains'].includes(condition.action) && field) {
|
||||||
|
if (field.options) {
|
||||||
|
const option = field.options.find((o) => o.uuid === condition.value)
|
||||||
|
const values = [this.values[condition.field_uuid] ?? defaultValue].flat()
|
||||||
|
|
||||||
|
return values.includes(this.optionValue(option, field.options.indexOf(option)))
|
||||||
|
} else {
|
||||||
|
return [this.values[condition.field_uuid] ?? defaultValue].flat().includes(condition.value)
|
||||||
|
}
|
||||||
|
} else if (['not_equal', 'does_not_contain'].includes(condition.action) && field) {
|
||||||
|
const option = field.options.find((o) => o.uuid === condition.value)
|
||||||
|
const values = [this.values[condition.field_uuid] ?? defaultValue].flat()
|
||||||
|
|
||||||
|
return !values.includes(this.optionValue(option, field.options.indexOf(option)))
|
||||||
} else {
|
} else {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -1087,7 +1301,9 @@ export default {
|
|||||||
|
|
||||||
if (!this.isCompleted) {
|
if (!this.isCompleted) {
|
||||||
if (scrollToArea) {
|
if (scrollToArea) {
|
||||||
this.scrollIntoField(this.currentField)
|
this.$nextTick(() => {
|
||||||
|
setTimeout(() => this.scrollIntoField(this.currentField), 1)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
this.enableScrollIntoField = false
|
this.enableScrollIntoField = false
|
||||||
@@ -1104,6 +1320,10 @@ export default {
|
|||||||
const currentFieldUuids = this.currentStepFields.map((f) => f.uuid)
|
const currentFieldUuids = this.currentStepFields.map((f) => f.uuid)
|
||||||
const currentFieldType = this.currentField.type
|
const currentFieldType = this.currentField.type
|
||||||
|
|
||||||
|
if (!formData && !this.$refs.form.checkValidity() && currentFieldUuids.every((fieldUuid) => isEmpty(this.submittedValues[fieldUuid]) || !isEmpty(this.values[fieldUuid]))) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (this.dryRun) {
|
if (this.dryRun) {
|
||||||
currentFieldUuids.forEach((fieldUuid) => {
|
currentFieldUuids.forEach((fieldUuid) => {
|
||||||
this.submittedValues[fieldUuid] = this.values[fieldUuid]
|
this.submittedValues[fieldUuid] = this.values[fieldUuid]
|
||||||
@@ -1139,16 +1359,24 @@ export default {
|
|||||||
scrollIntoArea (area) {
|
scrollIntoArea (area) {
|
||||||
return this.$refs.areas.scrollIntoArea(area)
|
return this.$refs.areas.scrollIntoArea(area)
|
||||||
},
|
},
|
||||||
async submitStep () {
|
async submitStep (e) {
|
||||||
this.isSubmitting = true
|
this.isSubmitting = true
|
||||||
|
|
||||||
const stepPromise = ['signature', 'phone', 'initials', 'payment'].includes(this.currentField.type)
|
const forceComplete = e?.submitter?.getAttribute('name') === 'completed'
|
||||||
|
|
||||||
|
if (forceComplete) {
|
||||||
|
this.isSubmittingComplete = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitStep = this.currentStep
|
||||||
|
|
||||||
|
const stepPromise = ['signature', 'phone', 'initials', 'payment', 'verification'].includes(this.currentField.type)
|
||||||
? this.$refs.currentStep.submit
|
? this.$refs.currentStep.submit
|
||||||
: () => Promise.resolve({})
|
: () => Promise.resolve({})
|
||||||
|
|
||||||
stepPromise().then(async () => {
|
stepPromise().then(async () => {
|
||||||
const emptyRequiredField = this.stepFields.find((fields, index) => {
|
const emptyRequiredField = this.stepFields.find((fields, index) => {
|
||||||
if (index >= this.currentStep) {
|
if (forceComplete ? index === submitStep : index >= submitStep) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1158,9 +1386,9 @@ export default {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const formData = new FormData(this.$refs.form)
|
const formData = new FormData(this.$refs.form)
|
||||||
const isLastStep = this.currentStep === this.stepFields.length - 1
|
const isLastStep = (submitStep === this.stepFields.length - 1) || forceComplete
|
||||||
|
|
||||||
if (isLastStep && !emptyRequiredField && !this.inviteSubmitters.length) {
|
if (isLastStep && !emptyRequiredField && !this.inviteSubmitters.length && !this.optionalInviteSubmitters.length) {
|
||||||
formData.append('completed', 'true')
|
formData.append('completed', 'true')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1176,18 +1404,32 @@ export default {
|
|||||||
if (response.status === 422 || response.status === 500) {
|
if (response.status === 422 || response.status === 500) {
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
|
|
||||||
const i18nError = data.error ? this.t(data.error.replace(/\s+/g, '_').toLowerCase()) : ''
|
if (data.field_uuid) {
|
||||||
|
const field = this.fieldsUuidIndex[data.field_uuid]
|
||||||
|
|
||||||
alert(i18nError !== data.error ? i18nError : (data.error || this.t('value_is_invalid')))
|
if (field) {
|
||||||
|
const step = this.stepFields.findIndex((fields) => fields.includes(field))
|
||||||
|
|
||||||
|
if (step !== -1) {
|
||||||
|
this.goToStep(step, this.autoscrollFields)
|
||||||
|
|
||||||
|
this.showFillAllRequiredFields = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject(new Error('Required field: ' + data.field_uuid))
|
||||||
|
} else if (data.error) {
|
||||||
|
const i18nKey = data.error.replace(/\s+/g, '_').toLowerCase()
|
||||||
|
|
||||||
|
alert(this.t(i18nKey) !== i18nKey ? this.t(i18nKey) : data.error)
|
||||||
|
} else {
|
||||||
|
alert(this.t('value_is_invalid'))
|
||||||
|
}
|
||||||
|
|
||||||
return Promise.reject(new Error(data.error))
|
return Promise.reject(new Error(data.error))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isLastStep) {
|
const nextStep = (isLastStep && emptyRequiredField) || (forceComplete ? null : this.stepFields[submitStep + 1])
|
||||||
this.isSecondWalkthrough = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextStep = (isLastStep && emptyRequiredField) || this.stepFields[this.currentStep + 1]
|
|
||||||
|
|
||||||
if (nextStep) {
|
if (nextStep) {
|
||||||
if (this.alwaysMinimize) {
|
if (this.alwaysMinimize) {
|
||||||
@@ -1199,7 +1441,7 @@ export default {
|
|||||||
if (emptyRequiredField === nextStep) {
|
if (emptyRequiredField === nextStep) {
|
||||||
this.showFillAllRequiredFields = true
|
this.showFillAllRequiredFields = true
|
||||||
}
|
}
|
||||||
} else if (this.inviteSubmitters.length) {
|
} else if (this.inviteSubmitters.length || this.optionalInviteSubmitters.length) {
|
||||||
this.isInvite = true
|
this.isInvite = true
|
||||||
} else {
|
} else {
|
||||||
this.performComplete(response)
|
this.performComplete(response)
|
||||||
@@ -1208,15 +1450,13 @@ export default {
|
|||||||
console.error(error)
|
console.error(error)
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
this.isSubmitting = false
|
this.isSubmitting = false
|
||||||
|
this.isSubmittingComplete = false
|
||||||
})
|
})
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
if (error?.message === 'Image too small') {
|
console.log(error)
|
||||||
alert('Signature is too small - please redraw.')
|
|
||||||
} else {
|
|
||||||
console.log(error)
|
|
||||||
}
|
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
this.isSubmitting = false
|
this.isSubmitting = false
|
||||||
|
this.isSubmittingComplete = false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
minimizeForm () {
|
minimizeForm () {
|
||||||
|
|||||||
@@ -38,6 +38,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
|
readonlyValues: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
values: {
|
values: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -87,7 +92,7 @@ export default {
|
|||||||
},
|
},
|
||||||
calculateFormula (field) {
|
calculateFormula (field) {
|
||||||
const transformedFormula = field.preferences.formula.replace(/{{(.*?)}}/g, (match, uuid) => {
|
const transformedFormula = field.preferences.formula.replace(/{{(.*?)}}/g, (match, uuid) => {
|
||||||
return this.values[uuid] || 0.0
|
return this.readonlyValues[uuid] || this.values[uuid] || 0.0
|
||||||
})
|
})
|
||||||
|
|
||||||
return this.math.evaluate(transformedFormula.toLowerCase())
|
return this.math.evaluate(transformedFormula.toLowerCase())
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
const en = {
|
const en = {
|
||||||
|
complete_all_required_fields_to_proceed_with_identity_verification: 'Complete all required fields to proceed with identity verification.',
|
||||||
|
verify_id: 'Verify ID',
|
||||||
|
identity_verification: 'Identity verification',
|
||||||
complete: 'Complete',
|
complete: 'Complete',
|
||||||
|
fill_all_required_fields_to_complete: 'Fill all required fields to complete',
|
||||||
sign_and_complete: 'Sign and Complete',
|
sign_and_complete: 'Sign and Complete',
|
||||||
text: 'Text',
|
text: 'Text',
|
||||||
by_clicking_you_agree_to_the: 'By clicking "{button}", you agree to the',
|
by_clicking_you_agree_to_the: 'By clicking "{button}", you agree to the',
|
||||||
@@ -66,7 +70,7 @@ const en = {
|
|||||||
documents_have_been_signed: 'Documents have been signed!',
|
documents_have_been_signed: 'Documents have been signed!',
|
||||||
create_a_free_account: 'Create a Free Account',
|
create_a_free_account: 'Create a Free Account',
|
||||||
powered_by: 'Powered by',
|
powered_by: 'Powered by',
|
||||||
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 international format: +1xxx',
|
use_international_format: 'Use international format: +1xxx',
|
||||||
@@ -88,11 +92,17 @@ const en = {
|
|||||||
pay_with_strip: 'Pay with Stripe',
|
pay_with_strip: 'Pay with Stripe',
|
||||||
reupload: 'Reupload',
|
reupload: 'Reupload',
|
||||||
upload: 'Upload',
|
upload: 'Upload',
|
||||||
files: 'Files'
|
files: 'Files',
|
||||||
|
signature_is_too_small_or_simple_please_redraw: 'Signature is too small or simple. Please redraw.',
|
||||||
|
wait_countdown_seconds: 'Wait {countdown} seconds'
|
||||||
}
|
}
|
||||||
|
|
||||||
const es = {
|
const es = {
|
||||||
|
complete_all_required_fields_to_proceed_with_identity_verification: 'Complete todos los campos requeridos para continuar con la verificación de identidad.',
|
||||||
|
verify_id: 'Verificar ID',
|
||||||
|
identity_verification: 'Verificación de identidad',
|
||||||
complete: 'Completar',
|
complete: 'Completar',
|
||||||
|
fill_all_required_fields_to_complete: 'Complete todos los campos requeridos para finalizar',
|
||||||
sign_and_complete: 'Firmar y Completar',
|
sign_and_complete: 'Firmar y Completar',
|
||||||
invite: 'Invitar',
|
invite: 'Invitar',
|
||||||
email: 'Correo electrónico',
|
email: 'Correo electrónico',
|
||||||
@@ -138,6 +148,7 @@ const es = {
|
|||||||
sign_now: 'Firmar ahora',
|
sign_now: 'Firmar ahora',
|
||||||
type_here_: 'Escribe aquí...',
|
type_here_: 'Escribe aquí...',
|
||||||
optional: 'opcional',
|
optional: 'opcional',
|
||||||
|
option: 'Opción',
|
||||||
appears_on: 'Aparece en',
|
appears_on: 'Aparece en',
|
||||||
page: 'Página',
|
page: 'Página',
|
||||||
select_your_option: 'Selecciona tu opción',
|
select_your_option: 'Selecciona tu opción',
|
||||||
@@ -158,7 +169,7 @@ const es = {
|
|||||||
documents_have_been_signed: '¡Los documentos han sido firmados!',
|
documents_have_been_signed: '¡Los documentos han sido firmados!',
|
||||||
create_a_free_account: 'Crear una Cuenta Gratuita',
|
create_a_free_account: 'Crear una Cuenta Gratuita',
|
||||||
powered_by: 'Desarrollado por',
|
powered_by: 'Desarrollado por',
|
||||||
please_check_the_box_to_continue: 'Por favor marque la casilla para continuar',
|
please_check_the_box_to_continue: 'Por favor marque la casilla para continuar.',
|
||||||
open_source_documents_software: 'software de documentos de código abierto',
|
open_source_documents_software: 'software de documentos de código abierto',
|
||||||
verified_phone_number: 'Verificar número de teléfono',
|
verified_phone_number: 'Verificar número de teléfono',
|
||||||
use_international_format: 'Usar formato internacional: +1xxx',
|
use_international_format: 'Usar formato internacional: +1xxx',
|
||||||
@@ -180,11 +191,17 @@ const es = {
|
|||||||
pay_with_strip: 'Pagar con Stripe',
|
pay_with_strip: 'Pagar con Stripe',
|
||||||
reupload: 'Volver a subir',
|
reupload: 'Volver a subir',
|
||||||
upload: 'Subir',
|
upload: 'Subir',
|
||||||
files: 'Archivos'
|
files: 'Archivos',
|
||||||
|
signature_is_too_small_or_simple_please_redraw: 'La firma es demasiado pequeña o simple. Por favor, vuelve a dibujarla.',
|
||||||
|
wait_countdown_seconds: 'Espera {countdown} segundos'
|
||||||
}
|
}
|
||||||
|
|
||||||
const it = {
|
const it = {
|
||||||
|
complete_all_required_fields_to_proceed_with_identity_verification: "Compila tutti i campi obbligatori per procedere con la verifica dell'identità.",
|
||||||
|
verify_id: 'Verifica ID',
|
||||||
|
identity_verification: "Verifica dell'identità",
|
||||||
complete: 'Completa',
|
complete: 'Completa',
|
||||||
|
fill_all_required_fields_to_complete: 'Compila tutti i campi obbligatori per completare',
|
||||||
sign_and_complete: 'Firma e Completa',
|
sign_and_complete: 'Firma e Completa',
|
||||||
invite: 'Invita',
|
invite: 'Invita',
|
||||||
email: 'Email',
|
email: 'Email',
|
||||||
@@ -216,7 +233,7 @@ const it = {
|
|||||||
pay: 'Pagamento',
|
pay: 'Pagamento',
|
||||||
file: 'File',
|
file: 'File',
|
||||||
select: 'Seleziona',
|
select: 'Seleziona',
|
||||||
checkbox: 'Casella di controllo',
|
checkbox: 'Checkbox',
|
||||||
multiple: 'Multiplo',
|
multiple: 'Multiplo',
|
||||||
radio: 'Radio',
|
radio: 'Radio',
|
||||||
cells: 'Celle',
|
cells: 'Celle',
|
||||||
@@ -229,6 +246,7 @@ const it = {
|
|||||||
sign_now: 'Firma ora',
|
sign_now: 'Firma ora',
|
||||||
type_here_: 'Digita qui...',
|
type_here_: 'Digita qui...',
|
||||||
optional: 'opzionale',
|
optional: 'opzionale',
|
||||||
|
option: 'Opzione',
|
||||||
appears_on: 'Compare su',
|
appears_on: 'Compare su',
|
||||||
page: 'Pagina',
|
page: 'Pagina',
|
||||||
take_photo: 'Scattare una foto',
|
take_photo: 'Scattare una foto',
|
||||||
@@ -249,8 +267,8 @@ const it = {
|
|||||||
document_has_been_signed: 'Il documento è stato firmato!',
|
document_has_been_signed: 'Il documento è stato firmato!',
|
||||||
documents_have_been_signed: 'I documenti sono stati firmati!',
|
documents_have_been_signed: 'I documenti sono stati firmati!',
|
||||||
create_a_free_account: 'Crea un Account Gratuito',
|
create_a_free_account: 'Crea un Account Gratuito',
|
||||||
powered_by: 'Desarrollado por',
|
powered_by: 'Fornito da',
|
||||||
please_check_the_box_to_continue: 'Si prega di spuntare la casella per continuare',
|
please_check_the_box_to_continue: 'Si prega di spuntare la casella per continuare.',
|
||||||
open_source_documents_software: 'software di documenti open source',
|
open_source_documents_software: 'software di documenti open source',
|
||||||
verified_phone_number: 'Verifica numero di telefono',
|
verified_phone_number: 'Verifica numero di telefono',
|
||||||
use_international_format: 'Usa formato internazionale: +1xxx',
|
use_international_format: 'Usa formato internazionale: +1xxx',
|
||||||
@@ -272,11 +290,17 @@ const it = {
|
|||||||
pay_with_strip: 'Paga con Stripe',
|
pay_with_strip: 'Paga con Stripe',
|
||||||
reupload: 'Ricarica',
|
reupload: 'Ricarica',
|
||||||
upload: 'Carica',
|
upload: 'Carica',
|
||||||
files: 'File'
|
files: 'File',
|
||||||
|
signature_is_too_small_or_simple_please_redraw: 'La firma è troppo piccola o semplice. Ridisegnala, per favore.',
|
||||||
|
wait_countdown_seconds: 'Attendi {countdown} secondi'
|
||||||
}
|
}
|
||||||
|
|
||||||
const de = {
|
const de = {
|
||||||
|
complete_all_required_fields_to_proceed_with_identity_verification: 'Vervollständigen Sie alle erforderlichen Felder, um mit der Identitätsverifizierung fortzufahren.',
|
||||||
|
verify_id: 'ID überprüfen',
|
||||||
|
identity_verification: 'Identitätsüberprüfung',
|
||||||
complete: 'Abschließen',
|
complete: 'Abschließen',
|
||||||
|
fill_all_required_fields_to_complete: 'Alle erforderlichen Felder ausfüllen, um abzuschließen',
|
||||||
sign_and_complete: 'Signieren und Abschließen',
|
sign_and_complete: 'Signieren und Abschließen',
|
||||||
invite: 'Einladen',
|
invite: 'Einladen',
|
||||||
email: 'E-Mail',
|
email: 'E-Mail',
|
||||||
@@ -321,6 +345,7 @@ const de = {
|
|||||||
sign_now: 'Jetzt unterschreiben',
|
sign_now: 'Jetzt unterschreiben',
|
||||||
type_here_: 'Hier eingeben...',
|
type_here_: 'Hier eingeben...',
|
||||||
optional: 'optional',
|
optional: 'optional',
|
||||||
|
option: 'Option',
|
||||||
appears_on: 'Erscheint auf',
|
appears_on: 'Erscheint auf',
|
||||||
page: 'Seite',
|
page: 'Seite',
|
||||||
take_photo: 'Foto aufnehmen',
|
take_photo: 'Foto aufnehmen',
|
||||||
@@ -342,7 +367,7 @@ const de = {
|
|||||||
documents_have_been_signed: 'Dokumente wurden unterschrieben!',
|
documents_have_been_signed: 'Dokumente wurden unterschrieben!',
|
||||||
create_a_free_account: 'Kostenloses Konto erstellen',
|
create_a_free_account: 'Kostenloses Konto erstellen',
|
||||||
powered_by: 'Bereitgestellt von',
|
powered_by: 'Bereitgestellt von',
|
||||||
please_check_the_box_to_continue: 'Bitte setzen Sie das Häkchen, um fortzufahren',
|
please_check_the_box_to_continue: 'Bitte setzen Sie das Häkchen, um fortzufahren.',
|
||||||
open_source_documents_software: 'Open-Source-Dokumentensoftware',
|
open_source_documents_software: 'Open-Source-Dokumentensoftware',
|
||||||
verified_phone_number: 'Telefonnummer überprüfen',
|
verified_phone_number: 'Telefonnummer überprüfen',
|
||||||
use_international_format: 'Internationales Format verwenden: +1xxx',
|
use_international_format: 'Internationales Format verwenden: +1xxx',
|
||||||
@@ -364,11 +389,17 @@ const de = {
|
|||||||
pay_with_strip: 'Mit Stripe bezahlen',
|
pay_with_strip: 'Mit Stripe bezahlen',
|
||||||
reupload: 'Erneut hochladen',
|
reupload: 'Erneut hochladen',
|
||||||
upload: 'Hochladen',
|
upload: 'Hochladen',
|
||||||
files: 'Dateien'
|
files: 'Dateien',
|
||||||
|
signature_is_too_small_or_simple_please_redraw: 'Die Unterschrift ist zu klein oder zu einfach. Bitte erneut zeichnen.',
|
||||||
|
wait_countdown_seconds: 'Warte {countdown} Sekunden'
|
||||||
}
|
}
|
||||||
|
|
||||||
const fr = {
|
const fr = {
|
||||||
|
complete_all_required_fields_to_proceed_with_identity_verification: "Veuillez remplir tous les champs obligatoires pour continuer la vérification de l'identité.",
|
||||||
|
verify_id: "Vérification de l'ID",
|
||||||
|
identity_verification: "Vérification de l'identité",
|
||||||
complete: 'Terminer',
|
complete: 'Terminer',
|
||||||
|
fill_all_required_fields_to_complete: 'Veuillez remplir tous les champs obligatoires pour compléter',
|
||||||
sign_and_complete: 'Signer et Terminer',
|
sign_and_complete: 'Signer et Terminer',
|
||||||
invite: 'Inviter',
|
invite: 'Inviter',
|
||||||
email: 'Courriel',
|
email: 'Courriel',
|
||||||
@@ -413,6 +444,7 @@ const fr = {
|
|||||||
sign_now: 'Signer maintenant',
|
sign_now: 'Signer maintenant',
|
||||||
type_here_: 'Tapez ici...',
|
type_here_: 'Tapez ici...',
|
||||||
optional: 'facultatif',
|
optional: 'facultatif',
|
||||||
|
option: 'Option',
|
||||||
appears_on: 'Apparaît sur',
|
appears_on: 'Apparaît sur',
|
||||||
page: 'Page',
|
page: 'Page',
|
||||||
take_photo: 'Prendre une photo',
|
take_photo: 'Prendre une photo',
|
||||||
@@ -434,7 +466,7 @@ const fr = {
|
|||||||
documents_have_been_signed: 'Les documents ont été signés!',
|
documents_have_been_signed: 'Les documents ont été signés!',
|
||||||
create_a_free_account: 'Créer un Compte Gratuit',
|
create_a_free_account: 'Créer un Compte Gratuit',
|
||||||
powered_by: 'Propulsé par',
|
powered_by: 'Propulsé par',
|
||||||
please_check_the_box_to_continue: 'Veuillez cocher la case pour continuer',
|
please_check_the_box_to_continue: 'Veuillez cocher la case pour continuer.',
|
||||||
open_source_documents_software: 'logiciel de documents open source',
|
open_source_documents_software: 'logiciel de documents open source',
|
||||||
verified_phone_number: 'Vérifier le numéro de téléphone',
|
verified_phone_number: 'Vérifier le numéro de téléphone',
|
||||||
use_international_format: 'Utiliser le format international : +1xxx',
|
use_international_format: 'Utiliser le format international : +1xxx',
|
||||||
@@ -456,11 +488,17 @@ const fr = {
|
|||||||
pay_with_strip: 'Paiement avec Stripe',
|
pay_with_strip: 'Paiement avec Stripe',
|
||||||
reupload: 'Recharger',
|
reupload: 'Recharger',
|
||||||
upload: 'Télécharger',
|
upload: 'Télécharger',
|
||||||
files: 'Fichiers'
|
files: 'Fichiers',
|
||||||
|
signature_is_too_small_or_simple_please_redraw: 'La signature est trop petite ou trop simple. Veuillez la redessiner.',
|
||||||
|
wait_countdown_seconds: 'Attendez {countdown} secondes'
|
||||||
}
|
}
|
||||||
|
|
||||||
const pl = {
|
const pl = {
|
||||||
|
complete_all_required_fields_to_proceed_with_identity_verification: 'Uzupełnij wszystkie wymagane pola, aby kontynuować weryfikację tożsamości.',
|
||||||
|
verify_id: 'Zweryfikuj ID',
|
||||||
|
identity_verification: 'Weryfikacja tożsamości',
|
||||||
complete: 'Zakończ',
|
complete: 'Zakończ',
|
||||||
|
fill_all_required_fields_to_complete: 'Uzupełnij wszystkie wymagane pola, aby zakończyć',
|
||||||
sign_and_complete: 'Podpisz i zakończ',
|
sign_and_complete: 'Podpisz i zakończ',
|
||||||
invite: 'Zaproś',
|
invite: 'Zaproś',
|
||||||
email: 'E-mail',
|
email: 'E-mail',
|
||||||
@@ -505,6 +543,7 @@ const pl = {
|
|||||||
sign_now: 'Podpisz teraz',
|
sign_now: 'Podpisz teraz',
|
||||||
type_here_: 'Wpisz tutaj...',
|
type_here_: 'Wpisz tutaj...',
|
||||||
optional: 'opcjonalny',
|
optional: 'opcjonalny',
|
||||||
|
option: 'Opcja',
|
||||||
appears_on: 'Pojawia się na',
|
appears_on: 'Pojawia się na',
|
||||||
page: 'Strona',
|
page: 'Strona',
|
||||||
select_your_option: 'Wybierz swoją opcję',
|
select_your_option: 'Wybierz swoją opcję',
|
||||||
@@ -526,7 +565,7 @@ const pl = {
|
|||||||
documents_have_been_signed: 'Dokumenty zostały podpisane!',
|
documents_have_been_signed: 'Dokumenty zostały podpisane!',
|
||||||
create_a_free_account: 'Utwórz darmowe konto',
|
create_a_free_account: 'Utwórz darmowe konto',
|
||||||
powered_by: 'Napędzany przez',
|
powered_by: 'Napędzany przez',
|
||||||
please_check_the_box_to_continue: 'Proszę zaznaczyć pole, aby kontynuować',
|
please_check_the_box_to_continue: 'Proszę zaznaczyć pole, aby kontynuować.',
|
||||||
open_source_documents_software: 'oprogramowanie do dokumentów open source',
|
open_source_documents_software: 'oprogramowanie do dokumentów open source',
|
||||||
verified_phone_number: 'Zweryfikuj numer telefonu',
|
verified_phone_number: 'Zweryfikuj numer telefonu',
|
||||||
use_international_format: 'Użyj międzynarodowego formatu: +1xxx',
|
use_international_format: 'Użyj międzynarodowego formatu: +1xxx',
|
||||||
@@ -548,11 +587,17 @@ const pl = {
|
|||||||
pay_with_strip: 'Płatność za pomocą Stripe',
|
pay_with_strip: 'Płatność za pomocą Stripe',
|
||||||
reupload: 'Ponowne przesłanie',
|
reupload: 'Ponowne przesłanie',
|
||||||
upload: 'Przesyłanie',
|
upload: 'Przesyłanie',
|
||||||
files: 'Pliki'
|
files: 'Pliki',
|
||||||
|
signature_is_too_small_or_simple_please_redraw: 'Podpis jest zbyt mały lub zbyt prosty. Proszę narysować go ponownie.',
|
||||||
|
wait_countdown_seconds: 'Poczekaj {countdown} sekund'
|
||||||
}
|
}
|
||||||
|
|
||||||
const uk = {
|
const uk = {
|
||||||
|
complete_all_required_fields_to_proceed_with_identity_verification: "Заповніть всі обов'язкові поля, щоб продовжити перевірку особи.",
|
||||||
|
verify_id: 'Підтвердження ідентичності',
|
||||||
|
identity_verification: 'Ідентифікація особи',
|
||||||
complete: 'Завершити',
|
complete: 'Завершити',
|
||||||
|
fill_all_required_fields_to_complete: "Заповніть всі обов'язкові поля для завершення",
|
||||||
sign_and_complete: 'Підписати і завершити',
|
sign_and_complete: 'Підписати і завершити',
|
||||||
invite: 'Запросити',
|
invite: 'Запросити',
|
||||||
email: 'Електронна пошта',
|
email: 'Електронна пошта',
|
||||||
@@ -597,6 +642,7 @@ const uk = {
|
|||||||
sign_now: 'Підписати зараз',
|
sign_now: 'Підписати зараз',
|
||||||
type_here_: 'Введіть тут',
|
type_here_: 'Введіть тут',
|
||||||
optional: 'необов’язково',
|
optional: 'необов’язково',
|
||||||
|
option: 'Опція',
|
||||||
appears_on: "З'являється на",
|
appears_on: "З'являється на",
|
||||||
page: 'Сторінка',
|
page: 'Сторінка',
|
||||||
take_photo: 'Зробити фото',
|
take_photo: 'Зробити фото',
|
||||||
@@ -618,8 +664,8 @@ const uk = {
|
|||||||
documents_have_been_signed: 'Документи були підписані!',
|
documents_have_been_signed: 'Документи були підписані!',
|
||||||
create_a_free_account: 'Створити безкоштовний обліковий запис',
|
create_a_free_account: 'Створити безкоштовний обліковий запис',
|
||||||
powered_by: 'Працює на базі',
|
powered_by: 'Працює на базі',
|
||||||
please_check_the_box_to_continue: 'Будь ласка, позначте прапорець, щоб продовжити',
|
please_check_the_box_to_continue: 'Будь ласка, позначте прапорець, щоб продовжити.',
|
||||||
open_source_documents_software: 'відкритий програмний засіб для документів',
|
open_source_documents_software: 'відкрите програмне забезпечення для документів',
|
||||||
verified_phone_number: 'Підтвердіть номер телефону',
|
verified_phone_number: 'Підтвердіть номер телефону',
|
||||||
use_international_format: 'Використовуйте міжнародний формат: +1xxx',
|
use_international_format: 'Використовуйте міжнародний формат: +1xxx',
|
||||||
six_digits_code: '6-значний код',
|
six_digits_code: '6-значний код',
|
||||||
@@ -640,11 +686,17 @@ const uk = {
|
|||||||
pay_with_strip: 'Сплатити за допомогою Stripe',
|
pay_with_strip: 'Сплатити за допомогою Stripe',
|
||||||
reupload: 'Перезавантажити',
|
reupload: 'Перезавантажити',
|
||||||
upload: 'Завантажити',
|
upload: 'Завантажити',
|
||||||
files: 'Файли'
|
files: 'Файли',
|
||||||
|
signature_is_too_small_or_simple_please_redraw: 'Підпис занадто маленький або надто простий. Будь ласка, перемалюйте.',
|
||||||
|
wait_countdown_seconds: 'Зачекайте {countdown} секунд'
|
||||||
}
|
}
|
||||||
|
|
||||||
const cs = {
|
const cs = {
|
||||||
|
complete_all_required_fields_to_proceed_with_identity_verification: 'Vyplňte všechna povinná pole, abyste mohli pokračovat v ověření identity.',
|
||||||
|
verify_id: 'Ověřit ID',
|
||||||
|
identity_verification: 'Ověření identity',
|
||||||
complete: 'Dokončit',
|
complete: 'Dokončit',
|
||||||
|
fill_all_required_fields_to_complete: 'Please complete all mandatory fields',
|
||||||
sign_and_complete: 'Podepsat a dokončit',
|
sign_and_complete: 'Podepsat a dokončit',
|
||||||
invite: 'Pozvat',
|
invite: 'Pozvat',
|
||||||
email: 'E-mail',
|
email: 'E-mail',
|
||||||
@@ -689,6 +741,7 @@ const cs = {
|
|||||||
sign_now: 'Podepsat nyní',
|
sign_now: 'Podepsat nyní',
|
||||||
type_here_: 'Zadejte zde',
|
type_here_: 'Zadejte zde',
|
||||||
optional: 'volitelné',
|
optional: 'volitelné',
|
||||||
|
option: 'Možnost',
|
||||||
appears_on: 'Zobrazuje se na',
|
appears_on: 'Zobrazuje se na',
|
||||||
page: 'Stránka',
|
page: 'Stránka',
|
||||||
select_your_option: 'Vyberte svou volbu',
|
select_your_option: 'Vyberte svou volbu',
|
||||||
@@ -710,7 +763,7 @@ const cs = {
|
|||||||
documents_have_been_signed: 'Dokumenty byly podepsány!',
|
documents_have_been_signed: 'Dokumenty byly podepsány!',
|
||||||
create_a_free_account: 'Vytvořit bezplatný účet',
|
create_a_free_account: 'Vytvořit bezplatný účet',
|
||||||
powered_by: 'Poháněno',
|
powered_by: 'Poháněno',
|
||||||
please_check_the_box_to_continue: 'Prosím, zaškrtněte políčko pro pokračování',
|
please_check_the_box_to_continue: 'Prosím, zaškrtněte políčko pro pokračování.',
|
||||||
open_source_documents_software: 'open source software pro dokumenty',
|
open_source_documents_software: 'open source software pro dokumenty',
|
||||||
verified_phone_number: 'Ověřte telefonní číslo',
|
verified_phone_number: 'Ověřte telefonní číslo',
|
||||||
use_international_format: 'Použijte mezinárodní formát: +1xxx',
|
use_international_format: 'Použijte mezinárodní formát: +1xxx',
|
||||||
@@ -732,11 +785,17 @@ const cs = {
|
|||||||
pay_with_strip: 'Zaplacení přes Stripe',
|
pay_with_strip: 'Zaplacení přes Stripe',
|
||||||
reupload: 'Znovu nahrát',
|
reupload: 'Znovu nahrát',
|
||||||
upload: 'Nahrát',
|
upload: 'Nahrát',
|
||||||
files: 'Soubory'
|
files: 'Soubory',
|
||||||
|
signature_is_too_small_or_simple_please_redraw: 'Podpis je příliš malý nebo jednoduchý. Nakreslete jej prosím znovu.',
|
||||||
|
wait_countdown_seconds: 'Počkejte {countdown} sekund'
|
||||||
}
|
}
|
||||||
|
|
||||||
const pt = {
|
const pt = {
|
||||||
|
complete_all_required_fields_to_proceed_with_identity_verification: 'Preencha todos os campos obrigatórios para prosseguir com a verificação de identidade.',
|
||||||
|
verify_id: 'Verificar ID',
|
||||||
|
identity_verification: 'Verificação de identidade',
|
||||||
complete: 'Completar',
|
complete: 'Completar',
|
||||||
|
fill_all_required_fields_to_complete: 'Preencher todos os campos obrigatórios para concluir',
|
||||||
sign_and_complete: 'Assinar e Completar',
|
sign_and_complete: 'Assinar e Completar',
|
||||||
invite: 'Convidar',
|
invite: 'Convidar',
|
||||||
email: 'E-mail',
|
email: 'E-mail',
|
||||||
@@ -781,6 +840,7 @@ const pt = {
|
|||||||
sign_now: 'Assinar agora',
|
sign_now: 'Assinar agora',
|
||||||
type_here_: 'Digite aqui',
|
type_here_: 'Digite aqui',
|
||||||
optional: 'opcional',
|
optional: 'opcional',
|
||||||
|
option: 'Opção',
|
||||||
appears_on: 'Aparece em',
|
appears_on: 'Aparece em',
|
||||||
page: 'Página',
|
page: 'Página',
|
||||||
take_photo: 'Tirar foto',
|
take_photo: 'Tirar foto',
|
||||||
@@ -802,7 +862,7 @@ const pt = {
|
|||||||
documents_have_been_signed: 'Os documentos foram assinados!',
|
documents_have_been_signed: 'Os documentos foram assinados!',
|
||||||
create_a_free_account: 'Criar uma Conta Gratuita',
|
create_a_free_account: 'Criar uma Conta Gratuita',
|
||||||
powered_by: 'Desenvolvido por',
|
powered_by: 'Desenvolvido por',
|
||||||
please_check_the_box_to_continue: 'Por favor, marque a caixa para continuar',
|
please_check_the_box_to_continue: 'Por favor, marque a caixa para continuar.',
|
||||||
open_source_documents_software: 'software de documentos de código aberto',
|
open_source_documents_software: 'software de documentos de código aberto',
|
||||||
verified_phone_number: 'Verificar Número de Telefone',
|
verified_phone_number: 'Verificar Número de Telefone',
|
||||||
use_international_format: 'Use formato internacional: +1xxx',
|
use_international_format: 'Use formato internacional: +1xxx',
|
||||||
@@ -824,11 +884,17 @@ const pt = {
|
|||||||
pay_with_strip: 'Pagar com Stripe',
|
pay_with_strip: 'Pagar com Stripe',
|
||||||
reupload: 'Reenviar',
|
reupload: 'Reenviar',
|
||||||
upload: 'Carregar',
|
upload: 'Carregar',
|
||||||
files: 'Arquivos'
|
files: 'Arquivos',
|
||||||
|
signature_is_too_small_or_simple_please_redraw: 'A assinatura é muito pequena ou simples. Por favor, redesenhe.',
|
||||||
|
wait_countdown_seconds: 'Aguarde {countdown} segundos'
|
||||||
}
|
}
|
||||||
|
|
||||||
const he = {
|
const he = {
|
||||||
|
complete_all_required_fields_to_proceed_with_identity_verification: 'מלא את כל השדות הנדרשים כדי להמשיך עם אימות זהות.',
|
||||||
|
verify_id: 'אמת מזהה',
|
||||||
|
identity_verification: 'אימות זהות',
|
||||||
complete: 'השלם',
|
complete: 'השלם',
|
||||||
|
fill_all_required_fields_to_complete: 'נא למלא את כל השדות הנדרשים להשלמה',
|
||||||
sign_and_complete: 'חתום והשלם',
|
sign_and_complete: 'חתום והשלם',
|
||||||
invite: 'הזמן',
|
invite: 'הזמן',
|
||||||
email: 'דוא"ל',
|
email: 'דוא"ל',
|
||||||
@@ -895,7 +961,7 @@ const he = {
|
|||||||
documents_have_been_signed: 'המסמכים נחתמו!',
|
documents_have_been_signed: 'המסמכים נחתמו!',
|
||||||
create_a_free_account: 'צור חשבון חינם',
|
create_a_free_account: 'צור חשבון חינם',
|
||||||
powered_by: 'מופעל על ידי',
|
powered_by: 'מופעל על ידי',
|
||||||
please_check_the_box_to_continue: 'אנא סמן את התיבה כדי להמשיך',
|
please_check_the_box_to_continue: 'אנא סמן את התיבה כדי להמשיך.',
|
||||||
open_source_documents_software: 'תוכנה פתוחה למסמכים',
|
open_source_documents_software: 'תוכנה פתוחה למסמכים',
|
||||||
verified_phone_number: 'אימות מספר טלפון',
|
verified_phone_number: 'אימות מספר טלפון',
|
||||||
use_international_format: 'השתמש בפורמט בינלאומי: +1xxx',
|
use_international_format: 'השתמש בפורמט בינלאומי: +1xxx',
|
||||||
@@ -917,11 +983,17 @@ const he = {
|
|||||||
pay_with_strip: 'שלם עם סטרייפ',
|
pay_with_strip: 'שלם עם סטרייפ',
|
||||||
reupload: 'העלה שוב',
|
reupload: 'העלה שוב',
|
||||||
upload: 'העלאה',
|
upload: 'העלאה',
|
||||||
files: 'קבצים'
|
files: 'קבצים',
|
||||||
|
signature_is_too_small_or_simple_please_redraw: 'החתימה קטנה או פשוטה מדי. אנא חתום מחדש.',
|
||||||
|
wait_countdown_seconds: 'המתן {countdown} שניות'
|
||||||
}
|
}
|
||||||
|
|
||||||
const nl = {
|
const nl = {
|
||||||
|
complete_all_required_fields_to_proceed_with_identity_verification: 'Vul alle verplichte velden in om door te gaan met de identiteitsverificatie.',
|
||||||
|
verify_id: 'Verifiëren ID',
|
||||||
|
identity_verification: 'Identiteitsverificatie',
|
||||||
complete: 'Voltooien',
|
complete: 'Voltooien',
|
||||||
|
fill_all_required_fields_to_complete: 'Vul alle verplichte velden in om te voltooien',
|
||||||
sign_and_complete: 'Ondertekenen en voltooien',
|
sign_and_complete: 'Ondertekenen en voltooien',
|
||||||
invite: 'Uitnodigen',
|
invite: 'Uitnodigen',
|
||||||
email: 'E-mail',
|
email: 'E-mail',
|
||||||
@@ -988,7 +1060,7 @@ const nl = {
|
|||||||
documents_have_been_signed: 'De documenten zijn ondertekend!',
|
documents_have_been_signed: 'De documenten zijn ondertekend!',
|
||||||
create_a_free_account: 'Maak een gratis account aan',
|
create_a_free_account: 'Maak een gratis account aan',
|
||||||
powered_by: 'Aangedreven door',
|
powered_by: 'Aangedreven door',
|
||||||
please_check_the_box_to_continue: 'Vink het vakje aan om door te gaan',
|
please_check_the_box_to_continue: 'Vink het vakje aan om door te gaan.',
|
||||||
open_source_documents_software: 'Open source documenten software',
|
open_source_documents_software: 'Open source documenten software',
|
||||||
verified_phone_number: 'Geverifieerd telefoonnummer',
|
verified_phone_number: 'Geverifieerd telefoonnummer',
|
||||||
use_international_format: 'Gebruik internationaal formaat: +1xxx',
|
use_international_format: 'Gebruik internationaal formaat: +1xxx',
|
||||||
@@ -1010,11 +1082,17 @@ const nl = {
|
|||||||
pay_with_strip: 'Betalen met Stripe',
|
pay_with_strip: 'Betalen met Stripe',
|
||||||
reupload: 'Opnieuw uploaden',
|
reupload: 'Opnieuw uploaden',
|
||||||
upload: 'Uploaden',
|
upload: 'Uploaden',
|
||||||
files: 'Bestanden'
|
files: 'Bestanden',
|
||||||
|
signature_is_too_small_or_simple_please_redraw: 'De handtekening is te klein of te eenvoudig. Teken opnieuw.',
|
||||||
|
wait_countdown_seconds: 'Wacht {countdown} seconden'
|
||||||
}
|
}
|
||||||
|
|
||||||
const ar = {
|
const ar = {
|
||||||
|
complete_all_required_fields_to_proceed_with_identity_verification: 'أكمل جميع الحقول المطلوبة للمتابعة في التحقق من الهوية.',
|
||||||
|
verify_id: 'تحقق من الهوية',
|
||||||
|
identity_verification: 'التحقق من الهوية',
|
||||||
complete: 'اكتمال',
|
complete: 'اكتمال',
|
||||||
|
fill_all_required_fields_to_complete: 'يرجى ملء جميع الحقول المطلوبة لإكمال',
|
||||||
sign_and_complete: 'التوقيع والاكتمال',
|
sign_and_complete: 'التوقيع والاكتمال',
|
||||||
invite: 'دعوة',
|
invite: 'دعوة',
|
||||||
email: 'البريد الإلكتروني',
|
email: 'البريد الإلكتروني',
|
||||||
@@ -1028,6 +1106,7 @@ const ar = {
|
|||||||
select_a_reason: 'اختر سببًا',
|
select_a_reason: 'اختر سببًا',
|
||||||
value_is_invalid: 'القيمة غير صالحة',
|
value_is_invalid: 'القيمة غير صالحة',
|
||||||
verification_code_is_invalid: 'رمز التحقق غير صالح',
|
verification_code_is_invalid: 'رمز التحقق غير صالح',
|
||||||
|
already_paid: 'تم الدفع بالفعل',
|
||||||
drawn_signature_on_a_touchscreen_device: 'توقيع مرسوم على جهاز بشاشة تعمل باللمس',
|
drawn_signature_on_a_touchscreen_device: 'توقيع مرسوم على جهاز بشاشة تعمل باللمس',
|
||||||
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'امسح رمز الاستجابة السريعة باستخدام تطبيق الكاميرا لفتح النموذج على الهاتف المحمول ورسم توقيعك',
|
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'امسح رمز الاستجابة السريعة باستخدام تطبيق الكاميرا لفتح النموذج على الهاتف المحمول ورسم توقيعك',
|
||||||
by_clicking_you_agree_to_the: 'بالنقر فوق "{button}"، أنت توافق على',
|
by_clicking_you_agree_to_the: 'بالنقر فوق "{button}"، أنت توافق على',
|
||||||
@@ -1080,7 +1159,7 @@ const ar = {
|
|||||||
documents_have_been_signed: 'تم توقيع الوثائق!',
|
documents_have_been_signed: 'تم توقيع الوثائق!',
|
||||||
create_a_free_account: 'إنشاء حساب مجاني',
|
create_a_free_account: 'إنشاء حساب مجاني',
|
||||||
powered_by: 'مدعوم من',
|
powered_by: 'مدعوم من',
|
||||||
please_check_the_box_to_continue: 'الرجاء التحقق من الخانة للمتابعة',
|
please_check_the_box_to_continue: 'الرجاء التحقق من الخانة للمتابعة.',
|
||||||
open_source_documents_software: 'برنامج وثائق مفتوح المصدر',
|
open_source_documents_software: 'برنامج وثائق مفتوح المصدر',
|
||||||
verified_phone_number: 'تحقق من رقم الهاتف',
|
verified_phone_number: 'تحقق من رقم الهاتف',
|
||||||
use_international_format: 'استخدم الشكل الدولي: +1xxx',
|
use_international_format: 'استخدم الشكل الدولي: +1xxx',
|
||||||
@@ -1102,11 +1181,17 @@ const ar = {
|
|||||||
pay_with_strip: 'الدفع بواسطة Stripe',
|
pay_with_strip: 'الدفع بواسطة Stripe',
|
||||||
reupload: 'إعادة التحميل',
|
reupload: 'إعادة التحميل',
|
||||||
upload: 'تحميل',
|
upload: 'تحميل',
|
||||||
files: 'الملفات'
|
files: 'الملفات',
|
||||||
|
signature_is_too_small_or_simple_please_redraw: 'التوقيع صغير جدًا أو بسيط جدًا. يرجى إعادة رسمه.',
|
||||||
|
wait_countdown_seconds: 'انتظر {countdown} ثانية'
|
||||||
}
|
}
|
||||||
|
|
||||||
const ko = {
|
const ko = {
|
||||||
|
complete_all_required_fields_to_proceed_with_identity_verification: '신원 확인을 진행하려면 모든 필수 필드를 작성하십시오.',
|
||||||
|
verify_id: '아이디 확인',
|
||||||
|
identity_verification: '신원 확인',
|
||||||
complete: '완료',
|
complete: '완료',
|
||||||
|
fill_all_required_fields_to_complete: '모든 필수 필드를 작성하여 완료하세요',
|
||||||
sign_and_complete: '서명하고 완료하기',
|
sign_and_complete: '서명하고 완료하기',
|
||||||
invite: '초대하기',
|
invite: '초대하기',
|
||||||
email: '이메일',
|
email: '이메일',
|
||||||
@@ -1123,6 +1208,8 @@ const ko = {
|
|||||||
by_clicking_you_agree_to_the: '"{button}"를 클릭함으로써, 다음에 동의하게 됩니다',
|
by_clicking_you_agree_to_the: '"{button}"를 클릭함으로써, 다음에 동의하게 됩니다',
|
||||||
electronic_signature_disclosure: '전자 서명 공개',
|
electronic_signature_disclosure: '전자 서명 공개',
|
||||||
esignature_disclosure: '전자 서명 공개',
|
esignature_disclosure: '전자 서명 공개',
|
||||||
|
value_is_invalid: '값이 올바르지 않습니다',
|
||||||
|
verification_code_is_invalid: '인증 코드가 올바르지 않습니다',
|
||||||
already_paid: '이미 지불됨',
|
already_paid: '이미 지불됨',
|
||||||
text: '텍스트',
|
text: '텍스트',
|
||||||
signature: '서명',
|
signature: '서명',
|
||||||
@@ -1171,7 +1258,7 @@ const ko = {
|
|||||||
documents_have_been_signed: '문서가 서명되었습니다!',
|
documents_have_been_signed: '문서가 서명되었습니다!',
|
||||||
create_a_free_account: '무료 계정 생성',
|
create_a_free_account: '무료 계정 생성',
|
||||||
powered_by: '구동',
|
powered_by: '구동',
|
||||||
please_check_the_box_to_continue: '계속하려면 확인란을 선택하십시오',
|
please_check_the_box_to_continue: '계속하려면 확인란을 선택하십시오.',
|
||||||
open_source_documents_software: '오픈 소스 문서 소프트웨어',
|
open_source_documents_software: '오픈 소스 문서 소프트웨어',
|
||||||
verified_phone_number: '전화번호 확인됨',
|
verified_phone_number: '전화번호 확인됨',
|
||||||
use_international_format: '국제 포맷 사용: +1xxx',
|
use_international_format: '국제 포맷 사용: +1xxx',
|
||||||
@@ -1193,9 +1280,110 @@ const ko = {
|
|||||||
pay_with_strip: '스트라이프로 결제',
|
pay_with_strip: '스트라이프로 결제',
|
||||||
reupload: '다시 업로드',
|
reupload: '다시 업로드',
|
||||||
upload: '업로드',
|
upload: '업로드',
|
||||||
files: '파일'
|
files: '파일',
|
||||||
|
signature_is_too_small_or_simple_please_redraw: '서명이 너무 작거나 단순합니다. 다시 그려주세요.',
|
||||||
|
wait_countdown_seconds: '{countdown}초 기다리세요'
|
||||||
}
|
}
|
||||||
|
|
||||||
const i18n = { en, es, it, de, fr, pl, uk, cs, pt, he, nl, ar, ko }
|
const ja = {
|
||||||
|
complete_all_required_fields_to_proceed_with_identity_verification: '本人確認を進めるには、すべての必須項目を入力してください。',
|
||||||
|
verify_id: '本人確認',
|
||||||
|
identity_verification: '本人確認',
|
||||||
|
complete: '完了',
|
||||||
|
fill_all_required_fields_to_complete: '完了するにはすべての必須項目を入力してください',
|
||||||
|
sign_and_complete: '署名して完了',
|
||||||
|
text: 'テキスト',
|
||||||
|
by_clicking_you_agree_to_the: '"{button}" をクリックすることで、次に同意したことになります:',
|
||||||
|
electronic_signature_disclosure: '電子署名に関する開示',
|
||||||
|
esignature_disclosure: '電子署名開示',
|
||||||
|
signature: '署名',
|
||||||
|
initials: 'イニシャル',
|
||||||
|
drawn_signature_on_a_touchscreen_device: 'タッチスクリーンデバイスで描かれた署名',
|
||||||
|
approved: '承認済み',
|
||||||
|
reviewed: '確認済み',
|
||||||
|
other: 'その他',
|
||||||
|
authored_by_me: '自分が作成',
|
||||||
|
invite: '招待',
|
||||||
|
email: 'メール',
|
||||||
|
approved_by: '承認者',
|
||||||
|
reviewed_by: '確認者',
|
||||||
|
authored_by: '作成者',
|
||||||
|
select_a_reason: '理由を選択',
|
||||||
|
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'QRコードをカメラアプリで読み取り、モバイルでフォームを開いて署名を描いてください',
|
||||||
|
date: '日付',
|
||||||
|
number: '数値',
|
||||||
|
value_is_invalid: '無効な値です',
|
||||||
|
verification_code_is_invalid: '認証コードが無効です',
|
||||||
|
already_paid: 'すでに支払い済み',
|
||||||
|
image: '画像',
|
||||||
|
pay: '支払う',
|
||||||
|
take_photo: '写真を撮る',
|
||||||
|
number_phone_is_invalid: '{number} の電話番号は無効です',
|
||||||
|
file: 'ファイル',
|
||||||
|
digitally_signed_by: '電子署名者:',
|
||||||
|
reason: '理由',
|
||||||
|
select: '選択',
|
||||||
|
checkbox: 'チェックボックス',
|
||||||
|
multiple: '複数選択',
|
||||||
|
radio: 'ラジオボタン',
|
||||||
|
cells: 'セル',
|
||||||
|
stamp: 'スタンプ',
|
||||||
|
minimize: '最小化',
|
||||||
|
payment: '支払い',
|
||||||
|
phone: '電話',
|
||||||
|
start_now: '今すぐ開始',
|
||||||
|
continue: '続行',
|
||||||
|
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: 'フォームが完了しました!',
|
||||||
|
document_has_been_signed: 'ドキュメントが署名されました!',
|
||||||
|
documents_have_been_signed: 'ドキュメントがすべて署名されました!',
|
||||||
|
create_a_free_account: '無料アカウントを作成',
|
||||||
|
powered_by: '提供元:',
|
||||||
|
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: 'Stripeで支払う',
|
||||||
|
reupload: '再アップロード',
|
||||||
|
upload: 'アップロード',
|
||||||
|
files: 'ファイル',
|
||||||
|
signature_is_too_small_or_simple_please_redraw: '署名が小さすぎるか単純すぎます。もう一度描いてください。',
|
||||||
|
wait_countdown_seconds: '{countdown} 秒お待ちください'
|
||||||
|
}
|
||||||
|
|
||||||
|
const i18n = { en, es, it, de, fr, pl, uk, cs, pt, he, nl, ar, ko, ja }
|
||||||
|
|
||||||
export default i18n
|
export default i18n
|
||||||
@@ -1,11 +1,21 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="modelValue">
|
<div v-if="modelValue">
|
||||||
<div class="flex justify-between items-center w-full mb-2">
|
<div class="flex justify-between items-end w-full mb-3.5 md:mb-4">
|
||||||
<label
|
<label
|
||||||
class="label text-2xl"
|
v-if="showFieldNames"
|
||||||
>{{ showFieldNames && field.name ? field.name : t('image') }}</label>
|
:for="field.uuid"
|
||||||
|
class="label text-xl sm:text-2xl py-0 field-name-label"
|
||||||
|
>
|
||||||
|
<MarkdownContent
|
||||||
|
v-if="field.title"
|
||||||
|
:string="field.title"
|
||||||
|
/>
|
||||||
|
<template v-else>
|
||||||
|
{{ field.name || t('image') }}
|
||||||
|
</template>
|
||||||
|
</label>
|
||||||
<button
|
<button
|
||||||
class="btn btn-outline btn-sm"
|
class="btn btn-outline btn-sm reupload-button"
|
||||||
@click.prevent="remove"
|
@click.prevent="remove"
|
||||||
>
|
>
|
||||||
<IconReload :width="16" />
|
<IconReload :width="16" />
|
||||||
@@ -15,7 +25,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<img
|
<img
|
||||||
:src="attachmentsIndex[modelValue].url"
|
:src="attachmentsIndex[modelValue].url"
|
||||||
class="h-52 border border-base-300 rounded mx-auto"
|
class="h-52 border border-base-300 rounded mx-auto uploaded-image-preview"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
@@ -30,12 +40,12 @@
|
|||||||
<div
|
<div
|
||||||
v-if="field.description"
|
v-if="field.description"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
class="mb-3 px-1"
|
class="mb-3 px-1 field-description-text"
|
||||||
>
|
>
|
||||||
<MarkdownContent :string="field.description" />
|
<MarkdownContent :string="field.description" />
|
||||||
</div>
|
</div>
|
||||||
<FileDropzone
|
<FileDropzone
|
||||||
:message="`${t('upload')} ${field.name || t('image')}${field.required ? '' : ` (${t('optional')})`}`"
|
:message="`${t('upload')} ${(field.title || field.name) || t('image')}${field.required ? '' : ` (${t('optional')})`}`"
|
||||||
:submitter-slug="submitterSlug"
|
:submitter-slug="submitterSlug"
|
||||||
:dry-run="dryRun"
|
:dry-run="dryRun"
|
||||||
:accept="'image/*'"
|
:accept="'image/*'"
|
||||||
|
|||||||
@@ -1,21 +1,22 @@
|
|||||||
<template>
|
<template>
|
||||||
<div dir="auto">
|
<div dir="auto">
|
||||||
<div
|
<div
|
||||||
class="flex justify-between items-center w-full"
|
class="flex justify-between items-end w-full mb-3.5 md:mb-4"
|
||||||
:class="{ 'mb-2': !field.description }"
|
:class="{ 'mb-2': !field.description }"
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
class="label text-2xl"
|
v-if="showFieldNames"
|
||||||
|
class="label text-xl sm:text-2xl py-0 field-name-label"
|
||||||
>
|
>
|
||||||
<MarkdownContent
|
<MarkdownContent
|
||||||
v-if="field.title"
|
v-if="field.title"
|
||||||
:string="field.title"
|
:string="field.title"
|
||||||
/>
|
/>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
{{ showFieldNames && field.name ? field.name : t('initials') }}
|
{{ field.name || t('initials') }}
|
||||||
</template>
|
</template>
|
||||||
</label>
|
</label>
|
||||||
<div class="space-x-2 flex">
|
<div class="space-x-2 flex flex-none">
|
||||||
<span
|
<span
|
||||||
v-if="isDrawInitials"
|
v-if="isDrawInitials"
|
||||||
class="tooltip"
|
class="tooltip"
|
||||||
@@ -24,7 +25,7 @@
|
|||||||
<a
|
<a
|
||||||
id="type_text_button"
|
id="type_text_button"
|
||||||
href="#"
|
href="#"
|
||||||
class="btn btn-outline font-medium btn-sm"
|
class="btn btn-outline font-medium btn-sm type-text-button"
|
||||||
@click.prevent="toggleTextInput"
|
@click.prevent="toggleTextInput"
|
||||||
>
|
>
|
||||||
<IconTextSize :width="16" />
|
<IconTextSize :width="16" />
|
||||||
@@ -41,7 +42,7 @@
|
|||||||
<a
|
<a
|
||||||
id="type_text_button"
|
id="type_text_button"
|
||||||
href="#"
|
href="#"
|
||||||
class="btn btn-outline font-medium btn-sm"
|
class="btn btn-outline font-medium btn-sm type-text-button"
|
||||||
@click.prevent="toggleTextInput"
|
@click.prevent="toggleTextInput"
|
||||||
>
|
>
|
||||||
<IconSignature :width="16" />
|
<IconSignature :width="16" />
|
||||||
@@ -54,9 +55,7 @@
|
|||||||
class="tooltip"
|
class="tooltip"
|
||||||
:data-tip="t('click_to_upload')"
|
:data-tip="t('click_to_upload')"
|
||||||
>
|
>
|
||||||
<label
|
<label class="btn btn-outline btn-sm font-medium inline-flex flex-nowrap upload-image-button">
|
||||||
class="btn btn-outline btn-sm font-medium inline-flex flex-nowrap"
|
|
||||||
>
|
|
||||||
<IconUpload :width="16" />
|
<IconUpload :width="16" />
|
||||||
<input
|
<input
|
||||||
:key="uploadImageInputKey"
|
:key="uploadImageInputKey"
|
||||||
@@ -73,7 +72,7 @@
|
|||||||
<a
|
<a
|
||||||
v-if="modelValue || computedPreviousValue"
|
v-if="modelValue || computedPreviousValue"
|
||||||
href="#"
|
href="#"
|
||||||
class="btn font-medium btn-outline btn-sm"
|
class="btn font-medium btn-outline btn-sm clear-canvas-button"
|
||||||
@click.prevent="remove"
|
@click.prevent="remove"
|
||||||
>
|
>
|
||||||
<IconReload :width="16" />
|
<IconReload :width="16" />
|
||||||
@@ -82,7 +81,7 @@
|
|||||||
<a
|
<a
|
||||||
v-else
|
v-else
|
||||||
href="#"
|
href="#"
|
||||||
class="btn font-medium btn-outline btn-sm"
|
class="btn font-medium btn-outline btn-sm clear-canvas-button"
|
||||||
@click.prevent="clear"
|
@click.prevent="clear"
|
||||||
>
|
>
|
||||||
<IconReload :width="16" />
|
<IconReload :width="16" />
|
||||||
@@ -104,7 +103,7 @@
|
|||||||
<div
|
<div
|
||||||
v-if="field.description"
|
v-if="field.description"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
class="mb-3 px-1"
|
class="mb-3 px-1 field-description-text"
|
||||||
>
|
>
|
||||||
<MarkdownContent :string="field.description" />
|
<MarkdownContent :string="field.description" />
|
||||||
</div>
|
</div>
|
||||||
@@ -119,11 +118,17 @@
|
|||||||
:src="attachmentsIndex[modelValue || computedPreviousValue].url"
|
:src="attachmentsIndex[modelValue || computedPreviousValue].url"
|
||||||
class="mx-auto bg-white border border-base-300 rounded max-h-44"
|
class="mx-auto bg-white border border-base-300 rounded max-h-44"
|
||||||
>
|
>
|
||||||
<canvas
|
<div class="relative">
|
||||||
v-show="!modelValue && !computedPreviousValue"
|
<div
|
||||||
ref="canvas"
|
v-if="!isDrawInitials"
|
||||||
class="bg-white border border-base-300 rounded-2xl w-full"
|
class="absolute top-0 right-0 left-0 bottom-0"
|
||||||
/>
|
/>
|
||||||
|
<canvas
|
||||||
|
v-show="!modelValue && !computedPreviousValue"
|
||||||
|
ref="canvas"
|
||||||
|
class="bg-white border border-base-300 rounded-2xl w-full draw-canvas"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<input
|
<input
|
||||||
v-if="!isDrawInitials && !modelValue && !computedPreviousValue"
|
v-if="!isDrawInitials && !modelValue && !computedPreviousValue"
|
||||||
id="initials_text_input"
|
id="initials_text_input"
|
||||||
@@ -226,6 +231,10 @@ export default {
|
|||||||
if (this.$refs.canvas) {
|
if (this.$refs.canvas) {
|
||||||
this.pad = new SignaturePad(this.$refs.canvas)
|
this.pad = new SignaturePad(this.$refs.canvas)
|
||||||
|
|
||||||
|
if (this.field.preferences?.color) {
|
||||||
|
this.pad.penColor = this.field.preferences.color
|
||||||
|
}
|
||||||
|
|
||||||
this.pad.addEventListener('beginStroke', () => {
|
this.pad.addEventListener('beginStroke', () => {
|
||||||
this.isInitialsStarted = true
|
this.isInitialsStarted = true
|
||||||
|
|
||||||
@@ -330,8 +339,8 @@ export default {
|
|||||||
return Promise.resolve({})
|
return Promise.resolve({})
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve, reject) => {
|
||||||
cropCanvasAndExportToPNG(this.$refs.canvas).then(async (blob) => {
|
cropCanvasAndExportToPNG(this.$refs.canvas, { errorOnTooSmall: true }).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.dryRun) {
|
if (this.dryRun) {
|
||||||
@@ -364,6 +373,14 @@ export default {
|
|||||||
return resolve(attachment)
|
return resolve(attachment)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}).catch((error) => {
|
||||||
|
if (this.field.required === true) {
|
||||||
|
alert(this.t('signature_is_too_small_or_simple_please_redraw'))
|
||||||
|
|
||||||
|
return reject(error)
|
||||||
|
} else {
|
||||||
|
return resolve({})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
:value="authenticityToken"
|
:value="authenticityToken"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="(submitter, index) in submitters"
|
v-for="(submitter, index) in [...submitters, ...optionalSubmitters]"
|
||||||
:key="submitter.uuid"
|
:key="submitter.uuid"
|
||||||
:class="{ 'mt-4': index !== 0 }"
|
:class="{ 'mt-4': index !== 0 }"
|
||||||
>
|
>
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
dir="auto"
|
dir="auto"
|
||||||
class="label text-2xl"
|
class="label text-2xl"
|
||||||
>
|
>
|
||||||
{{ t('invite') }} {{ submitter.name }}
|
{{ t('invite') }} {{ submitter.name }} <template v-if="!submitters.includes(submitter)">({{ t('optional') }})</template>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
:id="submitter.uuid"
|
:id="submitter.uuid"
|
||||||
@@ -34,13 +34,13 @@
|
|||||||
class="base-input !text-2xl w-full"
|
class="base-input !text-2xl w-full"
|
||||||
:placeholder="t('email')"
|
:placeholder="t('email')"
|
||||||
type="email"
|
type="email"
|
||||||
required
|
:required="submitters.includes(submitter)"
|
||||||
autofocus="true"
|
autofocus="true"
|
||||||
name="submission[submitters][][email]"
|
name="submission[submitters][][email]"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="mt-6 md:mt-8"
|
class="mt-4 md:mt-6"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
@@ -53,7 +53,7 @@
|
|||||||
class="mr-1 animate-spin"
|
class="mr-1 animate-spin"
|
||||||
/>
|
/>
|
||||||
<span>
|
<span>
|
||||||
{{ t('submit') }}
|
{{ t('complete') }}
|
||||||
</span><span
|
</span><span
|
||||||
v-if="isSubmitting"
|
v-if="isSubmitting"
|
||||||
class="w-6 flex justify-start mr-1"
|
class="w-6 flex justify-start mr-1"
|
||||||
@@ -78,6 +78,11 @@ export default {
|
|||||||
type: Array,
|
type: Array,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
optionalSubmitters: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
url: {
|
url: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
@@ -3,18 +3,24 @@
|
|||||||
v-if="showFieldNames && (field.name || field.title)"
|
v-if="showFieldNames && (field.name || field.title)"
|
||||||
:for="field.uuid"
|
:for="field.uuid"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
class="label text-2xl"
|
class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5 field-name-label"
|
||||||
:class="{ 'mb-2': !field.description }"
|
:class="{ 'mb-2': !field.description }"
|
||||||
><MarkdownContent
|
>
|
||||||
v-if="field.title"
|
<MarkdownContent
|
||||||
:string="field.title"
|
v-if="field.title"
|
||||||
/>
|
:string="field.title"
|
||||||
|
/>
|
||||||
<template v-else>{{ field.name }}</template>
|
<template v-else>{{ field.name }}</template>
|
||||||
|
<template v-if="!field.required">
|
||||||
|
<span :class="{ 'hidden sm:inline': (field.title || field.name).length > 20 }">
|
||||||
|
({{ t('optional') }})
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
</label>
|
</label>
|
||||||
<div
|
<div
|
||||||
v-if="field.description"
|
v-if="field.description"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
class="mb-3 px-1"
|
class="mb-3 px-1 field-description-text"
|
||||||
>
|
>
|
||||||
<MarkdownContent :string="field.description" />
|
<MarkdownContent :string="field.description" />
|
||||||
</div>
|
</div>
|
||||||
@@ -44,7 +50,7 @@
|
|||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
:for="option.uuid"
|
:for="option.uuid"
|
||||||
class="flex items-center space-x-3"
|
class="flex items-center space-x-3 checkbox-label"
|
||||||
@click="scrollIntoField(field)"
|
@click="scrollIntoField(field)"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -3,15 +3,18 @@
|
|||||||
v-if="showFieldNames && (field.name || field.title)"
|
v-if="showFieldNames && (field.name || field.title)"
|
||||||
:for="field.uuid"
|
:for="field.uuid"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
class="label text-2xl"
|
class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5 field-name-label"
|
||||||
:class="{ 'mb-2': !field.description }"
|
:class="{ 'mb-2': !field.description }"
|
||||||
><MarkdownContent
|
>
|
||||||
v-if="field.title"
|
<MarkdownContent
|
||||||
:string="field.title"
|
v-if="field.title"
|
||||||
/>
|
:string="field.title"
|
||||||
|
/>
|
||||||
<template v-else>{{ field.name }}</template>
|
<template v-else>{{ field.name }}</template>
|
||||||
<template v-if="!field.required">
|
<template v-if="!field.required">
|
||||||
({{ t('optional') }})
|
<span :class="{ 'hidden sm:inline': (field.title || field.name).length > 20 }">
|
||||||
|
({{ t('optional') }})
|
||||||
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</label>
|
</label>
|
||||||
<div
|
<div
|
||||||
@@ -21,7 +24,7 @@
|
|||||||
<div
|
<div
|
||||||
v-if="field.description"
|
v-if="field.description"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
class="mb-3 px-1"
|
class="mb-3 px-1 field-description-text"
|
||||||
>
|
>
|
||||||
<MarkdownContent :string="field.description" />
|
<MarkdownContent :string="field.description" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<label
|
<label
|
||||||
v-if="!modelValue && !sessionId"
|
v-if="!modelValue && !sessionId"
|
||||||
:for="field.uuid"
|
class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5 field-name-label"
|
||||||
class="label text-2xl mb-2"
|
|
||||||
>
|
>
|
||||||
<MarkdownContent
|
<MarkdownContent
|
||||||
v-if="field.title"
|
v-if="field.title"
|
||||||
@@ -35,7 +34,7 @@
|
|||||||
<button
|
<button
|
||||||
v-if="sessionId"
|
v-if="sessionId"
|
||||||
disabled
|
disabled
|
||||||
class="base-button w-full"
|
class="base-button w-full modal-save-button"
|
||||||
>
|
>
|
||||||
<IconLoader
|
<IconLoader
|
||||||
width="22"
|
width="22"
|
||||||
|
|||||||
Loaded 100 of 444 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user