Compare commits

..

295 Commits

Author SHA1 Message Date
Alex Turchyn 1f48be135c Merge from docusealco/wip 2025-03-31 13:02:51 +03:00
Pete Matsyburka 1cae12b728 adjust validation 2025-03-29 23:15:45 +02:00
Alex Turchyn 86d2cd93f8 fix signature uploading 2025-03-29 15:44:19 +02:00
Alex Turchyn b0110d9340 update signature validation 2025-03-29 14:50:26 +02:00
Alex Turchyn 7e70f4fb14 update translations 2025-03-29 13:37:21 +02:00
Pete Matsyburka 28e3d65e63 improve required field handling 2025-03-28 22:42:27 +02:00
Pete Matsyburka f1a2b71b59 fix smtp 2025-03-27 19:50:09 +02:00
Pete Matsyburka 47c7270911 enforce signing order 2025-03-27 19:23:20 +02:00
Pete Matsyburka 4e1fa336a2 add personalization options 2025-03-27 19:23:19 +02:00
Pete Matsyburka 244bc32136 fix preview url 2025-03-27 00:52:35 +02:00
Pete Matsyburka 42c30bdfd0 ensure generated doc completed 2025-03-26 13:14:59 +02:00
Alex Turchyn 0abf48ddba update preview image 2025-03-26 13:14:07 +02:00
Alex Turchyn ef05f030de Merge from docusealco/wip 2025-03-24 14:03:04 +02:00
Pete Matsyburka b8c36ff09c update gems 2025-03-24 09:48:37 +02:00
Pete Matsyburka 1ba8a9a374 fix pdf fields extract 2025-03-24 09:44:10 +02:00
Pete Matsyburka 661e9b69fb do not validate values param 2025-03-21 13:42:20 +02:00
Pete Matsyburka a1aeabccff fix scroll 2025-03-20 21:07:28 +02:00
Pete Matsyburka 0e5e0e7772 fix pdf upload 2025-03-19 17:57:46 +02:00
Alex Turchyn 2a74fdd095 add field area classes 2025-03-19 15:33:02 +02:00
Alex Turchyn ac426e55b2 add missing CSS class for custom fields 2025-03-18 22:24:54 +02:00
Pete Matsyburka ad2b35673a fix class 2025-03-18 20:31:00 +02:00
Pete Matsyburka af1e8fa6f2 adjust i18n 2025-03-18 17:34:31 +02:00
Pete Matsyburka 5800cb6c5a fix default value condition 2025-03-18 17:34:31 +02:00
Alex Turchyn 64d3360e82 add ui classes 2025-03-18 17:34:11 +02:00
Alex Turchyn 3ae98bf03e improve multiple submitters error message 2025-03-18 17:31:40 +02:00
Alex Turchyn 65e81d90f4 fix acro radio fields 2025-03-17 22:38:42 +02:00
Pete Matsyburka 4fb9637e2d add merge roles param 2025-03-17 13:30:07 +02:00
Pete Matsyburka 6b3726dbd5 update gem 2025-03-15 15:01:46 +02:00
Pete Matsyburka c5b0f2c76c fix require value validation 2025-03-13 19:21:28 +02:00
Pete Matsyburka ea18f9e28a fix combined pdf 2025-03-13 14:21:53 +02:00
Pete Matsyburka d229c75978 fix draw field 2025-03-12 19:14:00 +02:00
Pete Matsyburka ecbe3c3e4f PRESIGNED_URLS_EXPIRE_MINUTES 2025-03-12 14:24:33 +02:00
Pete Matsyburka 38a5cb968b extract pdf date fields 2025-03-12 10:23:24 +02:00
Pete Matsyburka a30ee9e961 fix perform at 2025-03-11 11:28:22 +02:00
Pete Matsyburka eba27004dc font format date number 2025-03-11 10:51:04 +02:00
Pete Matsyburka 09849acd2f add submission.expired webhook 2025-03-10 22:53:56 +02:00
Pete Matsyburka 19e9047a39 reduce max attempts 2025-03-10 15:54:21 +02:00
Pete Matsyburka 82b6bdd9ad update gem 2025-03-10 15:05:09 +02:00
Alex Turchyn 5764b4dac1 Merge from docusealco/wip 2025-03-10 13:13:01 +02:00
Pete Matsyburka 3affd67d24 add font field preferences 2025-03-09 12:46:28 +02:00
Pete Matsyburka d5a98a0f9e web concurrency auto 2025-03-08 16:11:11 +02:00
Pete Matsyburka 9629cc7226 make remember configurable 2025-03-05 20:44:32 +02:00
Pete Matsyburka 06415ad869 add routes hook 2025-03-05 18:29:39 +02:00
Pete Matsyburka 3f872da435 fix mask 2025-03-05 18:07:38 +02:00
Pete Matsyburka bc1d5a9825 add font type 2025-03-05 12:12:16 +02:00
Pete Matsyburka 72c8ed9a00 update gem 2025-03-04 22:17:01 +02:00
Pete Matsyburka 3fec433846 fix builder editable 2025-03-04 19:46:55 +02:00
Pete Matsyburka 3b572702d7 add mask size 2025-03-04 01:02:18 +02:00
Alex Turchyn febdc0b48d add specs for masked fields 2025-03-03 22:43:27 +02:00
Pete Matsyburka 8773bb2f62 disable cfr toggle 2025-03-03 13:21:10 +02:00
Pete Matsyburka 55ad70fabc fix builder default date format 2025-03-03 11:24:38 +02:00
Pete Matsyburka 23dcd62672 add field mask 2025-03-03 00:18:56 +02:00
Alex Turchyn 42371d7ce3 Merge from docusealco/wip 2025-03-02 15:37:12 +02:00
Pete Matsyburka 4116746be3 destroy email events 2025-02-28 12:12:02 +02:00
Pete Matsyburka b2b2856ac0 handle required pdf fields 2025-02-27 21:37:32 +02:00
Alex Turchyn b65bca213f add brakeman to CI 2025-02-27 18:43:57 +02:00
Pete Matsyburka 7a72e2cbb7 allow http 2025-02-27 10:59:32 +02:00
Pete Matsyburka 3a73398a1e fix email 2025-02-27 10:01:33 +02:00
Pete Matsyburka cce20d9230 fix rubocop 2025-02-25 14:39:11 +02:00
Pete Matsyburka 8b72c91951 add console audit tables 2025-02-25 13:16:11 +02:00
Pete Matsyburka b0d8e47a75 fix annotate model 2025-02-25 13:10:33 +02:00
Pete Matsyburka 85824faa2b https webhooks 2025-02-25 10:33:16 +02:00
Pete Matsyburka 8a507339af add brakeman 2025-02-25 00:54:05 +02:00
Pete Matsyburka 4636849118 fix permitted params 2025-02-24 23:59:30 +02:00
Pete Matsyburka cc52cae9fc fix not found page 2025-02-24 14:19:48 +02:00
Alex Turchyn b6bb4d2c8b Merge from docusealco/wip 2025-02-24 14:05:38 +02:00
Pete Matsyburka 5032676bd2 fix url download 2025-02-24 13:24:16 +02:00
Pete Matsyburka 146b37f8cd fix clone 2025-02-21 22:21:31 +02:00
Pete Matsyburka 47b3a2d09e multiple recipient emails 2025-02-21 21:49:33 +02:00
Pete Matsyburka f2306b334b update gem 2025-02-20 13:02:58 +02:00
Pete Matsyburka 3affa65571 fix formula condition 2025-02-20 12:13:52 +02:00
Pete Matsyburka 6f3be16826 fix dockerfile 2025-02-18 16:50:48 +02:00
Pete Matsyburka 07ce76537b use ubuntu arm 2025-02-18 16:50:01 +02:00
Pete Matsyburka a1fb38b8cb add email variables 2025-02-18 12:18:59 +02:00
Alex Turchyn 1b52a99662 Merge from docusealco/wip 2025-02-17 22:55:09 +02:00
Pete Matsyburka f5facfa6b9 use ubuntu latest build 2025-02-17 22:49:14 +02:00
Pete Matsyburka 21c6144dc6 fix document controller 2025-02-17 18:45:52 +02:00
Pete Matsyburka 2c155f0e42 use ip to reopen 2025-02-17 12:46:44 +02:00
Alex Turchyn 33cc0a8ab5 Merge from docusealco/wip 2025-02-17 12:36:04 +02:00
Pete Matsyburka 1e65a7e52b fix combined 2025-02-14 22:28:59 +02:00
Pete Matsyburka 14e2dbe1aa detailed party email form 2025-02-14 18:12:15 +02:00
Pete Matsyburka a823a6ac56 fix template clone 2025-02-14 18:12:09 +02:00
Pete Matsyburka cfc4e001ba refactor annots 2025-02-14 12:33:49 +02:00
Pete Matsyburka 496cc855f6 refactor 2025-02-14 11:00:12 +02:00
Pete Matsyburka bfb2db07a5 fix events modal 2025-02-13 16:46:57 +02:00
Pete Matsyburka 9f4f868246 use erb_lint 2025-02-13 13:19:33 +02:00
Pete Matsyburka f91f02f786 remove seats info 2025-02-13 00:10:43 +02:00
Pete Matsyburka 1da72e3e7a update gem 2025-02-12 21:31:56 +02:00
Pete Matsyburka 2e2c0e087d skip missing font 2025-02-12 20:38:46 +02:00
Pete Matsyburka 0c72f58ab0 fix combined 2025-02-12 20:38:46 +02:00
Alex Turchyn d9d969cb79 add translations 2025-02-12 14:31:49 +02:00
Pete Matsyburka 8d685d1f73 adjust event log 2025-02-11 22:37:47 +02:00
Alex Turchyn e406620044 add 'Event Log' modal 2025-02-11 22:23:51 +02:00
Pete Matsyburka a315674421 update gem 2025-02-11 11:18:15 +02:00
Alex Turchyn 8d953f210f add attachment count attribute to template factory 2025-02-11 11:16:32 +02:00
Alex Turchyn 7439ef07ad Merge from docusealco/wip 2025-02-10 13:08:17 +02:00
Pete Matsyburka fc4b4de6ed customize template email per party 2025-02-09 14:13:04 +02:00
Alex Turchyn 96e4733f87 add submissions filtering by 'sent' and 'opened' statuses 2025-02-07 20:20:37 +02:00
Pete Matsyburka 8b6731a289 show logo 2025-02-07 14:48:04 +02:00
Pete Matsyburka f93ac68e9c adjust field drag 2025-02-07 14:39:23 +02:00
Alex Turchyn 439df4879c add stamp text logo 2025-02-07 14:15:35 +02:00
Alex Turchyn 83b6ce8574 fix logos 2025-02-07 14:15:18 +02:00
Pete Matsyburka 6ea07bc13b show minimize on mobile if with description 2025-02-06 13:49:09 +02:00
Alex Turchyn a15624690b show unauthorized message in test environment 2025-02-06 13:41:16 +02:00
Pete Matsyburka 974bb85fb7 rescue malformed PDF 2025-02-06 12:55:27 +02:00
Pete Matsyburka 5dc80601b2 courier font 2025-02-05 18:17:55 +02:00
Pete Matsyburka df2e53bc40 font mono 2025-02-05 16:13:31 +02:00
Alex Turchyn 6e685bbc32 Merge from docusealco/wip 2025-02-05 13:44:10 +02:00
Pete Matsyburka 0b3ca0de96 fix ci 2025-02-04 19:37:42 +02:00
Pete Matsyburka aed3f1f141 fix build 2025-02-04 18:47:15 +02:00
Pete Matsyburka 1a7981e919 update actions 2025-02-04 16:55:24 +02:00
Pete Matsyburka 4add805371 fix field align 2025-02-04 16:19:49 +02:00
Alex Turchyn 66c18317b7 Merge from docusealco/wip 2025-02-04 16:11:47 +02:00
Pete Matsyburka 9edbb885b4 use arm runner 2025-02-04 15:51:31 +02:00
Alex Turchyn c67c80d6bc Merge from docusealco/wip 2025-02-04 13:44:17 +02:00
Pete Matsyburka 0399531e0c html image 2025-02-03 18:38:52 +02:00
Pete Matsyburka 54426556d9 field conditions 2025-02-02 21:36:38 +02:00
Alex Turchyn fa74ee1755 add Progressive Web App 2025-02-02 14:31:42 +02:00
Pete Matsyburka eb53af12c4 adjust form 2025-02-02 12:36:42 +02:00
Pete Matsyburka 38efcb99c0 fix verification 2025-01-31 21:32:15 +02:00
Pete Matsyburka a92d914674 adjust form 2025-01-31 20:11:57 +02:00
Pete Matsyburka 219a698aa5 adjust i18n 2025-01-31 15:26:53 +02:00
Alex Turchyn 5bed6448dc make custom fields titles smaller on mobile devices 2025-01-31 15:19:04 +02:00
Alex Turchyn 6d2a8ca3d3 update translations 2025-01-31 14:43:06 +02:00
Alex Turchyn bfb2e0043d update email attribution footer for Test Mode 2025-01-31 14:43:06 +02:00
Pete Matsyburka 922101b12c fix dashboard toggle 2025-01-31 12:03:52 +02:00
Pete Matsyburka 9ff41a0c4a scroll on mobile field click 2025-01-29 16:08:09 +02:00
Pete Matsyburka 071f187605 fix template conditions 2025-01-29 00:56:14 +02:00
Pete Matsyburka ab6cd0e89c adjust send email job 2025-01-28 18:52:12 +02:00
Pete Matsyburka 97c0dad7ff add with fields search 2025-01-28 14:13:06 +02:00
Pete Matsyburka bc0917a190 fix sign in date 2025-01-28 11:16:24 +02:00
Alex Turchyn 5718acecff add translations 2025-01-28 11:15:33 +02:00
Alex Turchyn 395d43dbeb Merge from docusealco/wip 2025-01-27 13:21:32 +02:00
Pete Matsyburka 37de5e0d0d add resubmit from dashboard 2025-01-25 20:22:48 +02:00
Pete Matsyburka 44e848a1f0 fix heading move 2025-01-25 00:07:20 +02:00
Pete Matsyburka f6d59d731c fix email typo 2025-01-24 13:32:12 +02:00
Pete Matsyburka 87b039e709 fix submitter names 2025-01-23 22:21:01 +02:00
Pete Matsyburka e0c6a4e08b fix submission preview page 2025-01-23 17:11:50 +02:00
Pete Matsyburka 80b06550c2 adjust template link 2025-01-23 11:39:43 +02:00
Pete Matsyburka ec8e35ed7a fix email typo 2025-01-22 23:22:34 +02:00
Alex Turchyn 4dc325a6bb change open/read timeout for webhook requests 2025-01-22 20:58:47 +02:00
Pete Matsyburka d52c57df72 adjust i18n 2025-01-22 11:49:04 +02:00
Alex Turchyn 0cb2557447 add warning message to navbar 2025-01-21 13:10:27 +02:00
Pete Matsyburka cd05c1fd19 search field by title 2025-01-20 23:08:19 +02:00
Pete Matsyburka 5458e82b6e increase parties limit 2025-01-20 21:01:13 +02:00
Pete Matsyburka c59c58624b convert icon to png 2025-01-20 14:46:43 +02:00
Alex Turchyn 2da059d440 Merge from docusealco/wip 2025-01-20 12:48:34 +02:00
Pete Matsyburka b0aabd9e79 add signed option in filename 2025-01-19 16:16:00 +02:00
Pete Matsyburka f1cd60dfad error message 2025-01-19 16:02:40 +02:00
Alex Turchyn a264a8522a add compliances section 2025-01-18 16:39:20 +02:00
Pete Matsyburka 794e339425 add verification settings 2025-01-18 16:35:07 +02:00
Pete Matsyburka 9aaf61e658 add number currency format 2025-01-17 21:08:13 +02:00
Pete Matsyburka 37faa7ece6 do not remove condition on replace 2025-01-17 17:11:10 +02:00
Pete Matsyburka 666e3fde4c fix document remove 2025-01-17 17:06:37 +02:00
Pete Matsyburka 995e040a4e Revert "do not remove condition on replace"
This reverts commit c24ae1751e.
2025-01-17 16:55:11 +02:00
Pete Matsyburka 185ac2cbee refactor 2025-01-17 15:52:49 +02:00
Pete Matsyburka a4e372ff5c fix android scroll 2025-01-17 01:49:36 +02:00
Pete Matsyburka b4834c7674 fix filename date 2025-01-16 16:30:06 +02:00
Pete Matsyburka c24ae1751e do not remove condition on replace 2025-01-16 01:05:06 +02:00
Pete Matsyburka 9bd0ad70c4 add default field options 2025-01-16 00:40:22 +02:00
Pete Matsyburka f210f1d8bf Revert "fix scrolling to fields on Android devices"
This reverts commit e3f3895aac.
2025-01-15 13:57:30 +02:00
Alex Turchyn 92b61992f2 fix vertical scrolling for mobile devices 2025-01-15 02:52:31 +02:00
Alex Turchyn e3f3895aac fix scrolling to fields on Android devices 2025-01-14 23:23:09 +02:00
Alex Turchyn c696890cf5 fix dropdowns in Firefox 2025-01-14 01:57:20 +02:00
Alex Turchyn 288d204d8e Merge from docusealco/wip 2025-01-13 13:00:50 +02:00
Pete Matsyburka 5a96f4f1eb adjust ios mobile 2025-01-11 14:02:04 +02:00
Alex Turchyn 5ff4f9889e add user reviews 2025-01-11 13:09:38 +02:00
Alex Turchyn 78fb93fd02 fix detection of unknown mobile IOS devices 2025-01-11 13:06:21 +02:00
Pete Matsyburka 3c83d459bd add document page attributes 2025-01-10 20:33:23 +02:00
Pete Matsyburka 6cdb8e7132 fix draw cursor 2025-01-10 20:33:16 +02:00
Pete Matsyburka e2b93cbcab fix colors 2025-01-10 15:12:09 +02:00
Pete Matsyburka a59cc7c44e verification redirect 2025-01-10 11:50:54 +02:00
Pete Matsyburka 1984b99e04 fix formula with conditions 2025-01-09 22:25:46 +02:00
Pete Matsyburka fe3e8fba3e fix readonly formula values 2025-01-09 12:26:56 +02:00
Pete Matsyburka 97ecdf2ff0 fix invite submitters api order 2025-01-09 01:09:26 +02:00
Alex Turchyn e4c62087d0 fix translations 2025-01-08 18:17:46 +02:00
Pete Matsyburka 1f9faddfd0 add reply to 2025-01-07 20:38:56 +02:00
Pete Matsyburka 6dff6483bf fix default field title 2025-01-07 17:30:21 +02:00
Pete Matsyburka 2e43eb189f fix define submitters order 2025-01-07 13:04:59 +02:00
Alex Turchyn cee471bfa5 Merge from docusealco/wip 2025-01-06 12:18:51 +02:00
Pete Matsyburka 81521dee81 add ability to reorder fields 2025-01-04 14:20:47 +02:00
Pete Matsyburka 3cac62d0fd fix typo 2025-01-04 01:42:25 +02:00
Pete Matsyburka 24ed3f2e7c filter submissions api 2025-01-04 01:31:54 +02:00
Alex Turchyn 02ad620c9f add translations 2025-01-04 01:10:22 +02:00
Alex Turchyn 9051d5a835 add a text hint under user roles 2025-01-03 18:39:50 +02:00
Alex Turchyn 0abd270f56 update translations 2025-01-03 01:09:29 +02:00
Pete Matsyburka fde9affd70 fix send to next 2025-01-02 23:32:24 +02:00
Pete Matsyburka 4edee2971a superadmin file download 2025-01-02 22:42:08 +02:00
Pete Matsyburka 313d7f8408 adjust response 2025-01-02 22:42:02 +02:00
Alex Turchyn db60810272 add email OTP 2025-01-01 14:38:24 +02:00
Pete Matsyburka b3507c1a3a adjust test toggle size 2024-12-31 19:57:41 +02:00
Alex Turchyn 19b5a07bac Add 'Test mode' switch to the profile dropdown 2024-12-31 19:36:14 +02:00
Pete Matsyburka 616aced4e4 fix spec 2024-12-30 21:38:02 +02:00
Alex Turchyn cc218fc0b0 Merge from docusealco/wip 2024-12-30 13:32:14 +02:00
Pete Matsyburka c59f2a022a increase retries 2024-12-28 16:58:20 +02:00
Pete Matsyburka 2a240cebeb allow to move fields between pages 2024-12-28 14:03:09 +02:00
Pete Matsyburka b950b1c63e ruby 3.4.1 2024-12-27 01:02:45 +02:00
Pete Matsyburka 4ad0b04b7f remove sleep 2024-12-25 12:51:41 +02:00
Pete Matsyburka 8edf6405b0 adjust param validation 2024-12-24 00:29:25 +02:00
Pete Matsyburka 8e3caae849 render headings 2024-12-23 22:58:24 +02:00
Alex Turchyn ac9a5c9556 Merge from docusealco/wip 2024-12-23 14:04:15 +02:00
Alex Turchyn 20d09f6dff add countdown for resend code 2024-12-23 01:06:05 +02:00
Pete Matsyburka 333a758688 heading value 2024-12-22 20:44:58 +02:00
Pete Matsyburka 2c8b4d5816 fix typo 2024-12-22 18:33:07 +02:00
Pete Matsyburka 9ce822bce8 add user tokens 2024-12-22 11:33:15 +02:00
Pete Matsyburka 0a382a688c remove schema rb 2024-12-22 01:11:25 +02:00
Pete Matsyburka bd3008a35a fix sidekiq 2024-12-22 00:51:38 +02:00
Alex Turchyn e9bd11cb2e update rails to v8.0.1 2024-12-22 00:04:08 +02:00
Pete Matsyburka ca429bc445 adjust default name 2024-12-20 16:00:38 +02:00
Pete Matsyburka 21a0c54843 fix 2024-12-20 13:58:32 +02:00
Pete Matsyburka 2f41d13009 add order fields 2024-12-20 13:58:12 +02:00
Alex Turchyn f69dcf77de set field alignment from PDF form field preferences 2024-12-20 13:58:00 +02:00
Pete Matsyburka 32596d738c optional invite 2024-12-19 17:33:24 +02:00
Pete Matsyburka e7e5d0e776 add signature formats 2024-12-19 00:42:35 +02:00
Alex Turchyn bb3b38e76f Merge from docusealco/wip 2024-12-18 21:30:59 +02:00
Alex Turchyn bc4c21f804 fix CI 2024-12-18 21:11:07 +02:00
Pete Matsyburka 2fb5de4fa9 fix submission schema 2024-12-18 16:53:35 +02:00
Pete Matsyburka 2e58b2e37b disable certs 2024-12-17 23:25:04 +02:00
Alex Turchyn 780f5512f4 Merge from docusealco/wip 2024-12-16 18:24:32 +02:00
Pete Matsyburka 2cf4908d53 fix filter 2024-12-13 23:19:38 +02:00
Pete Matsyburka 4a9b898713 edit required field 2024-12-13 21:24:30 +02:00
Alex Turchyn bb552d13d4 don't raise validation error if the message is empty 2024-12-13 20:55:37 +02:00
Pete Matsyburka 15d06f4fb0 add status filters 2024-12-13 20:46:26 +02:00
Pete Matsyburka 35a2aba887 adjust permanently remove 2024-12-13 13:31:54 +02:00
Pete Matsyburka 48a82b1d49 add import image 2024-12-13 12:48:44 +02:00
Pete Matsyburka b8453e14c4 add version 2024-12-13 12:04:45 +02:00
Pete Matsyburka 29fbdfbe2f cells align 2024-12-13 00:24:29 +02:00
Pete Matsyburka 5340af0bc6 remove reason: 2024-12-12 19:16:24 +02:00
Pete Matsyburka 1881b6c50f add sidekiq queue 2024-12-12 18:38:14 +02:00
Alex Turchyn b6a2aae970 restrict user invites 2024-12-12 14:04:09 +02:00
Pete Matsyburka b317094192 custom field width height 2024-12-11 19:32:29 +02:00
Pete Matsyburka ab1f4cf297 refactor clone 2024-12-10 23:25:53 +02:00
Pete Matsyburka d3033c072a update document name on clone 2024-12-10 22:27:37 +02:00
Pete Matsyburka f380200e03 add clone document params 2024-12-10 17:42:49 +02:00
Pete Matsyburka a4928268ea fix folder templates 2024-12-09 21:48:47 +02:00
Alex Turchyn dd18c51384 Merge from docusealco/wip 2024-12-09 17:25:23 +02:00
Alex Turchyn c2014a1549 fix field drawings on tablets 2024-12-09 14:43:44 +02:00
Pete Matsyburka 46edc7dc37 adjust title 2024-12-09 12:55:02 +02:00
Pete Matsyburka d62d83ef8e fix duplciate email 2024-12-09 01:06:57 +02:00
Pete Matsyburka 79350c8fba fix submission page 2024-12-08 17:50:23 +02:00
Pete Matsyburka 6702dd52b6 update dep 2024-12-08 15:19:39 +02:00
Pete Matsyburka c8b74a2c43 adjust role 2024-12-08 15:12:02 +02:00
Pete Matsyburka 1746920eb5 add template access 2024-12-08 14:52:22 +02:00
Pete Matsyburka bacbc4309c skip heading 2024-12-07 12:59:28 +02:00
Pete Matsyburka 4429d742b3 document conditions 2024-12-07 02:04:50 +02:00
Pete Matsyburka 38b5eccb65 adjust readonly fields 2024-12-06 00:47:40 +02:00
Pete Matsyburka d8126c4a47 uniq submission.submitters 2024-12-05 19:39:10 +02:00
Pete Matsyburka b1c31672ee use tz country code 2024-12-04 17:40:46 +02:00
Pete Matsyburka 975f3a95fe add db search path 2024-12-04 14:00:13 +02:00
Alex Turchyn e3bed5ae67 add iso codes to phone data 2024-12-04 12:16:55 +02:00
Pete Matsyburka 4737b103c0 fix i18n 2024-12-02 20:17:39 +02:00
Alex Turchyn c5178b96d3 Merge from docusealco/wip 2024-12-02 14:30:33 +02:00
Pete Matsyburka 39a2159196 fix values hash 2024-12-01 11:41:19 +02:00
Pete Matsyburka 5724159b7b verification 2024-12-01 11:23:51 +02:00
Pete Matsyburka 05b7cc9fa6 fix link 2024-11-29 22:03:09 +02:00
Pete Matsyburka 18b13b5b1e adjust policy links 2024-11-29 19:49:44 +02:00
Alex Turchyn d25555b9b0 add policy links to personalization settings 2024-11-29 13:52:47 +02:00
Pete Matsyburka eec266ee83 fix archived false api 2024-11-29 13:30:33 +02:00
Alex Turchyn 75683631f0 add logical 'or' to field conditions 2024-11-29 13:28:11 +02:00
Pete Matsyburka 36c64d1de4 fix counters 2024-11-28 14:57:20 +02:00
Pete Matsyburka 2552b24f21 add documents copy attachment toggle 2024-11-27 21:59:45 +02:00
Alex Turchyn e6ca6dfd85 fix missing file format 2024-11-25 19:40:21 +02:00
Alex Turchyn fa2d86b20a Merge from docusealco/wip 2024-11-25 12:49:04 +02:00
Pete Matsyburka 8e212e4586 adjust title size 2024-11-24 13:18:37 +02:00
Pete Matsyburka a59c18f04b adjust filters ui 2024-11-24 13:03:58 +02:00
Pete Matsyburka 1ab0f1c90c fix form error message 2024-11-23 22:21:25 +02:00
Pete Matsyburka 9beaaa0851 adjust filters 2024-11-23 17:38:11 +02:00
Alex Turchyn b86aac86d1 add template submissions filters 2024-11-22 22:23:20 +02:00
Pete Matsyburka c2eb94b571 update url 2024-11-22 12:10:29 +02:00
Pete Matsyburka a377e45a63 use product email url 2024-11-22 10:30:42 +02:00
Pete Matsyburka 58574abc6b fix filename 2024-11-21 23:18:27 +02:00
Pete Matsyburka cbae888971 add file host param 2024-11-21 23:03:30 +02:00
Pete Matsyburka 944e1f31db adjust condition 2024-11-21 18:23:34 +02:00
Pete Matsyburka 711b3b3190 use filename format in email attachments 2024-11-20 21:49:37 +02:00
Pete Matsyburka e7abadcefb add filename format option 2024-11-20 01:12:26 +02:00
Alex Turchyn 18d60f5522 add signing form specs 2024-11-19 21:28:58 +02:00
Pete Matsyburka 6d5c6e086d use email host 2024-11-19 00:05:41 +02:00
Pete Matsyburka 5a48ab0360 configure form link 2024-11-18 21:08:32 +02:00
Pete Matsyburka 599785b6d1 fix number cast empty string 2024-11-17 21:29:20 +02:00
Pete Matsyburka f2fd5fb82b fix default number 2024-11-17 21:15:10 +02:00
Pete Matsyburka f2782bd84e fix form show 2024-11-16 23:06:00 +02:00
Pete Matsyburka e52a721fc9 fix typo 2024-11-16 18:19:25 +02:00
Pete Matsyburka b035123ff9 disable email var 2024-11-16 16:13:30 +02:00
Pete Matsyburka ca8de23082 use account archived at 2024-11-15 22:17:55 +02:00
Pete Matsyburka f514654c40 rails spec hook 2024-11-15 00:30:33 +02:00
Pete Matsyburka 99bc528a6c fix spec 2024-11-14 23:36:03 +02:00
Pete Matsyburka f3c77d0177 fix bulk order 2024-11-14 22:19:22 +02:00
Pete Matsyburka db2b5f35bd fix cells length 2024-11-14 21:15:40 +02:00
Pete Matsyburka 5728381725 fix role 2024-11-14 20:09:29 +02:00
Pete Matsyburka a7affb1a49 add link to export 2024-11-14 20:04:51 +02:00
Pete Matsyburka 21bf5ed9f7 fix fields message 2024-11-14 14:38:28 +02:00
Pete Matsyburka 8ddd4ee916 adjust duration 2024-11-14 09:35:20 +02:00
Pete Matsyburka 0e2fd8ee4f use email host 2024-11-13 17:53:55 +02:00
Pete Matsyburka 27901063ed fix preview 2024-11-12 17:11:09 +02:00
Pete Matsyburka ce31ebde1e add delay 2024-11-11 19:32:27 +02:00
288 changed files with 7502 additions and 1783 deletions
+58
View File
@@ -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:
- ''
View File
+40 -13
View File
@@ -7,13 +7,13 @@ jobs:
name: Rubocop
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.3.4
ruby-version: 3.4.1
- name: Cache gems
uses: actions/cache@v1
uses: actions/cache@v4
with:
path: vendor/bundle
key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }}
@@ -31,13 +31,13 @@ jobs:
name: Erblint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.3.4
ruby-version: 3.4.1
- name: Cache gems
uses: actions/cache@v1
uses: actions/cache@v4
with:
path: vendor/bundle
key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }}
@@ -49,13 +49,13 @@ jobs:
bundle config path vendor/bundle
bundle install --jobs 4 --retry 4
- name: Run Erblint
run: bundle exec erblint ./app
run: bundle exec erb_lint ./app
eslint:
name: ESLint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Install Node.js
uses: actions/setup-node@v1
with:
@@ -63,7 +63,7 @@ jobs:
- name: Cache directory path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v1
- uses: actions/cache@v4
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
@@ -77,6 +77,33 @@ jobs:
run: |
./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.1
- 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:
name: RSpec
runs-on: ubuntu-latest
@@ -96,11 +123,11 @@ jobs:
--health-retries 5
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.3.4
ruby-version: 3.4.1
- name: Set up Node
uses: actions/setup-node@v1
with:
@@ -110,12 +137,12 @@ jobs:
with:
chrome-version: 125
- name: Cache node_modules
uses: actions/cache@v1
uses: actions/cache@v4
with:
path: node_modules
key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }}
- name: Cache gems
uses: actions/cache@v1
uses: actions/cache@v4
with:
path: vendor/bundle
key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }}
+5 -5
View File
@@ -7,7 +7,7 @@ on:
jobs:
build:
runs-on: ubuntu-latest
runs-on: ubuntu-24.04-arm
steps:
- name: Checkout code
@@ -25,22 +25,22 @@ jobs:
tags: |
type=semver,pattern={{version}}
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v3
- name: Create .version file
run: echo ${{ github.ref_name }} > .version
- name: Login to Docker Hub
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v4
uses: docker/build-push-action@v6
with:
context: .
push: true
+3 -3
View File
@@ -60,16 +60,16 @@ RSpec/NestedGroups:
Max: 6
RSpec/MultipleExpectations:
Max: 20
Max: 25
RSpec/ExampleLength:
Max: 40
Max: 50
RSpec/MultipleMemoizedHelpers:
Max: 9
Metrics/BlockNesting:
Max: 4
Max: 5
Rails/I18nLocaleTexts:
Enabled: false
+5 -4
View File
@@ -1,4 +1,4 @@
FROM ruby:3.3.4-alpine as fonts
FROM ruby:3.4.1-alpine AS 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")'
FROM ruby:3.3.4-alpine as webpack
FROM ruby:3.4.1-alpine AS webpack
ENV RAILS_ENV=production
ENV NODE_ENV=production
@@ -32,7 +32,7 @@ COPY ./app/views ./app/views
RUN echo "gem 'shakapacker'" > Gemfile && ./bin/shakapacker
FROM ruby:3.3.4-alpine as app
FROM ruby:3.4.1-alpine AS app
ENV RAILS_ENV=production
ENV BUNDLE_WITHOUT="development:test"
@@ -62,12 +62,13 @@ RUN apk add --no-cache build-base && bundle install && apk del --no-cache build-
COPY ./bin ./bin
COPY ./app ./app
COPY ./config ./config
COPY ./db ./db
COPY ./db/migrate ./db/migrate
COPY ./log ./log
COPY ./lib ./lib
COPY ./public ./public
COPY ./tmp ./tmp
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/FreeSans.ttf /usr/share/fonts/freefont
+4 -2
View File
@@ -2,7 +2,7 @@
source 'https://rubygems.org'
ruby '3.3.4'
ruby '3.4.1'
gem 'arabic-letter-connector', require: 'arabic-letter-connector/logic'
gem 'aws-sdk-s3', require: false
@@ -46,7 +46,6 @@ gem 'twitter_cldr', require: false
gem 'tzinfo-data'
group :development, :test do
gem 'annotate'
gem 'better_html'
gem 'bullet'
gem 'debug'
@@ -63,6 +62,9 @@ group :development, :test do
end
group :development do
gem 'annotaterb'
gem 'brakeman', require: false
gem 'foreman', require: false
gem 'letter_opener_web'
gem 'web-console'
end
+184 -160
View File
@@ -1,67 +1,67 @@
GEM
remote: https://rubygems.org/
specs:
actioncable (7.2.1.1)
actionpack (= 7.2.1.1)
activesupport (= 7.2.1.1)
actioncable (8.0.1)
actionpack (= 8.0.1)
activesupport (= 8.0.1)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6)
actionmailbox (7.2.1.1)
actionpack (= 7.2.1.1)
activejob (= 7.2.1.1)
activerecord (= 7.2.1.1)
activestorage (= 7.2.1.1)
activesupport (= 7.2.1.1)
actionmailbox (8.0.1)
actionpack (= 8.0.1)
activejob (= 8.0.1)
activerecord (= 8.0.1)
activestorage (= 8.0.1)
activesupport (= 8.0.1)
mail (>= 2.8.0)
actionmailer (7.2.1.1)
actionpack (= 7.2.1.1)
actionview (= 7.2.1.1)
activejob (= 7.2.1.1)
activesupport (= 7.2.1.1)
actionmailer (8.0.1)
actionpack (= 8.0.1)
actionview (= 8.0.1)
activejob (= 8.0.1)
activesupport (= 8.0.1)
mail (>= 2.8.0)
rails-dom-testing (~> 2.2)
actionpack (7.2.1.1)
actionview (= 7.2.1.1)
activesupport (= 7.2.1.1)
actionpack (8.0.1)
actionview (= 8.0.1)
activesupport (= 8.0.1)
nokogiri (>= 1.8.5)
racc
rack (>= 2.2.4, < 3.2)
rack (>= 2.2.4)
rack-session (>= 1.0.1)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
useragent (~> 0.16)
actiontext (7.2.1.1)
actionpack (= 7.2.1.1)
activerecord (= 7.2.1.1)
activestorage (= 7.2.1.1)
activesupport (= 7.2.1.1)
actiontext (8.0.1)
actionpack (= 8.0.1)
activerecord (= 8.0.1)
activestorage (= 8.0.1)
activesupport (= 8.0.1)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
actionview (7.2.1.1)
activesupport (= 7.2.1.1)
actionview (8.0.1)
activesupport (= 8.0.1)
builder (~> 3.1)
erubi (~> 1.11)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
activejob (7.2.1.1)
activesupport (= 7.2.1.1)
activejob (8.0.1)
activesupport (= 8.0.1)
globalid (>= 0.3.6)
activemodel (7.2.1.1)
activesupport (= 7.2.1.1)
activerecord (7.2.1.1)
activemodel (= 7.2.1.1)
activesupport (= 7.2.1.1)
activemodel (8.0.1)
activesupport (= 8.0.1)
activerecord (8.0.1)
activemodel (= 8.0.1)
activesupport (= 8.0.1)
timeout (>= 0.4.0)
activestorage (7.2.1.1)
actionpack (= 7.2.1.1)
activejob (= 7.2.1.1)
activerecord (= 7.2.1.1)
activesupport (= 7.2.1.1)
activestorage (8.0.1)
actionpack (= 8.0.1)
activejob (= 8.0.1)
activerecord (= 8.0.1)
activesupport (= 8.0.1)
marcel (~> 1.0)
activesupport (7.2.1.1)
activesupport (8.0.1)
base64
benchmark (>= 0.3)
bigdecimal
concurrent-ruby (~> 1.0, >= 1.3.1)
connection_pool (>= 2.2.5)
@@ -71,31 +71,30 @@ GEM
minitest (>= 5.1)
securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5)
uri (>= 0.13.1)
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
annotate (3.2.0)
activerecord (>= 3.2, < 8.0)
rake (>= 10.4, < 14.0)
annotaterb (4.14.0)
arabic-letter-connector (0.1.1)
ast (2.4.2)
aws-eventstream (1.3.0)
aws-partitions (1.985.0)
aws-sdk-core (3.209.1)
aws-partitions (1.1027.0)
aws-sdk-core (3.214.0)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.651.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.94.0)
aws-sdk-core (~> 3, >= 3.207.0)
aws-sdk-kms (1.96.0)
aws-sdk-core (~> 3, >= 3.210.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.167.0)
aws-sdk-core (~> 3, >= 3.207.0)
aws-sdk-s3 (1.176.1)
aws-sdk-core (~> 3, >= 3.210.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
aws-sdk-secretsmanager (1.108.0)
aws-sdk-core (~> 3, >= 3.207.0)
aws-sdk-secretsmanager (1.110.0)
aws-sdk-core (~> 3, >= 3.210.0)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.10.0)
aws-sigv4 (1.10.1)
aws-eventstream (~> 1, >= 1.0.2)
azure-storage-blob (2.0.3)
azure-storage-common (~> 2.0)
@@ -107,6 +106,7 @@ GEM
nokogiri (~> 1, >= 1.10.8)
base64 (0.2.0)
bcrypt (3.1.20)
benchmark (0.4.0)
better_html (2.1.1)
actionview (>= 6.0)
activesupport (>= 6.0)
@@ -118,8 +118,10 @@ GEM
bindex (0.8.1)
bootsnap (1.18.4)
msgpack (~> 1.2)
brakeman (7.0.0)
racc
builder (3.3.0)
bullet (7.2.0)
bullet (8.0.0)
activesupport (>= 3.0.0)
uniform_notifier (~> 1.11)
camertron-eprun (1.1.1)
@@ -145,14 +147,14 @@ GEM
bigdecimal
rexml
crass (1.0.6)
css_parser (1.19.0)
css_parser (1.21.0)
addressable
csv (3.3.0)
csv (3.3.2)
cuprite (0.15.1)
capybara (~> 3.0)
ferrum (~> 0.15.0)
date (3.3.4)
debug (1.9.2)
date (3.4.1)
debug (1.10.0)
irb (~> 1.10)
reline (>= 0.3.8)
declarative (0.0.20)
@@ -162,32 +164,32 @@ GEM
railties (>= 4.1.0)
responders
warden (~> 1.2.3)
devise-two-factor (6.0.0)
activesupport (~> 7.0)
devise-two-factor (6.1.0)
activesupport (>= 7.0, < 8.1)
devise (~> 4.0)
railties (~> 7.0)
railties (>= 7.0, < 8.1)
rotp (~> 6.0)
diff-lcs (1.5.1)
digest-crc (0.6.5)
rake (>= 12.0.0, < 14.0.0)
docile (1.4.1)
dotenv (3.1.4)
dotenv (3.1.7)
drb (2.2.1)
email_typo (0.2.3)
erb_lint (0.6.0)
erb_lint (0.7.0)
activesupport
better_html (>= 2.0.1)
parser (>= 2.7.1.4)
rainbow
rubocop (>= 1)
smart_properties
erubi (1.13.0)
erubi (1.13.1)
factory_bot (6.5.0)
activesupport (>= 5.0.0)
factory_bot_rails (6.4.3)
factory_bot (~> 6.4)
factory_bot_rails (6.4.4)
factory_bot (~> 6.5)
railties (>= 5.0.0)
faker (3.4.2)
faker (3.5.1)
i18n (>= 1.8.11, < 2)
faraday (1.10.4)
faraday-em_http (~> 1.0)
@@ -207,8 +209,8 @@ GEM
faraday-follow_redirects (0.3.0)
faraday (>= 1, < 3)
faraday-httpclient (1.0.1)
faraday-multipart (1.0.4)
multipart-post (~> 2)
faraday-multipart (1.1.0)
multipart-post (~> 2.0)
faraday-net_http (1.0.2)
faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0)
@@ -221,9 +223,13 @@ GEM
concurrent-ruby (~> 1.1)
webrick (~> 1.7)
websocket-driver (~> 0.7)
ffi (1.17.0)
ffi (1.17.0-aarch64-linux-gnu)
ffi (1.17.0-x86_64-linux-gnu)
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)
globalid (1.2.1)
activesupport (>= 6.1)
@@ -235,9 +241,9 @@ GEM
mutex_m
representable (~> 3.0)
retriable (>= 2.0, < 4.a)
google-apis-iamcredentials_v1 (0.21.0)
google-apis-iamcredentials_v1 (0.22.0)
google-apis-core (>= 0.15.0, < 2.a)
google-apis-storage_v1 (0.46.0)
google-apis-storage_v1 (0.49.0)
google-apis-core (>= 0.15.0, < 2.a)
google-cloud-core (1.7.1)
google-cloud-env (>= 1.0, < 3.a)
@@ -245,7 +251,7 @@ GEM
google-cloud-env (2.2.1)
faraday (>= 1.0, < 3.a)
google-cloud-errors (1.4.0)
google-cloud-storage (1.52.0)
google-cloud-storage (1.54.0)
addressable (~> 2.8)
digest-crc (~> 0.4)
google-apis-core (~> 0.13)
@@ -254,15 +260,17 @@ GEM
google-cloud-core (~> 1.6)
googleauth (~> 1.9)
mini_mime (~> 1.0)
googleauth (1.11.1)
google-logging-utils (0.1.0)
googleauth (1.12.2)
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)
multi_json (~> 1.11)
os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a)
hashdiff (1.1.1)
hexapdf (0.47.0)
hashdiff (1.1.2)
hexapdf (1.0.3)
cmdparse (~> 3.0, >= 3.0.3)
geom2d (~> 0.4, >= 0.4.1)
openssl (>= 2.2.1)
@@ -273,12 +281,12 @@ GEM
image_processing (1.13.0)
mini_magick (>= 4.9.5, < 5)
ruby-vips (>= 2.0.17, < 3)
io-console (0.7.2)
irb (1.14.1)
io-console (0.8.0)
irb (1.14.3)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
jmespath (1.6.2)
json (2.7.2)
json (2.9.1)
jwt (2.9.3)
base64
language_server-protocol (3.17.0.3)
@@ -292,13 +300,13 @@ GEM
letter_opener (~> 1.9)
railties (>= 6.1)
rexml
logger (1.6.1)
logger (1.6.4)
lograge (0.14.0)
actionpack (>= 4)
activesupport (>= 4)
railties (>= 4)
request_store (~> 1.0)
loofah (2.22.0)
loofah (2.23.1)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
mail (2.8.1)
@@ -311,16 +319,16 @@ GEM
method_source (1.1.0)
mini_magick (4.13.2)
mini_mime (1.1.5)
mini_portile2 (2.8.7)
minitest (5.25.1)
msgpack (1.7.3)
mini_portile2 (2.8.8)
minitest (5.25.4)
msgpack (1.7.5)
multi_json (1.15.0)
multipart-post (2.4.1)
mutex_m (0.2.0)
mutex_m (0.3.0)
mysql2 (0.5.6)
net-http-persistent (4.0.4)
net-http-persistent (4.0.5)
connection_pool (~> 2.2)
net-imap (0.4.17)
net-imap (0.5.6)
date
net-protocol
net-pop (0.1.2)
@@ -329,28 +337,34 @@ GEM
timeout
net-smtp (0.5.0)
net-protocol
nio4r (2.7.3)
nokogiri (1.16.7)
nio4r (2.7.4)
nokogiri (1.18.5)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
nokogiri (1.16.7-aarch64-linux)
nokogiri (1.18.5-aarch64-linux-gnu)
racc (~> 1.4)
nokogiri (1.16.7-x86_64-linux)
nokogiri (1.18.5-aarch64-linux-musl)
racc (~> 1.4)
oj (3.16.6)
nokogiri (1.18.5-arm64-darwin)
racc (~> 1.4)
nokogiri (1.18.5-x86_64-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.5-x86_64-linux-musl)
racc (~> 1.4)
oj (3.16.8)
bigdecimal (>= 3.0)
ostruct (>= 0.2)
openssl (3.2.0)
openssl (3.3.0)
orm_adapter (0.5.0)
os (1.1.4)
ostruct (0.6.0)
ostruct (0.6.1)
package_json (0.1.0)
pagy (9.1.0)
pagy (9.3.3)
parallel (1.26.3)
parser (3.3.5.0)
parser (3.3.6.0)
ast (~> 2.4.1)
racc
pg (1.5.8)
pg (1.5.9)
premailer (1.27.0)
addressable
css_parser (>= 1.19.0)
@@ -361,58 +375,58 @@ GEM
premailer (~> 1.7, >= 1.7.9)
pretender (0.5.0)
actionpack (>= 6.1)
pry (0.14.2)
pry (0.15.0)
coderay (~> 1.1)
method_source (~> 1.0)
pry-rails (0.3.11)
pry (>= 0.13.0)
psych (5.1.2)
psych (5.2.2)
date
stringio
public_suffix (6.0.1)
puma (6.4.3)
puma (6.5.0)
nio4r (~> 2.0)
racc (1.8.1)
rack (3.1.8)
rack (3.1.12)
rack-proxy (0.7.7)
rack
rack-session (2.0.0)
rack (>= 3.0.0)
rack-test (2.1.0)
rack (>= 1.3)
rackup (2.1.0)
rackup (2.2.1)
rack (>= 3)
webrick (~> 1.8)
rails (7.2.1.1)
actioncable (= 7.2.1.1)
actionmailbox (= 7.2.1.1)
actionmailer (= 7.2.1.1)
actionpack (= 7.2.1.1)
actiontext (= 7.2.1.1)
actionview (= 7.2.1.1)
activejob (= 7.2.1.1)
activemodel (= 7.2.1.1)
activerecord (= 7.2.1.1)
activestorage (= 7.2.1.1)
activesupport (= 7.2.1.1)
rails (8.0.1)
actioncable (= 8.0.1)
actionmailbox (= 8.0.1)
actionmailer (= 8.0.1)
actionpack (= 8.0.1)
actiontext (= 8.0.1)
actionview (= 8.0.1)
activejob (= 8.0.1)
activemodel (= 8.0.1)
activerecord (= 8.0.1)
activestorage (= 8.0.1)
activesupport (= 8.0.1)
bundler (>= 1.15.0)
railties (= 7.2.1.1)
railties (= 8.0.1)
rails-dom-testing (2.2.0)
activesupport (>= 5.0.0)
minitest
nokogiri (>= 1.6)
rails-html-sanitizer (1.6.0)
rails-html-sanitizer (1.6.2)
loofah (~> 2.21)
nokogiri (~> 1.14)
rails-i18n (7.0.9)
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 (8.0.1)
i18n (>= 0.7, < 2)
railties (>= 6.0.0, < 8)
railties (>= 8.0.0, < 9)
rails_autolink (1.1.8)
actionview (> 3.1)
activesupport (> 3.1)
railties (> 3.1)
railties (7.2.1.1)
actionpack (= 7.2.1.1)
activesupport (= 7.2.1.1)
railties (8.0.1)
actionpack (= 8.0.1)
activesupport (= 8.0.1)
irb (~> 1.13)
rackup (>= 1.0.0)
rake (>= 12.2)
@@ -420,12 +434,12 @@ GEM
zeitwerk (~> 2.6)
rainbow (3.1.1)
rake (13.2.1)
rdoc (6.7.0)
rdoc (6.10.0)
psych (>= 4.0.0)
redis-client (0.22.2)
redis-client (0.23.0)
connection_pool
regexp_parser (2.9.2)
reline (0.5.10)
regexp_parser (2.9.3)
reline (0.6.0)
io-console (~> 0.5)
representable (3.2.0)
declarative (< 0.1.0)
@@ -437,13 +451,13 @@ GEM
actionpack (>= 5.2)
railties (>= 5.2)
retriable (3.1.2)
rexml (3.3.9)
rexml (3.4.0)
rotp (6.3.0)
rqrcode (2.2.0)
chunky_png (~> 1.0)
rqrcode_core (~> 1.0)
rqrcode_core (1.2.0)
rspec-core (3.13.1)
rspec-core (3.13.2)
rspec-support (~> 3.13.0)
rspec-expectations (3.13.3)
diff-lcs (>= 1.2.0, < 2.0)
@@ -451,7 +465,7 @@ GEM
rspec-mocks (3.13.2)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-rails (7.0.1)
rspec-rails (7.1.0)
actionpack (>= 7.0)
activesupport (>= 7.0)
railties (>= 7.0)
@@ -459,48 +473,47 @@ GEM
rspec-expectations (~> 3.13)
rspec-mocks (~> 3.13)
rspec-support (~> 3.13)
rspec-support (3.13.1)
rubocop (1.66.1)
rspec-support (3.13.2)
rubocop (1.69.2)
json (~> 2.3)
language_server-protocol (>= 3.17.0)
parallel (~> 1.10)
parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 2.4, < 3.0)
rubocop-ast (>= 1.32.2, < 2.0)
regexp_parser (>= 2.9.3, < 3.0)
rubocop-ast (>= 1.36.2, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.32.3)
unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.37.0)
parser (>= 3.3.1.0)
rubocop-performance (1.22.1)
rubocop-performance (1.23.0)
rubocop (>= 1.48.1, < 2.0)
rubocop-ast (>= 1.31.1, < 2.0)
rubocop-rails (2.26.2)
rubocop-rails (2.27.0)
activesupport (>= 4.2.0)
rack (>= 1.1)
rubocop (>= 1.52.0, < 2.0)
rubocop-ast (>= 1.31.1, < 2.0)
rubocop-rspec (3.1.0)
rubocop-rspec (3.3.0)
rubocop (~> 1.61)
ruby-progressbar (1.13.0)
ruby-vips (2.2.2)
ffi (~> 1.12)
logger
ruby2_keywords (0.0.5)
rubyXL (3.4.27)
rubyXL (3.4.33)
nokogiri (>= 1.10.8)
rubyzip (>= 1.3.0)
rubyzip (2.3.2)
securerandom (0.3.1)
semantic_range (3.0.0)
securerandom (0.4.1)
semantic_range (3.1.0)
shakapacker (8.0.2)
activesupport (>= 5.2)
package_json
rack-proxy (>= 0.6.1)
railties (>= 5.2)
semantic_range (>= 2.3.0)
sidekiq (7.3.2)
concurrent-ruby (< 2)
sidekiq (7.3.7)
connection_pool (>= 2.3.0)
logger
rack (>= 2.2.4)
@@ -517,17 +530,20 @@ GEM
simplecov-html (0.13.1)
simplecov_json_formatter (0.1.4)
smart_properties (1.17.0)
sqlite3 (2.1.0)
sqlite3 (2.5.0)
mini_portile2 (~> 2.8.0)
sqlite3 (2.1.0-aarch64-linux-gnu)
sqlite3 (2.1.0-x86_64-linux-gnu)
stringio (3.1.1)
strip_attributes (1.13.0)
activemodel (>= 3.0, < 8.0)
sqlite3 (2.5.0-aarch64-linux-gnu)
sqlite3 (2.5.0-aarch64-linux-musl)
sqlite3 (2.5.0-arm64-darwin)
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)
timeout (0.4.1)
timeout (0.4.3)
trailblazer-option (0.1.2)
turbo-rails (2.0.10)
turbo-rails (2.0.11)
actionpack (>= 6.0.0)
railties (>= 6.0.0)
twitter_cldr (6.12.1)
@@ -539,9 +555,12 @@ GEM
tzinfo-data (1.2024.2)
tzinfo (>= 1.0.0)
uber (0.1.0)
unicode-display_width (2.6.0)
unicode-display_width (3.1.2)
unicode-emoji (~> 4.0, >= 4.0.4)
unicode-emoji (4.0.4)
uniform_notifier (1.16.0)
useragent (0.16.10)
uri (1.0.3)
useragent (0.16.11)
warden (1.2.9)
rack (>= 2.0.9)
web-console (4.2.1)
@@ -553,27 +572,31 @@ GEM
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
webrick (1.8.2)
webrick (1.9.1)
websocket-driver (0.7.6)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
xpath (3.2.0)
nokogiri (~> 1.8)
zeitwerk (2.7.0)
zeitwerk (2.7.1)
PLATFORMS
aarch64-linux
aarch64-linux-musl
arm64-darwin
ruby
x86_64-linux
x86_64-linux-musl
DEPENDENCIES
annotate
annotaterb
arabic-letter-connector
aws-sdk-s3
aws-sdk-secretsmanager
azure-storage-blob
better_html
bootsnap
brakeman
bullet
cancancan
capybara
@@ -589,6 +612,7 @@ DEPENDENCIES
faker
faraday
faraday-follow_redirects
foreman
google-cloud-storage
hexapdf
image_processing
@@ -628,7 +652,7 @@ DEPENDENCIES
webmock
RUBY VERSION
ruby 3.3.4p94
ruby 3.4.1p0
BUNDLED WITH
2.5.3
@@ -15,9 +15,11 @@ class AccountConfigsController < ApplicationController
AccountConfig::DOWNLOAD_LINKS_AUTH_KEY,
AccountConfig::FORCE_SSO_AUTH_KEY,
AccountConfig::FLATTEN_RESULT_PDF_KEY,
AccountConfig::ENFORCE_SIGNING_ORDER_KEY,
AccountConfig::WITH_SIGNATURE_ID,
AccountConfig::COMBINE_PDF_RESULT_KEY,
AccountConfig::REQUIRE_SIGNING_REASON_KEY
AccountConfig::REQUIRE_SIGNING_REASON_KEY,
AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY
].freeze
InvalidKey = Class.new(StandardError)
@@ -38,7 +40,7 @@ class AccountConfigsController < ApplicationController
end
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])
end
end
+1 -1
View File
@@ -50,7 +50,7 @@ class AccountsController < ApplicationController
# rubocop:disable Layout/LineLength
render turbo_stream: turbo_stream.replace(
: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
end
@@ -42,6 +42,7 @@ module Api
is_authorized = attachment.name.in?(%w[logo preview_images]) ||
(current_user && attachment.record.account.id == current_user.account_id) ||
(current_user && !Docuseal.multitenant? && current_user.role == 'superadmin') ||
!attachment.record.account.account_configs
.find_or_initialize_by(key: AccountConfig::DOWNLOAD_LINKS_AUTH_KEY).value
@@ -10,7 +10,7 @@ module Api
last_submitter = @submission.submitters.max_by(&:completed_at)
if last_submitter.documents_attachments.blank?
last_submitter.documents_attachments = Submissions::EnsureResultGenerated.call(submitter)
last_submitter.documents_attachments = Submissions::EnsureResultGenerated.call(last_submitter)
end
last_submitter.documents_attachments
+21 -5
View File
@@ -17,6 +17,8 @@ module Api
submissions = submissions.joins(template: :folder).where(folder: { name: params[:template_folder] })
end
submissions = Submissions::Filter.call(submissions, current_user, params)
submissions = paginate(submissions.preload(:created_by_user, :submitters,
template: :folder,
combined_document_attachment: :blob,
@@ -78,7 +80,9 @@ module Api
submissions.each do |submission|
submission.submitters.each do |submitter|
ProcessSubmitterCompletionJob.perform_async('submitter_id' => submitter.id) if submitter.completed_at?
next unless submitter.completed_at?
ProcessSubmitterCompletionJob.perform_async('submitter_id' => submitter.id, 'send_invitation_email' => false)
end
end
@@ -91,7 +95,7 @@ module Api
end
def destroy
if params[:permanently] == 'true'
if params[:permanently].in?(['true', true])
@submission.destroy!
else
@submission.update!(archived_at: Time.current)
@@ -114,7 +118,19 @@ module Api
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
end
@@ -163,7 +179,7 @@ module Api
message: %i[subject body],
submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role,
:completed, :phone, :application_key, :external_id, :reply_to, :go_to_last,
{ metadata: {}, values: {}, readonly_fields: [], message: %i[subject body],
{ metadata: {}, values: {}, roles: [], readonly_fields: [], message: %i[subject body],
fields: [:name, :uuid, :default_value, :value, :title, :description,
:readonly, :validation_pattern, :invalid_message,
{ default_value: [], value: [], preferences: {} }] }]]
@@ -176,7 +192,7 @@ module Api
key = params.key?(:submission) ? :submission : :submissions
params.permit(
key => [permitted_attrs]
{ key => [permitted_attrs] }, { key => permitted_attrs }
).fetch(key, [])
end
end
+7 -1
View File
@@ -146,9 +146,15 @@ module Api
if attrs[:completed]
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)
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|
v == '{{date}}' ? Time.current.in_time_zone(submitter.account.timezone).to_date.to_s : v
end
@@ -23,7 +23,9 @@ module Api
cloned_template.source = :api
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])
WebhookUrls.for_account_id(cloned_template.account_id, 'template.created').each do |webhook_url|
SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => cloned_template.id,
+4 -4
View File
@@ -74,7 +74,7 @@ module Api
end
def destroy
if params[:permanently] == 'true'
if params[:permanently].in?(['true', true])
@template.destroy!
else
@template.update!(archived_at: Time.current)
@@ -87,7 +87,7 @@ module Api
def filter_templates(templates, params)
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[:external_id]) if params[:external_id].present?
templates = templates.joins(:folder).where(folder: { name: params[:folder] }) if params[:folder].present?
@@ -100,12 +100,12 @@ module Api
:name,
: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,
:required, :readonly, :default_value,
:title, :description,
{ preferences: {},
conditions: [%i[field_uuid value action]],
conditions: [%i[field_uuid value action operation]],
options: [%i[value uuid]],
validation: %i[message pattern],
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]]
+2
View File
@@ -34,6 +34,8 @@ module Api
}
end
}
rescue HexaPDF::MalformedPDFError
render json: { error: 'Malformed PDF' }, status: :unprocessable_entity
end
end
end
+6 -1
View File
@@ -15,6 +15,7 @@ class ApplicationController < ActionController::Base
helper_method :button_title,
:current_account,
:form_link_host,
:svg_icon
impersonates :user, with: ->(uuid) { User.find_by(uuid:) }
@@ -29,7 +30,7 @@ class ApplicationController < ActionController::Base
redirect_to request.referer, alert: 'Too many requests', status: :too_many_requests
end
if Rails.env.production?
if Rails.env.production? || Rails.env.test?
rescue_from CanCan::AccessDenied do |e|
Rollbar.warning(e) if defined?(Rollbar)
@@ -105,6 +106,10 @@ class ApplicationController < ActionController::Base
render_to_string(partial: "icons/#{icon_name}", locals: { class: })
end
def form_link_host
Docuseal.default_url_options[:host]
end
def maybe_redirect_com
return if request.domain != 'docuseal.co'
@@ -6,7 +6,7 @@ class ConsoleRedirectController < ApplicationController
def index
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
params[:redir] = "#{Docuseal::CONSOLE_URL}/manage" if request.path == '/manage'
+1 -1
View File
@@ -9,7 +9,7 @@ class EmbedScriptsController < ActionController::Metal
<h2>Upgrade to Pro</h2>
<p>Unlock embedded components by upgrading to Pro</p>
<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
</a>
</div>
@@ -11,6 +11,8 @@ class EsignSettingsController < ApplicationController
end
end
prepend_before_action :maybe_redirect_com, only: %i[show]
before_action :load_encrypted_config
authorize_resource :encrypted_config, parent: false, only: %i[new create]
authorize_resource :encrypted_config, only: %i[update destroy show]
@@ -39,7 +39,7 @@ class NotificationsSettingsController < ApplicationController
end
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])
end
end
@@ -6,7 +6,8 @@ class PersonalizationSettingsController < ApplicationController
AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY,
AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY,
AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY,
AccountConfig::FORM_COMPLETED_MESSAGE_KEY
AccountConfig::FORM_COMPLETED_MESSAGE_KEY,
*(Docuseal.multitenant? ? [] : [AccountConfig::POLICY_LINKS_KEY])
].freeze
InvalidKey = Class.new(StandardError)
@@ -49,7 +50,7 @@ class PersonalizationSettingsController < ApplicationController
end
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)
+4
View File
@@ -0,0 +1,4 @@
# frozen_string_literal: true
class PwaController < ActionController::Base
end
@@ -9,8 +9,6 @@ class SendSubmissionEmailController < ApplicationController
SEND_DURATION = 30.minutes
def success; end
def create
@submitter =
if params[:template_slug]
@@ -31,7 +29,7 @@ class SendSubmissionEmailController < ApplicationController
end
respond_to do |f|
f.html { redirect_to success_send_submission_email_index_path }
f.html { render :success }
f.json { head :ok }
end
end
+14 -2
View File
@@ -25,7 +25,7 @@ class StartFormController < ApplicationController
redirect_to start_form_completed_path(@template.slug, email: submitter_params[:email])
else
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
end
@@ -34,6 +34,8 @@ class StartFormController < ApplicationController
assign_submission_attributes(@submitter, @template)
Submissions::AssignDefinedSubmitters.call(@submitter.submission)
else
@submitter.assign_attributes(ip: request.remote_ip, ua: request.user_agent)
end
if @submitter.save
@@ -64,8 +66,10 @@ class StartFormController < ApplicationController
.or(template.submissions.where(expire_at: nil)).where(archived_at: nil))
.order(id: :desc)
.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 }
.find_or_initialize_by(**submitter_params.compact_blank)
.find_or_initialize_by(email: submitter_params[:email], **submitter_params.compact_blank)
end
def assign_submission_attributes(submitter, template)
@@ -113,4 +117,12 @@ class StartFormController < ApplicationController
@template = Template.find_by!(slug:)
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
@@ -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
@@ -8,8 +8,18 @@ class SubmissionsArchivedController < ApplicationController
@submissions = @submissions.where.not(archived_at: nil)
.or(@submissions.where.not(templates: { archived_at: nil }))
.preload(:created_by_user, template: :author)
@submissions = Submissions.search(@submissions, params[:q], search_template: true)
@pagy, @submissions = pagy(@submissions.preload(:submitters).order(id: :desc))
@submissions = @submissions.preload(:template_accesses) unless current_user.role.in?(%w[admin superadmin])
@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
+3 -1
View File
@@ -6,6 +6,8 @@ class SubmissionsController < ApplicationController
load_and_authorize_resource :submission, only: %i[show destroy]
prepend_before_action :maybe_redirect_com, only: %i[show]
def show
@submission = Submissions.preload_with_pages(@submission)
@@ -59,7 +61,7 @@ class SubmissionsController < ApplicationController
def destroy
notice =
if params[:permanently].present?
if params[:permanently].in?(['true', true])
@submission.destroy!
I18n.t('submission_has_been_removed')
@@ -10,11 +10,17 @@ class SubmissionsDashboardController < ApplicationController
.where(templates: { archived_at: nil })
.preload(:created_by_user, template: :author)
@submissions = @submissions.preload(:template_accesses) unless current_user.role.in?(%w[admin superadmin])
@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 = @submissions.completed if params[:status] == 'completed'
@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).order(id: :desc))
@pagy, @submissions = pagy(@submissions.preload(submitters: :start_form_submission_events))
end
end
@@ -23,9 +23,9 @@ class SubmissionsDownloadController < ApplicationController
last_submitter = submitter.submission.submitters.where.not(completed_at: nil).order(:completed_at).last
Submissions::EnsureResultGenerated.call(last_submitter)
return head :not_found unless last_submitter
return head :not_found unless last_submitter.completed_at?
Submissions::EnsureResultGenerated.call(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)
@@ -53,8 +53,15 @@ class SubmissionsDownloadController < ApplicationController
end
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|
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
@@ -65,6 +72,13 @@ class SubmissionsDownloadController < ApplicationController
attachment = submitter.submission.combined_document_attachment
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
@@ -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,7 +4,7 @@ class SubmissionsPreviewController < ApplicationController
skip_before_action :authenticate_user!
skip_authorization_check
before_action :maybe_redirect_com, only: %i[show completed]
prepend_before_action :maybe_redirect_com, only: %i[show completed]
TTL = 40.minutes
@@ -20,7 +20,10 @@ class SubmissionsPreviewController < ApplicationController
@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')
end
@@ -37,6 +40,7 @@ class SubmissionsPreviewController < ApplicationController
def completed
@submission = Submission.find_by!(slug: params[:submissions_preview_slug])
@template = @submission.template
render :completed, layout: 'form'
end
+20 -8
View File
@@ -7,8 +7,6 @@ class SubmitFormController < ApplicationController
skip_before_action :authenticate_user!
skip_authorization_check
before_action :maybe_redirect_com, only: %i[show completed]
CONFIG_KEYS = [].freeze
def show
@@ -17,20 +15,23 @@ class SubmitFormController < ApplicationController
submission = @submitter.submission
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
return render :archived if submission.template.archived_at? || submission.archived_at?
return render :archived if submission.template.archived_at? ||
submission.archived_at? ||
@submitter.account.archived_at?
return render :expired if submission.expired?
return render :declined if @submitter.declined_at?
return render :awaiting if submission.template.preferences['submitters_order'] == 'preserved' &&
@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::MaybeUpdateDefaultValues.call(@submitter, current_user)
@attachments_index = ActiveStorage::Attachment.where(record: submission.submitters, name: :attachments)
.preload(:blob).index_by(&:uuid)
@form_configs = Submitters::FormConfigs.call(@submitter, CONFIG_KEYS)
@attachments_index = build_attachments_index(submission)
return unless @form_configs[:prefill_signature]
@@ -71,6 +72,10 @@ class SubmitFormController < ApplicationController
Submitters::SubmitValues.call(submitter, params, request)
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
render json: { error: e.message }, status: :unprocessable_entity
end
@@ -80,4 +85,11 @@ class SubmitFormController < ApplicationController
end
def success; end
private
def build_attachments_index(submission)
ActiveStorage::Attachment.where(record: submission.submitters, name: :attachments)
.preload(:blob).index_by(&:uuid)
end
end
@@ -9,13 +9,15 @@ class SubmitFormInviteController < ApplicationController
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
invite_submitters.each do |item|
(invite_submitters + optional_invite_submitters).each do |item|
attrs = submitters_attributes.find { |e| e[:uuid] == item['uuid'] }
next unless attrs
next if attrs[:email].blank?
submitter.submission.submitters.create!(**attrs, account_id: submitter.account_id)
@@ -46,9 +48,9 @@ class SubmitFormInviteController < ApplicationController
!submitter.submission.template.archived_at?
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|
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
@@ -9,7 +9,8 @@ class SubmittersAutocompleteController < ApplicationController
def index
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 = attrs.uniq { |e| e[params[:field]] } if params[:field].present?
@@ -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
if Docuseal.multitenant? && SubmissionEvent.exists?(submitter: @submitter,
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)
return redirect_back(fallback_location: submission_path(@submitter.submission),
@@ -4,7 +4,8 @@ class TemplateFoldersController < ApplicationController
load_and_authorize_resource :template_folder
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).order(id: :desc)
@templates = Templates.search(@templates, params[:q])
@pagy, @templates = pagy(@templates, limit: 12)
@@ -4,7 +4,7 @@ class TemplatesArchivedController < ApplicationController
load_and_authorize_resource :template, parent: false
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])
@pagy, @templates = pagy(@templates, limit: 12)
@@ -7,8 +7,15 @@ class TemplatesArchivedSubmissionsController < ApplicationController
def index
@submissions = @submissions.where.not(archived_at: nil)
@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
redirect_to root_path
end
+15 -7
View File
@@ -9,13 +9,21 @@ class TemplatesController < ApplicationController
submissions = @template.submissions.accessible_by(current_ability)
submissions = submissions.active if @template.archived_at.blank?
submissions = Submissions.search(submissions, params[:q], search_values: true)
submissions = Submissions::Filter.call(submissions, current_user, params.except(:status))
@base_submissions = submissions
submissions = submissions.pending if params[:status] == 'pending'
submissions = submissions.completed if params[:status] == 'completed'
submissions = Submissions::Filter.filter_by_status(submissions, params)
@pagy, @submissions = pagy(submissions.preload(submitters: :start_form_submission_events).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
submissions = submissions.preload(:template_accesses) unless current_user.role.in?(%w[admin superadmin])
@pagy, @submissions = pagy(submissions.preload(submitters: :start_form_submission_events))
rescue ActiveRecord::RecordNotFound
redirect_to root_path
end
@@ -84,7 +92,7 @@ class TemplatesController < ApplicationController
def destroy
notice =
if params[:permanently].present?
if params[:permanently].in?(['true', true])
@template.destroy!
I18n.t('template_has_been_removed')
@@ -102,13 +110,13 @@ class TemplatesController < ApplicationController
def template_params
params.require(:template).permit(
:name,
{ schema: [%i[attachment_uuid name]],
submitters: [%i[name uuid is_requester linked_to_uuid invite_by_uuid email]],
{ schema: [[:attachment_uuid, :name, { conditions: [%i[field_uuid value action operation]] }]],
submitters: [%i[name uuid is_requester linked_to_uuid invite_by_uuid optional_invite_by_uuid email]],
fields: [[:uuid, :submitter_uuid, :name, :type,
:required, :readonly, :default_value,
:title, :description,
{ preferences: {},
conditions: [%i[field_uuid value action]],
conditions: [%i[field_uuid value action operation]],
options: [%i[value uuid]],
validation: %i[message pattern],
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]] }
@@ -9,7 +9,9 @@ class TemplatesDashboardController < ApplicationController
FOLDERS_PER_PAGE = 18
def index
@template_folders = filter_template_folders(@template_folders)
@template_folders = @template_folders.where(id: @templates.active.select(:folder_id)).order(id: :desc)
@template_folders = TemplateFolders.search(@template_folders, params[:q])
@pagy, @template_folders = pagy(
@template_folders,
@@ -36,16 +38,8 @@ class TemplatesDashboardController < ApplicationController
private
def filter_template_folders(template_folders)
rel = template_folders.joins(:active_templates)
.order(id: :desc)
.distinct
TemplateFolders.search(rel, params[:q])
end
def filter_templates(templates)
rel = templates.active.preload(:author).order(id: :desc)
rel = templates.active.preload(:author, :template_accesses).order(id: :desc)
if params[:q].blank?
if Docuseal.multitenant? && !current_account.testing?
@@ -6,9 +6,10 @@ class TemplatesDebugController < ApplicationController
def show
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']['fields'] = fields
@@ -22,13 +22,16 @@ class TemplatesPreferencesController < ApplicationController
preferences: %i[bcc_completed request_email_subject request_email_body
documents_copy_email_subject documents_copy_email_body
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_redirect_url
submitters_order
completed_notification_email_subject completed_notification_email_body
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|
attrs[:preferences].delete(:submitters) if params[:request_email_per_submitter] != '1'
attrs[:preferences] = attrs[:preferences].transform_values do |value|
if %w[true false].include?(value)
value == 'true'
@@ -17,38 +17,47 @@ class TemplatesRecipientsController < ApplicationController
private
def submitters_params
params.require(:template).permit(
submitters: [%i[name uuid is_requester invite_by_uuid linked_to_uuid email option]]
).fetch(:submitters, {}).values.filter_map do |s|
permit_params = { submitters: [%i[name uuid is_requester optional_invite_by_uuid
invite_by_uuid linked_to_uuid email option]] }
params.require(:template).permit(permit_params).fetch(:submitters, {}).values.filter_map do |s|
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
else
s.delete(:is_requester)
end
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)
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
normalize_option_value(s)
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
@@ -55,11 +55,12 @@ class TemplatesUploadsController < ApplicationController
tempfile.write(DownloadUtils.call(params[:url]).body)
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(
tempfile:,
filename: File.basename(
URI.decode_www_form_component(params[:filename].presence || params[:url]), '.*'
),
filename:,
type: Marcel::MimeType.for(tempfile)
)
+1 -1
View File
@@ -26,7 +26,7 @@ class UserConfigsController < ApplicationController
end
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])
end
end
+13 -20
View File
@@ -24,16 +24,14 @@ class UsersController < ApplicationController
def edit; end
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
existing_user.archived_at = nil
existing_user.assign_attributes(user_params)
existing_user.account = current_account
@user = existing_user
return render turbo_stream: turbo_stream.replace(:modal, template: 'users/new'), status: :unprocessable_entity
end
@user.role = User::ADMIN_ROLE unless role_valid?(@user.role)
if @user.save
UserMailer.invitation_email(@user).deliver_later!
@@ -47,17 +45,16 @@ class UsersController < ApplicationController
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.delete(:role) if !role_valid?(attrs[:role]) || current_user == @user
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)
@user.account = account
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')
else
render turbo_stream: turbo_stream.replace(:modal, template: 'users/edit'), status: :unprocessable_entity
@@ -81,20 +78,16 @@ class UsersController < ApplicationController
end
def build_user
@user = current_account.users.find_by(email: user_params[:email])&.tap do |user|
user.assign_attributes(user_params)
user.archived_at = nil
end
@user ||= current_account.users.new(user_params)
@user
@user = current_account.users.new(user_params)
end
def user_params
if params.key?(:user)
params.require(:user).permit(:email, :first_name, :last_name, :password,
:role, :archived_at, :account_id)
permitted_params = %i[email first_name last_name password archived_at]
permitted_params << :role if role_valid?(params.dig(:user, :role))
params.require(:user).permit(permitted_params)
else
{}
end
+5 -2
View File
@@ -30,11 +30,11 @@ import ToggleAttribute from './elements/toggle_attribute'
import LinkedInput from './elements/linked_input'
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 * as TurboInstantClick from './lib/turbo_instant_click'
import './images/preview.png'
TurboInstantClick.start()
document.addEventListener('turbo:before-cache', () => {
@@ -97,6 +97,8 @@ safeRegisterElement('toggle-attribute', ToggleAttribute)
safeRegisterElement('linked-input', LinkedInput)
safeRegisterElement('checkbox-group', CheckboxGroup)
safeRegisterElement('masked-input', MaskedInput)
safeRegisterElement('set-date-button', SetDateButton)
safeRegisterElement('indeterminate-checkbox', IndeterminateCheckbox)
safeRegisterElement('template-builder', class extends HTMLElement {
connectedCallback () {
@@ -111,6 +113,7 @@ safeRegisterElement('template-builder', class extends HTMLElement {
backgroundColor: '#faf7f5',
locale: this.dataset.locale,
withPhone: this.dataset.withPhone === 'true',
withVerification: ['true', 'false'].includes(this.dataset.withVerification) ? this.dataset.withVerification === 'true' : null,
withLogo: this.dataset.withLogo !== 'false',
editable: this.dataset.editable !== 'false',
authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content,
+17
View File
@@ -97,6 +97,23 @@ button[disabled] .enabled {
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 {
background: white;
z-index: 1000;
+20 -17
View File
@@ -1,5 +1,6 @@
import SignaturePad from 'signature_pad'
import { cropCanvasAndExportToPNG } from './submission_form/crop_canvas'
import { isValidSignatureCanvas } from './submission_form/validate_signature'
window.customElements.define('draw-signature', class extends HTMLElement {
connectedCallback () {
@@ -43,6 +44,8 @@ window.customElements.define('draw-signature', class extends HTMLElement {
return response
})
}).catch(error => {
console.log(error)
}).finally(() => {
this.submitButton.disabled = false
})
@@ -65,26 +68,26 @@ window.customElements.define('draw-signature', class extends HTMLElement {
}
async submitImage () {
return new Promise((resolve, reject) => {
cropCanvasAndExportToPNG(this.canvas, { errorOnTooSmall: true }).then(async (blob) => {
const file = new File([blob], 'signature.png', { type: 'image/png' })
if (!isValidSignatureCanvas(this.pad.toData())) {
alert('Signature is too small or simple. Please redraw.')
const formData = new FormData()
return Promise.reject(new Error('Image too small or simple'))
}
formData.append('file', file)
formData.append('submitter_slug', this.dataset.slug)
formData.append('name', 'attachments')
formData.append('remember_signature', 'true')
return cropCanvasAndExportToPNG(this.canvas).then(async (blob) => {
const file = new File([blob], 'signature.png', { type: 'image/png' })
return fetch('/api/attachments', {
method: 'POST',
body: formData
}).then((resp) => resp.json()).then((attachment) => {
return resolve(attachment)
})
}).catch((error) => {
return reject(error)
})
const formData = new FormData()
formData.append('file', file)
formData.append('submitter_slug', this.dataset.slug)
formData.append('name', 'attachments')
formData.append('remember_signature', 'true')
return fetch('/api/attachments', {
method: 'POST',
body: formData
}).then(resp => resp.json())
})
}
@@ -3,6 +3,8 @@ export default class extends HTMLElement {
this.resize()
this.textarea.addEventListener('input', () => this.resize())
this.observeVisibility()
}
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 () {
return this.querySelector('textarea')
}
+2 -1
View File
@@ -20,7 +20,8 @@ export default targetable(class extends HTMLElement {
fetch(this.dataset.src).then(async (response) => {
if (response.ok) {
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) {
this.downloadSafariIos(urls)
@@ -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,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)
}
}
+2
View File
@@ -15,6 +15,8 @@ safeRegisterElement('submission-form', class extends HTMLElement {
this.app = createApp(Form, {
submitter: JSON.parse(this.dataset.submitter),
inviteSubmitters: JSON.parse(this.dataset.inviteSubmitters),
optionalInviteSubmitters: JSON.parse(this.dataset.optionalInviteSubmitters),
schema: JSON.parse(this.dataset.schema),
canSendEmail: this.dataset.canSendEmail === 'true',
previousSignatureValue: this.dataset.previousSignatureValue,
goToLast: this.dataset.goToLast === 'true',
Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

+51 -13
View File
@@ -1,9 +1,9 @@
<template>
<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"
: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
v-if="(!withFieldPlaceholder || !field.name || field.type === 'cells') && !isActive && !isValueSet && field.type !== 'checkbox' && submittable && !area.option_uuid"
@@ -74,7 +74,7 @@
ID: {{ signature.uuid }}
</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">
&lt;{{ submitter.email }}&gt;
</template>
@@ -168,6 +168,7 @@
<div
v-else-if="field.type === 'cells'"
class="w-full flex items-center"
:class="{ 'justify-end': field.preferences?.align === 'right', ...fontClasses }"
>
<div
v-for="(char, index) in modelValue"
@@ -183,20 +184,29 @@
ref="textContainer"
dir="auto"
class="flex items-center px-0.5 w-full"
:class="alignClasses[field.preferences?.align]"
:class="{ ...alignClasses, ...fontClasses }"
>
<span
v-if="field && field.name && withFieldPlaceholder && !modelValue && modelValue !== 0"
class="whitespace-pre-wrap text-gray-400"
:class="{ 'w-full': field.preferences?.align }"
>{{ 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(', ') }}
</span>
<span v-else-if="field.type === 'date'">
<span
v-else-if="field.type === 'date'"
:class="{ 'w-full': field.preferences?.align }"
>
{{ formattedDate }}
</span>
<span v-else-if="field.type === 'number'">
<span
v-else-if="field.type === 'number'"
class="w-full"
>
{{ formatNumber(modelValue, field.preferences?.format) }}
</span>
<span
@@ -209,7 +219,7 @@
</template>
<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 {
name: 'FieldArea',
@@ -311,14 +321,31 @@ export default {
cells: this.t('cells'),
stamp: this.t('stamp'),
payment: this.t('payment'),
phone: this.t('phone')
phone: this.t('phone'),
verification: this.t('verify_id')
}
},
alignClasses () {
if (!this.field.preferences) {
return {}
}
return {
center: 'text-center',
left: 'text-left',
right: 'text-right'
'text-center': this.field.preferences.align === 'center',
'text-left': this.field.preferences.align === 'left',
'text-right': this.field.preferences.align === 'right'
}
},
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 () {
@@ -340,7 +367,8 @@ export default {
cells: IconColumns3,
multiple: IconChecks,
phone: IconPhoneCheck,
payment: IconCreditCard
payment: IconCreditCard,
verification: IconId
}
},
image () {
@@ -444,8 +472,18 @@ export default {
}
},
formatNumber (number, format) {
if (!number && number !== 0) {
return ''
}
if (format === 'comma') {
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') {
return new Intl.NumberFormat('de-DE').format(number)
} else if (format === 'space') {
+19 -7
View File
@@ -31,7 +31,7 @@
:with-label="withLabel && !withFieldPlaceholder && step.length < 2"
:is-value-set="step.some((f) => f.uuid in values)"
:attachments-index="attachmentsIndex"
@click="$emit('focus-step', stepIndex)"
@click="[$emit('focus-step', stepIndex), maybeScrollOnClick(field, area)]"
/>
</Teleport>
</template>
@@ -109,6 +109,14 @@ export default {
areaRefs: []
}
},
computed: {
isMobileContainer () {
const root = this.$root.$el.parentNode.getRootNode()
const container = root.body || root.querySelector('div')
return container.style.overflow === 'hidden'
}
},
beforeUpdate () {
this.areaRefs = []
},
@@ -121,14 +129,16 @@ export default {
this.scrollIntoArea(field.areas[0])
}
},
maybeScrollOnClick (field, area) {
if (['text', 'number', 'cells'].includes(field.type) && this.isMobileContainer) {
this.scrollIntoArea(area)
}
},
scrollIntoArea (area) {
const areaRef = this.areaRefs.find((a) => a.area === area)
if (areaRef) {
const root = this.$root.$el.parentNode.getRootNode()
const container = root.body || root.querySelector('div')
if (container.style.overflow === 'hidden') {
if (this.isMobileContainer) {
this.scrollInContainer(areaRef.$el)
} else {
const targetRect = areaRef.$refs.scrollToElem.getBoundingClientRect()
@@ -157,13 +167,15 @@ export default {
const formContainer = root.getElementById('form_container')
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 targetRect = target.getBoundingClientRect()
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) {
if (el) {
@@ -13,7 +13,7 @@
>
<a
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"
target="_blank"
>
@@ -26,7 +26,10 @@
{{ attachmentsIndex[val].filename }}
</span>
</a>
<button @click.prevent="removeAttachment(val)">
<button
class="remove-attachment-button"
@click.prevent="removeAttachment(val)"
>
<IconTrashX
:width="18"
:heigh="19"
@@ -44,12 +47,12 @@
<div
v-if="field.description && !modelValue.length"
dir="auto"
class="mb-3 px-1"
class="mb-3 px-1 field-description-text"
>
<MarkdownContent :string="field.description" />
</div>
<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"
:multiple="true"
:dry-run="dryRun"
+8 -7
View File
@@ -1,7 +1,7 @@
<template>
<div
id="form_completed"
class="mx-auto max-w-md flex flex-col"
class="mx-auto max-w-md flex flex-col completed-form"
dir="auto"
>
<div class="font-medium text-2xl flex items-center space-x-1.5 mx-auto">
@@ -10,13 +10,13 @@
:width="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')) }}
</span>
</div>
<div
v-if="completedMessage.body"
class="mt-2"
class="mt-2 completed-form-message-body"
>
<MarkdownContent
:string="completedMessage.body"
@@ -27,7 +27,7 @@
v-if="completedButton.url"
:href="sanitizeHref(completedButton.url)"
rel="noopener noreferrer nofollow"
class="white-button flex items-center w-full"
class="white-button flex items-center w-full completed-form-completed-button"
>
<span>
{{ completedButton.title || 'Back to Website' }}
@@ -35,7 +35,7 @@
</a>
<button
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"
@click.prevent="sendCopyToEmail"
>
@@ -50,7 +50,7 @@
</button>
<button
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"
@click.prevent="download"
>
@@ -215,7 +215,8 @@ export default {
fetch(this.baseUrl + `/submitters/${this.submitterSlug}/download`).then(async (response) => {
if (response.ok) {
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) {
this.downloadSafariIos(urls)
@@ -1,4 +1,4 @@
function cropCanvasAndExportToPNG (canvas, { errorOnTooSmall } = { errorOnTooSmall: false }) {
function cropCanvasAndExportToPNG (canvas) {
const ctx = canvas.getContext('2d')
const width = canvas.width
@@ -33,10 +33,6 @@ function cropCanvasAndExportToPNG (canvas, { errorOnTooSmall } = { errorOnTooSma
croppedCanvas.height = croppedHeight
const croppedCtx = croppedCanvas.getContext('2d')
if (errorOnTooSmall && (croppedWidth < 20 || croppedHeight < 20)) {
return Promise.reject(new Error('Image too small'))
}
croppedCtx.drawImage(canvas, leftmost, topmost, croppedWidth, croppedHeight, 0, 0, croppedWidth, croppedHeight)
return new Promise((resolve, reject) => {
+14 -7
View File
@@ -1,26 +1,32 @@
<template>
<div dir="auto">
<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 }"
>
<label
v-if="showFieldNames"
:for="field.uuid"
class="label text-2xl"
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 && showFieldNames ? field.name : t('date') }}
<template v-if="!field.required">
{{ field.name || t('date') }}
</template>
<template v-if="!field.required">
<span
class="ml-1"
:class="{ 'hidden sm:inline': (field.title || field.name || t('date') ).length > 10 }"
>
({{ t('optional') }})
</template>
</span>
</template>
</label>
<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')]"
>
<IconCalendarCheck :width="16" />
@@ -29,7 +35,7 @@
</div>
<div
v-if="field.description"
class="mb-3 px-1"
class="mb-3 px-1 field-description-text"
dir="auto"
>
<MarkdownContent :string="field.description" />
@@ -37,6 +43,7 @@
<AppearsOn :field="field" />
<div class="text-center">
<input
:id="field.uuid"
ref="input"
v-model="value"
class="base-input !text-2xl text-center w-full"
+3 -2
View File
@@ -1,12 +1,13 @@
<template>
<div
id="dropzone"
class="flex h-32 w-full"
@dragover.prevent
@drop.prevent="onDropFiles"
>
<label
: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 }"
>
<div class="absolute top-0 right-0 left-0 bottom-0 flex items-center justify-center">
@@ -127,7 +128,7 @@ export default {
}
})
} else {
if (file.type === 'image/bmp') {
if (file.type === 'image/bmp' || file.type === 'image/vnd.microsoft.icon') {
file = await this.convertBmpToPng(file)
}
+237 -62
View File
@@ -11,17 +11,19 @@
:with-label="!isAnonymousChecboxes && showFieldNames"
:current-step="currentStepFields"
:scroll-padding="scrollPadding"
@focus-step="[saveStep(), currentField.type !== 'checkbox' ? isFormVisible = true : '', goToStep($event, false, true)]"
@focus-step="[saveStep(), goToStep($event, false, true), currentField.type !== 'checkbox' ? isFormVisible = true : '']"
/>
<FieldAreas
:steps="readonlyConditionalFields.map((e) => [e])"
:values="readonlyConditionalFields.reduce((acc, f) => { acc[f.uuid] = f.default_value; return acc }, {})"
:values="readonlyConditionalFieldValues"
:submitter="submitter"
:attachments-index="attachmentsIndex"
:submittable="false"
/>
<FormulaFieldAreas
v-if="formulaFields.length"
:fields="formulaFields"
:readonly-values="readonlyConditionalFieldValues"
:values="values"
/>
<Teleport
@@ -65,7 +67,7 @@
<button
v-if="!isFormVisible"
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%"
@click.prevent="[isFormVisible = true, scrollIntoField(currentField)]"
>
@@ -91,14 +93,15 @@
<div
v-show="isFormVisible"
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"
:class="{ 'md:bottom-4': isBreakpointMd }"
:style="{ backgroundColor: backgroundColor }"
>
<button
v-if="!isCompleted"
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')"
@click.prevent="minimizeForm"
>
@@ -116,7 +119,7 @@
ref="form"
:action="submitPath"
method="post"
class="mx-auto"
class="mx-auto steps-form"
:style="{ maxWidth: isBreakpointMd ? '582px' : '' }"
@submit.prevent="submitStep"
>
@@ -162,7 +165,7 @@
v-if="showFieldNames && (currentField.name || currentField.title)"
:for="currentField.uuid"
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 }"
>
<MarkdownContent
@@ -171,7 +174,11 @@
/>
<template v-else>
{{ 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>
</label>
<div
@@ -181,7 +188,7 @@
<div
v-if="currentField.description"
dir="auto"
class="mb-3 px-1"
class="mb-3 px-1 field-description-text"
>
<MarkdownContent :string="currentField.description" />
</div>
@@ -219,7 +226,7 @@
v-if="showFieldNames && (currentField.name || currentField.title)"
:for="currentField.uuid"
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 }"
>
<MarkdownContent
@@ -228,13 +235,17 @@
/>
<template v-else>
{{ 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>
</label>
<div
v-if="currentField.description"
dir="auto"
class="mb-3 px-1"
class="mb-3 px-1 field-description-text"
>
<MarkdownContent :string="currentField.description" />
</div>
@@ -259,7 +270,7 @@
>
<label
:for="option.uuid"
class="flex items-center space-x-3"
class="flex items-center space-x-3 radio-label"
>
<input
:id="option.uuid"
@@ -293,7 +304,7 @@
<div
v-if="currentField.description"
dir="auto"
class="mb-3 px-1"
class="mb-3 px-1 field-description-text"
>
<MarkdownContent :string="currentField.description" />
</div>
@@ -327,7 +338,7 @@
>
<label
:for="field.uuid"
class="flex items-center space-x-3"
class="flex items-center space-x-3 checkbox-label"
>
<input
type="hidden"
@@ -448,16 +459,29 @@
@focus="scrollIntoField(currentField)"
@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
v-if="currentField.type !== 'payment' || submittedValues[currentField.uuid]"
:class="currentField.type === 'signature' ? 'mt-2' : 'mt-6 md:mt-8'"
v-if="(currentField.type !== 'payment' && currentField.type !== 'verification') || submittedValues[currentField.uuid]"
:class="currentField.type === 'signature' ? 'mt-2' : 'mt-4 md:mt-6'"
>
<button
id="submit_form_button"
ref="submitButton"
type="submit"
class="base-button w-full flex justify-center"
class="base-button w-full flex justify-center submit-form-button"
:disabled="isButtonDisabled"
>
<span class="flex">
@@ -484,6 +508,7 @@
<InviteForm
v-else-if="isInvite"
:submitters="inviteSubmitters"
:optional-submitters="optionalInviteSubmitters"
:submitter-slug="submitterSlug"
:authenticity-token="authenticityToken"
:url="baseUrl + submitPath + '/invite'"
@@ -506,15 +531,15 @@
/>
<div
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
v-for="(step, index) in stepFields"
:key="step[0].uuid"
href="#"
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)]"
/>
</div>
@@ -537,6 +562,7 @@ import AttachmentStep from './attachment_step'
import MultiSelectStep from './multi_select_step'
import PhoneStep from './phone_step'
import PaymentStep from './payment_step'
import VerificationStep from './verification_step'
import TextStep from './text_step'
import NumberStep from './number_step'
import DateStep from './date_step'
@@ -579,6 +605,7 @@ export default {
IconWritingSign,
AttachmentStep,
InitialsStep,
VerificationStep,
InviteForm,
MultiSelectStep,
IconInnerShadowTop,
@@ -611,6 +638,11 @@ export default {
required: false,
default: () => []
},
optionalInviteSubmitters: {
type: Array,
required: false,
default: () => []
},
withSignatureId: {
type: Boolean,
required: false,
@@ -621,6 +653,11 @@ export default {
required: false,
default: '-80px'
},
orderAsOnPage: {
type: Boolean,
required: false,
default: false
},
requireSigningReason: {
type: Boolean,
required: false,
@@ -636,6 +673,11 @@ export default {
required: false,
default: null
},
schema: {
type: Array,
required: false,
default: () => []
},
attachments: {
type: Array,
required: false,
@@ -823,7 +865,31 @@ export default {
},
computed: {
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) => {
@@ -905,13 +971,61 @@ export default {
return this.currentStepFields[0]
},
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 () {
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]
if (this.checkFieldConditions(f)) {
if (this.checkFieldConditions(f) && this.checkFieldDocumentsConditions(f)) {
if (f.type === 'checkbox' && Array.isArray(prevStep) && prevStep[0].type === 'checkbox' && !f.description) {
prevStep.push(f)
} else {
@@ -923,7 +1037,7 @@ export default {
}, [])
},
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 () {
return this.attachments.reduce((acc, a) => {
@@ -944,6 +1058,23 @@ export default {
if (isEmpty(value) && this.currentStep > 0) {
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 () {
@@ -991,7 +1122,9 @@ export default {
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(() => {
const root = this.$root.$el.parentNode.getRootNode()
const scrollbox = root.getElementById('scrollbox')
@@ -1025,33 +1158,59 @@ export default {
onOrientationChange (event) {
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) {
if (field.conditions?.length) {
return field.conditions.reduce((acc, c) => {
const field = this.fieldsUuidIndex[c.field_uuid]
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)))
const result = field.conditions.reduce((acc, cond) => {
if (cond.operation === 'or') {
acc.push(acc.pop() || this.checkFieldCondition(cond))
} 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 {
return true
}
@@ -1142,7 +1301,9 @@ export default {
if (!this.isCompleted) {
if (scrollToArea) {
this.scrollIntoField(this.currentField)
this.$nextTick(() => {
setTimeout(() => this.scrollIntoField(this.currentField), 1)
})
}
this.enableScrollIntoField = false
@@ -1159,7 +1320,7 @@ export default {
const currentFieldUuids = this.currentStepFields.map((f) => f.uuid)
const currentFieldType = this.currentField.type
if (!formData && !this.$refs.form.checkValidity()) {
if (!formData && !this.$refs.form.checkValidity() && currentFieldUuids.every((fieldUuid) => isEmpty(this.submittedValues[fieldUuid]) || !isEmpty(this.values[fieldUuid]))) {
return
}
@@ -1209,7 +1370,7 @@ export default {
const submitStep = this.currentStep
const stepPromise = ['signature', 'phone', 'initials', 'payment'].includes(this.currentField.type)
const stepPromise = ['signature', 'phone', 'initials', 'payment', 'verification'].includes(this.currentField.type)
? this.$refs.currentStep.submit
: () => Promise.resolve({})
@@ -1227,7 +1388,7 @@ export default {
const formData = new FormData(this.$refs.form)
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')
}
@@ -1243,9 +1404,27 @@ export default {
if (response.status === 422 || response.status === 500) {
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))
}
@@ -1262,7 +1441,7 @@ export default {
if (emptyRequiredField === nextStep) {
this.showFillAllRequiredFields = true
}
} else if (this.inviteSubmitters.length) {
} else if (this.inviteSubmitters.length || this.optionalInviteSubmitters.length) {
this.isInvite = true
} else {
this.performComplete(response)
@@ -1274,11 +1453,7 @@ export default {
this.isSubmittingComplete = false
})
}).catch(error => {
if (error?.message === 'Image too small') {
alert(this.t('signature_is_too_small_please_redraw'))
} else {
console.log(error)
}
console.log(error)
}).finally(() => {
this.isSubmitting = false
this.isSubmittingComplete = false
@@ -38,6 +38,11 @@ export default {
required: false,
default: () => []
},
readonlyValues: {
type: Object,
required: false,
default: () => ({})
},
values: {
type: Object,
required: false,
@@ -87,7 +92,7 @@ export default {
},
calculateFormula (field) {
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())
+92 -28
View File
@@ -1,4 +1,7 @@
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',
fill_all_required_fields_to_complete: 'Fill all required fields to complete',
sign_and_complete: 'Sign and Complete',
@@ -67,7 +70,7 @@ const en = {
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',
please_check_the_box_to_continue: 'Please check the box to continue.',
open_source_documents_software: 'open source documents software',
verified_phone_number: 'Verify Phone Number',
use_international_format: 'Use international format: +1xxx',
@@ -89,10 +92,15 @@ const en = {
pay_with_strip: 'Pay with Stripe',
reupload: 'Reupload',
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 = {
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',
fill_all_required_fields_to_complete: 'Complete todos los campos requeridos para finalizar',
sign_and_complete: 'Firmar y Completar',
@@ -160,7 +168,7 @@ const es = {
documents_have_been_signed: '¡Los documentos han sido firmados!',
create_a_free_account: 'Crear una Cuenta Gratuita',
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',
verified_phone_number: 'Verificar número de teléfono',
use_international_format: 'Usar formato internacional: +1xxx',
@@ -182,10 +190,15 @@ const es = {
pay_with_strip: 'Pagar con Stripe',
reupload: 'Volver a 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 = {
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',
fill_all_required_fields_to_complete: 'Compila tutti i campi obbligatori per completare',
sign_and_complete: 'Firma e Completa',
@@ -253,7 +266,7 @@ const it = {
documents_have_been_signed: 'I documenti sono stati firmati!',
create_a_free_account: 'Crea un Account Gratuito',
powered_by: 'Desarrollado por',
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',
verified_phone_number: 'Verifica numero di telefono',
use_international_format: 'Usa formato internazionale: +1xxx',
@@ -275,10 +288,15 @@ const it = {
pay_with_strip: 'Paga con Stripe',
reupload: 'Ricarica',
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 = {
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',
fill_all_required_fields_to_complete: 'Alle erforderlichen Felder ausfüllen, um abzuschließen',
sign_and_complete: 'Signieren und Abschließen',
@@ -346,7 +364,7 @@ const de = {
documents_have_been_signed: 'Dokumente wurden unterschrieben!',
create_a_free_account: 'Kostenloses Konto erstellen',
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',
verified_phone_number: 'Telefonnummer überprüfen',
use_international_format: 'Internationales Format verwenden: +1xxx',
@@ -368,10 +386,15 @@ const de = {
pay_with_strip: 'Mit Stripe bezahlen',
reupload: 'Erneut 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 = {
complete_all_required_fields_to_proceed_with_identity_verification: "Veuillez remplir tous les champs obligatoires pour continuer la vérification de l'identité.",
verif_id: "Vérification de l'ID",
verif_identite: "Vérification de l'identité",
complete: 'Terminer',
fill_all_required_fields_to_complete: 'Veuillez remplir tous les champs obligatoires pour compléter',
sign_and_complete: 'Signer et Terminer',
@@ -439,7 +462,7 @@ const fr = {
documents_have_been_signed: 'Les documents ont été signés!',
create_a_free_account: 'Créer un Compte Gratuit',
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',
verified_phone_number: 'Vérifier le numéro de téléphone',
use_international_format: 'Utiliser le format international : +1xxx',
@@ -461,10 +484,15 @@ const fr = {
pay_with_strip: 'Paiement avec Stripe',
reupload: 'Recharger',
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 = {
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',
fill_all_required_fields_to_complete: 'Uzupełnij wszystkie wymagane pola, aby zakończyć',
sign_and_complete: 'Podpisz i zakończ',
@@ -532,7 +560,7 @@ const pl = {
documents_have_been_signed: 'Dokumenty zostały podpisane!',
create_a_free_account: 'Utwórz darmowe konto',
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',
verified_phone_number: 'Zweryfikuj numer telefonu',
use_international_format: 'Użyj międzynarodowego formatu: +1xxx',
@@ -554,10 +582,14 @@ const pl = {
pay_with_strip: 'Płatność za pomocą Stripe',
reupload: 'Ponowne przesł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.'
}
const uk = {
complete_all_required_fields_to_proceed_with_identity_verification: "Заповніть всі обов'язкові поля, щоб продовжити перевірку особи.",
verify_id: 'Підтвердження ідентичності',
identity_verification: 'Ідентифікація особи',
complete: 'Завершити',
fill_all_required_fields_to_complete: "Заповніть всі обов'язкові поля для завершення",
sign_and_complete: 'Підписати і завершити',
@@ -625,7 +657,7 @@ const uk = {
documents_have_been_signed: 'Документи були підписані!',
create_a_free_account: 'Створити безкоштовний обліковий запис',
powered_by: 'Працює на базі',
please_check_the_box_to_continue: 'Будь ласка, позначте прапорець, щоб продовжити',
please_check_the_box_to_continue: 'Будь ласка, позначте прапорець, щоб продовжити.',
open_source_documents_software: 'відкритий програмний засіб для документів',
verified_phone_number: 'Підтвердіть номер телефону',
use_international_format: 'Використовуйте міжнародний формат: +1xxx',
@@ -647,10 +679,15 @@ const uk = {
pay_with_strip: 'Сплатити за допомогою Stripe',
reupload: 'Перезавантажити',
upload: 'Завантажити',
files: 'Файли'
files: 'Файли',
signature_is_too_small_or_simple_please_redraw: 'Підпис занадто маленький або надто простий. Будь ласка, перемалюйте.',
wait_countdown_seconds: 'Зачекайте {countdown} секунд'
}
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',
fill_all_required_fields_to_complete: 'Please complete all mandatory fields',
sign_and_complete: 'Podepsat a dokončit',
@@ -718,7 +755,7 @@ const cs = {
documents_have_been_signed: 'Dokumenty byly podepsány!',
create_a_free_account: 'Vytvořit bezplatný účet',
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',
verified_phone_number: 'Ověřte telefonní číslo',
use_international_format: 'Použijte mezinárodní formát: +1xxx',
@@ -740,12 +777,17 @@ const cs = {
pay_with_strip: 'Zaplacení přes Stripe',
reupload: 'Znovu 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 = {
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',
preencher_todos_os_campos_obrigatórios_para_concluir: 'Preencher todos os campos obrigatórios para concluir',
fill_all_required_fields_to_complete: 'Preencher todos os campos obrigatórios para concluir',
sign_and_complete: 'Assinar e Completar',
invite: 'Convidar',
email: 'E-mail',
@@ -811,7 +853,7 @@ const pt = {
documents_have_been_signed: 'Os documentos foram assinados!',
create_a_free_account: 'Criar uma Conta Gratuita',
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',
verified_phone_number: 'Verificar Número de Telefone',
use_international_format: 'Use formato internacional: +1xxx',
@@ -833,10 +875,15 @@ const pt = {
pay_with_strip: 'Pagar com Stripe',
reupload: 'Reenviar',
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 = {
complete_all_required_fields_to_proceed_with_identity_verification: 'מלא את כל השדות הנדרשים כדי להמשיך עם אימות זהות.',
verify_id: 'אמת מזהה',
identity_verification: 'אימות זהות',
complete: 'השלם',
fill_all_required_fields_to_complete: 'נא למלא את כל השדות הנדרשים להשלמה',
sign_and_complete: 'חתום והשלם',
@@ -905,7 +952,7 @@ const he = {
documents_have_been_signed: 'המסמכים נחתמו!',
create_a_free_account: 'צור חשבון חינם',
powered_by: 'מופעל על ידי',
please_check_the_box_to_continue: 'אנא סמן את התיבה כדי להמשיך',
please_check_the_box_to_continue: 'אנא סמן את התיבה כדי להמשיך.',
open_source_documents_software: 'תוכנה פתוחה למסמכים',
verified_phone_number: 'אימות מספר טלפון',
use_international_format: 'השתמש בפורמט בינלאומי: +1xxx',
@@ -927,12 +974,17 @@ const he = {
pay_with_strip: 'שלם עם סטרייפ',
reupload: 'העלה שוב',
upload: 'העלאה',
files: 'קבצים'
files: 'קבצים',
signature_is_too_small_or_simple_please_redraw: 'החתימה קטנה או פשוטה מדי. אנא חתום מחדש.',
wait_countdown_seconds: 'המתן {countdown} שניות'
}
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',
vul_alle_verplichte_velden_in_om_te_voltooien: 'Vul alle verplichte velden in om te voltooien',
fill_all_required_fields_to_complete: 'Vul alle verplichte velden in om te voltooien',
sign_and_complete: 'Ondertekenen en voltooien',
invite: 'Uitnodigen',
email: 'E-mail',
@@ -999,7 +1051,7 @@ const nl = {
documents_have_been_signed: 'De documenten zijn ondertekend!',
create_a_free_account: 'Maak een gratis account aan',
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',
verified_phone_number: 'Geverifieerd telefoonnummer',
use_international_format: 'Gebruik internationaal formaat: +1xxx',
@@ -1021,10 +1073,15 @@ const nl = {
pay_with_strip: 'Betalen met Stripe',
reupload: 'Opnieuw 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 = {
complete_all_required_fields_to_proceed_with_identity_verification: 'أكمل جميع الحقول المطلوبة للمتابعة في التحقق من الهوية.',
verify_id: 'تحقق من الهوية',
identity_verification: 'التحقق من الهوية',
complete: 'اكتمال',
fill_all_required_fields_to_complete: 'يرجى ملء جميع الحقول المطلوبة لإكمال',
sign_and_complete: 'التوقيع والاكتمال',
@@ -1092,7 +1149,7 @@ const ar = {
documents_have_been_signed: 'تم توقيع الوثائق!',
create_a_free_account: 'إنشاء حساب مجاني',
powered_by: 'مدعوم من',
please_check_the_box_to_continue: 'الرجاء التحقق من الخانة للمتابعة',
please_check_the_box_to_continue: 'الرجاء التحقق من الخانة للمتابعة.',
open_source_documents_software: 'برنامج وثائق مفتوح المصدر',
verified_phone_number: 'تحقق من رقم الهاتف',
use_international_format: 'استخدم الشكل الدولي: +1xxx',
@@ -1114,10 +1171,15 @@ const ar = {
pay_with_strip: 'الدفع بواسطة Stripe',
reupload: 'إعادة التحميل',
upload: 'تحميل',
files: 'الملفات'
files: 'الملفات',
signature_is_too_small_or_simple_please_redraw: 'التوقيع صغير جدًا أو بسيط جدًا. يرجى إعادة رسمه.',
wait_countdown_seconds: 'انتظر {countdown} ثانية'
}
const ko = {
complete_all_required_fields_to_proceed_with_identity_verification: '신원 확인을 진행하려면 모든 필수 필드를 작성하십시오.',
verify_id: '아이디 확인',
identity_verification: '신원 확인',
complete: '완료',
fill_all_required_fields_to_complete: '모든 필수 필드를 작성하여 완료하세요',
sign_and_complete: '서명하고 완료하기',
@@ -1184,7 +1246,7 @@ const ko = {
documents_have_been_signed: '문서가 서명되었습니다!',
create_a_free_account: '무료 계정 생성',
powered_by: '구동',
please_check_the_box_to_continue: '계속하려면 확인란을 선택하십시오',
please_check_the_box_to_continue: '계속하려면 확인란을 선택하십시오.',
open_source_documents_software: '오픈 소스 문서 소프트웨어',
verified_phone_number: '전화번호 확인됨',
use_international_format: '국제 포맷 사용: +1xxx',
@@ -1206,7 +1268,9 @@ const ko = {
pay_with_strip: '스트라이프로 결제',
reupload: '다시 업로드',
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 }
+17 -7
View File
@@ -1,11 +1,21 @@
<template>
<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
class="label text-2xl"
>{{ showFieldNames && field.name ? field.name : t('image') }}</label>
v-if="showFieldNames"
: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
class="btn btn-outline btn-sm"
class="btn btn-outline btn-sm reupload-button"
@click.prevent="remove"
>
<IconReload :width="16" />
@@ -15,7 +25,7 @@
<div>
<img
: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>
<input
@@ -30,12 +40,12 @@
<div
v-if="field.description"
dir="auto"
class="mb-3 px-1"
class="mb-3 px-1 field-description-text"
>
<MarkdownContent :string="field.description" />
</div>
<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"
:dry-run="dryRun"
:accept="'image/*'"
@@ -1,21 +1,22 @@
<template>
<div dir="auto">
<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 }"
>
<label
class="label text-2xl"
v-if="showFieldNames"
class="label text-xl sm:text-2xl py-0 field-name-label"
>
<MarkdownContent
v-if="field.title"
:string="field.title"
/>
<template v-else>
{{ showFieldNames && field.name ? field.name : t('initials') }}
{{ field.name || t('initials') }}
</template>
</label>
<div class="space-x-2 flex">
<div class="space-x-2 flex flex-none">
<span
v-if="isDrawInitials"
class="tooltip"
@@ -24,7 +25,7 @@
<a
id="type_text_button"
href="#"
class="btn btn-outline font-medium btn-sm"
class="btn btn-outline font-medium btn-sm type-text-button"
@click.prevent="toggleTextInput"
>
<IconTextSize :width="16" />
@@ -41,7 +42,7 @@
<a
id="type_text_button"
href="#"
class="btn btn-outline font-medium btn-sm"
class="btn btn-outline font-medium btn-sm type-text-button"
@click.prevent="toggleTextInput"
>
<IconSignature :width="16" />
@@ -54,9 +55,7 @@
class="tooltip"
:data-tip="t('click_to_upload')"
>
<label
class="btn btn-outline btn-sm font-medium inline-flex flex-nowrap"
>
<label class="btn btn-outline btn-sm font-medium inline-flex flex-nowrap upload-image-button">
<IconUpload :width="16" />
<input
:key="uploadImageInputKey"
@@ -73,7 +72,7 @@
<a
v-if="modelValue || computedPreviousValue"
href="#"
class="btn font-medium btn-outline btn-sm"
class="btn font-medium btn-outline btn-sm clear-canvas-button"
@click.prevent="remove"
>
<IconReload :width="16" />
@@ -82,7 +81,7 @@
<a
v-else
href="#"
class="btn font-medium btn-outline btn-sm"
class="btn font-medium btn-outline btn-sm clear-canvas-button"
@click.prevent="clear"
>
<IconReload :width="16" />
@@ -104,7 +103,7 @@
<div
v-if="field.description"
dir="auto"
class="mb-3 px-1"
class="mb-3 px-1 field-description-text"
>
<MarkdownContent :string="field.description" />
</div>
@@ -119,11 +118,17 @@
:src="attachmentsIndex[modelValue || computedPreviousValue].url"
class="mx-auto bg-white border border-base-300 rounded max-h-44"
>
<canvas
v-show="!modelValue && !computedPreviousValue"
ref="canvas"
class="bg-white border border-base-300 rounded-2xl w-full"
/>
<div class="relative">
<div
v-if="!isDrawInitials"
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
v-if="!isDrawInitials && !modelValue && !computedPreviousValue"
id="initials_text_input"
+10 -5
View File
@@ -12,7 +12,7 @@
:value="authenticityToken"
>
<div
v-for="(submitter, index) in submitters"
v-for="(submitter, index) in [...submitters, ...optionalSubmitters]"
:key="submitter.uuid"
:class="{ 'mt-4': index !== 0 }"
>
@@ -26,7 +26,7 @@
dir="auto"
class="label text-2xl"
>
{{ t('invite') }} {{ submitter.name }}
{{ t('invite') }} {{ submitter.name }} <template v-if="!submitters.includes(submitter)">({{ t('optional') }})</template>
</label>
<input
:id="submitter.uuid"
@@ -34,13 +34,13 @@
class="base-input !text-2xl w-full"
:placeholder="t('email')"
type="email"
required
:required="submitters.includes(submitter)"
autofocus="true"
name="submission[submitters][][email]"
>
</div>
<div
class="mt-6 md:mt-8"
class="mt-4 md:mt-6"
>
<button
type="submit"
@@ -53,7 +53,7 @@
class="mr-1 animate-spin"
/>
<span>
{{ t('submit') }}
{{ t('complete') }}
</span><span
v-if="isSubmitting"
class="w-6 flex justify-start mr-1"
@@ -78,6 +78,11 @@ export default {
type: Array,
required: true
},
optionalSubmitters: {
type: Array,
required: false,
default: () => []
},
url: {
type: String,
required: true
@@ -3,18 +3,24 @@
v-if="showFieldNames && (field.name || field.title)"
:for="field.uuid"
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 }"
><MarkdownContent
v-if="field.title"
:string="field.title"
/>
>
<MarkdownContent
v-if="field.title"
:string="field.title"
/>
<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>
<div
v-if="field.description"
dir="auto"
class="mb-3 px-1"
class="mb-3 px-1 field-description-text"
>
<MarkdownContent :string="field.description" />
</div>
@@ -44,7 +50,7 @@
>
<label
:for="option.uuid"
class="flex items-center space-x-3"
class="flex items-center space-x-3 checkbox-label"
@click="scrollIntoField(field)"
>
<input
+10 -7
View File
@@ -3,15 +3,18 @@
v-if="showFieldNames && (field.name || field.title)"
:for="field.uuid"
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 }"
><MarkdownContent
v-if="field.title"
:string="field.title"
/>
>
<MarkdownContent
v-if="field.title"
:string="field.title"
/>
<template v-else>{{ field.name }}</template>
<template v-if="!field.required">
({{ t('optional') }})
<span :class="{ 'hidden sm:inline': (field.title || field.name).length > 20 }">
({{ t('optional') }})
</span>
</template>
</label>
<div
@@ -21,7 +24,7 @@
<div
v-if="field.description"
dir="auto"
class="mb-3 px-1"
class="mb-3 px-1 field-description-text"
>
<MarkdownContent :string="field.description" />
</div>
@@ -1,8 +1,7 @@
<template>
<label
v-if="!modelValue && !sessionId"
:for="field.uuid"
class="label text-2xl mb-2"
class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5 field-name-label"
>
<MarkdownContent
v-if="field.title"
@@ -35,7 +34,7 @@
<button
v-if="sessionId"
disabled
class="base-button w-full"
class="base-button w-full modal-save-button"
>
<IconLoader
width="22"
File diff suppressed because it is too large Load Diff
+60 -21
View File
@@ -1,8 +1,9 @@
<template>
<div>
<label
v-if="showFieldNames"
:for="isCodeSent ? 'one_time_code' : field.uuid"
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 }"
>
<MarkdownContent
@@ -10,16 +11,13 @@
:string="field.title"
/>
<template v-else>
{{ showFieldNames && field.name ? field.name : t('verified_phone_number') }}
<template v-if="!field.required">
({{ t('optional') }})
</template>
{{ field.name || t('verified_phone_number') }}
</template>
</label>
<div
v-if="field.description"
dir="auto"
class="mb-3 px-1"
class="mb-3 px-1 field-description-text"
>
<MarkdownContent :string="field.description" />
</div>
@@ -47,14 +45,21 @@
<a
v-if="!defaultValue"
href="#"
class="link"
class="link change-phone-number-link"
@click.prevent="isCodeSent = false"
>
{{ t('change_phone_number') }}
</a>
<a
href="#"
<span
v-if="resendCodeCountdown > 0"
class="link"
>
{{ t('wait_countdown_seconds').replace('{countdown}', resendCodeCountdown) }}
</span>
<a
v-else
href="#"
class="link resend-code-link"
@click.prevent="resendCode"
>
{{ isResendLoading ? t('sending') : t('resend_code') }}
@@ -63,13 +68,17 @@
</div>
<div
v-show="!isCodeSent"
class="flex w-full rounded-full outline-neutral-content outline-2 outline-offset-2 focus-within:outline"
class="flex w-full rounded-full outline-neutral-content outline-2 outline-offset-2 focus-within:outline phone-number-input-container"
>
<div class="relative inline-block">
<div class="btn bg-base-200 border border-neutral-300 text-2xl whitespace-nowrap font-normal rounded-l-full">
<div
id="country_code"
class="relative inline-block"
>
<div class="btn bg-base-200 border border-neutral-300 text-2xl whitespace-nowrap font-normal rounded-l-full country-code-select-label">
{{ selectedCountry.flag }} +{{ selectedCountry.dial }}
</div>
<select
id="country_code_select"
class="absolute top-0 bottom-0 right-0 left-0 opacity-0 w-full h-full cursor-pointer"
:disabled="!!defaultValue"
@change="onCountrySelect(countries.find((country) => country.flag === $event.target.value))"
@@ -169,6 +178,8 @@ export default {
data () {
return {
isCodeSent: false,
codeSentAt: null,
resendCodeCountdown: 0,
isResendLoading: false,
phoneValue: this.modelValue || this.defaultValue || '',
selectedCountry: {}
@@ -176,8 +187,8 @@ export default {
},
computed: {
countries () {
return phoneData.map(([name, dial, flag, tz]) => {
return { name, dial, flag, tz }
return phoneData.map(([iso, name, dial, flag, tz]) => {
return { iso, name, dial, flag, tz }
})
},
countriesDialIndex () {
@@ -216,6 +227,11 @@ export default {
this.selectedCountry = this.countries.find((country) => country.tz.includes(tz)) || this.countries[0]
}
},
beforeUnmount () {
if (this.interval) {
clearInterval(this.interval)
}
},
methods: {
emitSubmit: throttle(function (e) {
this.$emit('submit')
@@ -242,13 +258,28 @@ export default {
}
},
resendCode () {
this.isResendLoading = true
if (this.codeSentAt && Date.now() - this.codeSentAt < 15000) {
this.startResendCodeCountdown()
} else {
this.isResendLoading = true
this.sendVerificationCode().finally(() => {
alert(this.t('verification_code_has_been_resent'))
this.sendVerificationCode().then(() => {
alert(this.t('verification_code_has_been_resent'))
}).finally(() => {
this.isResendLoading = false
})
}
},
startResendCodeCountdown () {
this.resendCodeCountdown = 15 - parseInt((Date.now() - this.codeSentAt) / 1000)
this.isResendLoading = false
})
this.interval = setInterval(() => {
this.resendCodeCountdown--
if (this.resendCodeCountdown <= 0) {
clearInterval(this.interval)
}
}, 1000)
},
sendVerificationCode () {
return fetch(this.baseUrl + '/api/send_phone_verification_code', {
@@ -260,12 +291,20 @@ export default {
}),
headers: { 'Content-Type': 'application/json' }
}).then(async (resp) => {
if (resp.status === 422) {
if ([422, 429].includes(resp.status)) {
const data = await resp.json()
alert(this.t('number_phone_is_invalid').replace('{number}', this.fullInternationalPhoneValue))
if (resp.status === 422) {
alert(data.error || this.t('number_phone_is_invalid').replace('{number}', this.fullInternationalPhoneValue))
} else if (resp.status === 429) {
alert(data.error)
}
return Promise.reject(new Error(data.error))
} else if (resp.ok) {
this.codeSentAt = Date.now()
return resp
}
})
},
@@ -4,30 +4,31 @@
class="relative"
>
<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 }"
>
<label
class="label text-2xl"
v-if="showFieldNames"
class="label text-xl sm:text-2xl py-0 field-name-label"
>
<MarkdownContent
v-if="field.title"
:string="field.title"
/>
<template v-else>
{{ showFieldNames && field.name ? field.name : t('signature') }}
{{ field.name || t('signature') }}
</template>
</label>
<div class="space-x-2 flex">
<div class="space-x-2 flex flex-none">
<span
v-if="isTextSignature && field.preferences?.format !== 'typed'"
v-if="isTextSignature && format !== 'typed' && format !== 'upload'"
class="tooltip"
:data-tip="t('draw_signature')"
>
<a
id="type_text_button"
href="#"
class="btn btn-outline btn-sm font-medium"
class="btn btn-outline btn-sm font-medium type-text-button"
@click.prevent="[toggleTextInput(), hideQr()]"
>
<IconSignature :width="16" />
@@ -37,14 +38,15 @@
</a>
</span>
<span
v-else-if="withTypedSignature && field.preferences?.format !== 'typed' && field.preferences?.format !== 'drawn'"
v-else-if="withTypedSignature && format !== 'typed' && format !== 'drawn' && format !== 'upload'"
class="tooltip ml-2"
:class="{ 'hidden sm:inline': modelValue || computedPreviousValue }"
:data-tip="t('type_text')"
>
<a
id="type_text_button"
href="#"
class="btn btn-outline btn-sm font-medium inline-flex flex-nowrap"
class="btn btn-outline btn-sm font-medium inline-flex flex-nowrap type-text-button"
@click.prevent="[toggleTextInput(), hideQr()]"
>
<IconTextSize :width="16" />
@@ -54,13 +56,12 @@
</a>
</span>
<span
v-if="field.preferences?.format !== 'typed' && field.preferences?.format !== 'drawn'"
v-if="format !== 'typed' && format !== 'drawn' && format !== 'upload' && format !== 'drawn_or_typed'"
class="tooltip"
:class="{ 'hidden sm:inline': modelValue || computedPreviousValue }"
:data-tip="t('take_photo')"
>
<label
class="btn btn-outline btn-sm font-medium inline-flex flex-nowrap"
>
<label class="btn btn-outline btn-sm font-medium inline-flex flex-nowrap upload-image-button">
<IconCamera :width="16" />
<input
:key="uploadImageInputKey"
@@ -77,14 +78,14 @@
<a
v-if="modelValue || computedPreviousValue"
href="#"
class="btn btn-outline btn-sm font-medium"
class="btn btn-outline btn-sm font-medium reupload-button"
@click.prevent="remove"
>
<IconReload :width="16" />
{{ t('redraw') }}
{{ t(format === 'upload' ? 'reupload' : 'redraw') }}
</a>
<span
v-if="withQrButton && !modelValue && !computedPreviousValue && field.preferences?.format !== 'typed'"
v-if="withQrButton && !modelValue && !computedPreviousValue && format !== 'typed' && format !== 'upload'"
class=" tooltip"
:data-tip="t('drawn_signature_on_a_touchscreen_device')"
>
@@ -116,7 +117,7 @@
<div
v-if="field.description"
dir="auto"
class="mb-3 px-1"
class="mb-3 px-1 field-description-text"
>
<MarkdownContent :string="field.description" />
</div>
@@ -131,7 +132,18 @@
:src="attachmentsIndex[modelValue || computedPreviousValue].url"
class="mx-auto bg-white border border-base-300 rounded max-h-44"
>
<div class="relative">
<FileDropzone
v-if="format === 'upload' && !modelValue"
:message="`${t('upload')} ${field.name || t('signature')}`"
:submitter-slug="submitterSlug"
:dry-run="dryRun"
:accept="'image/*'"
@upload="[$emit('attached', $event[0]), $emit('update:model-value', $event[0].uuid)]"
/>
<div
v-else
class="relative"
>
<div
v-if="!modelValue && !computedPreviousValue && !isShowQr && !isTextSignature && isSignatureStarted"
class="absolute top-0.5 right-0.5"
@@ -153,7 +165,7 @@
v-show="!modelValue && !computedPreviousValue"
ref="canvas"
style="padding: 1px; 0"
class="bg-white border border-base-300 rounded-2xl w-full"
class="bg-white border border-base-300 rounded-2xl w-full draw-canvas"
/>
<div
v-if="isShowQr"
@@ -280,8 +292,10 @@
<script>
import { IconReload, IconCamera, IconSignature, IconTextSize, IconArrowsDiagonalMinimize2, IconQrcode, IconX } from '@tabler/icons-vue'
import { cropCanvasAndExportToPNG } from './crop_canvas'
import { isValidSignatureCanvas } from './validate_signature'
import SignaturePad from 'signature_pad'
import AppearsOn from './appears_on'
import FileDropzone from './dropzone'
import MarkdownContent from './markdown_content'
import { v4 } from 'uuid'
@@ -294,6 +308,7 @@ export default {
components: {
AppearsOn,
IconReload,
FileDropzone,
IconCamera,
IconQrcode,
MarkdownContent,
@@ -388,6 +403,9 @@ export default {
submitterSlug () {
return this.submitter.slug
},
format () {
return this.field.preferences?.format
},
defaultReasons () {
return {
[this.t('approved_by')]: this.t('approved'),
@@ -581,6 +599,7 @@ export default {
},
drawImage (event) {
this.remove()
this.clear()
this.isSignatureStarted = true
this.drawOnCanvas(event.target.files[0], this.$refs.canvas)
@@ -663,8 +682,14 @@ export default {
return Promise.resolve({})
}
return new Promise((resolve, reject) => {
cropCanvasAndExportToPNG(this.$refs.canvas, { errorOnTooSmall: true }).then(async (blob) => {
if (this.isSignatureStarted && this.pad.toData().length > 0 && !isValidSignatureCanvas(this.pad.toData())) {
alert(this.t('signature_is_too_small_or_simple_please_redraw'))
return Promise.reject(new Error('Image too small or simple'))
}
return new Promise((resolve) => {
cropCanvasAndExportToPNG(this.$refs.canvas).then(async (blob) => {
const file = new File([blob], 'signature.png', { type: 'image/png' })
if (this.dryRun) {
@@ -700,12 +725,6 @@ export default {
return resolve(attachment)
})
}
}).catch((error) => {
if (error.message === 'Image too small' && this.field.required === false) {
return resolve({})
} else {
return reject(error)
}
})
})
}
+14 -9
View File
@@ -3,15 +3,18 @@
v-if="showFieldNames && (field.name || field.title)"
:for="field.uuid"
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 }"
><MarkdownContent
v-if="field.title"
:string="field.title"
/>
>
<MarkdownContent
v-if="field.title"
:string="field.title"
/>
<template v-else>{{ field.name }}</template>
<template v-if="!field.required">
({{ t('optional') }})
<span :class="{ 'hidden sm:inline': (field.title || field.name).length > 20 }">
({{ t('optional') }})
</span>
</template>
</label>
<div
@@ -21,7 +24,7 @@
<div
v-if="field.description"
dir="auto"
class="mb-3 px-1"
class="mb-3 px-1 field-description-text"
>
<MarkdownContent :string="field.description" />
</div>
@@ -64,7 +67,7 @@
>
<a
href="#"
class="btn btn-ghost btn-circle btn-sm"
class="btn btn-ghost btn-circle btn-sm toggle-multiline-text-button"
@click.prevent="toggleTextArea"
>
<IconAlignBoxLeftTop />
@@ -114,7 +117,9 @@ export default {
const area = this.field.areas?.[0]
if (area) {
return parseInt(area.w / area.cell_w) + 1
const num = area.w / area.cell_w
return (num % 1) > 0.2 ? parseInt(num) + 1 : parseInt(num)
} else {
return null
}
@@ -0,0 +1,38 @@
function isValidSignatureCanvas (data) {
if (data.length === 0) return false
const strokes = data.filter(stroke => Array.isArray(stroke.points) && stroke.points.length > 2)
if (strokes.length === 0) return false
let skippedStraightLine = 0
const validStrokes = strokes.filter(stroke => {
const points = stroke.points
const first = points[0]
const last = points[points.length - 1]
const A = last.y - first.y
const B = first.x - last.x
const C = last.x * first.y - first.x * last.y
const lineLength = Math.sqrt(A * A + B * B)
const totalDeviation = points.reduce((sum, p) => {
const distanceToLine = Math.abs(A * p.x + B * p.y + C) / lineLength
return sum + distanceToLine
}, 0)
const avgDeviation = totalDeviation / points.length
if (avgDeviation < 3 && skippedStraightLine < 2) {
skippedStraightLine++
return false
}
return true
})
return validStrokes.length > 0
}
export { isValidSignatureCanvas }
@@ -0,0 +1,193 @@
<template>
<label class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5 field-name-label">
<MarkdownContent
v-if="field.title"
:string="field.title"
/>
<template v-else>{{ field.name || t('identity_verification') }}</template>
</label>
<div
v-if="field.description"
dir="auto"
class="mb-4 px-1 field-description-text"
>
<MarkdownContent :string="field.description" />
</div>
<div
v-if="emptyValueRequiredStep && emptyValueRequiredStep[0] !== field"
class="px-1 field-description-text"
>
{{ t('complete_all_required_fields_to_proceed_with_identity_verification') }}
</div>
<div v-else>
<div
v-if="isLoading"
class="w-full flex space-x-2 justify-center mb-2"
>
<IconInnerShadowTop
width="40"
class="animate-spin h-10"
/>
</div>
<div v-else-if="redirectUrl">
<a
:href="redirectUrl"
target="_blank"
class="white-button w-full"
>
{{ t('verify_id') }}
</a>
</div>
<div
ref="widgetContainer"
/>
</div>
</template>
<script>
import MarkdownContent from './markdown_content'
import { IconInnerShadowTop } from '@tabler/icons-vue'
import phoneData from './phone_data'
export default {
name: 'VerificationStep',
components: {
MarkdownContent,
IconInnerShadowTop
},
inject: ['baseUrl', 't'],
props: {
modelValue: {
type: String,
required: false,
default: ''
},
submitter: {
type: Object,
required: true
},
field: {
type: Object,
required: true
},
emptyValueRequiredStep: {
type: Object,
required: false,
default: null
},
values: {
type: Object,
required: true
},
locale: {
type: String,
required: false,
default: 'en'
},
submitterSlug: {
type: String,
required: true
}
},
emits: ['focus', 'submit', 'update:model-value', 'attached'],
data () {
return {
isCreatingCheckout: false,
isMathLoaded: false,
redirectUrl: '',
isLoading: false,
eidEasyData: {}
}
},
computed: {
countryCode () {
const browserTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone
const browserTz = browserTimeZone.split('/')[1]
const country = phoneData.find(([a, b, c, e, tz]) => tz.includes(browserTz))
return country[0]
},
browserCountry () {
return (navigator.language || navigator.userLanguage || 'en').split('-')[1]
},
widgetSettings () {
return {
clientId: this.eidEasyData.client_id,
docId: this.eidEasyData.doc_id,
language: this.locale,
countryCode: this.countryCode,
enabledMethods: {
signature: this.eidEasyData.available_methods
},
selectedMethod: null,
enabledCountries: 'all',
onSuccess: (data) => {
this.$emit('submit')
},
onFail: (error) => {
console.error(error)
}
}
}
},
async mounted () {
this.isLoading = true
if (new URLSearchParams(window.location.search).get('submit') === 'true') {
this.$emit('submit')
} else {
Promise.all([
import('@eid-easy/eideasy-widget'),
this.start()
]).finally(() => {
this.isLoading = false
})
}
},
methods: {
start () {
return fetch(this.baseUrl + `/api/identity_verification/${this.field.uuid}`, {
method: 'PUT',
body: JSON.stringify({
submitter_slug: this.submitterSlug
}),
headers: { 'Content-Type': 'application/json' }
}).then(async (resp) => {
this.eidEasyData = await resp.json()
if (this.eidEasyData.available_methods[0] === 'itsme-qes-signature' &&
this.eidEasyData.available_methods.length === 1) {
const redirectUrl = new URL('https://id.eideasy.com/sign_contract_external')
redirectUrl.searchParams.append('client_id', this.eidEasyData.client_id)
redirectUrl.searchParams.append('doc_id', this.eidEasyData.doc_id)
redirectUrl.searchParams.append('country', this.countryCode)
redirectUrl.searchParams.append('lang', this.locale)
this.redirectUrl = redirectUrl.toString()
} else {
const eidEasyWidget = document.createElement('eideasy-widget')
for (const key in this.widgetSettings) {
eidEasyWidget[key] = this.widgetSettings[key]
}
this.$refs.widgetContainer.innerHTML = ''
this.$refs.widgetContainer.appendChild(eidEasyWidget)
}
})
},
async submit () {
return fetch(this.baseUrl + '/api/identity_verification', {
method: 'POST',
body: JSON.stringify({
submitter_slug: this.submitterSlug
}),
headers: { 'Content-Type': 'application/json' }
}).then(async (resp) => {
return resp
})
}
}
}
</script>
+99 -53
View File
@@ -1,7 +1,8 @@
<template>
<div
class="absolute overflow-visible group"
class="absolute overflow-visible group field-area-container"
:style="positionStyle"
:class="{ 'z-[1]': isMoved || isDragged }"
@pointerdown.stop
@mousedown="startMouseMove"
@touchstart="startTouchDrag"
@@ -9,7 +10,7 @@
<div
v-if="isSelected || isDraw"
class="top-0 bottom-0 right-0 left-0 absolute border border-1.5 pointer-events-none"
:class="field.type === 'heading' ? '' : borderColors[submitterIndex]"
:class="field.type === 'heading' ? '' : borderColors[submitterIndex % borderColors.length]"
/>
<div
v-if="field.type === 'cells' && (isSelected || isDraw)"
@@ -19,7 +20,7 @@
v-for="(cellW, index) in cells"
:key="index"
class="absolute top-0 bottom-0 border-r"
:class="field.type === 'heading' ? '' : borderColors[submitterIndex]"
:class="field.type === 'heading' ? '' : borderColors[submitterIndex % borderColors.length]"
:style="{ left: (cellW / area.w * 100) + '%' }"
>
<span
@@ -32,7 +33,7 @@
</div>
<div
v-if="field?.type && (isSelected || isNameFocus)"
class="absolute bg-white rounded-t border overflow-visible whitespace-nowrap flex z-10"
class="absolute bg-white rounded-t border overflow-visible whitespace-nowrap flex z-10 field-area-controls"
style="top: -25px; height: 25px"
@mousedown.stop
@pointerdown.stop
@@ -40,7 +41,7 @@
<FieldSubmitter
v-if="field.type != 'heading'"
v-model="field.submitter_uuid"
class="border-r"
class="border-r roles-dropdown"
:compact="true"
:editable="editable && (!defaultField || defaultField.role !== submitter?.name)"
:allow-add-new="!defaultSubmitters.length"
@@ -69,7 +70,7 @@
@keydown.enter.prevent="onNameEnter"
@focus="onNameFocus"
@blur="onNameBlur"
>{{ optionIndexText }} {{ (defaultField ? (field.title || field.name) : field.name) || defaultName }}</span>
>{{ optionIndexText }} {{ (defaultField ? (defaultField.title || field.title || field.name) : field.name) || defaultName }}</span>
<div
v-if="isSettingsFocus || (isValueInput && field.type !== 'heading') || (isNameFocus && !['checkbox', 'phone'].includes(field.type))"
class="flex items-center ml-1.5"
@@ -107,7 +108,7 @@
>{{ t('editable') }}</label>
<span
v-if="field.type !== 'payment' && !isValueInput"
class="dropdown dropdown-end"
class="dropdown dropdown-end field-area-settings-dropdown"
@mouseenter="renderDropdown = true"
@touchstart="renderDropdown = true"
>
@@ -140,6 +141,7 @@
:with-required="false"
:with-areas="false"
@click-formula="isShowFormulaModal = true"
@click-font="isShowFontModal = true"
@click-description="isShowDescriptionModal = true"
@click-condition="isShowConditionsModal = true"
@scroll-to="[selectedAreaRef.value = $event, $emit('scroll-to', $event)]"
@@ -158,9 +160,9 @@
</div>
<div
ref="touchValueTarget"
class="flex items-center h-full w-full"
class="flex items-center h-full w-full field-area"
dir="auto"
:class="[isValueInput ? 'bg-opacity-50' : 'bg-opacity-80', field.type === 'heading' ? 'bg-gray-50' : bgColors[submitterIndex], isDefaultValuePresent || isValueInput || (withFieldPlaceholder && field.areas) ? (alignClasses[field.preferences?.align] || '') : 'justify-center']"
:class="[isValueInput ? 'bg-opacity-50' : 'bg-opacity-80', field.type === 'heading' ? 'bg-gray-50' : bgColors[submitterIndex % bgColors.length], isDefaultValuePresent || isValueInput || (withFieldPlaceholder && field.areas) ? fontClasses : 'justify-center']"
@click="focusValueInput"
>
<span
@@ -211,7 +213,7 @@
:contenteditable="isValueInput"
class="whitespace-pre-wrap outline-none empty:before:content-[attr(placeholder)] before:text-gray-400"
:class="{ 'cursor-text': isValueInput }"
:placeholder="withFieldPlaceholder && !isValueInput ? field.name || defaultName : t('type_value')"
:placeholder="withFieldPlaceholder && !isValueInput ? defaultField?.title || field.title || field.name || defaultName : t('type_value')"
@blur="onDefaultValueBlur"
@paste.prevent="onPaste"
@keydown.enter="onDefaultValueEnter"
@@ -237,7 +239,7 @@
/>
<span
v-if="field?.type && editable"
class="h-4 w-4 md:h-2.5 md:w-2.5 -right-1 rounded-full -bottom-1 border-gray-400 bg-white shadow-md border absolute cursor-nwse-resize"
class="h-4 w-4 lg:h-2.5 lg:w-2.5 -right-1 rounded-full -bottom-1 border-gray-400 bg-white shadow-md border absolute cursor-nwse-resize"
@mousedown.stop="startResize"
@touchstart="startTouchResize"
/>
@@ -252,12 +254,23 @@
@close="isShowFormulaModal = false"
/>
</Teleport>
<Teleport
v-if="isShowFontModal"
:to="modalContainerEl"
>
<FontModal
:field="field"
:editable="editable && !defaultField"
:build-default-name="buildDefaultName"
@close="isShowFontModal = false"
/>
</Teleport>
<Teleport
v-if="isShowConditionsModal"
:to="modalContainerEl"
>
<ConditionsModal
:field="field"
:item="field"
:build-default-name="buildDefaultName"
@close="isShowConditionsModal = false"
/>
@@ -282,6 +295,7 @@ import FieldType from './field_type'
import Field from './field'
import FieldSettings from './field_settings'
import FormulaModal from './formula_modal'
import FontModal from './font_modal'
import ConditionsModal from './conditions_modal'
import DescriptionModal from './description_modal'
import { IconX, IconCheck, IconDotsVertical } from '@tabler/icons-vue'
@@ -294,6 +308,7 @@ export default {
IconCheck,
FieldSettings,
FormulaModal,
FontModal,
IconDotsVertical,
DescriptionModal,
ConditionsModal,
@@ -316,6 +331,11 @@ export default {
required: false,
default: false
},
maxPage: {
type: Number,
required: false,
default: null
},
defaultField: {
type: Object,
required: false,
@@ -346,6 +366,7 @@ export default {
data () {
return {
isShowFormulaModal: false,
isShowFontModal: false,
isShowConditionsModal: false,
isContenteditable: false,
isSettingsFocus: false,
@@ -355,6 +376,7 @@ export default {
isMoved: false,
renderDropdown: false,
isNameFocus: false,
isHeadingSelected: false,
textOverflowChars: 0,
dragFrom: { x: 0, y: 0 }
}
@@ -371,7 +393,7 @@ export default {
}
},
isValueInput () {
return (this.field.type === 'heading' && this.isSelected) || this.isContenteditable || (this.inputMode && ['text', 'number', 'date'].includes(this.field.type))
return (this.field.type === 'heading' && this.isHeadingSelected) || this.isContenteditable || (this.inputMode && ['text', 'number', 'date'].includes(this.field.type))
},
modalContainerEl () {
return this.$el.getRootNode().querySelector('#docuseal_modal_container')
@@ -379,11 +401,19 @@ export default {
defaultName () {
return this.buildDefaultName(this.field, this.template.fields)
},
alignClasses () {
fontClasses () {
if (!this.field.preferences) {
return {}
}
return {
center: 'justify-center',
left: 'justify-start',
right: 'justify-end'
'justify-center': this.field.preferences.align === 'center',
'justify-start': this.field.preferences.align === 'left',
'justify-end': this.field.preferences.align === 'right',
'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)
}
},
optionIndexText () {
@@ -414,16 +444,6 @@ export default {
},
borderColors () {
return [
'border-red-500/80',
'border-sky-500/80',
'border-emerald-500/80',
'border-yellow-300/80',
'border-purple-600/80',
'border-pink-500/80',
'border-cyan-500/80',
'border-orange-500/80',
'border-lime-500/80',
'border-indigo-500/80',
'border-red-500/80',
'border-sky-500/80',
'border-emerald-500/80',
@@ -438,16 +458,6 @@ export default {
},
bgColors () {
return [
'bg-red-100',
'bg-sky-100',
'bg-emerald-100',
'bg-yellow-100',
'bg-purple-100',
'bg-pink-100',
'bg-cyan-100',
'bg-orange-100',
'bg-lime-100',
'bg-indigo-100',
'bg-red-100',
'bg-sky-100',
'bg-emerald-100',
@@ -493,13 +503,17 @@ export default {
methods: {
buildDefaultName: Field.methods.buildDefaultName,
closeDropdown () {
document.activeElement.blur()
this.$el.getRootNode().activeElement.blur()
},
maybeToggleDefaultValue () {
if (!this.editable) {
return
}
if (['text', 'number'].includes(this.field.type)) {
this.isContenteditable = true
this.$nextTick(() => this.focusValueInput())
this.focusValueInput()
} else if (this.field.type === 'checkbox') {
this.field.readonly = !this.field.readonly
this.field.default_value === true ? delete this.field.default_value : this.field.default_value = true
@@ -521,20 +535,28 @@ export default {
}
},
focusValueInput (e) {
if (this.$refs.defaultValue !== document.activeElement) {
this.$refs.defaultValue.focus()
this.$nextTick(() => {
if (this.$refs.defaultValue && this.$refs.defaultValue !== document.activeElement) {
this.$refs.defaultValue.focus()
if (this.$refs.defaultValue.innerText.length && this.$refs.defaultValue !== e?.target) {
window.getSelection().collapse(
this.$refs.defaultValue.firstChild,
this.$refs.defaultValue.innerText.length
)
if (this.$refs.defaultValue.innerText.length && this.$refs.defaultValue !== e?.target) {
window.getSelection().collapse(
this.$refs.defaultValue.firstChild,
this.$refs.defaultValue.innerText.length
)
}
}
}
})
},
formatNumber (number, format) {
if (format === 'comma') {
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') {
return new Intl.NumberFormat('de-DE').format(number)
} else if (format === 'space') {
@@ -640,6 +662,7 @@ export default {
const text = this.$refs.defaultValue.innerText.trim()
this.isContenteditable = false
this.isHeadingSelected = false
if (text) {
if (this.field.type === 'number') {
@@ -720,12 +743,18 @@ export default {
const rect = page.getBoundingClientRect()
this.area.x = Math.min(Math.max((this.dragFrom.x + e.touches[0].clientX - rect.left) / rect.width, 0), 1 - this.area.w)
this.area.y = Math.min(Math.max((this.dragFrom.y + e.touches[0].clientY - rect.top) / rect.height, 0), 1 - this.area.h)
this.area.y = (this.dragFrom.y + e.touches[0].clientY - rect.top) / rect.height
if ((this.area.page === 0 && this.area.y < 0) || (this.area.page === this.maxPage && this.area.y > 1 - this.area.h)) {
this.area.y = Math.min(Math.max(this.area.y, 0), 1 - this.area.h)
}
},
stopTouchDrag () {
this.$el.getRootNode().removeEventListener('touchmove', this.touchDrag)
this.$el.getRootNode().removeEventListener('touchend', this.stopTouchDrag)
this.maybeChangeAreaPage(this.area)
if (this.isDragged) {
this.save()
}
@@ -751,10 +780,6 @@ export default {
this.selectedAreaRef.value = this.area
if (this.field.type === 'heading') {
this.$nextTick(() => this.focusValueInput())
}
this.dragFrom = { x: rect.left - e.clientX, y: rect.top - e.clientY }
this.$el.getRootNode().addEventListener('mousemove', this.mouseMove)
@@ -773,21 +798,42 @@ export default {
const rect = page.getBoundingClientRect()
this.area.x = Math.min(Math.max((this.dragFrom.x + e.clientX - rect.left) / rect.width, 0), 1 - this.area.w)
this.area.y = Math.min(Math.max((this.dragFrom.y + e.clientY - rect.top) / rect.height, 0), 1 - this.area.h)
this.area.y = (this.dragFrom.y + e.clientY - rect.top) / rect.height
if ((this.area.page === 0 && this.area.y < 0) || (this.area.page === this.maxPage && this.area.y > 1 - this.area.h)) {
this.area.y = Math.min(Math.max(this.area.y, 0), 1 - this.area.h)
}
},
stopMouseMove (e) {
this.$el.getRootNode().removeEventListener('mousemove', this.mouseMove)
this.$el.getRootNode().removeEventListener('mouseup', this.stopMouseMove)
this.maybeChangeAreaPage(this.area)
if (this.isMoved) {
this.save()
}
if (this.field.type === 'heading') {
this.isHeadingSelected = !this.isMoved
this.focusValueInput()
}
this.isDragged = false
this.isMoved = false
this.$emit('stop-drag')
},
maybeChangeAreaPage (area) {
if (area.y < -(area.h / 2)) {
area.page -= 1
area.y = 1 + area.y + (16.0 / this.$parent.$refs.mask.previousSibling.offsetHeight)
} else if (area.y > 1 - (area.h / 2)) {
area.page += 1
area.y = area.y - 1 - (16.0 / this.$parent.$refs.mask.previousSibling.offsetHeight)
}
},
stopDrag () {
this.$el.getRootNode().removeEventListener('mousemove', this.drag)
this.$el.getRootNode().removeEventListener('mouseup', this.stopDrag)
+192 -42
View File
@@ -1,10 +1,11 @@
<template>
<div
style="max-width: 1600px"
class="mx-auto pl-3 md:pl-4 h-full"
class="mx-auto pl-3 h-full"
:class="isMobile ? 'pl-4' : 'md:pl-4'"
>
<div
v-if="pendingFieldAttachmentUuids.length"
v-if="pendingFieldAttachmentUuids.length && editable"
class="top-1.5 sticky h-0 z-20 max-w-2xl mx-auto"
>
<div class="alert border-base-content/30 py-2 px-2.5">
@@ -31,7 +32,7 @@
<div
v-if="$slots.buttons || withTitle"
id="title_container"
class="flex justify-between py-1.5 items-center pr-4 top-0 z-10"
class="flex justify-between py-1.5 items-center pr-4 top-0 z-10 title-container"
:class="{ sticky: withStickySubmitters || isBreakpointLg }"
:style="{ backgroundColor }"
>
@@ -46,7 +47,7 @@
v-if="withTitle"
:model-value="template.name"
:editable="editable"
class="text-xl md:text-3xl font-semibold focus:text-clip"
class="text-xl md:text-3xl font-semibold focus:text-clip template-name"
:icon-stroke-width="2.3"
@update:model-value="updateName"
/>
@@ -162,8 +163,8 @@
</div>
<div
id="main_container"
class="flex"
:class="$slots.buttons || withTitle ? 'md:max-h-[calc(100%_-_60px)]' : 'md:max-h-[100%]'"
class="flex main-container"
:class="$slots.buttons || withTitle ? (isMobile ? 'max-h-[calc(100%_-_60px)]' : 'md:max-h-[calc(100%_-_60px)]') : (isMobile ? 'max-h-[100%]' : 'md:max-h-[100%]')"
>
<div
v-if="withDocumentsList"
@@ -178,6 +179,7 @@
:with-arrows="template.schema.length > 1"
:item="item"
:document="sortedDocuments[index]"
:data-document-uuid="item.attachment_uuid"
:accept-file-types="acceptFileTypes"
:with-replace-button="withUploadButton"
:editable="editable"
@@ -186,6 +188,7 @@
@remove="onDocumentRemove"
@replace="onDocumentReplace"
@up="moveDocument(item, -1)"
@reorder="reorderFields"
@down="moveDocument(item, 1)"
@change="save"
/>
@@ -202,7 +205,7 @@
<button
v-if="sortedDocuments.length && editable && withAddPageButton"
id="add_blank_page_button"
class="btn btn-outline w-full"
class="btn btn-outline w-full add-blank-page-button"
@click.prevent="addBlankPage"
>
<IconInnerShadowTop
@@ -219,7 +222,8 @@
</div>
<div
id="pages_container"
class="w-full overflow-y-hidden md:overflow-y-auto overflow-x-hidden mt-0.5 pt-0.5"
class="w-full overflow-x-hidden mt-0.5 pt-0.5"
:class="isMobile ? 'overflow-y-auto' : 'overflow-y-hidden md:overflow-y-auto'"
>
<div
ref="documents"
@@ -235,7 +239,7 @@
<button
v-if="withAddPageButton"
id="add_blank_page_button"
class="btn btn-outline w-full mt-4"
class="btn btn-outline w-full mt-4 add-blank-page-button"
@click.prevent="addBlankPage"
>
<IconInnerShadowTop
@@ -262,7 +266,8 @@
:is-drag="!!dragField"
:input-mode="inputMode"
:default-fields="[...defaultRequiredFields, ...defaultFields]"
:allow-draw="!onlyDefinedFields"
:allow-draw="!onlyDefinedFields || drawField"
:data-document-uuid="document.uuid"
:default-submitters="defaultSubmitters"
:with-field-placeholder="withFieldPlaceholder"
:draw-field="drawField"
@@ -302,7 +307,7 @@
<button
v-if="withAddPageButton"
id="add_blank_page_button"
class="btn btn-outline w-full mt-4"
class="btn btn-outline w-full mt-4 add-blank-page-button"
@click.prevent="addBlankPage"
>
<IconInnerShadowTop
@@ -320,9 +325,9 @@
</div>
</div>
<div
v-if="withFieldsList"
v-if="withFieldsList && !isMobile"
id="fields_list_container"
class="relative w-80 flex-none mt-1 pr-4 pl-0.5 hidden md:block"
class="relative w-80 flex-none mt-1 pr-4 pl-0.5 hidden md:block fields-list-container"
:class="drawField ? 'overflow-hidden' : 'overflow-y-auto overflow-x-hidden'"
>
<div
@@ -330,13 +335,13 @@
class="sticky inset-0 h-full z-20"
:style="{ backgroundColor }"
>
<div class="bg-base-200 rounded-lg p-5 text-center space-y-4">
<div class="bg-base-200 rounded-lg p-5 text-center space-y-4 draw-field-container">
<p>
{{ t('draw_field_on_the_document') }}
</p>
<div>
<button
class="base-button"
class="base-button cancel-draw-button"
@click="clearDrawField"
>
{{ t('cancel') }}
@@ -361,7 +366,9 @@
:with-help="withHelp"
:default-submitters="defaultSubmitters"
:draw-field-type="drawFieldType"
:with-fields-search="withFieldsSearch"
:default-fields="[...defaultRequiredFields, ...defaultFields]"
:template="template"
:default-required-fields="defaultRequiredFields"
:field-types="fieldTypes"
:with-sticky-submitters="withStickySubmitters"
@@ -378,14 +385,14 @@
</div>
</div>
</div>
<div class="sticky bottom-0">
<div class="sticky bottom-0 z-10">
<MobileDrawField
v-if="drawField && isBreakpointLg"
v-if="drawField && (isBreakpointLg || isMobile)"
:draw-field="drawField"
:fields="template.fields"
:submitters="template.submitters"
:selected-submitter="selectedSubmitter"
class="md:hidden"
:class="{ 'md:hidden': !isMobile }"
:editable="editable"
@cancel="[drawField = null, drawOption = null]"
@change-submitter="[selectedSubmitter = $event, drawField.submitter_uuid = $event.uuid]"
@@ -396,11 +403,15 @@
:default-fields="[...defaultRequiredFields, ...defaultFields]"
:default-required-fields="defaultRequiredFields"
:field-types="fieldTypes"
:class="{ 'md:hidden': !isMobile }"
:selected-submitter="selectedSubmitter"
@select="startFieldDraw($event)"
/>
</div>
<div id="docuseal_modal_container" />
<div
id="docuseal_modal_container"
class="modal-container"
/>
</div>
</template>
@@ -455,6 +466,7 @@ export default {
fieldTypes: this.fieldTypes,
backgroundColor: this.backgroundColor,
withPhone: this.withPhone,
withVerification: this.withVerification,
withPayment: this.withPayment,
isPaymentConnected: this.isPaymentConnected,
withFormula: this.withFormula,
@@ -615,6 +627,11 @@ export default {
required: false,
default: true
},
withFieldsSearch: {
type: Boolean,
required: false,
default: null
},
withFieldsList: {
type: Boolean,
required: false,
@@ -635,6 +652,11 @@ export default {
required: false,
default: false
},
withVerification: {
type: Boolean,
required: false,
default: null
},
withPayment: {
type: Boolean,
required: false,
@@ -689,6 +711,11 @@ export default {
language () {
return this.locale.split('-')[0].toLowerCase()
},
isMobile () {
const isMobileSafariIos = 'ontouchstart' in window && navigator.maxTouchPoints > 0 && /AppleWebKit/i.test(navigator.userAgent)
return isMobileSafariIos || /android|iphone|ipad/i.test(navigator.userAgent)
},
defaultDateFormat () {
const isUsBrowser = Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US')
const isUsTimezone = new Intl.DateTimeFormat('en-US', { timeZoneName: 'short' }).format(new Date()).match(/\s(?:CST|CDT|PST|PDT|EST|EDT)$/)
@@ -756,13 +783,17 @@ export default {
}
})
const defineSubmittersUuids = this.defineSubmitters.map((name) => {
return this.template.submitters.find(e => e.name === name)?.uuid
})
this.defineSubmitters.forEach((name, index) => {
const submitter = (this.template.submitters[index] ||= {})
submitter.name = name || this.submitterDefaultNames[index]
if (existingSubmittersUuids.filter(Boolean).length) {
submitter.uuid = existingSubmittersUuids[index] || submitter.uuid || v4()
if (defineSubmittersUuids.filter(Boolean).length || existingSubmittersUuids.filter(Boolean).length) {
submitter.uuid = defineSubmittersUuids[index] || existingSubmittersUuids[index] || submitter.uuid || v4()
} else {
submitter.uuid ||= v4()
}
@@ -805,6 +836,89 @@ export default {
this.documentRefs = []
},
methods: {
reorderFields (item) {
const itemFields = []
const fields = []
const fieldAreasIndex = {}
const attachmentUuids = this.template.schema.map((e) => e.attachment_uuid)
this.template.fields.forEach((f) => {
if (f.areas?.length) {
const firstArea = f.areas.reduce((min, a) => {
return attachmentUuids.indexOf(a.attachment_uuid) < attachmentUuids.indexOf(min.attachment_uuid) ? a : min
}, f.areas[0])
if (firstArea.attachment_uuid === item.attachment_uuid) {
itemFields.push(f)
} else {
fields.push(f)
}
} else {
fields.push(f)
}
})
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)
}
}
itemFields.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)
})
const insertBeforeAttachmentUuids = attachmentUuids.slice(this.template.schema.indexOf(item) + 1)
let sortedFields = []
if (insertBeforeAttachmentUuids.length) {
const insertAfterField = fields.find((f) => {
if (f.areas?.length) {
return f.areas.find((a) => insertBeforeAttachmentUuids.includes(a.attachment_uuid))
} else {
return false
}
})
if (insertAfterField) {
fields.splice(fields.indexOf(insertAfterField), 0, ...itemFields)
sortedFields = fields
} else {
sortedFields = fields.concat(itemFields)
}
} else {
if (fields.length && itemFields.length && this.template.fields.indexOf(fields[0]) > this.template.fields.indexOf(itemFields[0])) {
sortedFields = itemFields.concat(fields)
} else {
sortedFields = fields.concat(itemFields)
}
}
if (this.template.fields.length === sortedFields.length) {
this.template.fields = sortedFields
this.save()
}
},
closeDropdown () {
document.activeElement.blur()
},
@@ -1017,6 +1131,22 @@ export default {
if (!field.areas.length) {
this.template.fields.splice(this.template.fields.indexOf(field), 1)
this.template.fields.forEach((f) => {
(f.conditions || []).forEach((c) => {
if (c.field_uuid === field.uuid) {
f.conditions.splice(f.conditions.indexOf(c), 1)
}
})
})
this.template.schema.forEach((item) => {
(item.conditions || []).forEach((c) => {
if (c.field_uuid === field.uuid) {
item.conditions.splice(item.conditions.indexOf(c), 1)
}
})
})
}
this.save()
@@ -1075,7 +1205,7 @@ export default {
} else if (type === 'image') {
area.w = pageMask.clientWidth / 5 / pageMask.clientWidth
area.h = (pageMask.clientWidth / 5 / pageMask.clientWidth) * (pageMask.clientWidth / pageMask.clientHeight)
} else if (type === 'signature' || type === 'stamp') {
} else if (type === 'signature' || type === 'stamp' || type === 'verification') {
area.w = pageMask.clientWidth / 5 / pageMask.clientWidth
area.h = (pageMask.clientWidth / 5 / pageMask.clientWidth) * (pageMask.clientWidth / pageMask.clientHeight) / 2
} else if (type === 'initials') {
@@ -1189,6 +1319,10 @@ export default {
}
},
onDropfield (area) {
if (!this.editable) {
return
}
const field = this.fieldsDragFieldRef.value || {
name: '',
uuid: v4(),
@@ -1199,7 +1333,11 @@ export default {
if (!this.fieldsDragFieldRef.value) {
if (['select', 'multiple', 'radio'].includes(field.type)) {
field.options = [{ value: '', uuid: v4() }]
if (this.dragField?.options?.length) {
field.options = this.dragField.options.map(option => ({ value: option, uuid: v4() }))
} else {
field.options = [{ value: '', uuid: v4() }]
}
}
if (['stamp', 'heading'].includes(field.type)) {
@@ -1207,9 +1345,8 @@ export default {
}
if (field.type === 'date') {
field.preferences = {
format: this.defaultDateFormat
}
field.preferences ||= {}
field.preferences.format ||= this.defaultDateFormat
}
}
@@ -1239,7 +1376,7 @@ export default {
w: area.maskW / 5 / area.maskW,
h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH)
}
} else if (field.type === 'signature' || field.type === 'stamp') {
} else if (field.type === 'signature' || field.type === 'stamp' || field.type === 'verification') {
baseArea = {
w: area.maskW / 5 / area.maskW,
h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH) / 2
@@ -1270,11 +1407,20 @@ export default {
const lastArea = field.areas[field.areas.length - 1]
if (lastArea) {
fieldArea.x -= lastArea.w / 2
fieldArea.w = lastArea.w
fieldArea.h = lastArea.h
}
if (field.width) {
fieldArea.w = field.width / area.maskW
delete field.width
}
if (field.height) {
fieldArea.h = field.height / area.maskH
delete field.height
}
field.areas.push(fieldArea)
this.selectedAreaRef.value = fieldArea
@@ -1292,6 +1438,8 @@ export default {
const documentRef = this.documentRefs.find((e) => e.document.uuid === area.attachment_uuid)
const areaRef = documentRef.pageRefs[area.page].areaRefs.find((ref) => ref.area === this.selectedAreaRef.value)
areaRef.isHeadingSelected = true
areaRef.focusValueInput()
})
}
@@ -1342,7 +1490,9 @@ export default {
}
this.$nextTick(() => {
this.$refs.previews.scrollTop = this.$refs.previews.scrollHeight
if (this.$refs.previews) {
this.$refs.previews.scrollTop = this.$refs.previews.scrollHeight
}
this.scrollIntoDocument(data.schema[0])
})
@@ -1377,29 +1527,29 @@ export default {
onDocumentRemove (item) {
if (window.confirm(this.t('are_you_sure_'))) {
this.template.schema.splice(this.template.schema.indexOf(item), 1)
}
const removedFieldUuids = []
const removedFieldUuids = []
this.template.fields.forEach((field) => {
[...(field.areas || [])].forEach((area) => {
if (area.attachment_uuid === item.attachment_uuid) {
field.areas.splice(field.areas.indexOf(area), 1)
this.template.fields.forEach((field) => {
[...(field.areas || [])].forEach((area) => {
if (area.attachment_uuid === item.attachment_uuid) {
field.areas.splice(field.areas.indexOf(area), 1)
removedFieldUuids.push(field.uuid)
}
removedFieldUuids.push(field.uuid)
}
})
})
})
this.template.fields =
this.template.fields.filter((f) => !removedFieldUuids.includes(f.uuid) || f.areas?.length)
this.template.fields =
this.template.fields.filter((f) => !removedFieldUuids.includes(f.uuid) || f.areas?.length)
this.save()
this.save()
}
},
onDocumentReplace (data) {
const { replaceSchemaItem, schema, documents } = data
this.template.schema.splice(this.template.schema.indexOf(replaceSchemaItem), 1, schema[0])
this.template.schema.splice(this.template.schema.indexOf(replaceSchemaItem), 1, { ...replaceSchemaItem, ...schema[0] })
this.template.documents.push(...documents)
if (data.fields) {
@@ -8,12 +8,12 @@
/>
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl">
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
<span>
{{ t('condition') }} - {{ field.name || buildDefaultName(field, template.fields) }}
<span class="modal-title">
{{ t('condition') }} - {{ item.name || buildDefaultName(item, template.fields) }}
</span>
<a
href="#"
class="text-xl"
class="text-xl modal-close-button"
@click.prevent="$emit('close')"
>&times;</a>
</div>
@@ -29,19 +29,30 @@
>{{ t('available_in_pro') }}</a>
</div>
<form @submit.prevent="validateSaveAndClose">
<div class="my-4 space-y-5">
<div class="my-4">
<div
v-for="(condition, cindex) in conditions"
:key="cindex"
class="space-y-4 relative"
>
<div
v-if="conditions.length > 1"
class="flex justify-between border-b mx-1 -mb-1 pb-1"
v-if="cindex > 0"
class="divider -mb-2 mx-1"
>
<span class="text-sm">
<button
class="btn btn-xs btn-primary w-24"
@click.prevent="condition.operation === 'or' ? delete condition.operation : condition.operation = 'or'"
>
{{ condition.operation === 'or' ? t('or') : t('and') }}
</button>
</div>
<div
v-if="conditions.length > 1"
class="flex justify-between mx-1"
>
<label class="text-sm">
{{ t('condition') }} {{ cindex + 1 }}
</span>
</label>
<a
href="#"
class="link text-sm"
@@ -120,19 +131,17 @@
class="inline float-right link text-right mb-3 px-2"
@click.prevent="conditions.push({})"
> + {{ t('add_condition') }}</a>
<button
class="base-button w-full mt-2"
>
<button class="base-button w-full mt-2 modal-save-button">
{{ t('save') }}
</button>
</form>
<div
v-if="field.conditions?.[0]?.field_uuid"
v-if="item.conditions?.[0]?.field_uuid"
class="text-center w-full mt-4"
>
<button
class="link"
@click="[conditions = [], delete field.conditions, validateSaveAndClose()]"
@click="[conditions = [], delete item.conditions, validateSaveAndClose()]"
>
{{ t('remove_condition') }}
</button>
@@ -147,7 +156,7 @@ export default {
name: 'ConditionModal',
inject: ['t', 'save', 'template', 'withConditions'],
props: {
field: {
item: {
type: Object,
required: true
},
@@ -159,22 +168,26 @@ export default {
emits: ['close'],
data () {
return {
conditions: this.field.conditions?.[0] ? JSON.parse(JSON.stringify(this.field.conditions)) : [{}]
conditions: this.item.conditions?.[0] ? JSON.parse(JSON.stringify(this.item.conditions)) : [{}]
}
},
computed: {
fields () {
return this.template.fields.reduce((acc, f) => {
if (f !== this.field && f.submitter_uuid === this.field.submitter_uuid) {
acc.push(f)
}
if (this.item.submitter_uuid) {
return this.template.fields.reduce((acc, f) => {
if (f !== this.item) {
acc.push(f)
}
return acc
}, [])
return acc
}, [])
} else {
return this.template.fields
}
}
},
created () {
this.field.conditions ||= []
this.item.conditions ||= []
},
methods: {
conditionField (condition) {
@@ -208,9 +221,9 @@ export default {
}
if (this.conditions.find((f) => f.field_uuid)) {
this.field.conditions = this.conditions
this.item.conditions = this.conditions
} else {
delete this.field.conditions
delete this.item.conditions
}
this.save()
+4 -4
View File
@@ -1,7 +1,7 @@
<template>
<div class="flex space-x-2">
<Contenteditable
class="w-full block mr-6"
class="w-full block mr-6 document-preview-name"
:model-value="item.name"
:icon-width="16"
@update:model-value="onUpdateName"
@@ -15,7 +15,7 @@
/>
<button
v-if="withArrows"
class="btn border-base-200 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors"
class="btn border-base-200 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors document-control-button"
style="width: 24px; height: 24px"
@click.stop="$emit('up', item)"
>
@@ -23,14 +23,14 @@
</button>
<button
v-if="withArrows"
class="btn border-base-200 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors"
class="btn border-base-200 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors document-control-button"
style="width: 24px; height: 24px"
@click.stop="$emit('down', item)"
>
&darr;
</button>
<button
class="btn border-base-200 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors"
class="btn border-base-200 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors document-control-button"
style="width: 24px; height: 24px"
@click.stop="$emit('remove', item)"
>
@@ -8,12 +8,12 @@
/>
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl">
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
<span>
<span class="modal-title">
{{ field.name || buildDefaultName(field, template.fields) }}
</span>
<a
href="#"
class="text-xl"
class="text-xl modal-close-button"
@click.prevent="$emit('close')"
>&times;</a>
</div>
@@ -55,9 +55,7 @@
>
</div>
</div>
<button
class="base-button w-full mt-4"
>
<button class="base-button w-full mt-4 modal-save-button">
{{ t('save') }}
</button>
</form>
@@ -7,6 +7,7 @@
:input-mode="inputMode"
:number="index"
:editable="editable"
:data-page="index"
:areas="areasIndex[index]"
:allow-draw="allowDraw"
:is-drag="isDrag"
@@ -16,6 +17,7 @@
:draw-field="drawField"
:draw-field-type="drawFieldType"
:selected-submitter="selectedSubmitter"
:total-pages="sortedPreviewImages.length"
:image="image"
@drop-field="$emit('drop-field', {...$event, attachment_uuid: document.uuid })"
@remove-area="$emit('remove-area', $event)"
+26 -11
View File
@@ -3,7 +3,7 @@
class="list-field group mb-2"
>
<div
class="border border-base-300 rounded rounded-tr-none relative group"
class="border border-base-300 rounded rounded-tr-none relative group fields-list-item"
:style="{ backgroundColor: backgroundColor }"
>
<div class="flex items-center justify-between relative group/contenteditable-container">
@@ -23,7 +23,7 @@
/>
<Contenteditable
ref="name"
:model-value="(defaultField ? (field.title || field.name) : field.name) || defaultName"
:model-value="(defaultField ? (defaultField.title || field.title || field.name) : field.name) || defaultName"
:editable="editable && !defaultField && field.type != 'heading'"
:icon-inline="true"
:icon-width="18"
@@ -98,7 +98,7 @@
/>
<span
v-else-if="field.type !== 'heading'"
class="dropdown dropdown-end"
class="dropdown dropdown-end field-settings-dropdown"
@mouseenter="renderDropdown = true"
@touchstart="renderDropdown = true"
>
@@ -127,6 +127,7 @@
:editable="editable"
:background-color="dropdownBgColor"
@click-formula="isShowFormulaModal = true"
@click-font="isShowFontModal = true"
@click-description="isShowDescriptionModal = true"
@click-condition="isShowConditionsModal = true"
@set-draw="$emit('set-draw', $event)"
@@ -136,7 +137,7 @@
</ul>
</span>
<button
class="relative text-transparent group-hover:text-base-content pr-1"
class="relative text-transparent group-hover:text-base-content pr-1 field-remove-button"
:title="t('remove')"
@click="$emit('remove', field)"
>
@@ -192,14 +193,14 @@
class="w-full input input-primary input-xs text-sm bg-transparent"
:placeholder="`${t('option')} ${index + 1}`"
type="text"
:readonly="!editable"
:readonly="!editable || defaultField"
required
dir="auto"
@focus="maybeFocusOnOptionArea(option)"
@blur="save"
>
<button
v-if="editable"
v-if="editable && !defaultField"
class="text-sm w-3.5"
tabindex="-1"
@click="removeOption(option)"
@@ -208,11 +209,11 @@
</button>
</div>
<div
v-if="field.options && !editable"
v-if="field.options && (!editable || defaultField)"
class="pb-1"
/>
<button
v-else-if="field.options && editable"
v-else-if="field.options && editable && !defaultField"
class="text-center text-sm w-full pb-1"
@click="addOption"
>
@@ -231,12 +232,23 @@
@close="isShowFormulaModal = false"
/>
</Teleport>
<Teleport
v-if="isShowFontModal"
:to="modalContainerEl"
>
<FontModal
:field="field"
:editable="editable && !defaultField"
:build-default-name="buildDefaultName"
@close="isShowFontModal = false"
/>
</Teleport>
<Teleport
v-if="isShowConditionsModal"
:to="modalContainerEl"
>
<ConditionsModal
:field="field"
:item="field"
:build-default-name="buildDefaultName"
@close="isShowConditionsModal = false"
/>
@@ -261,6 +273,7 @@ import FieldType from './field_type'
import PaymentSettings from './payment_settings'
import FieldSettings from './field_settings'
import FormulaModal from './formula_modal'
import FontModal from './font_modal'
import ConditionsModal from './conditions_modal'
import DescriptionModal from './description_modal'
import { IconRouteAltLeft, IconMathFunction, IconNewSection, IconTrashX, IconSettings } from '@tabler/icons-vue'
@@ -275,6 +288,7 @@ export default {
PaymentSettings,
IconNewSection,
FormulaModal,
FontModal,
DescriptionModal,
ConditionsModal,
IconRouteAltLeft,
@@ -305,6 +319,7 @@ export default {
isNameFocus: false,
showPaymentModal: false,
isShowFormulaModal: false,
isShowFontModal: false,
isShowConditionsModal: false,
isShowDescriptionModal: false,
renderDropdown: false
@@ -365,7 +380,7 @@ export default {
} else {
const typeIndex = fields.filter((f) => f.type === field.type).indexOf(field)
if (this.field.type === 'heading') {
if (field.type === 'heading') {
return `${this.fieldNames[field.type]} ${typeIndex + 1}`
} else {
return `${this.fieldLabels[field.type]} ${typeIndex + 1}`
@@ -392,7 +407,7 @@ export default {
return this.sortedAreas[0] && this.$emit('scroll-to', this.sortedAreas[0])
},
closeDropdown () {
document.activeElement.blur()
this.$el.getRootNode().activeElement.blur()
},
addOption () {
this.field.options.push({ value: '', uuid: v4() })
@@ -27,7 +27,34 @@
</label>
</div>
<div
v-if="['number'].includes(field.type)"
v-if="field.type === 'verification'"
class="py-1.5 px-1 relative"
@click.stop
>
<select
:placeholder="t('method')"
class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0 bg-transparent"
@change="[field.preferences ||= {}, field.preferences.method = $event.target.value, save()]"
>
<option
v-for="method in ['QeS', 'AeS']"
:key="method"
:value="method.toLowerCase()"
:selected="method.toLowerCase() === field.preferences?.method || (method === 'QeS' && !field.preferences?.method)"
>
{{ method }}
</option>
</select>
<label
:style="{ backgroundColor }"
class="absolute -top-1 left-2.5 px-1 h-4"
style="font-size: 8px"
>
{{ t('method') }}
</label>
</div>
<div
v-if="['cells'].includes(field.type)"
class="py-1.5 px-1 relative"
@click.stop
>
@@ -36,7 +63,7 @@
@change="[field.preferences ||= {}, field.preferences.align = $event.target.value, save()]"
>
<option
v-for="value in ['left', 'right', 'center']"
v-for="value in ['left', 'right', field.type === 'cells' ? null : 'center'].filter(Boolean)"
:key="value"
:selected="field.preferences?.align ? value === field.preferences.align : value === 'left'"
:value="value"
@@ -212,16 +239,12 @@
{{ t('any') }}
</option>
<option
value="drawn"
:selected="field.preferences?.format === 'drawn'"
v-for="type in ['drawn', 'typed', 'drawn_or_typed', 'upload']"
:key="type"
:value="type"
:selected="field.preferences?.format === type"
>
{{ t('drawn') }}
</option>
<option
value="typed"
:selected="field.preferences?.format === 'typed'"
>
{{ t('typed') }}
{{ t(type) }}
</option>
</select>
<label
@@ -233,14 +256,14 @@
</label>
</div>
<li
v-if="withRequired && field.type != 'phone' && field.type != 'stamp'"
v-if="withRequired && field.type !== 'phone' && field.type !== 'stamp' && field.type !== 'verification'"
@click.stop
>
<label class="cursor-pointer py-1.5">
<input
v-model="field.required"
type="checkbox"
:disabled="!editable || defaultField"
:disabled="!editable || (defaultField && [true, false].includes(defaultField.required))"
class="toggle toggle-xs"
@update:model-value="save"
>
@@ -307,6 +330,19 @@
v-if="field.type != 'stamp'"
class="pb-0.5 mt-0.5"
>
<li v-if="['text', 'number', 'date'].includes(field.type)">
<label
class="label-text cursor-pointer text-center w-full flex items-center"
@click="$emit('click-font')"
>
<IconTypography
width="18"
/>
<span class="text-sm">
{{ t('font') }}
</span>
</label>
</li>
<li
v-if="field.type != 'stamp'"
>
@@ -404,7 +440,7 @@
</template>
<script>
import { IconRouteAltLeft, IconShape, IconX, IconMathFunction, IconNewSection, IconInfoCircle, IconCopy } from '@tabler/icons-vue'
import { IconRouteAltLeft, IconTypography, IconShape, IconX, IconMathFunction, IconNewSection, IconInfoCircle, IconCopy } from '@tabler/icons-vue'
export default {
name: 'FieldSettings',
@@ -415,6 +451,7 @@ export default {
IconRouteAltLeft,
IconCopy,
IconNewSection,
IconTypography,
IconX
},
inject: ['template', 'save', 't'],
@@ -449,7 +486,7 @@ export default {
default: true
}
},
emits: ['set-draw', 'scroll-to', 'click-formula', 'click-description', 'click-condition', 'remove-area'],
emits: ['set-draw', 'scroll-to', 'click-formula', 'click-description', 'click-condition', 'click-font', 'remove-area'],
data () {
return {
}
@@ -465,6 +502,9 @@ export default {
numberFormats () {
return [
'none',
'usd',
'eur',
'gbp',
'comma',
'dot',
'space'
@@ -545,6 +585,12 @@ export default {
formatNumber (number, format) {
if (format === 'comma') {
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') {
return new Intl.NumberFormat('de-DE').format(number)
} else if (format === 'space') {
@@ -5,11 +5,11 @@
@touchstart="renderDropdown = true"
>
<div class="flex space-x-2 items-end">
<div class="group/contenteditable-container bg-base-100 rounded-md p-2 border border-base-300 w-full flex justify-between items-end">
<div class="group/contenteditable-container bg-base-100 rounded-md p-2 border border-base-300 w-full flex justify-between items-end roles-dropdown-label-mobile">
<div class="flex items-center space-x-2">
<span
class="w-3 h-3 flex-shrink-0 rounded-full"
:class="colors[submitters.indexOf(selectedSubmitter)]"
:class="colors[submitters.indexOf(selectedSubmitter) % colors.length]"
/>
<Contenteditable
v-model="selectedSubmitter.name"
@@ -22,7 +22,7 @@
/>
</div>
</div>
<div class="dropdown dropdown-top dropdown-end">
<div class="dropdown dropdown-top dropdown-end roles-dropdown-mobile">
<label
tabindex="0"
class="bg-base-100 cursor-pointer rounded-md p-2 border border-base-300 w-full flex justify-center"
@@ -53,7 +53,7 @@
<span class="py-1 flex items-center">
<span
class="rounded-full w-3 h-3 ml-1 mr-3"
:class="colors[index]"
:class="colors[index % colors.length]"
/>
<span>
{{ submitter.name }}
@@ -101,7 +101,7 @@
>
<button
class="mx-1 w-3 h-3 rounded-full"
:class="colors[submitters.indexOf(selectedSubmitter)]"
:class="colors[submitters.indexOf(selectedSubmitter) % colors.length]"
/>
</label>
<label
@@ -113,7 +113,7 @@
<div class="flex items-center space-x-2">
<span
class="w-3 h-3 rounded-full"
:class="colors[submitters.indexOf(selectedSubmitter)]"
:class="colors[submitters.indexOf(selectedSubmitter) % colors.length]"
/>
<Contenteditable
v-model="selectedSubmitter.name"
@@ -143,6 +143,7 @@
<li
v-for="(submitter, index) in submitters"
:key="submitter.uuid"
class="w-full"
>
<a
href="#"
@@ -153,7 +154,7 @@
<span class="py-1 flex items-center">
<span
class="rounded-full w-3 h-3 ml-1 mr-3"
:class="colors[index]"
:class="colors[index % colors.length]"
/>
<span>
{{ submitter.name }}
@@ -191,7 +192,10 @@
</div>
</a>
</li>
<li v-if="submitters.length < names.length && editable && allowAddNew">
<li
v-if="submitters.length < names.length && editable && allowAddNew"
class="w-full"
>
<a
href="#"
class="flex px-2"
@@ -215,6 +219,14 @@ import { IconUserPlus, IconTrashX, IconPlus, IconChevronUp, IconChevronDown } fr
import Contenteditable from './contenteditable'
import { v4 } from 'uuid'
function getOrdinalSuffix (num) {
if (num % 10 === 1 && num % 100 !== 11) return 'st'
if (num % 10 === 2 && num % 100 !== 12) return 'nd'
if (num % 10 === 3 && num % 100 !== 13) return 'rd'
return 'th'
}
export default {
name: 'FieldSubmitter',
components: {
@@ -275,16 +287,6 @@ export default {
computed: {
colors () {
return [
'bg-red-500',
'bg-sky-500',
'bg-emerald-500',
'bg-yellow-300',
'bg-purple-600',
'bg-pink-500',
'bg-cyan-500',
'bg-orange-500',
'bg-lime-500',
'bg-indigo-500',
'bg-red-500',
'bg-sky-500',
'bg-emerald-500',
@@ -298,6 +300,14 @@ export default {
]
},
names () {
const generatedNames = []
for (let i = 21; i < 101; i++) {
const suffix = getOrdinalSuffix(i)
generatedNames.push(`${i}${suffix} ${this.t('party')}`)
}
return [
this.t('first_party'),
this.t('second_party'),
@@ -318,7 +328,8 @@ export default {
this.t('seventeenth_party'),
this.t('eighteenth_party'),
this.t('nineteenth_party'),
this.t('twentieth_party')
this.t('twentieth_party'),
...generatedNames
]
},
lastPartyIndex () {
@@ -372,7 +383,7 @@ export default {
this.$emit('new-submitter', newSubmitter)
},
closeDropdown () {
document.activeElement.blur()
this.$el.getRootNode().activeElement.blur()
}
}
}
+11 -8
View File
@@ -1,6 +1,6 @@
<template>
<span
class="dropdown"
class="dropdown field-types-dropdown"
@mouseenter="renderDropdown = true"
@touchstart="renderDropdown = true"
>
@@ -30,7 +30,7 @@
v-for="(icon, type) in fieldIconsSorted"
:key="type"
>
<li v-if="(fieldTypes.length === 0 || fieldTypes.includes(type)) && (withPhone || type != 'phone') && (withPayment || type != 'payment')">
<li v-if="(fieldTypes.length === 0 || fieldTypes.includes(type)) && (withPhone || type != 'phone') && (withPayment || type != 'payment') && (withVerification || type != 'verification')">
<a
href="#"
class="text-sm py-1 px-2"
@@ -51,11 +51,11 @@
</template>
<script>
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp, IconSquareNumber1, IconHeading } from '@tabler/icons-vue'
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp, IconSquareNumber1, IconHeading, IconId } from '@tabler/icons-vue'
export default {
name: 'FiledTypeDropdown',
inject: ['withPhone', 'withPayment', 't', 'fieldTypes'],
inject: ['withPhone', 'withPayment', 'withVerification', 't', 'fieldTypes'],
props: {
modelValue: {
type: String,
@@ -111,7 +111,8 @@ export default {
cells: this.t('cells'),
stamp: this.t('stamp'),
payment: this.t('payment'),
phone: this.t('phone')
phone: this.t('phone'),
verification: this.t('verify_id')
}
},
fieldLabels () {
@@ -130,7 +131,8 @@ export default {
cells: this.t('cells_field'),
stamp: this.t('stamp_field'),
payment: this.t('payment_field'),
phone: this.t('phone_field')
phone: this.t('phone_field'),
verification: this.t('verify_id')
}
},
fieldIcons () {
@@ -150,7 +152,8 @@ export default {
cells: IconColumns3,
stamp: IconRubberStamp,
payment: IconCreditCard,
phone: IconPhoneCheck
phone: IconPhoneCheck,
verification: IconId
}
},
fieldIconsSorted () {
@@ -167,7 +170,7 @@ export default {
},
methods: {
closeDropdown () {
document.activeElement.blur()
this.$el.getRootNode().activeElement.blur()
}
}
}
+66 -12
View File
@@ -2,10 +2,10 @@
<div :class="withStickySubmitters ? 'sticky top-0 z-[1]' : ''">
<FieldSubmitter
:model-value="selectedSubmitter.uuid"
class="roles-dropdown w-full rounded-lg"
class="roles-dropdown w-full rounded-lg roles-dropdown"
:style="withStickySubmitters ? { backgroundColor } : {}"
:submitters="submitters"
:menu-style="{ backgroundColor: ['', null, 'transparent'].includes(backgroundColor) ? 'white' : backgroundColor }"
:menu-style="{ overflow: 'auto', display: 'flex', flexDirection: 'row', maxHeight: 'calc(100vh - 120px)', backgroundColor: ['', null, 'transparent'].includes(backgroundColor) ? 'white' : backgroundColor }"
:editable="editable && !defaultSubmitters.length"
@new-submitter="save"
@remove="removeSubmitter"
@@ -26,7 +26,7 @@
:field="field"
:type-index="fields.filter((f) => f.type === field.type).indexOf(field)"
:editable="editable && (!fieldsDragFieldRef.value || fieldsDragFieldRef.value !== field)"
:default-field="defaultFields.find((f) => f.name === field.name)"
:default-field="defaultFieldsIndex[field.name]"
:draggable="editable"
@dragstart="fieldsDragFieldRef.value = field"
@dragend="fieldsDragFieldRef.value = null"
@@ -74,7 +74,7 @@
<div
:style="{ backgroundColor }"
draggable="true"
class="default-field border border-base-300 rounded rounded-tr-none relative group mb-2"
class="border border-base-300 rounded rounded-tr-none relative group mb-2 default-field fields-list-item"
@dragstart="onDragstart({ type: 'text', ...field })"
@dragend="$emit('drag-end')"
>
@@ -104,21 +104,21 @@
</div>
<div
v-if="editable && !onlyDefinedFields"
class="grid grid-cols-3 gap-1 pb-2"
class="grid grid-cols-3 gap-1 pb-2 fields-grid"
>
<template
v-for="(icon, type) in fieldIconsSorted"
:key="type"
>
<button
v-if="(fieldTypes.length === 0 || fieldTypes.includes(type)) && (withPhone || type != 'phone') && (withPayment || type != 'payment')"
v-if="(fieldTypes.length === 0 || fieldTypes.includes(type)) && (withPhone || type != 'phone') && (withPayment || type != 'payment') && (withVerification || type != 'verification')"
draggable="true"
class="field-type-button group flex items-center justify-center border border-dashed w-full rounded relative"
class="field-type-button group flex items-center justify-center border border-dashed w-full rounded relative fields-grid-item"
:style="{ backgroundColor }"
:class="drawFieldType === type ? 'border-base-content/40' : 'border-base-300 hover:border-base-content/20'"
@dragstart="onDragstart({ type: type })"
@dragend="$emit('drag-end')"
@click="['file', 'payment'].includes(type) ? $emit('add-field', type) : $emit('set-draw-type', type)"
@click="['file', 'payment', 'verification'].includes(type) ? $emit('add-field', type) : $emit('set-draw-type', type)"
>
<div
class="flex items-console transition-all cursor-grab h-full absolute left-0"
@@ -142,7 +142,33 @@
<a
href="https://www.docuseal.com/pricing"
target="_blank"
class="opacity-50 flex items-center justify-center border border-dashed border-base-300 w-full rounded relative"
class="opacity-50 flex items-center justify-center border border-dashed border-base-300 w-full rounded relative fields-grid-item"
:style="{ backgroundColor }"
>
<div class="w-0 absolute left-0">
<IconLock
width="18"
height="18"
stroke-width="1.5"
/>
</div>
<div class="flex items-center flex-col px-2 py-2">
<component :is="icon" />
<span class="text-xs mt-1">
{{ fieldNames[type] }}
</span>
</div>
</a>
</div>
<div
v-else-if="withVerification === false && type == 'verification' && (fieldTypes.length === 0 || fieldTypes.includes(type))"
class="tooltip tooltip-bottom flex tooltip-bottom-start"
:data-tip="t('obtain_qualified_electronic_signature_with_the_trusted_provider_click_to_learn_more')"
>
<a
href="https://www.docuseal.com/contact"
target="_blank"
class="opacity-50 flex items-center justify-center border border-dashed border-base-300 w-full rounded relative fields-grid-item"
:style="{ backgroundColor }"
>
<div class="w-0 absolute left-0">
@@ -197,12 +223,21 @@ export default {
IconDrag,
IconLock
},
inject: ['save', 'backgroundColor', 'withPhone', 'withPayment', 't', 'fieldsDragFieldRef'],
inject: ['save', 'backgroundColor', 'withPhone', 'withVerification', 'withPayment', 't', 'fieldsDragFieldRef'],
props: {
fields: {
type: Array,
required: true
},
withFieldsSearch: {
type: Boolean,
required: false,
default: null
},
template: {
type: Object,
required: true
},
withHelp: {
type: Boolean,
required: false,
@@ -267,7 +302,18 @@ export default {
fieldNames: FieldType.computed.fieldNames,
fieldIcons: FieldType.computed.fieldIcons,
isShowFieldSearch () {
return this.submitterDefaultFields.length > 15
if (this.withFieldsSearch === false) {
return false
} else {
return this.submitterDefaultFields.length > 15
}
},
defaultFieldsIndex () {
return this.defaultFields.reduce((acc, field) => {
acc[field.name] = field
return acc
}, {})
},
fieldIconsSorted () {
if (this.fieldTypes.length) {
@@ -290,7 +336,7 @@ export default {
},
filteredSubmitterDefaultFields () {
if (this.defaultFieldsSearch) {
return this.submitterDefaultFields.filter((f) => f.name.toLowerCase().includes(this.defaultFieldsSearch.toLowerCase()))
return this.submitterDefaultFields.filter((f) => (f.title || f.name).toLowerCase().includes(this.defaultFieldsSearch.toLowerCase()))
} else {
return this.submitterDefaultFields
}
@@ -354,6 +400,14 @@ export default {
})
})
this.template.schema.forEach((item) => {
(item.conditions || []).forEach((c) => {
if (c.field_uuid === field.uuid) {
item.conditions.splice(item.conditions.indexOf(c), 1)
}
})
})
this.save()
}
}
@@ -0,0 +1,290 @@
<template>
<div
class="modal modal-open items-start !animate-none overflow-y-auto"
>
<div
class="absolute top-0 bottom-0 right-0 left-0"
@click.prevent="$emit('close')"
/>
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl">
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
<span class="modal-title">
{{ t('font') }} - {{ field.name || buildDefaultName(field, template.fields) }}
</span>
<a
href="#"
class="text-xl modal-close-button"
@click.prevent="$emit('close')"
>&times;</a>
</div>
<div class="mt-4">
<div>
<div class="flex items-center space-x-1.5">
<span>
<div class="dropdown modal-field-font-dropdown">
<label
tabindex="0"
class="base-input flex items-center justify-between"
style="height: 32px; padding-right: 0; width: 120px"
:class="fonts.find((f) => f.value === preferences.font)?.class"
>
<span style="margin-top: 1px">
{{ preferences.font || 'Default' }}
</span>
<IconChevronDown
class="ml-2 mr-2 mt-0.5"
width="18"
height="18"
/>
</label>
<div
tabindex="0"
class="dropdown-content p-0 mt-1 block z-10 menu shadow bg-white border border-base-300 rounded-md w-52"
>
<div
v-for="(font, index) in fonts"
:key="index"
:value="font.value"
:class="{ 'bg-base-300': preferences.font == font.value, [font.class]: true }"
class="hover:bg-base-300 px-2 py-1.5 cursor-pointer"
@click="[font.value ? preferences.font = font.value : delete preferences.font, closeDropdown()]"
>
{{ font.label }}
</div>
</div>
</div>
</span>
<span class="relative">
<select
class="select input-bordered bg-white select-sm text-center pl-2"
style="font-size: 16px; width: 86px; text-align-last: center;"
@change="$event.target.value ? preferences.font_size = parseInt($event.target.value) : delete preferences.font_size"
>
<option
:selected="!preferences.font_size"
value=""
>
Auto
</option>
<option
v-for="size in sizes"
:key="size"
:value="size"
:selected="size === preferences.font_size"
>
{{ size }}
</option>
</select>
<span
class="border-l pl-1.5 absolute bg-white bottom-0 pointer-events-none text-sm h-5"
style="right: 13px; top: 7px"
>
pt
</span>
</span>
<span>
<div
class="join"
style="height: 32px"
>
<button
v-for="(type, index) in types"
:key="index"
class="btn btn-sm join-item bg-white input-bordered hover:border-base-content/20 hover:bg-base-200/50 px-2"
:class="{ '!bg-base-300': preferences.font_type?.includes(type.value) }"
@click="setFontType(type.value)"
>
<component :is="type.icon" />
</button>
</div>
</span>
<span>
<div
class="join"
style="height: 32px"
>
<button
v-for="(align, index) in aligns"
:key="index"
class="btn btn-sm join-item bg-white input-bordered hover:border-base-content/20 hover:bg-base-200/50 px-2"
:class="{ '!bg-base-300': preferences.align === align.value }"
@click="align.value && preferences.align != align.value ? preferences.align = align.value : delete preferences.align"
>
<component :is="align.icon" />
</button>
</div>
</span>
<span>
<select
class="input input-bordered bg-white input-sm text-lg rounded-md"
style="-webkit-appearance: none; -moz-appearance: none; text-indent: 0px; text-overflow: ''; padding: 0px 6px; height: 32px"
@change="$event.target.value ? preferences.color = $event.target.value : delete preferences.color"
>
<option
v-for="(color, index) in colors"
:key="index"
:value="color.value"
:selected="color.value == preferences.color"
>
{{ color.label }}
</option>
</select>
</span>
</div>
</div>
<div class="mt-4">
<div
class="flex items-center border border-base-content/20 rounded-xl bg-white px-4 h-16 modal-field-font-preview"
:style="{
color: preferences.color || 'black',
fontSize: (preferences.font_size || 12) + 'pt',
}"
:class="textClasses"
>
<span
contenteditable="true"
class="outline-none"
>
{{ field.default_value || field.name || buildDefaultName(field, template.fields) }}
</span>
</div>
</div>
<div class="mt-4">
<button
class="base-button w-full modal-save-button"
@click.prevent="saveAndClose"
>
{{ t('save') }}
</button>
</div>
</div>
</div>
</div>
</template>
<script>
import { IconChevronDown, IconBold, IconItalic, IconAlignLeft, IconAlignRight, IconAlignCenter } from '@tabler/icons-vue'
export default {
name: 'FontModal',
components: {
IconChevronDown
},
inject: ['t', 'save', 'template'],
props: {
field: {
type: Object,
required: true
},
editable: {
type: Boolean,
required: false,
default: true
},
buildDefaultName: {
type: Function,
required: true
}
},
emits: ['close'],
data () {
return {
preferences: {}
}
},
computed: {
fonts () {
return [
{ value: null, label: 'Default' },
{ value: 'Times', label: 'Times', class: 'font-serif' },
{ value: 'Courier', label: 'Courier', class: 'font-mono' }
]
},
types () {
return [
{ icon: IconBold, value: 'bold' },
{ icon: IconItalic, value: 'italic' }
]
},
aligns () {
return [
{ icon: IconAlignLeft, value: 'left' },
{ icon: IconAlignCenter, value: 'center' },
{ icon: IconAlignRight, value: 'right' }
]
},
sizes () {
return [...Array(23).keys()].map(i => i + 6)
},
colors () {
return [
{ label: '⬛', value: 'black' },
{ label: '🟦', value: 'blue' },
{ label: '🟥', value: 'red' }
]
},
textClasses () {
return {
'font-mono': this.preferences.font === 'Courier',
'font-serif': this.preferences.font === 'Times',
'justify-center': this.preferences.align === 'center',
'justify-start': this.preferences.align === 'left',
'justify-end': this.preferences.align === 'right',
'font-bold': ['bold_italic', 'bold'].includes(this.preferences.font_type),
italic: ['bold_italic', 'italic'].includes(this.preferences.font_type)
}
},
keys () {
return ['font_type', 'font_size', 'color', 'align', 'font']
}
},
created () {
this.preferences = this.keys.reduce((acc, key) => {
acc[key] = this.field.preferences?.[key]
return acc
}, {})
},
methods: {
closeDropdown () {
this.$el.getRootNode().activeElement.blur()
},
setFontType (value) {
if (value === 'bold') {
if (this.preferences.font_type === 'bold') {
delete this.preferences.font_type
} else if (this.preferences.font_type === 'italic') {
this.preferences.font_type = 'bold_italic'
} else if (this.preferences.font_type === 'bold_italic') {
this.preferences.font_type = 'italic'
} else {
this.preferences.font_type = value
}
}
if (value === 'italic') {
if (this.preferences.font_type === 'italic') {
delete this.preferences.font_type
} else if (this.preferences.font_type === 'bold') {
this.preferences.font_type = 'bold_italic'
} else if (this.preferences.font_type === 'bold_italic') {
this.preferences.font_type = 'bold'
} else {
this.preferences.font_type = value
}
}
},
saveAndClose () {
this.field.preferences ||= {}
this.keys.forEach((key) => delete this.field.preferences[key])
Object.assign(this.field.preferences, this.preferences)
this.save()
this.$emit('close')
}
}
}
</script>
@@ -8,12 +8,12 @@
/>
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl">
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
<span>
<span class="modal-title">
{{ t('formula') }} - {{ field.name || buildDefaultName(field, template.fields) }}
</span>
<a
href="#"
class="text-xl"
class="text-xl modal-close-button"
@click.prevent="$emit('close')"
>&times;</a>
</div>
@@ -111,7 +111,7 @@
</div>
</div>
<button
class="base-button w-full"
class="base-button w-full modal-save-button"
@click.prevent="validateSaveAndClose"
>
{{ t('save') }}
+66 -12
View File
@@ -1,4 +1,10 @@
const en = {
font: 'Font',
party: 'Party',
method: 'Method',
reorder_fields: 'Reorder fields',
verify_id: 'Verify ID',
obtain_qualified_electronic_signature_with_the_trusted_provider_click_to_learn_more: 'Obtain qualified electronic signature (QeS) with the trusted provider. Click to learn more.',
editable: 'Editable',
recurrent: 'Recurrent',
one_off: 'One-off',
@@ -46,6 +52,8 @@ const en = {
cancel: 'Cancel',
any: 'Any',
drawn: 'Drawn',
drawn_or_typed: 'Drawn or Typed',
upload: 'Upload',
formula: 'Formula',
typed: 'Typed',
draw_field_on_the_document: 'Draw a field on the document',
@@ -126,7 +134,6 @@ const en = {
unlock_sms_verified_phone_number_field_with_paid_plan_use_text_field_for_phone_numbers_without_verification: 'Unlock SMS-verified phone number field with paid plan. Use text field for phone numbers without verification.',
available_only_in_pro: 'Available only in Pro',
failed_to_download_files: 'Failed to download files',
signature_is_too_small_please_redraw: 'Signature is too small - please redraw.',
please_add_fields_for_the_submitter_name_or_remove_the_submitter_name_if_not_needed: 'Please add fields for the {submitter_name}. Or, remove the {submitter_name} if not needed.',
draw_field: 'Draw {field} Field',
replace: 'Replace',
@@ -149,10 +156,18 @@ const en = {
preferences: 'Preferences',
available_in_pro: 'Available in Pro',
some_fields_are_missing_in_the_formula: 'Some fields are missing in the formula.',
learn_more: 'Learn more'
learn_more: 'Learn more',
and: 'and',
or: 'or'
}
const es = {
fuente: 'Fuente',
party: 'Parte',
method: 'Método',
reorder_fields: 'Reordenar campos',
verify_id: 'Verificar ID',
obtain_qualified_electronic_signature_with_the_trusted_provider_click_to_learn_more: 'Obtenga una firma electrónica cualificada (QeS) con el proveedor de confianza. Haga clic para obtener más información.',
recurrent: 'Recurrente',
one_off: 'Único',
editable: 'Editable',
@@ -277,7 +292,6 @@ const es = {
unlock_sms_verified_phone_number_field_with_paid_plan_use_text_field_for_phone_numbers_without_verification: 'Desbloquea el campo de número de teléfono verificado por SMS con un plan pago. Usa el campo de texto para números de teléfono sin verificación.',
available_only_in_pro: 'Disponible solo en Pro',
failed_to_download_files: 'Error al descargar los archivos',
signature_is_too_small_please_redraw: 'La firma es demasiado pequeña. Por favor, dibújala de nuevo.',
please_add_fields_for_the_submitter_name_or_remove_the_submitter_name_if_not_needed: 'Por favor, añade campos para {submitter_name} o elimina {submitter_name} si no es necesario.',
draw_field: 'Dibujar campo {field}',
replace: 'Reemplazar',
@@ -285,6 +299,8 @@ const es = {
add_document: 'Subir',
any: 'Cualquier',
drawn: 'Dibujado',
drawn_or_typed: 'Dibujado o Escrito',
upload: 'Subir',
typed: 'Escrito',
none: 'Ninguno',
ssn: 'SSN',
@@ -303,10 +319,18 @@ const es = {
preferences: 'Preferencias',
available_in_pro: 'Disponible en Pro',
some_fields_are_missing_in_the_formula: 'Faltan algunos campos en la fórmula.',
learn_more: 'Aprende más'
learn_more: 'Aprende más',
and: 'y',
or: 'o'
}
const it = {
font: 'Carattere',
party: 'Parte',
method: 'Metodo',
reorder_fields: 'Riordina i campi',
verify_id: 'Verifica ID',
obtain_qualified_electronic_signature_with_the_trusted_provider_click_to_learn_more: 'Ottieni una firma elettronica qualificata (QeS) con il fornitore di fiducia. Clicca per saperne di più.',
ricorrente: 'Ricorrente',
una_volta: 'Una volta',
editable: 'Modificabile',
@@ -354,6 +378,8 @@ const it = {
cancel: 'Annulla',
any: 'Qualsiasi',
drawn: 'Disegnato',
drawn_or_typed: 'Disegnato o Digitato',
upload: 'Caricare',
formula: 'Formula',
typed: 'Digitato',
draw_field_on_the_document: 'Disegnare un campo sul documento',
@@ -434,7 +460,6 @@ const it = {
unlock_sms_verified_phone_number_field_with_paid_plan_use_text_field_for_phone_numbers_without_verification: 'Sblocca il campo numero di telefono verificato tramite SMS con un piano a pagamento. Usa il campo di testo per numeri di telefono senza verifica.',
available_only_in_pro: 'Disponibile solo in Pro',
failed_to_download_files: 'Impossibile scaricare i file',
signature_is_too_small_please_redraw: 'La firma è troppo piccola. Ridisegnala per favore.',
please_add_fields_for_the_submitter_name_or_remove_the_submitter_name_if_not_needed: 'Aggiungi campi per {submitter_name} o rimuovi {submitter_name} se non necessario.',
draw_field: 'Disegna il campo {field}',
replace: 'Sostituisci',
@@ -457,10 +482,18 @@ const it = {
preferences: 'Preferenze',
available_in_pro: 'Disponibile in Pro',
some_fields_are_missing_in_the_formula: 'Alcuni campi mancano nella formula.',
learn_more: 'Scopri di più'
learn_more: 'Scopri di più',
and: 'e',
or: 'o'
}
const pt = {
fonte: 'Fonte',
party: 'Parte',
method: 'Método',
reorder_fields: 'Reorganizar campos',
verify_id: 'Verificar ID',
obtain_qualified_electronic_signature_with_the_trusted_provider_click_to_learn_more: 'Obtenha a assinatura eletrônica qualificada (QeS) com o provedor confiável. Clique para saber mais.',
recurrent: 'Recurrente',
one_off: 'Único',
editable: 'Editável',
@@ -585,7 +618,6 @@ const pt = {
unlock_sms_verified_phone_number_field_with_paid_plan_use_text_field_for_phone_numbers_without_verification: 'Desbloqueie o campo de número de telefone verificado por SMS com um plano pago. Use o campo de texto para números de telefone sem verificação.',
available_only_in_pro: 'Disponível apenas no Pro',
failed_to_download_files: 'Falha ao baixar arquivos',
signature_is_too_small_please_redraw: 'A assinatura é muito pequena. Por favor, redesenhe.',
please_add_fields_for_the_submitter_name_or_remove_the_submitter_name_if_not_needed: 'Adicione campos para {submitter_name} ou remova {submitter_name} se não for necessário.',
draw_field: 'Desenhar campo {field}',
replace: 'Substituir',
@@ -593,6 +625,8 @@ const pt = {
add_document: 'Enviar',
any: 'Qualquer',
drawn: 'Desenhado',
drawn_or_typed: 'Desenhado ou Digitado',
upload: 'Carregar',
typed: 'Digitado',
none: 'Nenhum',
ssn: 'SSN',
@@ -611,10 +645,18 @@ const pt = {
preferences: 'Preferências',
available_in_pro: 'Disponível no Pro',
some_fields_are_missing_in_the_formula: 'Faltam alguns campos na fórmula.',
learn_more: 'Saiba mais'
learn_more: 'Saiba mais',
and: 'e',
or: 'ou'
}
const fr = {
font: 'Police',
party: 'Partie',
method: 'Méthode',
reorder_fields: 'Réorganiser les champs',
verify_id: "Vérifier l'ID",
obtain_qualified_electronic_signature_with_the_trusted_provider_click_to_learn_more: 'Obtenez une signature électronique qualifiée (QeS) avec le fournisseur de confiance. Cliquez pour en savoir plus.',
recurrent: 'Récurrent',
one_off: 'Ponctuel',
editable: 'Éditable',
@@ -739,7 +781,6 @@ const fr = {
unlock_sms_verified_phone_number_field_with_paid_plan_use_text_field_for_phone_numbers_without_verification: 'Débloquez le champ de numéro de téléphone vérifié par SMS avec un plan payant. Utilisez un champ texte pour les numéros de téléphone sans vérification.',
available_only_in_pro: 'Disponible uniquement en Pro',
failed_to_download_files: 'Échec du téléchargement des fichiers',
signature_is_too_small_please_redraw: 'La signature est trop petite. Veuillez la redessiner.',
please_add_fields_for_the_submitter_name_or_remove_the_submitter_name_if_not_needed: 'Veuillez ajouter des champs pour {submitter_name} ou retirer {submitter_name} si ce n\'est pas nécessaire.',
draw_field: 'Dessiner le champ {field}',
replace: 'Remplacer',
@@ -747,6 +788,8 @@ const fr = {
add_document: 'Télécharger',
any: 'Tout',
drawn: 'Dessiné',
drawn_or_typed: 'Dessiné ou Tapé',
upload: 'Téléverser',
typed: 'Tapé',
none: 'Aucun',
ssn: 'SSN',
@@ -765,10 +808,18 @@ const fr = {
preferences: 'Préférences',
available_in_pro: 'Disponible en version Pro',
some_fields_are_missing_in_the_formula: 'Certains champs manquent dans la formule.',
learn_more: 'En savoir plus'
learn_more: 'En savoir plus',
and: 'et',
or: 'ou'
}
const de = {
font: 'Schriftart',
party: 'Partei',
method: 'Verfahren',
reorder_fields: 'Felder neu anordnen',
verify_id: 'ID überprüfen',
obtain_qualified_electronic_signature_with_the_trusted_provider_click_to_learn_more: 'Erhalten Sie eine qualifizierte elektronische Signatur (QeS) beim vertrauenswürdigen Anbieter. Klicken Sie hier, um mehr zu erfahren.',
wiederkehrend: 'Wiederkehrend',
einmalig: 'Einmalig',
editable: 'Bearbeitbar',
@@ -893,7 +944,6 @@ const de = {
unlock_sms_verified_phone_number_field_with_paid_plan_use_text_field_for_phone_numbers_without_verification: 'Schalte das SMS-verifizierte Telefonnummernfeld mit einem kostenpflichtigen Plan frei. Verwende das Textfeld für Telefonnummern ohne Verifizierung.',
available_only_in_pro: 'Nur in Pro verfügbar',
failed_to_download_files: 'Fehler beim Herunterladen der Dateien',
signature_is_too_small_please_redraw: 'Die Unterschrift ist zu klein. Bitte erneut zeichnen.',
please_add_fields_for_the_submitter_name_or_remove_the_submitter_name_if_not_needed: 'Bitte füge Felder für {submitter_name} hinzu oder entferne {submitter_name}, falls nicht erforderlich.',
draw_field: 'Feld {field} zeichnen',
replace: 'Ersetzen',
@@ -901,6 +951,8 @@ const de = {
add_document: 'Hochladen',
any: 'Jede',
drawn: 'Gezeichnet',
drawn_or_typed: 'Gezeichnet oder getippt',
upload: 'Upload',
typed: 'Getippt',
none: 'Keine',
ssn: 'SSN',
@@ -919,7 +971,9 @@ const de = {
preferences: 'Einstellungen',
available_in_pro: 'In Pro verfügbar',
some_fields_are_missing_in_the_formula: 'Einige Felder fehlen in der Formel.',
learn_more: 'Erfahren Sie mehr'
learn_more: 'Erfahren Sie mehr',
and: 'und',
or: 'oder'
}
export { en, es, it, pt, fr, de }
@@ -279,7 +279,11 @@ export default {
this.mappings.forEach((mapping) => {
if (mapping.field_name && mapping.column_index != null) {
submittersIndex[mapping.submitter_uuid] ||= { uuid: mapping.submitter_uuid, fields: [] }
submittersIndex[mapping.submitter_uuid] ||= {
uuid: mapping.submitter_uuid,
role: this.submitters.find((s) => s.uuid === mapping.submitter_uuid).name,
fields: []
}
if (['name', 'email', 'phone', 'external_id'].includes(mapping.field_name.toLowerCase())) {
submittersIndex[mapping.submitter_uuid][mapping.field_name.toLowerCase()] = row[mapping.column_index]
@@ -296,7 +300,7 @@ export default {
})
if (Object.keys(submittersIndex).length !== 0) {
submissions.push({ submitters: Object.values(submittersIndex) })
submissions.push({ submitters: this.submitters.map((s) => submittersIndex[s.uuid]).filter(Boolean) })
}
})
@@ -342,7 +346,7 @@ export default {
return this.$el.closest('form')
},
fieldTypes () {
return ['text', 'cells', 'date', 'number', 'radio', 'select', 'checkbox']
return ['text', 'cells', 'date', 'number', 'radio', 'select', 'checkbox', 'image']
},
defaultFields () {
return [
@@ -1,6 +1,6 @@
<template>
<div class="absolute text-center w-full bottom-0 pr-3 mb-4">
<span class="w-full bg-base-200 px-4 py-2 rounded-md inline-flex space-x-2 mx-auto items-center justify-between mb-2 z-20 md:hidden">
<span class="w-full bg-base-200 px-4 py-2 rounded-md inline-flex space-x-2 mx-auto items-center justify-between mb-2 z-20 draw-field-container-mobile">
<div class="flex items-center space-x-2">
<component
:is="fieldIcons[drawField.type]"
@@ -1,7 +1,5 @@
<template>
<span
class="dropdown dropdown-top dropdown-end absolute bottom-4 right-4 z-10 md:hidden"
>
<span class="dropdown dropdown-top dropdown-end absolute bottom-4 right-4 z-10 fields-dropdown-mobile">
<label
class="btn btn-neutral text-white btn-circle btn-lg group"
tabindex="0"
@@ -12,9 +10,10 @@
height="28"
/>
<IconX
class="hidden group-focus:inline"
width="28"
height="28"
class="hidden group-focus:inline p-3"
width="64"
height="50"
@click="closeDropdown"
/>
</label>
<ul
@@ -55,7 +54,7 @@
v-for="(icon, type) in fieldIconsSorted"
:key="type"
>
<li v-if="(fieldTypes.length === 0 || fieldTypes.includes(type)) && (withPhone || type != 'phone') && (withPayment || type != 'payment')">
<li v-if="(fieldTypes.length === 0 || fieldTypes.includes(type)) && (withPhone || type != 'phone') && (withPayment || type != 'payment') && (withVerification || type != 'verification')">
<a
href="#"
class="text-sm py-1 px-2"
@@ -85,7 +84,7 @@ export default {
IconPlus,
IconX
},
inject: ['withPhone', 'withPayment', 'backgroundColor', 't'],
inject: ['withPhone', 'withPayment', 'withVerification', 'backgroundColor', 't'],
props: {
modelValue: {
type: String,
+18 -3
View File
@@ -1,6 +1,7 @@
<template>
<div
class="relative cursor-crosshair select-none"
class="relative select-none"
:class="{ 'cursor-crosshair': allowDraw }"
:style="drawField ? 'touch-action: none' : ''"
>
<img
@@ -25,8 +26,9 @@
:field="item.field"
:editable="editable"
:with-field-placeholder="withFieldPlaceholder"
:default-field="defaultFields.find((f) => f.name === item.field.name)"
:default-field="defaultFieldsIndex[item.field.name]"
:default-submitters="defaultSubmitters"
:max-page="totalPages - 1"
@start-resize="resizeDirection = $event"
@stop-resize="resizeDirection = null"
@remove="$emit('remove-area', item.area)"
@@ -88,6 +90,10 @@ export default {
required: false,
default: false
},
totalPages: {
type: Number,
required: true
},
drawFieldType: {
type: String,
required: false,
@@ -137,6 +143,13 @@ export default {
}
},
computed: {
defaultFieldsIndex () {
return this.defaultFields.reduce((acc, field) => {
acc[field.name] = field
return acc
}, {})
},
defaultFieldType () {
if (this.drawFieldType) {
return this.drawFieldType
@@ -149,7 +162,9 @@ export default {
}
},
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)
},
resizeDirectionClasses () {
return {
@@ -1,6 +1,6 @@
<template>
<span
class="dropdown dropdown-end"
class="dropdown dropdown-end field-settings-dropdown"
:class="{ 'dropdown-open': ((!field.preferences?.price && !field.preferences?.formula) || !isConnected) && !isLoading }"
>
<label
+81 -20
View File
@@ -9,14 +9,30 @@
loading="lazy"
>
<div
class="group flex justify-end cursor-pointer top-0 bottom-0 left-0 right-0 absolute p-1"
class="group flex justify-end cursor-pointer top-0 bottom-0 left-0 right-0 absolute p-1 hover:bg-black/10 transition-colors"
@click="$emit('scroll-to', item)"
>
<div
v-if="editable"
class="flex justify-between w-full"
>
<div style="width: 26px" />
<div
style="width: 26px"
class="flex flex-col justify-between group-hover:opacity-100"
:class="{'opacity-0': !item.conditions?.length }"
>
<div>
<button
class="btn border-base-200 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors p-0 document-control-button"
@click.stop="isShowConditionsModal = true"
>
<IconRouteAltLeft
:width="14"
:stroke-width="1.6"
/>
</button>
</div>
</div>
<div class="">
<ReplaceButton
v-if="withReplaceButton"
@@ -32,7 +48,7 @@
>
<div>
<button
class="btn border-base-200 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors"
class="btn border-base-200 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors document-control-button"
style="width: 24px; height: 24px"
@click.stop="$emit('remove', item)"
>
@@ -40,29 +56,45 @@
</button>
</div>
<div
v-if="withArrows"
class="flex flex-col space-y-1"
>
<button
class="btn border-base-200 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors"
style="width: 24px; height: 24px"
@click.stop="$emit('up', item)"
<span
:data-tip="t('reorder_fields')"
class="tooltip tooltip-left before:text-xs"
>
&uarr;
</button>
<button
class="btn border-base-200 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors"
style="width: 24px; height: 24px"
@click.stop="$emit('down', item)"
>
&darr;
</button>
<button
class="btn border-base-200 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors p-0 document-control-button"
@click.stop="$emit('reorder', item)"
>
<IconSortDescending2
:width="18"
:height="18"
:stroke-width="1.6"
/>
</button>
</span>
<template v-if="withArrows">
<button
class="btn border-base-200 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors document-control-button"
style="width: 24px; height: 24px"
@click.stop="$emit('up', item)"
>
&uarr;
</button>
<button
class="btn border-base-200 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors document-control-button"
style="width: 24px; height: 24px"
@click.stop="$emit('down', item)"
>
&darr;
</button>
</template>
</div>
</div>
</div>
</div>
</div>
<div class="flex pb-2 pt-1.5">
<div class="flex pb-2 pt-1.5 document-preview-name">
<Contenteditable
:model-value="item.name"
:icon-width="16"
@@ -72,20 +104,38 @@
@update:model-value="onUpdateName"
/>
</div>
<Teleport
v-if="isShowConditionsModal"
:to="modalContainerEl"
>
<ConditionsModal
:item="item"
:build-default-name="buildDefaultName"
@close="isShowConditionsModal = false"
/>
</Teleport>
</div>
</template>
<script>
import Contenteditable from './contenteditable'
import Upload from './upload'
import { IconRouteAltLeft, IconSortDescending2 } from '@tabler/icons-vue'
import ConditionsModal from './conditions_modal'
import ReplaceButton from './replace'
import Field from './field'
import FieldType from './field_type'
export default {
name: 'DocumentPreview',
components: {
Contenteditable,
ReplaceButton
IconRouteAltLeft,
ConditionsModal,
ReplaceButton,
IconSortDescending2
},
inject: ['t'],
props: {
item: {
type: Object,
@@ -120,14 +170,25 @@ export default {
default: true
}
},
emits: ['scroll-to', 'change', 'remove', 'up', 'down', 'replace'],
emits: ['scroll-to', 'change', 'remove', 'up', 'down', 'replace', 'reorder'],
data () {
return {
isShowConditionsModal: false
}
},
computed: {
fieldNames: FieldType.computed.fieldNames,
fieldLabels: FieldType.computed.fieldLabels,
previewImage () {
return [...this.document.preview_images].sort((a, b) => parseInt(a.filename) - parseInt(b.filename))[0]
},
modalContainerEl () {
return this.$el.getRootNode().querySelector('#docuseal_modal_container')
}
},
methods: {
upload: Upload.methods.upload,
buildDefaultName: Field.methods.buildDefaultName,
onUpdateName (value) {
this.item.name = value

Some files were not shown because too many files have changed in this diff Show More