Compare commits

...
330 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
Alex Turchyn 0c2abf211e Merge from docusealco/wip 2024-11-11 17:44:28 +02:00
Pete Matsyburka 8ecaa4fd7a adjust custom font size 2024-11-11 14:25:07 +02:00
Pete Matsyburka 6759c26d86 fix rubocop 2024-11-10 13:11:25 +02:00
Pete Matsyburka 76f1f140ac add field color 2024-11-10 12:48:53 +02:00
Pete Matsyburka ca7ab5aab7 adjust webhooks controller 2024-11-09 11:15:19 +02:00
Alex Turchyn fd3530ac62 update Webhooks UI 2024-11-09 00:41:46 +02:00
Pete Matsyburka 4ea863592b fix form 2024-11-08 22:12:07 +02:00
Pete Matsyburka 3f87d0336d do not use same reply to as to 2024-11-08 13:02:32 +02:00
Pete Matsyburka e251482481 unique party names 2024-11-08 12:15:13 +02:00
Pete Matsyburka c79fca75f3 add complete button 2024-11-07 23:41:49 +02:00
Alex Turchyn 0b0f0b497e fix acroform select field 2024-11-06 20:30:01 +02:00
Pete Matsyburka e9034d8da7 webhook secret priority 2024-11-06 14:06:33 +02:00
Pete Matsyburka 8fbb8b8f6e do not touch updated_at 2024-11-05 22:55:03 +02:00
Alex Turchyn 4635fdd32b remove replace_timestamps helper 2024-11-05 22:34:14 +02:00
Pete Matsyburka 6dd4382446 optimize webhooks query 2024-11-04 16:19:43 +02:00
Alex Turchyn 50acff2e81 Merge from docusealco/wip 2024-11-04 16:13:29 +02:00
Pete Matsyburka cb6d24dfca add canonical urls 2024-11-04 11:37:58 +02:00
Pete Matsyburka ab4a22d8ba fix test links 2024-11-03 13:43:28 +02:00
Pete Matsyburka bc40a15de1 use com 2024-11-03 10:19:59 +02:00
Pete Matsyburka 2542f26d96 adjust webhooks 2024-11-03 09:49:30 +02:00
Alex Turchyn 84edb6dbda use webhook urls instead of encrypted configs 2024-11-02 22:12:41 +02:00
Pete Matsyburka 88776e7316 Merge from docusealco/wip 2024-11-02 14:48:21 +02:00
Pete Matsyburka f669045362 redirect com 2024-11-02 09:48:47 +02:00
Pete Matsyburka 68d3efbdba use com 2024-11-02 09:02:50 +02:00
Pete Matsyburka 22d4833496 allow to unarchive submissions 2024-11-01 21:37:02 +02:00
Pete Matsyburka ff14caa9f7 fix image 2024-11-01 19:01:51 +02:00
Pete Matsyburka 05647135bc add status to export 2024-10-31 17:22:14 +02:00
Pete Matsyburka ae76c2a4a8 disable draw when type 2024-10-31 16:43:43 +02:00
Pete Matsyburka fb2b3d44fe fix bulk 2024-10-31 00:51:21 +02:00
Pete Matsyburka 3868557efd add define submitters 2024-10-30 23:07:10 +02:00
dependabot[bot] 9bef28c8ba Bump rexml from 3.3.8 to 3.3.9 in the bundler group across 1 directory (#50) 2024-10-30 12:12:36 +02:00
Pete Matsyburka f25fd3b03d do not edit if declined 2024-10-29 17:11:44 +02:00
Pete Matsyburka d1d7571ee4 disable decline email 2024-10-29 16:44:07 +02:00
Pete Matsyburka 6a298a068c fix updated templates 2024-10-29 12:11:21 +02:00
Pete Matsyburka d4aef79b68 use tsa fallback url 2024-10-29 08:29:26 +02:00
322 changed files with 9301 additions and 2118 deletions

No files matched your search

+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
File renamed without changes.
+40 -13
View File
@@ -7,13 +7,13 @@ jobs:
name: Rubocop name: Rubocop
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v4
- name: Install Ruby - name: Install Ruby
uses: ruby/setup-ruby@v1 uses: ruby/setup-ruby@v1
with: with:
ruby-version: 3.3.4 ruby-version: 3.4.1
- name: Cache gems - name: Cache gems
uses: actions/cache@v1 uses: actions/cache@v4
with: with:
path: vendor/bundle path: vendor/bundle
key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }} key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }}
@@ -31,13 +31,13 @@ jobs:
name: Erblint name: Erblint
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v4
- name: Install Ruby - name: Install Ruby
uses: ruby/setup-ruby@v1 uses: ruby/setup-ruby@v1
with: with:
ruby-version: 3.3.4 ruby-version: 3.4.1
- name: Cache gems - name: Cache gems
uses: actions/cache@v1 uses: actions/cache@v4
with: with:
path: vendor/bundle path: vendor/bundle
key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }} key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }}
@@ -49,13 +49,13 @@ jobs:
bundle config path vendor/bundle bundle config path vendor/bundle
bundle install --jobs 4 --retry 4 bundle install --jobs 4 --retry 4
- name: Run Erblint - name: Run Erblint
run: bundle exec erblint ./app run: bundle exec erb_lint ./app
eslint: eslint:
name: ESLint name: ESLint
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v4
- name: Install Node.js - name: Install Node.js
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
@@ -63,7 +63,7 @@ jobs:
- name: Cache directory path - name: Cache directory path
id: yarn-cache-dir-path id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)" run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v1 - uses: actions/cache@v4
id: yarn-cache id: yarn-cache
with: with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }} path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
@@ -77,6 +77,33 @@ jobs:
run: | run: |
./node_modules/eslint/bin/eslint.js "app/javascript/**/*.js" ./node_modules/eslint/bin/eslint.js "app/javascript/**/*.js"
brakeman:
name: Brakeman
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.4.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: rspec:
name: RSpec name: RSpec
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -96,11 +123,11 @@ jobs:
--health-retries 5 --health-retries 5
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v4
- name: Install Ruby - name: Install Ruby
uses: ruby/setup-ruby@v1 uses: ruby/setup-ruby@v1
with: with:
ruby-version: 3.3.4 ruby-version: 3.4.1
- name: Set up Node - name: Set up Node
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
@@ -110,12 +137,12 @@ jobs:
with: with:
chrome-version: 125 chrome-version: 125
- name: Cache node_modules - name: Cache node_modules
uses: actions/cache@v1 uses: actions/cache@v4
with: with:
path: node_modules path: node_modules
key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }} key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }}
- name: Cache gems - name: Cache gems
uses: actions/cache@v1 uses: actions/cache@v4
with: with:
path: vendor/bundle path: vendor/bundle
key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }} key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }}
+5 -5
View File
@@ -7,7 +7,7 @@ on:
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-24.04-arm
steps: steps:
- name: Checkout code - name: Checkout code
@@ -25,22 +25,22 @@ jobs:
tags: | tags: |
type=semver,pattern={{version}} type=semver,pattern={{version}}
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v2 uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v3
- name: Create .version file - name: Create .version file
run: echo ${{ github.ref_name }} > .version run: echo ${{ github.ref_name }} > .version
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@v2 uses: docker/login-action@v3
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Docker image - name: Build and push Docker image
uses: docker/build-push-action@v4 uses: docker/build-push-action@v6
with: with:
context: . context: .
push: true push: true
+5 -5
View File
@@ -51,25 +51,25 @@ Metrics/AbcSize:
- spec/**/* - spec/**/*
Metrics/ModuleLength: Metrics/ModuleLength:
Max: 500 Max: 1000
Metrics/ClassLength: Metrics/ClassLength:
Max: 500 Max: 1000
RSpec/NestedGroups: RSpec/NestedGroups:
Max: 6 Max: 6
RSpec/MultipleExpectations: RSpec/MultipleExpectations:
Max: 20 Max: 25
RSpec/ExampleLength: RSpec/ExampleLength:
Max: 40 Max: 50
RSpec/MultipleMemoizedHelpers: RSpec/MultipleMemoizedHelpers:
Max: 9 Max: 9
Metrics/BlockNesting: Metrics/BlockNesting:
Max: 4 Max: 5
Rails/I18nLocaleTexts: Rails/I18nLocaleTexts:
Enabled: false 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 WORKDIR /fonts
@@ -6,7 +6,7 @@ RUN apk --no-cache add fontforge wget && wget https://github.com/satbyy/go-noto-
RUN fontforge -lang=py -c 'font1 = fontforge.open("FreeSans.ttf"); font2 = fontforge.open("NotoSansSymbols2-Regular.ttf"); font1.mergeFonts(font2); font1.generate("FreeSans.ttf")' RUN fontforge -lang=py -c 'font1 = fontforge.open("FreeSans.ttf"); font2 = fontforge.open("NotoSansSymbols2-Regular.ttf"); font1.mergeFonts(font2); font1.generate("FreeSans.ttf")'
FROM ruby:3.3.4-alpine as webpack FROM ruby:3.4.1-alpine AS webpack
ENV RAILS_ENV=production ENV RAILS_ENV=production
ENV NODE_ENV=production ENV NODE_ENV=production
@@ -32,7 +32,7 @@ COPY ./app/views ./app/views
RUN echo "gem 'shakapacker'" > Gemfile && ./bin/shakapacker RUN echo "gem 'shakapacker'" > Gemfile && ./bin/shakapacker
FROM ruby:3.3.4-alpine as app FROM ruby:3.4.1-alpine AS app
ENV RAILS_ENV=production ENV RAILS_ENV=production
ENV BUNDLE_WITHOUT="development:test" 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 ./bin ./bin
COPY ./app ./app COPY ./app ./app
COPY ./config ./config COPY ./config ./config
COPY ./db ./db COPY ./db/migrate ./db/migrate
COPY ./log ./log COPY ./log ./log
COPY ./lib ./lib COPY ./lib ./lib
COPY ./public ./public COPY ./public ./public
COPY ./tmp ./tmp COPY ./tmp ./tmp
COPY LICENSE README.md Rakefile config.ru .version ./ COPY LICENSE README.md Rakefile config.ru .version ./
COPY .version ./public/version
COPY --from=fonts /fonts/GoNotoKurrent-Regular.ttf /fonts/GoNotoKurrent-Bold.ttf /fonts/DancingScript-Regular.otf /fonts/OFL.txt /fonts COPY --from=fonts /fonts/GoNotoKurrent-Regular.ttf /fonts/GoNotoKurrent-Bold.ttf /fonts/DancingScript-Regular.otf /fonts/OFL.txt /fonts
COPY --from=fonts /fonts/FreeSans.ttf /usr/share/fonts/freefont COPY --from=fonts /fonts/FreeSans.ttf /usr/share/fonts/freefont
+4 -2
View File
@@ -2,7 +2,7 @@
source 'https://rubygems.org' source 'https://rubygems.org'
ruby '3.3.4' ruby '3.4.1'
gem 'arabic-letter-connector', require: 'arabic-letter-connector/logic' gem 'arabic-letter-connector', require: 'arabic-letter-connector/logic'
gem 'aws-sdk-s3', require: false gem 'aws-sdk-s3', require: false
@@ -46,7 +46,6 @@ gem 'twitter_cldr', require: false
gem 'tzinfo-data' gem 'tzinfo-data'
group :development, :test do group :development, :test do
gem 'annotate'
gem 'better_html' gem 'better_html'
gem 'bullet' gem 'bullet'
gem 'debug' gem 'debug'
@@ -63,6 +62,9 @@ group :development, :test do
end end
group :development do group :development do
gem 'annotaterb'
gem 'brakeman', require: false
gem 'foreman', require: false
gem 'letter_opener_web' gem 'letter_opener_web'
gem 'web-console' gem 'web-console'
end end
+184 -160
View File
@@ -1,67 +1,67 @@
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
actioncable (7.2.1.1) actioncable (8.0.1)
actionpack (= 7.2.1.1) actionpack (= 8.0.1)
activesupport (= 7.2.1.1) activesupport (= 8.0.1)
nio4r (~> 2.0) nio4r (~> 2.0)
websocket-driver (>= 0.6.1) websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6) zeitwerk (~> 2.6)
actionmailbox (7.2.1.1) actionmailbox (8.0.1)
actionpack (= 7.2.1.1) actionpack (= 8.0.1)
activejob (= 7.2.1.1) activejob (= 8.0.1)
activerecord (= 7.2.1.1) activerecord (= 8.0.1)
activestorage (= 7.2.1.1) activestorage (= 8.0.1)
activesupport (= 7.2.1.1) activesupport (= 8.0.1)
mail (>= 2.8.0) mail (>= 2.8.0)
actionmailer (7.2.1.1) actionmailer (8.0.1)
actionpack (= 7.2.1.1) actionpack (= 8.0.1)
actionview (= 7.2.1.1) actionview (= 8.0.1)
activejob (= 7.2.1.1) activejob (= 8.0.1)
activesupport (= 7.2.1.1) activesupport (= 8.0.1)
mail (>= 2.8.0) mail (>= 2.8.0)
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
actionpack (7.2.1.1) actionpack (8.0.1)
actionview (= 7.2.1.1) actionview (= 8.0.1)
activesupport (= 7.2.1.1) activesupport (= 8.0.1)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
racc rack (>= 2.2.4)
rack (>= 2.2.4, < 3.2)
rack-session (>= 1.0.1) rack-session (>= 1.0.1)
rack-test (>= 0.6.3) rack-test (>= 0.6.3)
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6) rails-html-sanitizer (~> 1.6)
useragent (~> 0.16) useragent (~> 0.16)
actiontext (7.2.1.1) actiontext (8.0.1)
actionpack (= 7.2.1.1) actionpack (= 8.0.1)
activerecord (= 7.2.1.1) activerecord (= 8.0.1)
activestorage (= 7.2.1.1) activestorage (= 8.0.1)
activesupport (= 7.2.1.1) activesupport (= 8.0.1)
globalid (>= 0.6.0) globalid (>= 0.6.0)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
actionview (7.2.1.1) actionview (8.0.1)
activesupport (= 7.2.1.1) activesupport (= 8.0.1)
builder (~> 3.1) builder (~> 3.1)
erubi (~> 1.11) erubi (~> 1.11)
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6) rails-html-sanitizer (~> 1.6)
activejob (7.2.1.1) activejob (8.0.1)
activesupport (= 7.2.1.1) activesupport (= 8.0.1)
globalid (>= 0.3.6) globalid (>= 0.3.6)
activemodel (7.2.1.1) activemodel (8.0.1)
activesupport (= 7.2.1.1) activesupport (= 8.0.1)
activerecord (7.2.1.1) activerecord (8.0.1)
activemodel (= 7.2.1.1) activemodel (= 8.0.1)
activesupport (= 7.2.1.1) activesupport (= 8.0.1)
timeout (>= 0.4.0) timeout (>= 0.4.0)
activestorage (7.2.1.1) activestorage (8.0.1)
actionpack (= 7.2.1.1) actionpack (= 8.0.1)
activejob (= 7.2.1.1) activejob (= 8.0.1)
activerecord (= 7.2.1.1) activerecord (= 8.0.1)
activesupport (= 7.2.1.1) activesupport (= 8.0.1)
marcel (~> 1.0) marcel (~> 1.0)
activesupport (7.2.1.1) activesupport (8.0.1)
base64 base64
benchmark (>= 0.3)
bigdecimal bigdecimal
concurrent-ruby (~> 1.0, >= 1.3.1) concurrent-ruby (~> 1.0, >= 1.3.1)
connection_pool (>= 2.2.5) connection_pool (>= 2.2.5)
@@ -71,31 +71,30 @@ GEM
minitest (>= 5.1) minitest (>= 5.1)
securerandom (>= 0.3) securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5) tzinfo (~> 2.0, >= 2.0.5)
uri (>= 0.13.1)
addressable (2.8.7) addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0) public_suffix (>= 2.0.2, < 7.0)
annotate (3.2.0) annotaterb (4.14.0)
activerecord (>= 3.2, < 8.0)
rake (>= 10.4, < 14.0)
arabic-letter-connector (0.1.1) arabic-letter-connector (0.1.1)
ast (2.4.2) ast (2.4.2)
aws-eventstream (1.3.0) aws-eventstream (1.3.0)
aws-partitions (1.985.0) aws-partitions (1.1027.0)
aws-sdk-core (3.209.1) aws-sdk-core (3.214.0)
aws-eventstream (~> 1, >= 1.3.0) aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.651.0) aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9) aws-sigv4 (~> 1.9)
jmespath (~> 1, >= 1.6.1) jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.94.0) aws-sdk-kms (1.96.0)
aws-sdk-core (~> 3, >= 3.207.0) aws-sdk-core (~> 3, >= 3.210.0)
aws-sigv4 (~> 1.5) aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.167.0) aws-sdk-s3 (1.176.1)
aws-sdk-core (~> 3, >= 3.207.0) aws-sdk-core (~> 3, >= 3.210.0)
aws-sdk-kms (~> 1) aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5) aws-sigv4 (~> 1.5)
aws-sdk-secretsmanager (1.108.0) aws-sdk-secretsmanager (1.110.0)
aws-sdk-core (~> 3, >= 3.207.0) aws-sdk-core (~> 3, >= 3.210.0)
aws-sigv4 (~> 1.5) aws-sigv4 (~> 1.5)
aws-sigv4 (1.10.0) aws-sigv4 (1.10.1)
aws-eventstream (~> 1, >= 1.0.2) aws-eventstream (~> 1, >= 1.0.2)
azure-storage-blob (2.0.3) azure-storage-blob (2.0.3)
azure-storage-common (~> 2.0) azure-storage-common (~> 2.0)
@@ -107,6 +106,7 @@ GEM
nokogiri (~> 1, >= 1.10.8) nokogiri (~> 1, >= 1.10.8)
base64 (0.2.0) base64 (0.2.0)
bcrypt (3.1.20) bcrypt (3.1.20)
benchmark (0.4.0)
better_html (2.1.1) better_html (2.1.1)
actionview (>= 6.0) actionview (>= 6.0)
activesupport (>= 6.0) activesupport (>= 6.0)
@@ -118,8 +118,10 @@ GEM
bindex (0.8.1) bindex (0.8.1)
bootsnap (1.18.4) bootsnap (1.18.4)
msgpack (~> 1.2) msgpack (~> 1.2)
brakeman (7.0.0)
racc
builder (3.3.0) builder (3.3.0)
bullet (7.2.0) bullet (8.0.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
uniform_notifier (~> 1.11) uniform_notifier (~> 1.11)
camertron-eprun (1.1.1) camertron-eprun (1.1.1)
@@ -145,14 +147,14 @@ GEM
bigdecimal bigdecimal
rexml rexml
crass (1.0.6) crass (1.0.6)
css_parser (1.19.0) css_parser (1.21.0)
addressable addressable
csv (3.3.0) csv (3.3.2)
cuprite (0.15.1) cuprite (0.15.1)
capybara (~> 3.0) capybara (~> 3.0)
ferrum (~> 0.15.0) ferrum (~> 0.15.0)
date (3.3.4) date (3.4.1)
debug (1.9.2) debug (1.10.0)
irb (~> 1.10) irb (~> 1.10)
reline (>= 0.3.8) reline (>= 0.3.8)
declarative (0.0.20) declarative (0.0.20)
@@ -162,32 +164,32 @@ GEM
railties (>= 4.1.0) railties (>= 4.1.0)
responders responders
warden (~> 1.2.3) warden (~> 1.2.3)
devise-two-factor (6.0.0) devise-two-factor (6.1.0)
activesupport (~> 7.0) activesupport (>= 7.0, < 8.1)
devise (~> 4.0) devise (~> 4.0)
railties (~> 7.0) railties (>= 7.0, < 8.1)
rotp (~> 6.0) rotp (~> 6.0)
diff-lcs (1.5.1) diff-lcs (1.5.1)
digest-crc (0.6.5) digest-crc (0.6.5)
rake (>= 12.0.0, < 14.0.0) rake (>= 12.0.0, < 14.0.0)
docile (1.4.1) docile (1.4.1)
dotenv (3.1.4) dotenv (3.1.7)
drb (2.2.1) drb (2.2.1)
email_typo (0.2.3) email_typo (0.2.3)
erb_lint (0.6.0) erb_lint (0.7.0)
activesupport activesupport
better_html (>= 2.0.1) better_html (>= 2.0.1)
parser (>= 2.7.1.4) parser (>= 2.7.1.4)
rainbow rainbow
rubocop (>= 1) rubocop (>= 1)
smart_properties smart_properties
erubi (1.13.0) erubi (1.13.1)
factory_bot (6.5.0) factory_bot (6.5.0)
activesupport (>= 5.0.0) activesupport (>= 5.0.0)
factory_bot_rails (6.4.3) factory_bot_rails (6.4.4)
factory_bot (~> 6.4) factory_bot (~> 6.5)
railties (>= 5.0.0) railties (>= 5.0.0)
faker (3.4.2) faker (3.5.1)
i18n (>= 1.8.11, < 2) i18n (>= 1.8.11, < 2)
faraday (1.10.4) faraday (1.10.4)
faraday-em_http (~> 1.0) faraday-em_http (~> 1.0)
@@ -207,8 +209,8 @@ GEM
faraday-follow_redirects (0.3.0) faraday-follow_redirects (0.3.0)
faraday (>= 1, < 3) faraday (>= 1, < 3)
faraday-httpclient (1.0.1) faraday-httpclient (1.0.1)
faraday-multipart (1.0.4) faraday-multipart (1.1.0)
multipart-post (~> 2) multipart-post (~> 2.0)
faraday-net_http (1.0.2) faraday-net_http (1.0.2)
faraday-net_http_persistent (1.2.0) faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0) faraday-patron (1.0.0)
@@ -221,9 +223,13 @@ GEM
concurrent-ruby (~> 1.1) concurrent-ruby (~> 1.1)
webrick (~> 1.7) webrick (~> 1.7)
websocket-driver (~> 0.7) websocket-driver (~> 0.7)
ffi (1.17.0) ffi (1.17.1)
ffi (1.17.0-aarch64-linux-gnu) ffi (1.17.1-aarch64-linux-gnu)
ffi (1.17.0-x86_64-linux-gnu) ffi (1.17.1-aarch64-linux-musl)
ffi (1.17.1-arm64-darwin)
ffi (1.17.1-x86_64-linux-gnu)
ffi (1.17.1-x86_64-linux-musl)
foreman (0.88.1)
geom2d (0.4.1) geom2d (0.4.1)
globalid (1.2.1) globalid (1.2.1)
activesupport (>= 6.1) activesupport (>= 6.1)
@@ -235,9 +241,9 @@ GEM
mutex_m mutex_m
representable (~> 3.0) representable (~> 3.0)
retriable (>= 2.0, < 4.a) 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-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-apis-core (>= 0.15.0, < 2.a)
google-cloud-core (1.7.1) google-cloud-core (1.7.1)
google-cloud-env (>= 1.0, < 3.a) google-cloud-env (>= 1.0, < 3.a)
@@ -245,7 +251,7 @@ GEM
google-cloud-env (2.2.1) google-cloud-env (2.2.1)
faraday (>= 1.0, < 3.a) faraday (>= 1.0, < 3.a)
google-cloud-errors (1.4.0) google-cloud-errors (1.4.0)
google-cloud-storage (1.52.0) google-cloud-storage (1.54.0)
addressable (~> 2.8) addressable (~> 2.8)
digest-crc (~> 0.4) digest-crc (~> 0.4)
google-apis-core (~> 0.13) google-apis-core (~> 0.13)
@@ -254,15 +260,17 @@ GEM
google-cloud-core (~> 1.6) google-cloud-core (~> 1.6)
googleauth (~> 1.9) googleauth (~> 1.9)
mini_mime (~> 1.0) mini_mime (~> 1.0)
googleauth (1.11.1) google-logging-utils (0.1.0)
googleauth (1.12.2)
faraday (>= 1.0, < 3.a) faraday (>= 1.0, < 3.a)
google-cloud-env (~> 2.1) google-cloud-env (~> 2.2)
google-logging-utils (~> 0.1)
jwt (>= 1.4, < 3.0) jwt (>= 1.4, < 3.0)
multi_json (~> 1.11) multi_json (~> 1.11)
os (>= 0.9, < 2.0) os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a) signet (>= 0.16, < 2.a)
hashdiff (1.1.1) hashdiff (1.1.2)
hexapdf (0.47.0) hexapdf (1.0.3)
cmdparse (~> 3.0, >= 3.0.3) cmdparse (~> 3.0, >= 3.0.3)
geom2d (~> 0.4, >= 0.4.1) geom2d (~> 0.4, >= 0.4.1)
openssl (>= 2.2.1) openssl (>= 2.2.1)
@@ -273,12 +281,12 @@ GEM
image_processing (1.13.0) image_processing (1.13.0)
mini_magick (>= 4.9.5, < 5) mini_magick (>= 4.9.5, < 5)
ruby-vips (>= 2.0.17, < 3) ruby-vips (>= 2.0.17, < 3)
io-console (0.7.2) io-console (0.8.0)
irb (1.14.1) irb (1.14.3)
rdoc (>= 4.0.0) rdoc (>= 4.0.0)
reline (>= 0.4.2) reline (>= 0.4.2)
jmespath (1.6.2) jmespath (1.6.2)
json (2.7.2) json (2.9.1)
jwt (2.9.3) jwt (2.9.3)
base64 base64
language_server-protocol (3.17.0.3) language_server-protocol (3.17.0.3)
@@ -292,13 +300,13 @@ GEM
letter_opener (~> 1.9) letter_opener (~> 1.9)
railties (>= 6.1) railties (>= 6.1)
rexml rexml
logger (1.6.1) logger (1.6.4)
lograge (0.14.0) lograge (0.14.0)
actionpack (>= 4) actionpack (>= 4)
activesupport (>= 4) activesupport (>= 4)
railties (>= 4) railties (>= 4)
request_store (~> 1.0) request_store (~> 1.0)
loofah (2.22.0) loofah (2.23.1)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.12.0) nokogiri (>= 1.12.0)
mail (2.8.1) mail (2.8.1)
@@ -311,16 +319,16 @@ GEM
method_source (1.1.0) method_source (1.1.0)
mini_magick (4.13.2) mini_magick (4.13.2)
mini_mime (1.1.5) mini_mime (1.1.5)
mini_portile2 (2.8.7) mini_portile2 (2.8.8)
minitest (5.25.1) minitest (5.25.4)
msgpack (1.7.3) msgpack (1.7.5)
multi_json (1.15.0) multi_json (1.15.0)
multipart-post (2.4.1) multipart-post (2.4.1)
mutex_m (0.2.0) mutex_m (0.3.0)
mysql2 (0.5.6) mysql2 (0.5.6)
net-http-persistent (4.0.4) net-http-persistent (4.0.5)
connection_pool (~> 2.2) connection_pool (~> 2.2)
net-imap (0.4.17) net-imap (0.5.6)
date date
net-protocol net-protocol
net-pop (0.1.2) net-pop (0.1.2)
@@ -329,28 +337,34 @@ GEM
timeout timeout
net-smtp (0.5.0) net-smtp (0.5.0)
net-protocol net-protocol
nio4r (2.7.3) nio4r (2.7.4)
nokogiri (1.16.7) nokogiri (1.18.5)
mini_portile2 (~> 2.8.2) mini_portile2 (~> 2.8.2)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.16.7-aarch64-linux) nokogiri (1.18.5-aarch64-linux-gnu)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.16.7-x86_64-linux) nokogiri (1.18.5-aarch64-linux-musl)
racc (~> 1.4) 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) bigdecimal (>= 3.0)
ostruct (>= 0.2) ostruct (>= 0.2)
openssl (3.2.0) openssl (3.3.0)
orm_adapter (0.5.0) orm_adapter (0.5.0)
os (1.1.4) os (1.1.4)
ostruct (0.6.0) ostruct (0.6.1)
package_json (0.1.0) package_json (0.1.0)
pagy (9.1.0) pagy (9.3.3)
parallel (1.26.3) parallel (1.26.3)
parser (3.3.5.0) parser (3.3.6.0)
ast (~> 2.4.1) ast (~> 2.4.1)
racc racc
pg (1.5.8) pg (1.5.9)
premailer (1.27.0) premailer (1.27.0)
addressable addressable
css_parser (>= 1.19.0) css_parser (>= 1.19.0)
@@ -361,58 +375,58 @@ GEM
premailer (~> 1.7, >= 1.7.9) premailer (~> 1.7, >= 1.7.9)
pretender (0.5.0) pretender (0.5.0)
actionpack (>= 6.1) actionpack (>= 6.1)
pry (0.14.2) pry (0.15.0)
coderay (~> 1.1) coderay (~> 1.1)
method_source (~> 1.0) method_source (~> 1.0)
pry-rails (0.3.11) pry-rails (0.3.11)
pry (>= 0.13.0) pry (>= 0.13.0)
psych (5.1.2) psych (5.2.2)
date
stringio stringio
public_suffix (6.0.1) public_suffix (6.0.1)
puma (6.4.3) puma (6.5.0)
nio4r (~> 2.0) nio4r (~> 2.0)
racc (1.8.1) racc (1.8.1)
rack (3.1.8) rack (3.1.12)
rack-proxy (0.7.7) rack-proxy (0.7.7)
rack rack
rack-session (2.0.0) rack-session (2.0.0)
rack (>= 3.0.0) rack (>= 3.0.0)
rack-test (2.1.0) rack-test (2.1.0)
rack (>= 1.3) rack (>= 1.3)
rackup (2.1.0) rackup (2.2.1)
rack (>= 3) rack (>= 3)
webrick (~> 1.8) rails (8.0.1)
rails (7.2.1.1) actioncable (= 8.0.1)
actioncable (= 7.2.1.1) actionmailbox (= 8.0.1)
actionmailbox (= 7.2.1.1) actionmailer (= 8.0.1)
actionmailer (= 7.2.1.1) actionpack (= 8.0.1)
actionpack (= 7.2.1.1) actiontext (= 8.0.1)
actiontext (= 7.2.1.1) actionview (= 8.0.1)
actionview (= 7.2.1.1) activejob (= 8.0.1)
activejob (= 7.2.1.1) activemodel (= 8.0.1)
activemodel (= 7.2.1.1) activerecord (= 8.0.1)
activerecord (= 7.2.1.1) activestorage (= 8.0.1)
activestorage (= 7.2.1.1) activesupport (= 8.0.1)
activesupport (= 7.2.1.1)
bundler (>= 1.15.0) bundler (>= 1.15.0)
railties (= 7.2.1.1) railties (= 8.0.1)
rails-dom-testing (2.2.0) rails-dom-testing (2.2.0)
activesupport (>= 5.0.0) activesupport (>= 5.0.0)
minitest minitest
nokogiri (>= 1.6) nokogiri (>= 1.6)
rails-html-sanitizer (1.6.0) rails-html-sanitizer (1.6.2)
loofah (~> 2.21) loofah (~> 2.21)
nokogiri (~> 1.14) nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
rails-i18n (7.0.9) rails-i18n (8.0.1)
i18n (>= 0.7, < 2) i18n (>= 0.7, < 2)
railties (>= 6.0.0, < 8) railties (>= 8.0.0, < 9)
rails_autolink (1.1.8) rails_autolink (1.1.8)
actionview (> 3.1) actionview (> 3.1)
activesupport (> 3.1) activesupport (> 3.1)
railties (> 3.1) railties (> 3.1)
railties (7.2.1.1) railties (8.0.1)
actionpack (= 7.2.1.1) actionpack (= 8.0.1)
activesupport (= 7.2.1.1) activesupport (= 8.0.1)
irb (~> 1.13) irb (~> 1.13)
rackup (>= 1.0.0) rackup (>= 1.0.0)
rake (>= 12.2) rake (>= 12.2)
@@ -420,12 +434,12 @@ GEM
zeitwerk (~> 2.6) zeitwerk (~> 2.6)
rainbow (3.1.1) rainbow (3.1.1)
rake (13.2.1) rake (13.2.1)
rdoc (6.7.0) rdoc (6.10.0)
psych (>= 4.0.0) psych (>= 4.0.0)
redis-client (0.22.2) redis-client (0.23.0)
connection_pool connection_pool
regexp_parser (2.9.2) regexp_parser (2.9.3)
reline (0.5.10) reline (0.6.0)
io-console (~> 0.5) io-console (~> 0.5)
representable (3.2.0) representable (3.2.0)
declarative (< 0.1.0) declarative (< 0.1.0)
@@ -437,13 +451,13 @@ GEM
actionpack (>= 5.2) actionpack (>= 5.2)
railties (>= 5.2) railties (>= 5.2)
retriable (3.1.2) retriable (3.1.2)
rexml (3.3.8) rexml (3.4.0)
rotp (6.3.0) rotp (6.3.0)
rqrcode (2.2.0) rqrcode (2.2.0)
chunky_png (~> 1.0) chunky_png (~> 1.0)
rqrcode_core (~> 1.0) rqrcode_core (~> 1.0)
rqrcode_core (1.2.0) rqrcode_core (1.2.0)
rspec-core (3.13.1) rspec-core (3.13.2)
rspec-support (~> 3.13.0) rspec-support (~> 3.13.0)
rspec-expectations (3.13.3) rspec-expectations (3.13.3)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
@@ -451,7 +465,7 @@ GEM
rspec-mocks (3.13.2) rspec-mocks (3.13.2)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0) rspec-support (~> 3.13.0)
rspec-rails (7.0.1) rspec-rails (7.1.0)
actionpack (>= 7.0) actionpack (>= 7.0)
activesupport (>= 7.0) activesupport (>= 7.0)
railties (>= 7.0) railties (>= 7.0)
@@ -459,48 +473,47 @@ GEM
rspec-expectations (~> 3.13) rspec-expectations (~> 3.13)
rspec-mocks (~> 3.13) rspec-mocks (~> 3.13)
rspec-support (~> 3.13) rspec-support (~> 3.13)
rspec-support (3.13.1) rspec-support (3.13.2)
rubocop (1.66.1) rubocop (1.69.2)
json (~> 2.3) json (~> 2.3)
language_server-protocol (>= 3.17.0) language_server-protocol (>= 3.17.0)
parallel (~> 1.10) parallel (~> 1.10)
parser (>= 3.3.0.2) parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0) rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 2.4, < 3.0) regexp_parser (>= 2.9.3, < 3.0)
rubocop-ast (>= 1.32.2, < 2.0) rubocop-ast (>= 1.36.2, < 2.0)
ruby-progressbar (~> 1.7) ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0) unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.32.3) rubocop-ast (1.37.0)
parser (>= 3.3.1.0) parser (>= 3.3.1.0)
rubocop-performance (1.22.1) rubocop-performance (1.23.0)
rubocop (>= 1.48.1, < 2.0) rubocop (>= 1.48.1, < 2.0)
rubocop-ast (>= 1.31.1, < 2.0) rubocop-ast (>= 1.31.1, < 2.0)
rubocop-rails (2.26.2) rubocop-rails (2.27.0)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
rack (>= 1.1) rack (>= 1.1)
rubocop (>= 1.52.0, < 2.0) rubocop (>= 1.52.0, < 2.0)
rubocop-ast (>= 1.31.1, < 2.0) rubocop-ast (>= 1.31.1, < 2.0)
rubocop-rspec (3.1.0) rubocop-rspec (3.3.0)
rubocop (~> 1.61) rubocop (~> 1.61)
ruby-progressbar (1.13.0) ruby-progressbar (1.13.0)
ruby-vips (2.2.2) ruby-vips (2.2.2)
ffi (~> 1.12) ffi (~> 1.12)
logger logger
ruby2_keywords (0.0.5) ruby2_keywords (0.0.5)
rubyXL (3.4.27) rubyXL (3.4.33)
nokogiri (>= 1.10.8) nokogiri (>= 1.10.8)
rubyzip (>= 1.3.0) rubyzip (>= 1.3.0)
rubyzip (2.3.2) rubyzip (2.3.2)
securerandom (0.3.1) securerandom (0.4.1)
semantic_range (3.0.0) semantic_range (3.1.0)
shakapacker (8.0.2) shakapacker (8.0.2)
activesupport (>= 5.2) activesupport (>= 5.2)
package_json package_json
rack-proxy (>= 0.6.1) rack-proxy (>= 0.6.1)
railties (>= 5.2) railties (>= 5.2)
semantic_range (>= 2.3.0) semantic_range (>= 2.3.0)
sidekiq (7.3.2) sidekiq (7.3.7)
concurrent-ruby (< 2)
connection_pool (>= 2.3.0) connection_pool (>= 2.3.0)
logger logger
rack (>= 2.2.4) rack (>= 2.2.4)
@@ -517,17 +530,20 @@ GEM
simplecov-html (0.13.1) simplecov-html (0.13.1)
simplecov_json_formatter (0.1.4) simplecov_json_formatter (0.1.4)
smart_properties (1.17.0) smart_properties (1.17.0)
sqlite3 (2.1.0) sqlite3 (2.5.0)
mini_portile2 (~> 2.8.0) mini_portile2 (~> 2.8.0)
sqlite3 (2.1.0-aarch64-linux-gnu) sqlite3 (2.5.0-aarch64-linux-gnu)
sqlite3 (2.1.0-x86_64-linux-gnu) sqlite3 (2.5.0-aarch64-linux-musl)
stringio (3.1.1) sqlite3 (2.5.0-arm64-darwin)
strip_attributes (1.13.0) sqlite3 (2.5.0-x86_64-linux-gnu)
activemodel (>= 3.0, < 8.0) sqlite3 (2.5.0-x86_64-linux-musl)
stringio (3.1.2)
strip_attributes (1.14.1)
activemodel (>= 3.0, < 9.0)
thor (1.3.2) thor (1.3.2)
timeout (0.4.1) timeout (0.4.3)
trailblazer-option (0.1.2) trailblazer-option (0.1.2)
turbo-rails (2.0.10) turbo-rails (2.0.11)
actionpack (>= 6.0.0) actionpack (>= 6.0.0)
railties (>= 6.0.0) railties (>= 6.0.0)
twitter_cldr (6.12.1) twitter_cldr (6.12.1)
@@ -539,9 +555,12 @@ GEM
tzinfo-data (1.2024.2) tzinfo-data (1.2024.2)
tzinfo (>= 1.0.0) tzinfo (>= 1.0.0)
uber (0.1.0) uber (0.1.0)
unicode-display_width (2.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) uniform_notifier (1.16.0)
useragent (0.16.10) uri (1.0.3)
useragent (0.16.11)
warden (1.2.9) warden (1.2.9)
rack (>= 2.0.9) rack (>= 2.0.9)
web-console (4.2.1) web-console (4.2.1)
@@ -553,27 +572,31 @@ GEM
addressable (>= 2.8.0) addressable (>= 2.8.0)
crack (>= 0.3.2) crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0) hashdiff (>= 0.4.0, < 2.0.0)
webrick (1.8.2) webrick (1.9.1)
websocket-driver (0.7.6) websocket-driver (0.7.6)
websocket-extensions (>= 0.1.0) websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5) websocket-extensions (0.1.5)
xpath (3.2.0) xpath (3.2.0)
nokogiri (~> 1.8) nokogiri (~> 1.8)
zeitwerk (2.7.0) zeitwerk (2.7.1)
PLATFORMS PLATFORMS
aarch64-linux aarch64-linux
aarch64-linux-musl
arm64-darwin
ruby ruby
x86_64-linux x86_64-linux
x86_64-linux-musl
DEPENDENCIES DEPENDENCIES
annotate annotaterb
arabic-letter-connector arabic-letter-connector
aws-sdk-s3 aws-sdk-s3
aws-sdk-secretsmanager aws-sdk-secretsmanager
azure-storage-blob azure-storage-blob
better_html better_html
bootsnap bootsnap
brakeman
bullet bullet
cancancan cancancan
capybara capybara
@@ -589,6 +612,7 @@ DEPENDENCIES
faker faker
faraday faraday
faraday-follow_redirects faraday-follow_redirects
foreman
google-cloud-storage google-cloud-storage
hexapdf hexapdf
image_processing image_processing
@@ -628,7 +652,7 @@ DEPENDENCIES
webmock webmock
RUBY VERSION RUBY VERSION
ruby 3.3.4p94 ruby 3.4.1p0
BUNDLED WITH BUNDLED WITH
2.5.3 2.5.3
+10 -10
View File
@@ -1,6 +1,6 @@
<h1 align="center" style="border-bottom: none"> <h1 align="center" style="border-bottom: none">
<div> <div>
<a href="https://www.docuseal.co"> <a href="https://www.docuseal.com">
<img alt="DocuSeal" src="https://github.com/docusealco/docuseal/assets/5418788/c12cd051-81cd-4402-bc3a-92f2cfdc1b06" width="80" /> <img alt="DocuSeal" src="https://github.com/docusealco/docuseal/assets/5418788/c12cd051-81cd-4402-bc3a-92f2cfdc1b06" width="80" />
<br> <br>
</a> </a>
@@ -54,11 +54,11 @@ DocuSeal is an open source platform that provides secure and efficient digital d
- Conditional fields and formulas - Conditional fields and formulas
- Bulk send with CSV, XLSX spreadsheet import - Bulk send with CSV, XLSX spreadsheet import
- SSO / SAML - SSO / SAML
- Template creation with HTML API ([Guide](https://www.docuseal.co/guides/create-pdf-document-fillable-form-with-html-api)) - Template creation with HTML API ([Guide](https://www.docuseal.com/guides/create-pdf-document-fillable-form-with-html-api))
- Template creation with PDF or DOCX and field tags API ([Guide](https://www.docuseal.co/guides/use-embedded-text-field-tags-in-the-pdf-to-create-a-fillable-form)) - Template creation with PDF or DOCX and field tags API ([Guide](https://www.docuseal.com/guides/use-embedded-text-field-tags-in-the-pdf-to-create-a-fillable-form))
- Embedded signing form ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue), [Angular](https://github.com/docusealco/docuseal-angular) or [JavaScript](https://www.docuseal.co/docs/embedded)) - Embedded signing form ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue), [Angular](https://github.com/docusealco/docuseal-angular) or [JavaScript](https://www.docuseal.com/docs/embedded))
- Embedded document form builder ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue), [Angular](https://github.com/docusealco/docuseal-angular) or [JavaScript](https://www.docuseal.co/docs/embedded)) - Embedded document form builder ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue), [Angular](https://github.com/docusealco/docuseal-angular) or [JavaScript](https://www.docuseal.com/docs/embedded))
- [Learn more](https://www.docuseal.co/pricing) - [Learn more](https://www.docuseal.com/pricing)
## Deploy ## Deploy
@@ -93,7 +93,7 @@ sudo HOST=your-domain-name.com docker compose up
At DocuSeal we have expertise and technologies to make documents creation, filling, signing and processing seamlessly integrated with your product. We specialize in working with various industries, including **Banking, Healthcare, Transport, Real Estate, eCommerce, KYC, CRM, and other software products** that require bulk document signing. By leveraging DocuSeal, we can assist in reducing the overall cost of developing and processing electronic documents while ensuring security and compliance with local electronic document laws. At DocuSeal we have expertise and technologies to make documents creation, filling, signing and processing seamlessly integrated with your product. We specialize in working with various industries, including **Banking, Healthcare, Transport, Real Estate, eCommerce, KYC, CRM, and other software products** that require bulk document signing. By leveraging DocuSeal, we can assist in reducing the overall cost of developing and processing electronic documents while ensuring security and compliance with local electronic document laws.
[Book a Meeting](https://www.docuseal.co/contact) [Book a Meeting](https://www.docuseal.com/contact)
## License ## License
@@ -102,6 +102,6 @@ Unless otherwise noted, all files © 2023 DocuSeal LLC.
## Tools ## Tools
- [Signature Maker](https://www.docuseal.co/online-signature) - [Signature Maker](https://www.docuseal.com/online-signature)
- [Sign Document Online](https://www.docuseal.co/sign-documents-online) - [Sign Document Online](https://www.docuseal.com/sign-documents-online)
- [Fill PDF Online](https://www.docuseal.co/fill-pdf) - [Fill PDF Online](https://www.docuseal.com/fill-pdf)
@@ -15,9 +15,11 @@ class AccountConfigsController < ApplicationController
AccountConfig::DOWNLOAD_LINKS_AUTH_KEY, AccountConfig::DOWNLOAD_LINKS_AUTH_KEY,
AccountConfig::FORCE_SSO_AUTH_KEY, AccountConfig::FORCE_SSO_AUTH_KEY,
AccountConfig::FLATTEN_RESULT_PDF_KEY, AccountConfig::FLATTEN_RESULT_PDF_KEY,
AccountConfig::ENFORCE_SIGNING_ORDER_KEY,
AccountConfig::WITH_SIGNATURE_ID, AccountConfig::WITH_SIGNATURE_ID,
AccountConfig::COMBINE_PDF_RESULT_KEY, AccountConfig::COMBINE_PDF_RESULT_KEY,
AccountConfig::REQUIRE_SIGNING_REASON_KEY AccountConfig::REQUIRE_SIGNING_REASON_KEY,
AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY
].freeze ].freeze
InvalidKey = Class.new(StandardError) InvalidKey = Class.new(StandardError)
@@ -38,7 +40,7 @@ class AccountConfigsController < ApplicationController
end end
def account_config_params def account_config_params
params.required(:account_config).permit!.tap do |attrs| params.required(:account_config).permit(:key, :value, { value: {} }, { value: [] }).tap do |attrs|
attrs[:value] = attrs[:value] == '1' if attrs[:value].in?(%w[1 0]) attrs[:value] = attrs[:value] == '1' if attrs[:value].in?(%w[1 0])
end end
end end
+1 -1
View File
@@ -50,7 +50,7 @@ class AccountsController < ApplicationController
# rubocop:disable Layout/LineLength # rubocop:disable Layout/LineLength
render turbo_stream: turbo_stream.replace( render turbo_stream: turbo_stream.replace(
:account_delete_button, :account_delete_button,
html: helpers.tag.p(I18n.t('your_account_removal_request_will_be_processed_within_2_weeks_please_contact_us_if_you_want_to_keep_your_account')) html: helpers.tag.p(I18n.t('your_account_removal_request_will_be_processed_within_2_months_please_contact_us_if_you_want_to_keep_your_account'))
) )
# rubocop:enable Layout/LineLength # rubocop:enable Layout/LineLength
end end
@@ -42,6 +42,7 @@ module Api
is_authorized = attachment.name.in?(%w[logo preview_images]) || is_authorized = attachment.name.in?(%w[logo preview_images]) ||
(current_user && attachment.record.account.id == current_user.account_id) || (current_user && attachment.record.account.id == current_user.account_id) ||
(current_user && !Docuseal.multitenant? && current_user.role == 'superadmin') ||
!attachment.record.account.account_configs !attachment.record.account.account_configs
.find_or_initialize_by(key: AccountConfig::DOWNLOAD_LINKS_AUTH_KEY).value .find_or_initialize_by(key: AccountConfig::DOWNLOAD_LINKS_AUTH_KEY).value
@@ -10,7 +10,7 @@ module Api
last_submitter = @submission.submitters.max_by(&:completed_at) last_submitter = @submission.submitters.max_by(&:completed_at)
if last_submitter.documents_attachments.blank? if last_submitter.documents_attachments.blank?
last_submitter.documents_attachments = Submissions::EnsureResultGenerated.call(submitter) last_submitter.documents_attachments = Submissions::EnsureResultGenerated.call(last_submitter)
end end
last_submitter.documents_attachments last_submitter.documents_attachments
+30 -8
View File
@@ -17,6 +17,8 @@ module Api
submissions = submissions.joins(template: :folder).where(folder: { name: params[:template_folder] }) submissions = submissions.joins(template: :folder).where(folder: { name: params[:template_folder] })
end end
submissions = Submissions::Filter.call(submissions, current_user, params)
submissions = paginate(submissions.preload(:created_by_user, :submitters, submissions = paginate(submissions.preload(:created_by_user, :submitters,
template: :folder, template: :folder,
combined_document_attachment: :blob, combined_document_attachment: :blob,
@@ -67,15 +69,20 @@ module Api
submissions = create_submissions(@template, params) submissions = create_submissions(@template, params)
submissions.each do |submission| WebhookUrls.for_account_id(@template.account_id, 'submission.created').each do |webhook_url|
SendSubmissionCreatedWebhookRequestJob.perform_async({ 'submission_id' => submission.id }) submissions.each do |submission|
SendSubmissionCreatedWebhookRequestJob.perform_async('submission_id' => submission.id,
'webhook_url_id' => webhook_url.id)
end
end end
Submissions.send_signature_requests(submissions) Submissions.send_signature_requests(submissions)
submissions.each do |submission| submissions.each do |submission|
submission.submitters.each do |submitter| 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
end end
@@ -88,12 +95,15 @@ module Api
end end
def destroy def destroy
if params[:permanently] == 'true' if params[:permanently].in?(['true', true])
@submission.destroy! @submission.destroy!
else else
@submission.update!(archived_at: Time.current) @submission.update!(archived_at: Time.current)
SendSubmissionArchivedWebhookRequestJob.perform_async('submission_id' => @submission.id) WebhookUrls.for_account_id(@submission.account_id, 'submission.archived').each do |webhook_url|
SendSubmissionArchivedWebhookRequestJob.perform_async('submission_id' => @submission.id,
'webhook_url_id' => webhook_url.id)
end
end end
render json: @submission.as_json(only: %i[id archived_at]) render json: @submission.as_json(only: %i[id archived_at])
@@ -108,7 +118,19 @@ module Api
end end
end end
json = { submitters: json } if request.path.ends_with?('/init') if request.path.ends_with?('/init')
json =
if submissions.size == 1
{
id: submissions.first.id,
submitters: json,
expire_at: submissions.first.expire_at,
created_at: submissions.first.created_at
}
else
{ submitters: json }
end
end
json json
end end
@@ -157,7 +179,7 @@ module Api
message: %i[subject body], message: %i[subject body],
submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role, submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role,
:completed, :phone, :application_key, :external_id, :reply_to, :go_to_last, :completed, :phone, :application_key, :external_id, :reply_to, :go_to_last,
{ metadata: {}, values: {}, readonly_fields: [], message: %i[subject body], { metadata: {}, values: {}, roles: [], readonly_fields: [], message: %i[subject body],
fields: [:name, :uuid, :default_value, :value, :title, :description, fields: [:name, :uuid, :default_value, :value, :title, :description,
:readonly, :validation_pattern, :invalid_message, :readonly, :validation_pattern, :invalid_message,
{ default_value: [], value: [], preferences: {} }] }]] { default_value: [], value: [], preferences: {} }] }]]
@@ -170,7 +192,7 @@ module Api
key = params.key?(:submission) ? :submission : :submissions key = params.key?(:submission) ? :submission : :submissions
params.permit( params.permit(
key => [permitted_attrs] { key => [permitted_attrs] }, { key => permitted_attrs }
).fetch(key, []) ).fetch(key, [])
end end
end end
@@ -13,7 +13,10 @@ module Api
SubmissionEvents.create_with_tracking_data(submitter, 'view_form', request) SubmissionEvents.create_with_tracking_data(submitter, 'view_form', request)
SendFormViewedWebhookRequestJob.perform_async({ 'submitter_id' => submitter.id }) WebhookUrls.for_account_id(submitter.account_id, 'form.viewed').each do |webhook_url|
SendFormViewedWebhookRequestJob.perform_async('submitter_id' => submitter.id,
'webhook_url_id' => webhook_url.id)
end
render json: {} render json: {}
end end
+8 -2
View File
@@ -68,7 +68,7 @@ module Api
end end
if @submitter.completed_at? if @submitter.completed_at?
ProcessSubmitterCompletionJob.perform_async({ 'submitter_id' => @submitter.id }) ProcessSubmitterCompletionJob.perform_async('submitter_id' => @submitter.id)
elsif normalized_params[:send_email] || normalized_params[:send_sms] elsif normalized_params[:send_email] || normalized_params[:send_sms]
Submitters.send_signature_requests([@submitter]) Submitters.send_signature_requests([@submitter])
end end
@@ -146,9 +146,15 @@ module Api
if attrs[:completed] if attrs[:completed]
submitter.values = Submitters::SubmitValues.merge_default_values(submitter) submitter.values = Submitters::SubmitValues.merge_default_values(submitter)
submitter.values = Submitters::SubmitValues.merge_formula_values(submitter)
submitter.values = Submitters::SubmitValues.maybe_remove_condition_values(submitter) submitter.values = Submitters::SubmitValues.maybe_remove_condition_values(submitter)
formula_values = Submitters::SubmitValues.build_formula_values(submitter)
if formula_values.present?
submitter.values = submitter.values.merge(formula_values)
submitter.values = Submitters::SubmitValues.maybe_remove_condition_values(submitter)
end
submitter.values = submitter.values.transform_values do |v| submitter.values = submitter.values.transform_values do |v|
v == '{{date}}' ? Time.current.in_time_zone(submitter.account.timezone).to_date.to_s : v v == '{{date}}' ? Time.current.in_time_zone(submitter.account.timezone).to_date.to_s : v
end end
@@ -23,9 +23,14 @@ module Api
cloned_template.source = :api cloned_template.source = :api
cloned_template.save! cloned_template.save!
schema_documents = Templates::CloneAttachments.call(template: cloned_template, original_template: @template) schema_documents = Templates::CloneAttachments.call(template: cloned_template,
original_template: @template,
documents: params[:documents])
SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => cloned_template.id) WebhookUrls.for_account_id(cloned_template.account_id, 'template.created').each do |webhook_url|
SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => cloned_template.id,
'webhook_url_id' => webhook_url.id)
end
render json: Templates::SerializeForApi.call(cloned_template, schema_documents) render json: Templates::SerializeForApi.call(cloned_template, schema_documents)
end end
+8 -5
View File
@@ -65,13 +65,16 @@ module Api
@template.update!(template_params) @template.update!(template_params)
SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => @template.id) WebhookUrls.for_account_id(@template.account_id, 'template.updated').each do |webhook_url|
SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => @template.id,
'webhook_url_id' => webhook_url.id)
end
render json: @template.as_json(only: %i[id updated_at]) render json: @template.as_json(only: %i[id updated_at])
end end
def destroy def destroy
if params[:permanently] == 'true' if params[:permanently].in?(['true', true])
@template.destroy! @template.destroy!
else else
@template.update!(archived_at: Time.current) @template.update!(archived_at: Time.current)
@@ -84,7 +87,7 @@ module Api
def filter_templates(templates, params) def filter_templates(templates, params)
templates = Templates.search(templates, params[:q]) templates = Templates.search(templates, params[:q])
templates = params[:archived] ? templates.archived : templates.active templates = params[:archived].in?(['true', true]) ? templates.archived : templates.active
templates = templates.where(external_id: params[:application_key]) if params[:application_key].present? templates = templates.where(external_id: params[:application_key]) if params[:application_key].present?
templates = templates.where(external_id: params[:external_id]) if params[:external_id].present? templates = templates.where(external_id: params[:external_id]) if params[:external_id].present?
templates = templates.joins(:folder).where(folder: { name: params[:folder] }) if params[:folder].present? templates = templates.joins(:folder).where(folder: { name: params[:folder] }) if params[:folder].present?
@@ -97,12 +100,12 @@ module Api
:name, :name,
:external_id, :external_id,
{ {
submitters: [%i[name uuid is_requester invite_by_uuid linked_to_uuid email]], submitters: [%i[name uuid is_requester invite_by_uuid optional_invite_by_uuid linked_to_uuid email]],
fields: [[:uuid, :submitter_uuid, :name, :type, fields: [[:uuid, :submitter_uuid, :name, :type,
:required, :readonly, :default_value, :required, :readonly, :default_value,
:title, :description, :title, :description,
{ preferences: {}, { preferences: {},
conditions: [%i[field_uuid value action]], conditions: [%i[field_uuid value action operation]],
options: [%i[value uuid]], options: [%i[value uuid]],
validation: %i[message pattern], validation: %i[message pattern],
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]] areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]]
+2
View File
@@ -34,6 +34,8 @@ module Api
} }
end end
} }
rescue HexaPDF::MalformedPDFError
render json: { error: 'Malformed PDF' }, status: :unprocessable_entity
end end
end end
end end
+12 -1
View File
@@ -15,6 +15,7 @@ class ApplicationController < ActionController::Base
helper_method :button_title, helper_method :button_title,
:current_account, :current_account,
:form_link_host,
:svg_icon :svg_icon
impersonates :user, with: ->(uuid) { User.find_by(uuid:) } impersonates :user, with: ->(uuid) { User.find_by(uuid:) }
@@ -29,7 +30,7 @@ class ApplicationController < ActionController::Base
redirect_to request.referer, alert: 'Too many requests', status: :too_many_requests redirect_to request.referer, alert: 'Too many requests', status: :too_many_requests
end end
if Rails.env.production? if Rails.env.production? || Rails.env.test?
rescue_from CanCan::AccessDenied do |e| rescue_from CanCan::AccessDenied do |e|
Rollbar.warning(e) if defined?(Rollbar) Rollbar.warning(e) if defined?(Rollbar)
@@ -104,4 +105,14 @@ class ApplicationController < ActionController::Base
def svg_icon(icon_name, class: '') def svg_icon(icon_name, class: '')
render_to_string(partial: "icons/#{icon_name}", locals: { class: }) render_to_string(partial: "icons/#{icon_name}", locals: { class: })
end end
def form_link_host
Docuseal.default_url_options[:host]
end
def maybe_redirect_com
return if request.domain != 'docuseal.co'
redirect_to request.url.gsub('.co/', '.com/'), allow_other_host: true, status: :moved_permanently
end
end end
@@ -6,7 +6,7 @@ class ConsoleRedirectController < ApplicationController
def index def index
if request.path == '/upgrade' if request.path == '/upgrade'
params[:redir] = Docuseal.multitenant? ? "#{Docuseal::CONSOLE_URL}/plans" : "#{Docuseal::CONSOLE_URL}/on_premise" params[:redir] = Docuseal.multitenant? ? "#{Docuseal::CONSOLE_URL}/plans" : "#{Docuseal::CONSOLE_URL}/on_premises"
end end
params[:redir] = "#{Docuseal::CONSOLE_URL}/manage" if request.path == '/manage' params[:redir] = "#{Docuseal::CONSOLE_URL}/manage" if request.path == '/manage'
+1 -1
View File
@@ -9,7 +9,7 @@ class EmbedScriptsController < ActionController::Metal
<h2>Upgrade to Pro</h2> <h2>Upgrade to Pro</h2>
<p>Unlock embedded components by upgrading to Pro</p> <p>Unlock embedded components by upgrading to Pro</p>
<div style="margin-top: 40px;"> <div style="margin-top: 40px;">
<a href="#{Docuseal::CONSOLE_URL}/on_premise" target="_blank" style="padding: 15px 25px; background-color: #222; color: white; text-decoration: none; border-radius: 5px; font-size: 16px; cursor: pointer;"> <a href="#{Docuseal::CONSOLE_URL}/on_premises" target="_blank" style="padding: 15px 25px; background-color: #222; color: white; text-decoration: none; border-radius: 5px; font-size: 16px; cursor: pointer;">
Learn More Learn More
</a> </a>
</div> </div>
+1 -1
View File
@@ -2,7 +2,7 @@
class ErrorsController < ActionController::Base class ErrorsController < ActionController::Base
ENTERPRISE_FEATURE_MESSAGE = ENTERPRISE_FEATURE_MESSAGE =
'This feature is available in Pro Edition: https://www.docuseal.co/pricing' 'This feature is available in Pro Edition: https://www.docuseal.com/pricing'
ENTERPRISE_PATHS = [ ENTERPRISE_PATHS = [
'/templates/html', '/templates/html',
@@ -11,6 +11,8 @@ class EsignSettingsController < ApplicationController
end end
end end
prepend_before_action :maybe_redirect_com, only: %i[show]
before_action :load_encrypted_config before_action :load_encrypted_config
authorize_resource :encrypted_config, parent: false, only: %i[new create] authorize_resource :encrypted_config, parent: false, only: %i[new create]
authorize_resource :encrypted_config, only: %i[update destroy show] authorize_resource :encrypted_config, only: %i[update destroy show]
@@ -39,7 +39,7 @@ class NotificationsSettingsController < ApplicationController
end end
def email_config_params def email_config_params
params.require(:account_config).permit!.tap do |attrs| params.require(:account_config).permit(:key, :value, { value: {} }, { value: [] }).tap do |attrs|
attrs[:key] = nil unless attrs[:key].in?([AccountConfig::BCC_EMAILS, AccountConfig::SUBMITTER_REMINDERS]) attrs[:key] = nil unless attrs[:key].in?([AccountConfig::BCC_EMAILS, AccountConfig::SUBMITTER_REMINDERS])
end end
end end
@@ -6,7 +6,8 @@ class PersonalizationSettingsController < ApplicationController
AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY,
AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY, AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY,
AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY, AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY,
AccountConfig::FORM_COMPLETED_MESSAGE_KEY AccountConfig::FORM_COMPLETED_MESSAGE_KEY,
*(Docuseal.multitenant? ? [] : [AccountConfig::POLICY_LINKS_KEY])
].freeze ].freeze
InvalidKey = Class.new(StandardError) InvalidKey = Class.new(StandardError)
@@ -49,7 +50,7 @@ class PersonalizationSettingsController < ApplicationController
end end
def account_config_params def account_config_params
attrs = params.require(:account_config).permit! attrs = params.require(:account_config).permit(:key, :value, { value: {} }, { value: [] })
return attrs if attrs[:value].is_a?(String) return attrs if attrs[:value].is_a?(String)
+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 SEND_DURATION = 30.minutes
def success; end
def create def create
@submitter = @submitter =
if params[:template_slug] if params[:template_slug]
@@ -31,7 +29,7 @@ class SendSubmissionEmailController < ApplicationController
end end
respond_to do |f| respond_to do |f|
f.html { redirect_to success_send_submission_email_index_path } f.html { render :success }
f.json { head :ok } f.json { head :ok }
end end
end end
+19 -3
View File
@@ -7,6 +7,7 @@ class StartFormController < ApplicationController
skip_authorization_check skip_authorization_check
around_action :with_browser_locale, only: %i[show completed] around_action :with_browser_locale, only: %i[show completed]
before_action :maybe_redirect_com, only: %i[show completed]
before_action :load_template before_action :load_template
def show def show
@@ -24,7 +25,7 @@ class StartFormController < ApplicationController
redirect_to start_form_completed_path(@template.slug, email: submitter_params[:email]) redirect_to start_form_completed_path(@template.slug, email: submitter_params[:email])
else else
if filter_undefined_submitters(@template).size > 1 && @submitter.new_record? if filter_undefined_submitters(@template).size > 1 && @submitter.new_record?
@error_message = I18n.t('not_found') @error_message = multiple_submitters_error_message
return render :show return render :show
end end
@@ -33,11 +34,16 @@ class StartFormController < ApplicationController
assign_submission_attributes(@submitter, @template) assign_submission_attributes(@submitter, @template)
Submissions::AssignDefinedSubmitters.call(@submitter.submission) Submissions::AssignDefinedSubmitters.call(@submitter.submission)
else
@submitter.assign_attributes(ip: request.remote_ip, ua: request.user_agent)
end end
if @submitter.save if @submitter.save
if is_new_record if is_new_record
SendSubmissionCreatedWebhookRequestJob.perform_async({ 'submission_id' => @submitter.submission.id }) WebhookUrls.for_account_id(@submitter.account_id, 'submission.created').each do |webhook_url|
SendSubmissionCreatedWebhookRequestJob.perform_async('submission_id' => @submitter.submission_id,
'webhook_url_id' => webhook_url.id)
end
end end
redirect_to submit_form_path(@submitter.slug) redirect_to submit_form_path(@submitter.slug)
@@ -60,8 +66,10 @@ class StartFormController < ApplicationController
.or(template.submissions.where(expire_at: nil)).where(archived_at: nil)) .or(template.submissions.where(expire_at: nil)).where(archived_at: nil))
.order(id: :desc) .order(id: :desc)
.where(declined_at: nil) .where(declined_at: nil)
.where(external_id: nil)
.where(ip: [nil, request.remote_ip])
.then { |rel| params[:resubmit].present? ? rel.where(completed_at: nil) : rel } .then { |rel| params[:resubmit].present? ? rel.where(completed_at: nil) : rel }
.find_or_initialize_by(**submitter_params.compact_blank) .find_or_initialize_by(email: submitter_params[:email], **submitter_params.compact_blank)
end end
def assign_submission_attributes(submitter, template) def assign_submission_attributes(submitter, template)
@@ -109,4 +117,12 @@ class StartFormController < ApplicationController
@template = Template.find_by!(slug:) @template = Template.find_by!(slug:)
end end
def multiple_submitters_error_message
if current_user&.account_id == @template.account_id
helpers.t('this_submission_has_multiple_signers_which_prevents_the_use_of_a_sharing_link_html')
else
I18n.t('not_found')
end
end
end end
@@ -0,0 +1,25 @@
# frozen_string_literal: true
class SubmissionEventsController < ApplicationController
SUBMISSION_EVENT_ICONS = {
'view_form' => 'eye',
'start_form' => 'player_play',
'complete_form' => 'check',
'send_email' => 'mail_forward',
'click_email' => 'hand_click',
'api_complete_form' => 'check',
'send_reminder_email' => 'mail_forward',
'send_2fa_sms' => '2fa',
'send_sms' => 'send',
'phone_verified' => 'phone_check',
'click_sms' => 'hand_click',
'decline_form' => 'x',
'start_verification' => 'player_play',
'complete_verification' => 'check',
'invite_party' => 'user_plus'
}.freeze
load_and_authorize_resource :submission
def index; end
end
@@ -8,8 +8,18 @@ class SubmissionsArchivedController < ApplicationController
@submissions = @submissions.where.not(archived_at: nil) @submissions = @submissions.where.not(archived_at: nil)
.or(@submissions.where.not(templates: { archived_at: nil })) .or(@submissions.where.not(templates: { archived_at: nil }))
.preload(:created_by_user, template: :author) .preload(: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
end end
+17 -5
View File
@@ -6,6 +6,8 @@ class SubmissionsController < ApplicationController
load_and_authorize_resource :submission, only: %i[show destroy] load_and_authorize_resource :submission, only: %i[show destroy]
prepend_before_action :maybe_redirect_com, only: %i[show]
def show def show
@submission = Submissions.preload_with_pages(@submission) @submission = Submissions.preload_with_pages(@submission)
@@ -50,9 +52,7 @@ class SubmissionsController < ApplicationController
params: params.merge('send_completed_email' => true)) params: params.merge('send_completed_email' => true))
end end
submissions.each do |submission| enqueue_submission_created_webhooks(@template, submissions)
SendSubmissionCreatedWebhookRequestJob.perform_async({ 'submission_id' => submission.id })
end
Submissions.send_signature_requests(submissions) Submissions.send_signature_requests(submissions)
@@ -61,14 +61,17 @@ class SubmissionsController < ApplicationController
def destroy def destroy
notice = notice =
if params[:permanently].present? if params[:permanently].in?(['true', true])
@submission.destroy! @submission.destroy!
I18n.t('submission_has_been_removed') I18n.t('submission_has_been_removed')
else else
@submission.update!(archived_at: Time.current) @submission.update!(archived_at: Time.current)
SendSubmissionArchivedWebhookRequestJob.perform_async('submission_id' => @submission.id) WebhookUrls.for_account_id(@submission.account_id, 'submission.archived').each do |webhook_url|
SendSubmissionArchivedWebhookRequestJob.perform_async('submission_id' => @submission.id,
'webhook_url_id' => webhook_url.id)
end
I18n.t('submission_has_been_archived') I18n.t('submission_has_been_archived')
end end
@@ -85,6 +88,15 @@ class SubmissionsController < ApplicationController
template.save! template.save!
end end
def enqueue_submission_created_webhooks(template, submissions)
WebhookUrls.for_account_id(template.account_id, 'submission.created').each do |webhook_url|
submissions.each do |submission|
SendSubmissionCreatedWebhookRequestJob.perform_async('submission_id' => submission.id,
'webhook_url_id' => webhook_url.id)
end
end
end
def submissions_params def submissions_params
params.permit(submission: { submitters: [%i[uuid email phone name]] }) params.permit(submission: { submitters: [%i[uuid email phone name]] })
end end
@@ -10,11 +10,17 @@ class SubmissionsDashboardController < ApplicationController
.where(templates: { archived_at: nil }) .where(templates: { archived_at: nil })
.preload(:created_by_user, template: :author) .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.search(@submissions, params[:q], search_template: true)
@submissions = Submissions::Filter.call(@submissions, current_user, params)
@submissions = @submissions.pending if params[:status] == 'pending' @submissions = if params[:completed_at_from].present? || params[:completed_at_to].present?
@submissions = @submissions.completed if params[:status] == 'completed' @submissions.order(Submitter.arel_table[:completed_at].maximum.desc)
else
@submissions.order(id: :desc)
end
@pagy, @submissions = pagy(@submissions.preload(submitters: :start_form_submission_events).order(id: :desc)) @pagy, @submissions = pagy(@submissions.preload(submitters: :start_form_submission_events))
end end
end end
@@ -23,9 +23,9 @@ class SubmissionsDownloadController < ApplicationController
last_submitter = submitter.submission.submitters.where.not(completed_at: nil).order(:completed_at).last last_submitter = submitter.submission.submitters.where.not(completed_at: nil).order(:completed_at).last
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) if last_submitter.completed_at < TTL.ago && !signature_valid && !current_user_submitter?(last_submitter)
Rollbar.info("TTL: #{last_submitter.id}") if defined?(Rollbar) Rollbar.info("TTL: #{last_submitter.id}") if defined?(Rollbar)
@@ -53,8 +53,15 @@ class SubmissionsDownloadController < ApplicationController
end end
def build_urls(submitter) def build_urls(submitter)
filename_format = AccountConfig.find_or_initialize_by(account_id: submitter.account_id,
key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value
Submitters.select_attachments_for_download(submitter).map do |attachment| Submitters.select_attachments_for_download(submitter).map do |attachment|
ActiveStorage::Blob.proxy_url(attachment.blob, expires_at: FILES_TTL.from_now.to_i) ActiveStorage::Blob.proxy_url(
attachment.blob,
expires_at: FILES_TTL.from_now.to_i,
filename: Submitters.build_document_filename(submitter, attachment.blob, filename_format)
)
end end
end end
@@ -65,6 +72,13 @@ class SubmissionsDownloadController < ApplicationController
attachment = submitter.submission.combined_document_attachment attachment = submitter.submission.combined_document_attachment
attachment ||= Submissions::GenerateCombinedAttachment.call(submitter) attachment ||= Submissions::GenerateCombinedAttachment.call(submitter)
ActiveStorage::Blob.proxy_url(attachment.blob, expires_at: FILES_TTL.from_now.to_i) filename_format = AccountConfig.find_or_initialize_by(account_id: submitter.account_id,
key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value
ActiveStorage::Blob.proxy_url(
attachment.blob,
expires_at: FILES_TTL.from_now.to_i,
filename: Submitters.build_document_filename(submitter, attachment.blob, filename_format)
)
end end
end end
@@ -0,0 +1,18 @@
# frozen_string_literal: true
class SubmissionsFiltersController < ApplicationController
ALLOWED_NAMES = %w[
author
completed_at
status
created_at
].freeze
skip_authorization_check
def show
return head :not_found unless ALLOWED_NAMES.include?(params[:name])
render params[:name]
end
end
@@ -4,6 +4,8 @@ class SubmissionsPreviewController < ApplicationController
skip_before_action :authenticate_user! skip_before_action :authenticate_user!
skip_authorization_check skip_authorization_check
prepend_before_action :maybe_redirect_com, only: %i[show completed]
TTL = 40.minutes TTL = 40.minutes
def show def show
@@ -18,7 +20,10 @@ class SubmissionsPreviewController < ApplicationController
@submission ||= Submission.find_by!(slug: params[:slug]) @submission ||= Submission.find_by!(slug: params[:slug])
if !@submission.submitters.all?(&:completed_at?) && current_user.blank? raise ActionController::RoutingError, I18n.t('not_found') if @submission.account.archived_at?
if !@submission.submitters.all?(&:completed_at?) && !signature_valid &&
(!current_user || !current_ability.can?(:read, @submission))
raise ActionController::RoutingError, I18n.t('not_found') raise ActionController::RoutingError, I18n.t('not_found')
end end
@@ -35,6 +40,7 @@ class SubmissionsPreviewController < ApplicationController
def completed def completed
@submission = Submission.find_by!(slug: params[:submissions_preview_slug]) @submission = Submission.find_by!(slug: params[:submissions_preview_slug])
@template = @submission.template
render :completed, layout: 'form' render :completed, layout: 'form'
end end
@@ -0,0 +1,11 @@
# frozen_string_literal: true
class SubmissionsUnarchiveController < ApplicationController
load_and_authorize_resource :submission
def create
@submission.update!(archived_at: nil)
redirect_to submission_path(@submission), notice: I18n.t('submission_has_been_unarchived')
end
end
+20 -6
View File
@@ -15,20 +15,23 @@ class SubmitFormController < ApplicationController
submission = @submitter.submission submission = @submitter.submission
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at? return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
return render :archived if 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 :expired if submission.expired?
return render :declined if @submitter.declined_at? 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.current_submitter_order?(@submitter)
Submitters.preload_with_pages(@submitter) Submitters.preload_with_pages(@submitter)
Submitters::MaybeUpdateDefaultValues.call(@submitter, current_user) Submitters::MaybeUpdateDefaultValues.call(@submitter, current_user)
@attachments_index = ActiveStorage::Attachment.where(record: submission.submitters, name: :attachments) @attachments_index = build_attachments_index(submission)
.preload(:blob).index_by(&:uuid)
@form_configs = Submitters::FormConfigs.call(@submitter, CONFIG_KEYS)
return unless @form_configs[:prefill_signature] return unless @form_configs[:prefill_signature]
@@ -69,6 +72,10 @@ class SubmitFormController < ApplicationController
Submitters::SubmitValues.call(submitter, params, request) Submitters::SubmitValues.call(submitter, params, request)
head :ok head :ok
rescue Submitters::SubmitValues::RequiredFieldError => e
Rollbar.warning("Required field #{submitter.id}: #{e.message}") if defined?(Rollbar)
render json: { field_uuid: e.message }, status: :unprocessable_entity
rescue Submitters::SubmitValues::ValidationError => e rescue Submitters::SubmitValues::ValidationError => e
render json: { error: e.message }, status: :unprocessable_entity render json: { error: e.message }, status: :unprocessable_entity
end end
@@ -78,4 +85,11 @@ class SubmitFormController < ApplicationController
end end
def success; 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 end
@@ -21,8 +21,14 @@ class SubmitFormDeclineController < ApplicationController
user = submitter.submission.created_by_user || submitter.template.author user = submitter.submission.created_by_user || submitter.template.author
SubmitterMailer.declined_email(submitter, user).deliver_later! if user.user_configs.find_by(key: UserConfig::RECEIVE_DECLINED_EMAIL)&.value != false
SendFormDeclinedWebhookRequestJob.perform_async('submitter_id' => submitter.id) SubmitterMailer.declined_email(submitter, user).deliver_later!
end
WebhookUrls.for_account_id(submitter.account_id, 'form.declined').each do |webhook_url|
SendFormDeclinedWebhookRequestJob.perform_async('submitter_id' => submitter.id,
'webhook_url_id' => webhook_url.id)
end
redirect_to submit_form_path(submitter.slug) redirect_to submit_form_path(submitter.slug)
end end
@@ -9,13 +9,15 @@ class SubmitFormInviteController < ApplicationController
return head :unprocessable_entity unless can_invite?(submitter) return head :unprocessable_entity unless can_invite?(submitter)
invite_submitters = filter_invite_submitters(submitter) invite_submitters = filter_invite_submitters(submitter, 'invite_by_uuid')
optional_invite_submitters = filter_invite_submitters(submitter, 'optional_invite_by_uuid')
ApplicationRecord.transaction do ApplicationRecord.transaction do
invite_submitters.each do |item| (invite_submitters + optional_invite_submitters).each do |item|
attrs = submitters_attributes.find { |e| e[:uuid] == item['uuid'] } attrs = submitters_attributes.find { |e| e[:uuid] == item['uuid'] }
next unless attrs next unless attrs
next if attrs[:email].blank?
submitter.submission.submitters.create!(**attrs, account_id: submitter.account_id) submitter.submission.submitters.create!(**attrs, account_id: submitter.account_id)
@@ -46,9 +48,9 @@ class SubmitFormInviteController < ApplicationController
!submitter.submission.template.archived_at? !submitter.submission.template.archived_at?
end end
def filter_invite_submitters(submitter) def filter_invite_submitters(submitter, key = 'invite_by_uuid')
(submitter.submission.template_submitters || submitter.submission.template.submitters).select do |s| (submitter.submission.template_submitters || submitter.submission.template.submitters).select do |s|
s['invite_by_uuid'] == submitter.uuid && submitter.submission.submitters.none? { |e| e.uuid == s['uuid'] } s[key] == submitter.uuid && submitter.submission.submitters.none? { |e| e.uuid == s['uuid'] }
end end
end end
@@ -9,7 +9,8 @@ class SubmittersAutocompleteController < ApplicationController
def index def index
submitters = search_submitters(@submitters) submitters = search_submitters(@submitters)
values = submitters.limit(LIMIT).group(SELECT_COLUMNS.join(', ')).pluck(SELECT_COLUMNS.join(', ')) arel_columns = SELECT_COLUMNS.map { |col| Submitter.arel_table[col] }
values = submitters.limit(LIMIT).group(arel_columns).pluck(arel_columns)
attrs = values.map { |row| SELECT_COLUMNS.zip(row).to_h } attrs = values.map { |row| SELECT_COLUMNS.zip(row).to_h }
attrs = attrs.uniq { |e| e[params[:field]] } if params[:field].present? attrs = attrs.uniq { |e| e[params[:field]] } if params[:field].present?
@@ -0,0 +1,58 @@
# frozen_string_literal: true
class SubmittersResubmitController < ApplicationController
load_and_authorize_resource :submitter, parent: false
def update
return redirect_to submit_form_path(slug: @submitter.slug) if @submitter.email != current_user.email
submission = @submitter.template.submissions.new(created_by_user: current_user,
submitters_order: :preserved,
**@submitter.submission.slice(:template_fields,
:account_id,
:template_schema,
:template_submitters,
:preferences))
@submitter.submission.submitters.each do |submitter|
new_submitter = submission.submitters.new(submitter.slice(:uuid, :email, :phone, :name,
:preferences, :metadata, :account_id))
next unless submitter.uuid == @submitter.uuid
assign_submitter_values(new_submitter, submitter)
@new_submitter ||= new_submitter
end
submission.save!
redirect_to submit_form_path(slug: @new_submitter.slug)
end
private
def assign_submitter_values(new_submitter, submitter)
attachments_index = submitter.attachments.index_by(&:uuid)
submitter.submission.template_fields.each do |field|
next if field['submitter_uuid'] != submitter.uuid
next if field['default_value'] == '{{date}}'
next if field['type'] == 'stamp'
next if field['type'] == 'signature'
next if field.dig('preferences', 'formula').present?
value = submitter.values[field['uuid']]
next if value.blank?
if field['type'].in?(%w[image file initials])
Array.wrap(value).each do |attachment_uuid|
new_submitter.attachments << attachments_index[attachment_uuid].dup
end
end
new_submitter.values[field['uuid']] = value
end
end
end
@@ -6,7 +6,7 @@ class SubmittersSendEmailController < ApplicationController
def create def create
if Docuseal.multitenant? && SubmissionEvent.exists?(submitter: @submitter, if Docuseal.multitenant? && SubmissionEvent.exists?(submitter: @submitter,
event_type: 'send_email', event_type: 'send_email',
created_at: 24.hours.ago..Time.current) created_at: 10.hours.ago..Time.current)
Rollbar.warning("Already sent: #{@submitter.id}") if defined?(Rollbar) Rollbar.warning("Already sent: #{@submitter.id}") if defined?(Rollbar)
return redirect_back(fallback_location: submission_path(@submitter.submission), return redirect_back(fallback_location: submission_path(@submitter.submission),
@@ -4,7 +4,8 @@ class TemplateFoldersController < ApplicationController
load_and_authorize_resource :template_folder load_and_authorize_resource :template_folder
def show def show
@templates = @template_folder.templates.active.preload(:author).order(id: :desc) @templates = @template_folder.templates.active.accessible_by(current_ability)
.preload(:author, :template_accesses).order(id: :desc)
@templates = Templates.search(@templates, params[:q]) @templates = Templates.search(@templates, params[:q])
@pagy, @templates = pagy(@templates, limit: 12) @pagy, @templates = pagy(@templates, limit: 12)
@@ -4,7 +4,7 @@ class TemplatesArchivedController < ApplicationController
load_and_authorize_resource :template, parent: false load_and_authorize_resource :template, parent: false
def index def index
@templates = @templates.where.not(archived_at: nil).preload(:author, :folder).order(id: :desc) @templates = @templates.where.not(archived_at: nil).preload(:author, :folder, :template_accesses).order(id: :desc)
@templates = Templates.search(@templates, params[:q]) @templates = Templates.search(@templates, params[:q])
@pagy, @templates = pagy(@templates, limit: 12) @pagy, @templates = pagy(@templates, limit: 12)
@@ -7,8 +7,15 @@ class TemplatesArchivedSubmissionsController < ApplicationController
def index def index
@submissions = @submissions.where.not(archived_at: nil) @submissions = @submissions.where.not(archived_at: nil)
@submissions = Submissions.search(@submissions, params[:q], search_values: true) @submissions = Submissions.search(@submissions, params[:q], search_values: true)
@submissions = Submissions::Filter.call(@submissions, current_user, params)
@pagy, @submissions = pagy(@submissions.preload(:submitters).order(id: :desc)) @submissions = if params[:completed_at_from].present? || params[:completed_at_to].present?
@submissions.order(Submitter.arel_table[:completed_at].maximum.desc)
else
@submissions.order(id: :desc)
end
@pagy, @submissions = pagy(@submissions.preload(submitters: :start_form_submission_events))
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound
redirect_to root_path redirect_to root_path
end end
+31 -9
View File
@@ -9,13 +9,21 @@ class TemplatesController < ApplicationController
submissions = @template.submissions.accessible_by(current_ability) submissions = @template.submissions.accessible_by(current_ability)
submissions = submissions.active if @template.archived_at.blank? submissions = submissions.active if @template.archived_at.blank?
submissions = Submissions.search(submissions, params[:q], search_values: true) submissions = Submissions.search(submissions, params[:q], search_values: true)
submissions = Submissions::Filter.call(submissions, current_user, params.except(:status))
@base_submissions = submissions @base_submissions = submissions
submissions = submissions.pending if params[:status] == 'pending' submissions = Submissions::Filter.filter_by_status(submissions, params)
submissions = submissions.completed if params[:status] == 'completed'
@pagy, @submissions = pagy(submissions.preload(submitters: :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 rescue ActiveRecord::RecordNotFound
redirect_to root_path redirect_to root_path
end end
@@ -66,7 +74,7 @@ class TemplatesController < ApplicationController
if @template.save if @template.save
Templates::CloneAttachments.call(template: @template, original_template: @base_template) if @base_template Templates::CloneAttachments.call(template: @template, original_template: @base_template) if @base_template
SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => @template.id) enqueue_template_created_webhooks(@template)
maybe_redirect_to_template(@template) maybe_redirect_to_template(@template)
else else
@@ -77,14 +85,14 @@ class TemplatesController < ApplicationController
def update def update
@template.update!(template_params) @template.update!(template_params)
SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => @template.id) enqueue_template_updated_webhooks(@template)
head :ok head :ok
end end
def destroy def destroy
notice = notice =
if params[:permanently].present? if params[:permanently].in?(['true', true])
@template.destroy! @template.destroy!
I18n.t('template_has_been_removed') I18n.t('template_has_been_removed')
@@ -102,13 +110,13 @@ class TemplatesController < ApplicationController
def template_params def template_params
params.require(:template).permit( params.require(:template).permit(
:name, :name,
{ schema: [%i[attachment_uuid name]], { schema: [[:attachment_uuid, :name, { conditions: [%i[field_uuid value action operation]] }]],
submitters: [%i[name uuid is_requester linked_to_uuid invite_by_uuid email]], submitters: [%i[name uuid is_requester linked_to_uuid invite_by_uuid optional_invite_by_uuid email]],
fields: [[:uuid, :submitter_uuid, :name, :type, fields: [[:uuid, :submitter_uuid, :name, :type,
:required, :readonly, :default_value, :required, :readonly, :default_value,
:title, :description, :title, :description,
{ preferences: {}, { preferences: {},
conditions: [%i[field_uuid value action]], conditions: [%i[field_uuid value action operation]],
options: [%i[value uuid]], options: [%i[value uuid]],
validation: %i[message pattern], validation: %i[message pattern],
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]] } areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]] }
@@ -128,6 +136,20 @@ class TemplatesController < ApplicationController
end end
end end
def enqueue_template_created_webhooks(template)
WebhookUrls.for_account_id(template.account_id, 'template.created').each do |webhook_url|
SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => template.id,
'webhook_url_id' => webhook_url.id)
end
end
def enqueue_template_updated_webhooks(template)
WebhookUrls.for_account_id(template.account_id, 'template.updated').each do |webhook_url|
SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => template.id,
'webhook_url_id' => webhook_url.id)
end
end
def load_base_template def load_base_template
return if params[:base_template_id].blank? return if params[:base_template_id].blank?
@@ -9,7 +9,9 @@ class TemplatesDashboardController < ApplicationController
FOLDERS_PER_PAGE = 18 FOLDERS_PER_PAGE = 18
def index def index
@template_folders = filter_template_folders(@template_folders) @template_folders = @template_folders.where(id: @templates.active.select(:folder_id)).order(id: :desc)
@template_folders = TemplateFolders.search(@template_folders, params[:q])
@pagy, @template_folders = pagy( @pagy, @template_folders = pagy(
@template_folders, @template_folders,
@@ -36,16 +38,8 @@ class TemplatesDashboardController < ApplicationController
private private
def filter_template_folders(template_folders)
rel = template_folders.joins(:active_templates)
.order(id: :desc)
.distinct
TemplateFolders.search(rel, params[:q])
end
def filter_templates(templates) def filter_templates(templates)
rel = templates.active.preload(:author).order(id: :desc) rel = templates.active.preload(:author, :template_accesses).order(id: :desc)
if params[:q].blank? if params[:q].blank?
if Docuseal.multitenant? && !current_account.testing? if Docuseal.multitenant? && !current_account.testing?
@@ -6,9 +6,10 @@ class TemplatesDebugController < ApplicationController
def show def show
attachment = @template.documents.first attachment = @template.documents.first
pdf = HexaPDF::Document.new(io: StringIO.new(attachment.download)) data = attachment.download
pdf = HexaPDF::Document.new(io: StringIO.new(data))
fields = Templates::FindAcroFields.call(pdf, attachment) fields = Templates::FindAcroFields.call(pdf, attachment, data)
attachment.metadata['pdf'] ||= {} attachment.metadata['pdf'] ||= {}
attachment.metadata['pdf']['fields'] = fields attachment.metadata['pdf']['fields'] = fields
@@ -22,13 +22,16 @@ class TemplatesPreferencesController < ApplicationController
preferences: %i[bcc_completed request_email_subject request_email_body preferences: %i[bcc_completed request_email_subject request_email_body
documents_copy_email_subject documents_copy_email_body documents_copy_email_subject documents_copy_email_body
documents_copy_email_enabled documents_copy_email_attach_audit documents_copy_email_enabled documents_copy_email_attach_audit
documents_copy_email_attach_documents documents_copy_email_reply_to
completed_notification_email_attach_documents completed_notification_email_attach_documents
completed_redirect_url completed_redirect_url
submitters_order submitters_order
completed_notification_email_subject completed_notification_email_body completed_notification_email_subject completed_notification_email_body
completed_notification_email_enabled completed_notification_email_attach_audit] + completed_notification_email_enabled completed_notification_email_attach_audit] +
[completed_message: %i[title body]] [completed_message: %i[title body],
submitters: [%i[uuid request_email_subject request_email_body]]]
).tap do |attrs| ).tap do |attrs|
attrs[:preferences].delete(:submitters) if params[:request_email_per_submitter] != '1'
attrs[:preferences] = attrs[:preferences].transform_values do |value| attrs[:preferences] = attrs[:preferences].transform_values do |value|
if %w[true false].include?(value) if %w[true false].include?(value)
value == 'true' value == 'true'
@@ -17,38 +17,47 @@ class TemplatesRecipientsController < ApplicationController
private private
def submitters_params def submitters_params
params.require(:template).permit( permit_params = { submitters: [%i[name uuid is_requester optional_invite_by_uuid
submitters: [%i[name uuid is_requester invite_by_uuid linked_to_uuid email option]] invite_by_uuid linked_to_uuid email option]] }
).fetch(:submitters, {}).values.filter_map do |s|
params.require(:template).permit(permit_params).fetch(:submitters, {}).values.filter_map do |s|
next if s[:uuid].blank? next if s[:uuid].blank?
if s[:is_requester] == '1' && s[:invite_by_uuid].blank? if s[:is_requester] == '1' && s[:invite_by_uuid].blank? && s[:optional_invite_by_uuid].blank?
s[:is_requester] = true s[:is_requester] = true
else else
s.delete(:is_requester) s.delete(:is_requester)
end end
s.delete(:invite_by_uuid) if s[:invite_by_uuid].blank? s.delete(:invite_by_uuid) if s[:invite_by_uuid].blank?
s.delete(:optional_invite_by_uuid) if s[:optional_invite_by_uuid].blank?
option = s.delete(:option) normalize_option_value(s)
if option.present?
case option
when 'is_requester'
s[:is_requester] = true
when 'not_set'
s.delete(:is_requester)
s.delete(:email)
s.delete(:linked_to_uuid)
s.delete(:invite_by_uuid)
when /\Alinked_to_(.*)\z/
s[:linked_to_uuid] = ::Regexp.last_match(-1)
when /\Ainvite_by_(.*)\z/
s[:invite_by_uuid] = ::Regexp.last_match(-1)
end
end
s
end end
end end
def normalize_option_value(attrs)
option = attrs.delete(:option)
if option.present?
case option
when 'is_requester'
attrs[:is_requester] = true
when 'not_set'
attrs.delete(:is_requester)
attrs.delete(:email)
attrs.delete(:linked_to_uuid)
attrs.delete(:invite_by_uuid)
attrs.delete(:optional_invite_by_uuid)
when /\Alinked_to_(.*)\z/
attrs[:linked_to_uuid] = ::Regexp.last_match(-1)
when /\Aoptional_invite_by_(.*)\z/
attrs[:optional_invite_by_uuid] = ::Regexp.last_match(-1)
when /\Ainvite_by_(.*)\z/
attrs[:invite_by_uuid] = ::Regexp.last_match(-1)
end
end
attrs
end
end end
@@ -23,7 +23,7 @@ class TemplatesUploadsController < ApplicationController
@template.update!(schema:) @template.update!(schema:)
SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => @template.id) enqueue_template_created_webhooks(@template)
redirect_to edit_template_path(@template) redirect_to edit_template_path(@template)
rescue Templates::CreateAttachments::PdfEncrypted rescue Templates::CreateAttachments::PdfEncrypted
@@ -55,14 +55,22 @@ class TemplatesUploadsController < ApplicationController
tempfile.write(DownloadUtils.call(params[:url]).body) tempfile.write(DownloadUtils.call(params[:url]).body)
tempfile.rewind tempfile.rewind
filename = URI.decode_www_form_component(params[:filename]) if params[:filename].present?
filename ||= File.basename(URI.decode_www_form_component(params[:url]))
file = ActionDispatch::Http::UploadedFile.new( file = ActionDispatch::Http::UploadedFile.new(
tempfile:, tempfile:,
filename: File.basename( filename:,
URI.decode_www_form_component(params[:filename].presence || params[:url]), '.*'
),
type: Marcel::MimeType.for(tempfile) type: Marcel::MimeType.for(tempfile)
) )
{ files: [file] } { files: [file] }
end end
def enqueue_template_created_webhooks(template)
WebhookUrls.for_account_id(template.account_id, 'template.created').each do |webhook_url|
SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => template.id,
'webhook_url_id' => webhook_url.id)
end
end
end end
@@ -4,9 +4,8 @@ class TestingApiSettingsController < ApplicationController
def index def index
authorize!(:manage, current_user.access_token) authorize!(:manage, current_user.access_token)
@webhook_config = @webhook_url = current_account.webhook_urls.first_or_initialize
current_account.encrypted_configs.find_or_initialize_by(key: EncryptedConfig::WEBHOOK_URL_KEY)
authorize!(:manage, @webhook_config) authorize!(:manage, @webhook_url)
end end
end end
+1 -1
View File
@@ -26,7 +26,7 @@ class UserConfigsController < ApplicationController
end end
def user_config_params def user_config_params
params.required(:user_config).permit!.tap do |attrs| params.required(:user_config).permit(:key, :value, { value: {} }, { value: [] }).tap do |attrs|
attrs[:value] = attrs[:value] == '1' if attrs[:value].in?(%w[1 0]) attrs[:value] = attrs[:value] == '1' if attrs[:value].in?(%w[1 0])
end end
end end
+13 -20
View File
@@ -24,16 +24,14 @@ class UsersController < ApplicationController
def edit; end def edit; end
def create def create
existing_user = User.accessible_by(current_ability).find_by(email: @user.email) if User.accessible_by(current_ability).exists?(email: @user.email)
@user.errors.add(:email, I18n.t('already_exists'))
if existing_user return render turbo_stream: turbo_stream.replace(:modal, template: 'users/new'), status: :unprocessable_entity
existing_user.archived_at = nil
existing_user.assign_attributes(user_params)
existing_user.account = current_account
@user = existing_user
end end
@user.role = User::ADMIN_ROLE unless role_valid?(@user.role)
if @user.save if @user.save
UserMailer.invitation_email(@user).deliver_later! UserMailer.invitation_email(@user).deliver_later!
@@ -47,17 +45,16 @@ class UsersController < ApplicationController
return redirect_to settings_users_path, notice: I18n.t('unable_to_update_user') if Docuseal.demo? return redirect_to settings_users_path, notice: I18n.t('unable_to_update_user') if Docuseal.demo?
attrs = user_params.compact_blank.merge(user_params.slice(:archived_at)) attrs = user_params.compact_blank.merge(user_params.slice(:archived_at))
attrs.delete(:role) if !role_valid?(attrs[:role]) || current_user == @user
if params.dig(:user, :account_id).present? if params.dig(:user, :account_id).present?
account = Account.accessible_by(current_ability).find(params[:user][:account_id]) account = Account.accessible_by(current_ability).find(params.dig(:user, :account_id))
authorize!(:manage, account) authorize!(:manage, account)
@user.account = account @user.account = account
end end
if @user.update(attrs) if @user.update(attrs.except(current_user == @user ? :role : nil))
redirect_back fallback_location: settings_users_path, notice: I18n.t('user_has_been_updated') redirect_back fallback_location: settings_users_path, notice: I18n.t('user_has_been_updated')
else else
render turbo_stream: turbo_stream.replace(:modal, template: 'users/edit'), status: :unprocessable_entity render turbo_stream: turbo_stream.replace(:modal, template: 'users/edit'), status: :unprocessable_entity
@@ -81,20 +78,16 @@ class UsersController < ApplicationController
end end
def build_user def build_user
@user = current_account.users.find_by(email: user_params[:email])&.tap do |user| @user = current_account.users.new(user_params)
user.assign_attributes(user_params)
user.archived_at = nil
end
@user ||= current_account.users.new(user_params)
@user
end end
def user_params def user_params
if params.key?(:user) if params.key?(:user)
params.require(:user).permit(:email, :first_name, :last_name, :password, permitted_params = %i[email first_name last_name password archived_at]
:role, :archived_at, :account_id)
permitted_params << :role if role_valid?(params.dig(:user, :role))
params.require(:user).permit(permitted_params)
else else
{} {}
end end
@@ -1,37 +1,22 @@
# frozen_string_literal: true # frozen_string_literal: true
class WebhookPreferencesController < ApplicationController class WebhookPreferencesController < ApplicationController
EVENTS = %w[ load_and_authorize_resource :webhook_url, parent: false
form.viewed
form.started
form.completed
form.declined
template.created
template.updated
submission.created
submission.archived
].freeze
before_action :load_account_config def update
authorize_resource :account_config, parent: false webhook_preferences_params[:events].each do |event, val|
@webhook_url.events.delete(event) if val == '0'
@webhook_url.events.push(event) if val == '1' && @webhook_url.events.exclude?(event)
end
def create @webhook_url.save!
@account_config.value[account_config_params[:event]] = account_config_params[:value] == '1'
@account_config.save!
head :ok head :ok
end end
private private
def load_account_config def webhook_preferences_params
@account_config = params.require(:webhook_url).permit(events: {})
current_account.account_configs.find_or_initialize_by(key: AccountConfig::WEBHOOK_PREFERENCES_KEY)
@account_config.value ||= {}
end
def account_config_params
params.permit(:event, :value)
end end
end end
+9 -16
View File
@@ -1,29 +1,22 @@
# frozen_string_literal: true # frozen_string_literal: true
class WebhookSecretController < ApplicationController class WebhookSecretController < ApplicationController
before_action :load_encrypted_config load_and_authorize_resource :webhook_url, parent: false
authorize_resource :encrypted_config, parent: false
def index; end def show; end
def create def update
@encrypted_config.assign_attributes(value: { @webhook_url.update!(secret: {
encrypted_config_params[:key] => encrypted_config_params[:value] webhook_secret_params[:key] => webhook_secret_params[:value]
}.compact_blank) }.compact_blank)
@encrypted_config.value.present? ? @encrypted_config.save! : @encrypted_config.delete redirect_back(fallback_location: settings_webhook_path(@webhook_url),
notice: I18n.t('webhook_secret_has_been_saved'))
redirect_back(fallback_location: settings_webhooks_path, notice: I18n.t('webhook_secret_has_been_saved'))
end end
private private
def load_encrypted_config def webhook_secret_params
@encrypted_config = params.require(:webhook_url).permit(secret: %i[key value]).fetch(:secret, {})
current_account.encrypted_configs.find_or_initialize_by(key: EncryptedConfig::WEBHOOK_SECRET_KEY)
end
def encrypted_config_params
params.require(:encrypted_config).permit(value: %i[key value]).fetch(:value, {})
end end
end end
+37 -13
View File
@@ -1,36 +1,60 @@
# frozen_string_literal: true # frozen_string_literal: true
class WebhookSettingsController < ApplicationController class WebhookSettingsController < ApplicationController
before_action :load_encrypted_config load_and_authorize_resource :webhook_url, parent: false, only: %i[index show new create update destroy]
authorize_resource :encrypted_config, parent: false load_and_authorize_resource :webhook_url, only: %i[resend], id_param: :webhook_id
def index
@webhook_urls = @webhook_urls.order(id: :desc)
@webhook_url = @webhook_urls.first_or_initialize
render @webhook_urls.size > 1 ? 'index' : 'show'
end
def show; end def show; end
def new; end
def create def create
@encrypted_config.assign_attributes(encrypted_config_params) @webhook_url.save!
@encrypted_config.value.present? ? @encrypted_config.save! : @encrypted_config.delete redirect_to settings_webhooks_path, notice: I18n.t('webhook_url_has_been_saved')
redirect_back(fallback_location: settings_webhooks_path, notice: I18n.t('webhook_url_has_been_saved'))
end end
def update def update
@webhook_url.update!(update_params)
redirect_back(fallback_location: settings_webhook_path(@webhook_url),
notice: I18n.t('webhook_url_has_been_updated'))
end
def destroy
@webhook_url.destroy!
redirect_to settings_webhooks_path, notice: I18n.t('webhook_url_has_been_deleted')
end
def resend
submitter = current_account.submitters.where.not(completed_at: nil).order(:id).last submitter = current_account.submitters.where.not(completed_at: nil).order(:id).last
SendFormCompletedWebhookRequestJob.perform_async({ 'submitter_id' => submitter.id, if submitter.blank? || @webhook_url.blank?
'encrypted_config_id' => @encrypted_config.id }) return redirect_back(fallback_location: settings_webhooks_path,
alert: I18n.t('unable_to_resend_webhook_request'))
end
SendFormCompletedWebhookRequestJob.perform_async('submitter_id' => submitter.id,
'webhook_url_id' => @webhook_url.id)
redirect_back(fallback_location: settings_webhooks_path, notice: I18n.t('webhook_request_has_been_sent')) redirect_back(fallback_location: settings_webhooks_path, notice: I18n.t('webhook_request_has_been_sent'))
end end
private private
def load_encrypted_config def create_params
@encrypted_config = params.require(:webhook_url).permit(:url, events: []).reverse_merge(events: [])
current_account.encrypted_configs.find_or_initialize_by(key: EncryptedConfig::WEBHOOK_URL_KEY)
end end
def encrypted_config_params def update_params
params.require(:encrypted_config).permit(:value) params.require(:webhook_url).permit(:url)
end end
end end
+5 -2
View File
@@ -30,11 +30,11 @@ import ToggleAttribute from './elements/toggle_attribute'
import LinkedInput from './elements/linked_input' import LinkedInput from './elements/linked_input'
import CheckboxGroup from './elements/checkbox_group' import CheckboxGroup from './elements/checkbox_group'
import MaskedInput from './elements/masked_input' import 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 * as TurboInstantClick from './lib/turbo_instant_click'
import './images/preview.png'
TurboInstantClick.start() TurboInstantClick.start()
document.addEventListener('turbo:before-cache', () => { document.addEventListener('turbo:before-cache', () => {
@@ -97,6 +97,8 @@ safeRegisterElement('toggle-attribute', ToggleAttribute)
safeRegisterElement('linked-input', LinkedInput) safeRegisterElement('linked-input', LinkedInput)
safeRegisterElement('checkbox-group', CheckboxGroup) safeRegisterElement('checkbox-group', CheckboxGroup)
safeRegisterElement('masked-input', MaskedInput) safeRegisterElement('masked-input', MaskedInput)
safeRegisterElement('set-date-button', SetDateButton)
safeRegisterElement('indeterminate-checkbox', IndeterminateCheckbox)
safeRegisterElement('template-builder', class extends HTMLElement { safeRegisterElement('template-builder', class extends HTMLElement {
connectedCallback () { connectedCallback () {
@@ -111,6 +113,7 @@ safeRegisterElement('template-builder', class extends HTMLElement {
backgroundColor: '#faf7f5', backgroundColor: '#faf7f5',
locale: this.dataset.locale, locale: this.dataset.locale,
withPhone: this.dataset.withPhone === 'true', withPhone: this.dataset.withPhone === 'true',
withVerification: ['true', 'false'].includes(this.dataset.withVerification) ? this.dataset.withVerification === 'true' : null,
withLogo: this.dataset.withLogo !== 'false', withLogo: this.dataset.withLogo !== 'false',
editable: this.dataset.editable !== 'false', editable: this.dataset.editable !== 'false',
authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content, authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content,
+17
View File
@@ -97,6 +97,23 @@ button[disabled] .enabled {
bottom: auto; bottom: auto;
} }
.tooltip-bottom-start:before {
transform: translateX(-31%);
top: var(--tooltip-offset);
left: 100%;
right: auto;
bottom: auto;
}
.tooltip-bottom-start:after {
transform: translateX(-25%);
border-color: transparent transparent var(--tooltip-color) transparent;
top: var(--tooltip-tail-offset);
left: 50%;
right: auto;
bottom: auto;
}
.autocomplete { .autocomplete {
background: white; background: white;
z-index: 1000; z-index: 1000;
+24 -17
View File
@@ -1,5 +1,6 @@
import SignaturePad from 'signature_pad' import SignaturePad from 'signature_pad'
import { cropCanvasAndExportToPNG } from './submission_form/crop_canvas' import { cropCanvasAndExportToPNG } from './submission_form/crop_canvas'
import { isValidSignatureCanvas } from './submission_form/validate_signature'
window.customElements.define('draw-signature', class extends HTMLElement { window.customElements.define('draw-signature', class extends HTMLElement {
connectedCallback () { connectedCallback () {
@@ -12,6 +13,10 @@ window.customElements.define('draw-signature', class extends HTMLElement {
this.pad = new SignaturePad(this.canvas) this.pad = new SignaturePad(this.canvas)
if (this.dataset.color) {
this.pad.penColor = this.dataset.color
}
this.pad.addEventListener('endStroke', () => { this.pad.addEventListener('endStroke', () => {
this.updateSubmitButtonVisibility() this.updateSubmitButtonVisibility()
}) })
@@ -39,6 +44,8 @@ window.customElements.define('draw-signature', class extends HTMLElement {
return response return response
}) })
}).catch(error => {
console.log(error)
}).finally(() => { }).finally(() => {
this.submitButton.disabled = false this.submitButton.disabled = false
}) })
@@ -61,26 +68,26 @@ window.customElements.define('draw-signature', class extends HTMLElement {
} }
async submitImage () { async submitImage () {
return new Promise((resolve, reject) => { if (!isValidSignatureCanvas(this.pad.toData())) {
cropCanvasAndExportToPNG(this.canvas, { errorOnTooSmall: true }).then(async (blob) => { alert('Signature is too small or simple. Please redraw.')
const file = new File([blob], 'signature.png', { type: 'image/png' })
const formData = new FormData() return Promise.reject(new Error('Image too small or simple'))
}
formData.append('file', file) return cropCanvasAndExportToPNG(this.canvas).then(async (blob) => {
formData.append('submitter_slug', this.dataset.slug) const file = new File([blob], 'signature.png', { type: 'image/png' })
formData.append('name', 'attachments')
formData.append('remember_signature', 'true')
return fetch('/api/attachments', { const formData = new FormData()
method: 'POST',
body: formData formData.append('file', file)
}).then((resp) => resp.json()).then((attachment) => { formData.append('submitter_slug', this.dataset.slug)
return resolve(attachment) formData.append('name', 'attachments')
}) formData.append('remember_signature', 'true')
}).catch((error) => {
return reject(error) return fetch('/api/attachments', {
}) method: 'POST',
body: formData
}).then(resp => resp.json())
}) })
} }
@@ -3,6 +3,8 @@ export default class extends HTMLElement {
this.resize() this.resize()
this.textarea.addEventListener('input', () => this.resize()) this.textarea.addEventListener('input', () => this.resize())
this.observeVisibility()
} }
resize () { resize () {
@@ -11,6 +13,28 @@ export default class extends HTMLElement {
} }
} }
observeVisibility () {
this.observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
this.resize()
this.observer.unobserve(this.textarea)
}
})
},
{
threshold: 0.1
}
)
this.observer.observe(this.textarea)
}
disconnectedCallback () {
this.observer.unobserve(this.textarea)
}
get textarea () { get textarea () {
return this.querySelector('textarea') return this.querySelector('textarea')
} }
+2 -1
View File
@@ -20,7 +20,8 @@ export default targetable(class extends HTMLElement {
fetch(this.dataset.src).then(async (response) => { fetch(this.dataset.src).then(async (response) => {
if (response.ok) { if (response.ok) {
const urls = await response.json() const urls = await response.json()
const isSafariIos = /iPhone|iPad|iPod/i.test(navigator.userAgent) const isMobileSafariIos = 'ontouchstart' in window && navigator.maxTouchPoints > 0 && /AppleWebKit/i.test(navigator.userAgent)
const isSafariIos = isMobileSafariIos || /iPhone|iPad|iPod/i.test(navigator.userAgent)
if (isSafariIos && urls.length > 1) { if (isSafariIos && urls.length > 1) {
this.downloadSafariIos(urls) this.downloadSafariIos(urls)
@@ -0,0 +1,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, { this.app = createApp(Form, {
submitter: JSON.parse(this.dataset.submitter), submitter: JSON.parse(this.dataset.submitter),
inviteSubmitters: JSON.parse(this.dataset.inviteSubmitters), inviteSubmitters: JSON.parse(this.dataset.inviteSubmitters),
optionalInviteSubmitters: JSON.parse(this.dataset.optionalInviteSubmitters),
schema: JSON.parse(this.dataset.schema),
canSendEmail: this.dataset.canSendEmail === 'true', canSendEmail: this.dataset.canSendEmail === 'true',
previousSignatureValue: this.dataset.previousSignatureValue, previousSignatureValue: this.dataset.previousSignatureValue,
goToLast: this.dataset.goToLast === 'true', goToLast: this.dataset.goToLast === 'true',
Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

+57 -16
View File
@@ -1,9 +1,9 @@
<template> <template>
<div <div
class="field-area flex absolute lg:text-base -outline-offset-1" class="flex absolute lg:text-base -outline-offset-1 field-area"
dir="auto" dir="auto"
:style="computedStyle" :style="computedStyle"
:class="{ 'font-serif': field.preferences?.font === 'Times', 'text-[1.6vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars, 'cursor-default': !submittable, 'border border-red-100 bg-red-100 cursor-pointer': submittable, 'border border-red-100': !isActive && submittable, 'bg-opacity-80': !isActive && !isValueSet && submittable, 'field-area-active outline-red-500 outline-dashed outline-2 z-10': isActive && submittable, 'bg-opacity-40': (isActive || isValueSet) && submittable }" :class="{ 'text-[1.6vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars, 'cursor-default': !submittable, 'border border-red-100 bg-red-100 cursor-pointer': submittable, 'border border-red-100': !isActive && submittable, 'bg-opacity-80': !isActive && !isValueSet && submittable, 'outline-red-500 outline-dashed outline-2 z-10 field-area-active': isActive && submittable, 'bg-opacity-40': (isActive || isValueSet) && submittable }"
> >
<div <div
v-if="(!withFieldPlaceholder || !field.name || field.type === 'cells') && !isActive && !isValueSet && field.type !== 'checkbox' && submittable && !area.option_uuid" v-if="(!withFieldPlaceholder || !field.name || field.type === 'cells') && !isActive && !isValueSet && field.type !== 'checkbox' && submittable && !area.option_uuid"
@@ -39,7 +39,6 @@
</template> </template>
</div> </div>
<div <div
v-if="isActive"
ref="scrollToElem" ref="scrollToElem"
class="absolute" class="absolute"
:style="{ top: scrollPadding }" :style="{ top: scrollPadding }"
@@ -75,7 +74,7 @@
ID: {{ signature.uuid }} ID: {{ signature.uuid }}
</div> </div>
<div> <div>
{{ t('reason') }}: {{ values[field.preferences?.reason_field_uuid] || t('digitally_signed_by') }} {{ submitter.name }} <span v-if="values[field.preferences?.reason_field_uuid]">{{ t('reason') }}: </span>{{ values[field.preferences?.reason_field_uuid] || t('digitally_signed_by') }} {{ submitter.name }}
<template v-if="submitter.email"> <template v-if="submitter.email">
&lt;{{ submitter.email }}&gt; &lt;{{ submitter.email }}&gt;
</template> </template>
@@ -169,6 +168,7 @@
<div <div
v-else-if="field.type === 'cells'" v-else-if="field.type === 'cells'"
class="w-full flex items-center" class="w-full flex items-center"
:class="{ 'justify-end': field.preferences?.align === 'right', ...fontClasses }"
> >
<div <div
v-for="(char, index) in modelValue" v-for="(char, index) in modelValue"
@@ -184,20 +184,29 @@
ref="textContainer" ref="textContainer"
dir="auto" dir="auto"
class="flex items-center px-0.5 w-full" class="flex items-center px-0.5 w-full"
:class="alignClasses[field.preferences?.align]" :class="{ ...alignClasses, ...fontClasses }"
> >
<span <span
v-if="field && field.name && withFieldPlaceholder && !modelValue && modelValue !== 0" v-if="field && field.name && withFieldPlaceholder && !modelValue && modelValue !== 0"
class="whitespace-pre-wrap text-gray-400" class="whitespace-pre-wrap text-gray-400"
:class="{ 'w-full': field.preferences?.align }" :class="{ 'w-full': field.preferences?.align }"
>{{ field.name }}</span> >{{ field.name }}</span>
<span v-else-if="Array.isArray(modelValue)"> <span
v-else-if="Array.isArray(modelValue)"
:class="{ 'w-full': field.preferences?.align }"
>
{{ modelValue.join(', ') }} {{ modelValue.join(', ') }}
</span> </span>
<span v-else-if="field.type === 'date'"> <span
v-else-if="field.type === 'date'"
:class="{ 'w-full': field.preferences?.align }"
>
{{ formattedDate }} {{ formattedDate }}
</span> </span>
<span v-else-if="field.type === 'number'"> <span
v-else-if="field.type === 'number'"
class="w-full"
>
{{ formatNumber(modelValue, field.preferences?.format) }} {{ formatNumber(modelValue, field.preferences?.format) }}
</span> </span>
<span <span
@@ -210,7 +219,7 @@
</template> </template>
<script> <script>
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconCheck, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp, IconSquareNumber1 } from '@tabler/icons-vue' import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconCheck, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp, IconSquareNumber1, IconId } from '@tabler/icons-vue'
export default { export default {
name: 'FieldArea', name: 'FieldArea',
@@ -312,14 +321,31 @@ export default {
cells: this.t('cells'), cells: this.t('cells'),
stamp: this.t('stamp'), stamp: this.t('stamp'),
payment: this.t('payment'), payment: this.t('payment'),
phone: this.t('phone') phone: this.t('phone'),
verification: this.t('verify_id')
} }
}, },
alignClasses () { alignClasses () {
if (!this.field.preferences) {
return {}
}
return { return {
center: 'text-center', 'text-center': this.field.preferences.align === 'center',
left: 'text-left', 'text-left': this.field.preferences.align === 'left',
right: 'text-right' 'text-right': this.field.preferences.align === 'right'
}
},
fontClasses () {
if (!this.field.preferences) {
return {}
}
return {
'font-mono': this.field.preferences.font === 'Courier',
'font-serif': this.field.preferences.font === 'Times',
'font-bold': ['bold_italic', 'bold'].includes(this.field.preferences.font_type),
italic: ['bold_italic', 'italic'].includes(this.field.preferences.font_type)
} }
}, },
option () { option () {
@@ -341,7 +367,8 @@ export default {
cells: IconColumns3, cells: IconColumns3,
multiple: IconChecks, multiple: IconChecks,
phone: IconPhoneCheck, phone: IconPhoneCheck,
payment: IconCreditCard payment: IconCreditCard,
verification: IconId
} }
}, },
image () { image () {
@@ -405,8 +432,12 @@ export default {
} }
if (this.field.preferences?.font_size) { if (this.field.preferences?.font_size) {
style.fontSize = `clamp(4pt, 1.6vw, ${this.field.preferences.font_size}pt)` style.fontSize = `clamp(4pt, 1.6vw, ${parseInt(this.field.preferences.font_size) * 1.23}pt)`
style.lineHeight = `clamp(6pt, 2.0vw, ${parseInt(this.field.preferences.font_size) + 3}pt)` style.lineHeight = `clamp(6pt, 2.0vw, ${parseInt(this.field.preferences.font_size) * 1.23 + 3}pt)`
}
if (this.field.preferences?.color) {
style.color = this.field.preferences.color
} }
return style return style
@@ -441,8 +472,18 @@ export default {
} }
}, },
formatNumber (number, format) { formatNumber (number, format) {
if (!number && number !== 0) {
return ''
}
if (format === 'comma') { if (format === 'comma') {
return new Intl.NumberFormat('en-US').format(number) return new Intl.NumberFormat('en-US').format(number)
} else if (format === 'usd') {
return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(number)
} else if (format === 'gbp') {
return new Intl.NumberFormat('en-GB', { style: 'currency', currency: 'GBP', minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(number)
} else if (format === 'eur') {
return new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR', minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(number)
} else if (format === 'dot') { } else if (format === 'dot') {
return new Intl.NumberFormat('de-DE').format(number) return new Intl.NumberFormat('de-DE').format(number)
} else if (format === 'space') { } else if (format === 'space') {
+19 -7
View File
@@ -31,7 +31,7 @@
:with-label="withLabel && !withFieldPlaceholder && step.length < 2" :with-label="withLabel && !withFieldPlaceholder && step.length < 2"
:is-value-set="step.some((f) => f.uuid in values)" :is-value-set="step.some((f) => f.uuid in values)"
:attachments-index="attachmentsIndex" :attachments-index="attachmentsIndex"
@click="$emit('focus-step', stepIndex)" @click="[$emit('focus-step', stepIndex), maybeScrollOnClick(field, area)]"
/> />
</Teleport> </Teleport>
</template> </template>
@@ -109,6 +109,14 @@ export default {
areaRefs: [] areaRefs: []
} }
}, },
computed: {
isMobileContainer () {
const root = this.$root.$el.parentNode.getRootNode()
const container = root.body || root.querySelector('div')
return container.style.overflow === 'hidden'
}
},
beforeUpdate () { beforeUpdate () {
this.areaRefs = [] this.areaRefs = []
}, },
@@ -121,14 +129,16 @@ export default {
this.scrollIntoArea(field.areas[0]) this.scrollIntoArea(field.areas[0])
} }
}, },
maybeScrollOnClick (field, area) {
if (['text', 'number', 'cells'].includes(field.type) && this.isMobileContainer) {
this.scrollIntoArea(area)
}
},
scrollIntoArea (area) { scrollIntoArea (area) {
const areaRef = this.areaRefs.find((a) => a.area === area) const areaRef = this.areaRefs.find((a) => a.area === area)
if (areaRef) { if (areaRef) {
const root = this.$root.$el.parentNode.getRootNode() if (this.isMobileContainer) {
const container = root.body || root.querySelector('div')
if (container.style.overflow === 'hidden') {
this.scrollInContainer(areaRef.$el) this.scrollInContainer(areaRef.$el)
} else { } else {
const targetRect = areaRef.$refs.scrollToElem.getBoundingClientRect() const targetRect = areaRef.$refs.scrollToElem.getBoundingClientRect()
@@ -157,13 +167,15 @@ export default {
const formContainer = root.getElementById('form_container') const formContainer = root.getElementById('form_container')
const container = root.body || root.querySelector('div') const container = root.body || root.querySelector('div')
const padding = 64 const isAndroid = /android/i.test(navigator.userAgent)
const padding = isAndroid ? 128 : 64
const scrollboxTop = isAndroid ? scrollbox.getBoundingClientRect().top : 0
const boxRect = scrollbox.children[0].getBoundingClientRect() const boxRect = scrollbox.children[0].getBoundingClientRect()
const targetRect = target.getBoundingClientRect() const targetRect = target.getBoundingClientRect()
const targetTopRelativeToBox = targetRect.top - boxRect.top const targetTopRelativeToBox = targetRect.top - boxRect.top
scrollbox.scrollTo({ top: targetTopRelativeToBox - container.offsetHeight + formContainer.offsetHeight + target.offsetHeight + padding, behavior: 'smooth' }) scrollbox.scrollTo({ top: targetTopRelativeToBox + scrollboxTop - container.offsetHeight + formContainer.offsetHeight + target.offsetHeight + padding, behavior: 'smooth' })
}, },
setAreaRef (el) { setAreaRef (el) {
if (el) { if (el) {
@@ -13,7 +13,7 @@
> >
<a <a
v-if="val" v-if="val"
class="flex items-center space-x-1.5 w-full" class="flex items-center space-x-1.5 w-full attachment-file-name"
:href="attachmentsIndex[val].url" :href="attachmentsIndex[val].url"
target="_blank" target="_blank"
> >
@@ -26,7 +26,10 @@
{{ attachmentsIndex[val].filename }} {{ attachmentsIndex[val].filename }}
</span> </span>
</a> </a>
<button @click.prevent="removeAttachment(val)"> <button
class="remove-attachment-button"
@click.prevent="removeAttachment(val)"
>
<IconTrashX <IconTrashX
:width="18" :width="18"
:heigh="19" :heigh="19"
@@ -44,12 +47,12 @@
<div <div
v-if="field.description && !modelValue.length" v-if="field.description && !modelValue.length"
dir="auto" dir="auto"
class="mb-3 px-1" class="mb-3 px-1 field-description-text"
> >
<MarkdownContent :string="field.description" /> <MarkdownContent :string="field.description" />
</div> </div>
<FileDropzone <FileDropzone
:message="`${t('upload')} ${field.name || t('files')}${field.required ? '' : ` (${t('optional')})`}`" :message="`${t('upload')} ${(field.title || field.name) || t('files')}${field.required ? '' : ` (${t('optional')})`}`"
:submitter-slug="submitterSlug" :submitter-slug="submitterSlug"
:multiple="true" :multiple="true"
:dry-run="dryRun" :dry-run="dryRun"
+9 -8
View File
@@ -1,7 +1,7 @@
<template> <template>
<div <div
id="form_completed" 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" dir="auto"
> >
<div class="font-medium text-2xl flex items-center space-x-1.5 mx-auto"> <div class="font-medium text-2xl flex items-center space-x-1.5 mx-auto">
@@ -10,13 +10,13 @@
:width="30" :width="30"
:height="30" :height="30"
/> />
<span> <span class="completed-form-message-title">
{{ completedMessage.title || (hasSignatureFields ? (hasMultipleDocuments ? t('documents_have_been_signed') : t('document_has_been_signed')) : t('form_has_been_completed')) }} {{ completedMessage.title || (hasSignatureFields ? (hasMultipleDocuments ? t('documents_have_been_signed') : t('document_has_been_signed')) : t('form_has_been_completed')) }}
</span> </span>
</div> </div>
<div <div
v-if="completedMessage.body" v-if="completedMessage.body"
class="mt-2" class="mt-2 completed-form-message-body"
> >
<MarkdownContent <MarkdownContent
:string="completedMessage.body" :string="completedMessage.body"
@@ -27,7 +27,7 @@
v-if="completedButton.url" v-if="completedButton.url"
:href="sanitizeHref(completedButton.url)" :href="sanitizeHref(completedButton.url)"
rel="noopener noreferrer nofollow" rel="noopener noreferrer nofollow"
class="white-button flex items-center w-full" class="white-button flex items-center w-full completed-form-completed-button"
> >
<span> <span>
{{ completedButton.title || 'Back to Website' }} {{ completedButton.title || 'Back to Website' }}
@@ -35,7 +35,7 @@
</a> </a>
<button <button
v-if="canSendEmail && !isDemo && withSendCopyButton" v-if="canSendEmail && !isDemo && withSendCopyButton"
class="white-button !h-auto flex items-center space-x-1 w-full" class="white-button !h-auto flex items-center space-x-1 w-full completed-form-send-copy-button"
:disabled="isSendingCopy" :disabled="isSendingCopy"
@click.prevent="sendCopyToEmail" @click.prevent="sendCopyToEmail"
> >
@@ -50,7 +50,7 @@
</button> </button>
<button <button
v-if="!isWebView && withDownloadButton" v-if="!isWebView && withDownloadButton"
class="base-button flex items-center space-x-1 w-full" class="base-button flex items-center space-x-1 w-full completed-form-download-button"
:disabled="isDownloading" :disabled="isDownloading"
@click.prevent="download" @click.prevent="download"
> >
@@ -91,7 +91,7 @@
> >
{{ t('powered_by') }} {{ t('powered_by') }}
<a <a
href="https://www.docuseal.co/start" href="https://www.docuseal.com/start"
target="_blank" target="_blank"
class="underline" class="underline"
>DocuSeal</a> - {{ t('open_source_documents_software') }} >DocuSeal</a> - {{ t('open_source_documents_software') }}
@@ -215,7 +215,8 @@ export default {
fetch(this.baseUrl + `/submitters/${this.submitterSlug}/download`).then(async (response) => { fetch(this.baseUrl + `/submitters/${this.submitterSlug}/download`).then(async (response) => {
if (response.ok) { if (response.ok) {
const urls = await response.json() const urls = await response.json()
const isSafariIos = /iPhone|iPad|iPod/i.test(navigator.userAgent) const isMobileSafariIos = 'ontouchstart' in window && navigator.maxTouchPoints > 0 && /AppleWebKit/i.test(navigator.userAgent)
const isSafariIos = isMobileSafariIos || /iPhone|iPad|iPod/i.test(navigator.userAgent)
if (isSafariIos && urls.length > 1) { if (isSafariIos && urls.length > 1) {
this.downloadSafariIos(urls) this.downloadSafariIos(urls)
@@ -1,4 +1,4 @@
function cropCanvasAndExportToPNG (canvas, { errorOnTooSmall } = { errorOnTooSmall: false }) { function cropCanvasAndExportToPNG (canvas) {
const ctx = canvas.getContext('2d') const ctx = canvas.getContext('2d')
const width = canvas.width const width = canvas.width
@@ -33,10 +33,6 @@ function cropCanvasAndExportToPNG (canvas, { errorOnTooSmall } = { errorOnTooSma
croppedCanvas.height = croppedHeight croppedCanvas.height = croppedHeight
const croppedCtx = croppedCanvas.getContext('2d') const croppedCtx = croppedCanvas.getContext('2d')
if (errorOnTooSmall && (croppedWidth < 20 || croppedHeight < 20)) {
return Promise.reject(new Error('Image too small'))
}
croppedCtx.drawImage(canvas, leftmost, topmost, croppedWidth, croppedHeight, 0, 0, croppedWidth, croppedHeight) croppedCtx.drawImage(canvas, leftmost, topmost, croppedWidth, croppedHeight, 0, 0, croppedWidth, croppedHeight)
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
+14 -7
View File
@@ -1,26 +1,32 @@
<template> <template>
<div dir="auto"> <div dir="auto">
<div <div
class="flex justify-between items-center w-full" class="flex justify-between items-end w-full mb-3.5 sm:mb-4"
:class="{ 'mb-2': !field.description }" :class="{ 'mb-2': !field.description }"
> >
<label <label
v-if="showFieldNames"
:for="field.uuid" :for="field.uuid"
class="label text-2xl" class="label text-xl sm:text-2xl py-0 field-name-label"
> >
<MarkdownContent <MarkdownContent
v-if="field.title" v-if="field.title"
:string="field.title" :string="field.title"
/> />
<template v-else> <template v-else>
{{ field.name && showFieldNames ? field.name : t('date') }} {{ field.name || t('date') }}
<template v-if="!field.required"> </template>
<template v-if="!field.required">
<span
class="ml-1"
:class="{ 'hidden sm:inline': (field.title || field.name || t('date') ).length > 10 }"
>
({{ t('optional') }}) ({{ t('optional') }})
</template> </span>
</template> </template>
</label> </label>
<button <button
class="btn btn-outline btn-sm !normal-case font-normal" class="btn btn-outline btn-sm !normal-case font-normal set-current-date-button"
@click.prevent="[setCurrentDate(), $emit('focus')]" @click.prevent="[setCurrentDate(), $emit('focus')]"
> >
<IconCalendarCheck :width="16" /> <IconCalendarCheck :width="16" />
@@ -29,7 +35,7 @@
</div> </div>
<div <div
v-if="field.description" v-if="field.description"
class="mb-3 px-1" class="mb-3 px-1 field-description-text"
dir="auto" dir="auto"
> >
<MarkdownContent :string="field.description" /> <MarkdownContent :string="field.description" />
@@ -37,6 +43,7 @@
<AppearsOn :field="field" /> <AppearsOn :field="field" />
<div class="text-center"> <div class="text-center">
<input <input
:id="field.uuid"
ref="input" ref="input"
v-model="value" v-model="value"
class="base-input !text-2xl text-center w-full" class="base-input !text-2xl text-center w-full"
+3 -2
View File
@@ -1,12 +1,13 @@
<template> <template>
<div <div
id="dropzone"
class="flex h-32 w-full" class="flex h-32 w-full"
@dragover.prevent @dragover.prevent
@drop.prevent="onDropFiles" @drop.prevent="onDropFiles"
> >
<label <label
:for="inputId" :for="inputId"
class="w-full relative bg-base-300 hover:bg-base-200 rounded-md border border-base-content border-dashed" class="w-full relative bg-base-300 hover:bg-base-200 rounded-md border border-base-content border-dashed file-dropzone"
:class="{ 'opacity-50': isLoading }" :class="{ 'opacity-50': isLoading }"
> >
<div class="absolute top-0 right-0 left-0 bottom-0 flex items-center justify-center"> <div class="absolute top-0 right-0 left-0 bottom-0 flex items-center justify-center">
@@ -127,7 +128,7 @@ export default {
} }
}) })
} else { } else {
if (file.type === 'image/bmp') { if (file.type === 'image/bmp' || file.type === 'image/vnd.microsoft.icon') {
file = await this.convertBmpToPng(file) file = await this.convertBmpToPng(file)
} }
+301 -66
View File
@@ -11,23 +11,63 @@
:with-label="!isAnonymousChecboxes && showFieldNames" :with-label="!isAnonymousChecboxes && showFieldNames"
:current-step="currentStepFields" :current-step="currentStepFields"
:scroll-padding="scrollPadding" :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 <FieldAreas
:steps="readonlyConditionalFields.map((e) => [e])" :steps="readonlyConditionalFields.map((e) => [e])"
:values="readonlyConditionalFields.reduce((acc, f) => { acc[f.uuid] = f.default_value; return acc }, {})" :values="readonlyConditionalFieldValues"
:submitter="submitter" :submitter="submitter"
:attachments-index="attachmentsIndex"
:submittable="false" :submittable="false"
/> />
<FormulaFieldAreas <FormulaFieldAreas
v-if="formulaFields.length" v-if="formulaFields.length"
:fields="formulaFields" :fields="formulaFields"
:readonly-values="readonlyConditionalFieldValues"
:values="values" :values="values"
/> />
<Teleport
v-if="completeButtonToRef"
:to="completeButtonToRef"
>
<span
v-if="(emptyValueRequiredStep && ((stepFields.length - 1) !== currentStep || currentStepFields !== emptyValueRequiredStep)) || isCompleted"
class="tooltip-left"
:class="{ tooltip: !isCompleted }"
:data-tip="t('fill_all_required_fields_to_complete')"
>
<button
class="btn btn-sm btn-neutral text-white px-4 w-full flex justify-center btn-disabled pointer-events-auto"
@click="[isFormVisible = true, !isCompleted && goToStep(stepFields.indexOf(emptyValueRequiredStep), true, false)]"
>
{{ t('complete') }}
</button>
</span>
<button
v-else
id="complete_form_button"
class="btn btn-sm btn-neutral text-white px-4 w-full flex justify-center"
form="steps_form"
type="submit"
name="completed"
value="true"
:disabled="isSubmittingComplete"
>
<span class="flex items-center">
<IconInnerShadowTop
v-if="isSubmittingComplete"
class="mr-1 animate-spin w-5 h-5"
/>
<span>
{{ t('complete') }}
</span>
</span>
</button>
</Teleport>
<button <button
v-if="!isFormVisible" v-if="!isFormVisible"
id="expand_form_button" id="expand_form_button"
class="btn btn-neutral flex text-white absolute bottom-0 w-full mb-3" class="btn btn-neutral flex text-white absolute bottom-0 w-full mb-3 expand-form-button"
style="width: 96%; margin-left: 2%" style="width: 96%; margin-left: 2%"
@click.prevent="[isFormVisible = true, scrollIntoField(currentField)]" @click.prevent="[isFormVisible = true, scrollIntoField(currentField)]"
> >
@@ -53,14 +93,15 @@
<div <div
v-show="isFormVisible" v-show="isFormVisible"
id="form_container" id="form_container"
class="shadow-md bg-base-100 absolute bottom-0 w-full border-base-200 border p-4 rounded" class="shadow-md bg-base-100 absolute bottom-0 w-full border-base-200 border p-4 rounded form-container"
:class="{ 'md:bottom-4': isBreakpointMd }" :class="{ 'md:bottom-4': isBreakpointMd }"
:style="{ backgroundColor: backgroundColor }" :style="{ backgroundColor: backgroundColor }"
> >
<button <button
v-if="!isCompleted" v-if="!isCompleted"
id="minimize_form_button" id="minimize_form_button"
class="absolute right-0 mr-2 mt-2 top-0 hidden md:block" class="absolute right-0 top-0 minimize-form-button"
:class="currentField?.description?.length > 100 ? 'mr-1 mt-1 md:mr-2 md:mt-2': 'mr-2 mt-2 hidden md:block'"
:title="t('minimize')" :title="t('minimize')"
@click.prevent="minimizeForm" @click.prevent="minimizeForm"
> >
@@ -74,10 +115,11 @@
> >
<form <form
v-if="!isCompleted && !isInvite" v-if="!isCompleted && !isInvite"
id="steps_form"
ref="form" ref="form"
:action="submitPath" :action="submitPath"
method="post" method="post"
class="mx-auto" class="mx-auto steps-form"
:style="{ maxWidth: isBreakpointMd ? '582px' : '' }" :style="{ maxWidth: isBreakpointMd ? '582px' : '' }"
@submit.prevent="submitStep" @submit.prevent="submitStep"
> >
@@ -123,7 +165,7 @@
v-if="showFieldNames && (currentField.name || currentField.title)" v-if="showFieldNames && (currentField.name || currentField.title)"
:for="currentField.uuid" :for="currentField.uuid"
dir="auto" dir="auto"
class="label text-2xl" class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5 field-name-label"
:class="{ 'mb-2': !currentField.description }" :class="{ 'mb-2': !currentField.description }"
> >
<MarkdownContent <MarkdownContent
@@ -132,7 +174,11 @@
/> />
<template v-else> <template v-else>
{{ currentField.name }} {{ currentField.name }}
<template v-if="!currentField.required">({{ t('optional') }})</template> </template>
<template v-if="!currentField.required">
<span :class="{ 'hidden sm:inline': (currentField.title || currentField.name).length > 20 }">
({{ t('optional') }})
</span>
</template> </template>
</label> </label>
<div <div
@@ -142,7 +188,7 @@
<div <div
v-if="currentField.description" v-if="currentField.description"
dir="auto" dir="auto"
class="mb-3 px-1" class="mb-3 px-1 field-description-text"
> >
<MarkdownContent :string="currentField.description" /> <MarkdownContent :string="currentField.description" />
</div> </div>
@@ -180,7 +226,7 @@
v-if="showFieldNames && (currentField.name || currentField.title)" v-if="showFieldNames && (currentField.name || currentField.title)"
:for="currentField.uuid" :for="currentField.uuid"
dir="auto" dir="auto"
class="label text-2xl" class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5 field-name-label"
:class="{ 'mb-2': !currentField.description }" :class="{ 'mb-2': !currentField.description }"
> >
<MarkdownContent <MarkdownContent
@@ -189,13 +235,17 @@
/> />
<template v-else> <template v-else>
{{ currentField.name }} {{ currentField.name }}
<template v-if="!currentField.required">({{ t('optional') }})</template> </template>
<template v-if="!currentField.required">
<span :class="{ 'hidden sm:inline': (currentField.title || currentField.name).length > 20 }">
({{ t('optional') }})
</span>
</template> </template>
</label> </label>
<div <div
v-if="currentField.description" v-if="currentField.description"
dir="auto" dir="auto"
class="mb-3 px-1" class="mb-3 px-1 field-description-text"
> >
<MarkdownContent :string="currentField.description" /> <MarkdownContent :string="currentField.description" />
</div> </div>
@@ -220,7 +270,7 @@
> >
<label <label
:for="option.uuid" :for="option.uuid"
class="flex items-center space-x-3" class="flex items-center space-x-3 radio-label"
> >
<input <input
:id="option.uuid" :id="option.uuid"
@@ -254,7 +304,7 @@
<div <div
v-if="currentField.description" v-if="currentField.description"
dir="auto" dir="auto"
class="mb-3 px-1" class="mb-3 px-1 field-description-text"
> >
<MarkdownContent :string="currentField.description" /> <MarkdownContent :string="currentField.description" />
</div> </div>
@@ -288,7 +338,7 @@
> >
<label <label
:for="field.uuid" :for="field.uuid"
class="flex items-center space-x-3" class="flex items-center space-x-3 checkbox-label"
> >
<input <input
type="hidden" type="hidden"
@@ -409,16 +459,29 @@
@focus="scrollIntoField(currentField)" @focus="scrollIntoField(currentField)"
@submit="!isSubmitting && submitStep()" @submit="!isSubmitting && submitStep()"
/> />
<VerificationStep
v-else-if="currentField.type === 'verification'"
ref="currentStep"
:key="currentField.uuid"
:locale="language?.toLowerCase() || browserLanguage"
:submitter="submitter"
:empty-value-required-step="emptyValueRequiredStep"
:field="currentField"
:submitter-slug="submitterSlug"
:values="values"
@focus="scrollIntoField(currentField)"
@submit="!isSubmitting && submitStep()"
/>
</div> </div>
<div <div
v-if="currentField.type !== 'payment' || submittedValues[currentField.uuid]" v-if="(currentField.type !== 'payment' && currentField.type !== 'verification') || submittedValues[currentField.uuid]"
:class="currentField.type === 'signature' ? 'mt-2' : 'mt-6 md:mt-8'" :class="currentField.type === 'signature' ? 'mt-2' : 'mt-4 md:mt-6'"
> >
<button <button
id="submit_form_button" id="submit_form_button"
ref="submitButton" ref="submitButton"
type="submit" type="submit"
class="base-button w-full flex justify-center" class="base-button w-full flex justify-center submit-form-button"
:disabled="isButtonDisabled" :disabled="isButtonDisabled"
> >
<span class="flex"> <span class="flex">
@@ -445,6 +508,7 @@
<InviteForm <InviteForm
v-else-if="isInvite" v-else-if="isInvite"
:submitters="inviteSubmitters" :submitters="inviteSubmitters"
:optional-submitters="optionalInviteSubmitters"
:submitter-slug="submitterSlug" :submitter-slug="submitterSlug"
:authenticity-token="authenticityToken" :authenticity-token="authenticityToken"
:url="baseUrl + submitPath + '/invite'" :url="baseUrl + submitPath + '/invite'"
@@ -467,15 +531,15 @@
/> />
<div <div
v-if="stepFields.length < 80" v-if="stepFields.length < 80"
class="flex justify-center" class="flex justify-center mt-3 sm:mt-4 mb-0 sm:mb-1"
> >
<div class="flex items-center mt-4 mb-1 flex-wrap"> <div class="flex items-center flex-wrap steps-progress">
<a <a
v-for="(step, index) in stepFields" v-for="(step, index) in stepFields"
:key="step[0].uuid" :key="step[0].uuid"
href="#" href="#"
class="inline border border-base-300 h-3 w-3 rounded-full mx-1 mt-1" class="inline border border-base-300 h-3 w-3 rounded-full mx-1 mt-1"
:class="{ 'bg-base-300': index === currentStep, 'bg-base-content': (index < currentStep && stepFields[index].every((f) => !f.required || ![null, undefined, ''].includes(values[f.uuid]))) || isCompleted, 'bg-white': index > currentStep }" :class="{ 'bg-base-300 steps-progress-current': index === currentStep, 'bg-base-content': (index < currentStep && stepFields[index].every((f) => !f.required || ![null, undefined, ''].includes(values[f.uuid]))) || isCompleted, 'bg-white': index > currentStep }"
@click.prevent="isCompleted ? '' : [saveStep(), goToStep(index, true)]" @click.prevent="isCompleted ? '' : [saveStep(), goToStep(index, true)]"
/> />
</div> </div>
@@ -498,6 +562,7 @@ import AttachmentStep from './attachment_step'
import MultiSelectStep from './multi_select_step' import MultiSelectStep from './multi_select_step'
import PhoneStep from './phone_step' import PhoneStep from './phone_step'
import PaymentStep from './payment_step' import PaymentStep from './payment_step'
import VerificationStep from './verification_step'
import TextStep from './text_step' import TextStep from './text_step'
import NumberStep from './number_step' import NumberStep from './number_step'
import DateStep from './date_step' import DateStep from './date_step'
@@ -540,6 +605,7 @@ export default {
IconWritingSign, IconWritingSign,
AttachmentStep, AttachmentStep,
InitialsStep, InitialsStep,
VerificationStep,
InviteForm, InviteForm,
MultiSelectStep, MultiSelectStep,
IconInnerShadowTop, IconInnerShadowTop,
@@ -572,6 +638,11 @@ export default {
required: false, required: false,
default: () => [] default: () => []
}, },
optionalInviteSubmitters: {
type: Array,
required: false,
default: () => []
},
withSignatureId: { withSignatureId: {
type: Boolean, type: Boolean,
required: false, required: false,
@@ -582,6 +653,11 @@ export default {
required: false, required: false,
default: '-80px' default: '-80px'
}, },
orderAsOnPage: {
type: Boolean,
required: false,
default: false
},
requireSigningReason: { requireSigningReason: {
type: Boolean, type: Boolean,
required: false, required: false,
@@ -592,6 +668,16 @@ export default {
required: false, required: false,
default: false default: false
}, },
completeButtonToRef: {
type: Object,
required: false,
default: null
},
schema: {
type: Array,
required: false,
default: () => []
},
attachments: { attachments: {
type: Array, type: Array,
required: false, required: false,
@@ -772,13 +858,45 @@ export default {
phoneVerifiedValues: {}, phoneVerifiedValues: {},
orientation: screen?.orientation?.type, orientation: screen?.orientation?.type,
isSubmitting: false, isSubmitting: false,
isSubmittingComplete: false,
submittedValues: {}, submittedValues: {},
recalculateButtonDisabledKey: '' recalculateButtonDisabledKey: ''
} }
}, },
computed: { computed: {
isMobile () { isMobile () {
return /android|iphone|ipad/i.test(navigator.userAgent) const isMobileSafariIos = 'ontouchstart' in window && navigator.maxTouchPoints > 0 && /AppleWebKit/i.test(navigator.userAgent)
return isMobileSafariIos || /android|iphone|ipad/i.test(navigator.userAgent)
},
readonlyConditionalFieldValues () {
return this.readonlyConditionalFields.reduce((acc, f) => {
acc[f.uuid] = (this.values[f.uuid] || f.default_value)
return acc
}, {})
},
attachmentConditionsIndex () {
return this.schema.reduce((acc, item) => {
if (item.conditions?.length) {
if (item.conditions.every((c) => this.fieldsUuidIndex[c.field_uuid])) {
acc[item.attachment_uuid] = this.checkFieldConditions(item)
} else {
acc[item.attachment_uuid] = true
}
} else {
acc[item.attachment_uuid] = true
}
return acc
}, {})
},
emptyValueRequiredStep () {
return this.stepFields.find((fields, index) => {
return fields.some((f) => {
return f.required && isEmpty(this.values[f.uuid])
})
})
}, },
submitButtonText () { submitButtonText () {
if (this.alwaysMinimize) { if (this.alwaysMinimize) {
@@ -853,13 +971,61 @@ export default {
return this.currentStepFields[0] return this.currentStepFields[0]
}, },
readonlyConditionalFields () { readonlyConditionalFields () {
return this.fields.filter((f) => f.readonly && f.conditions?.length && this.checkFieldConditions(f)) return this.fields.filter((f) => f.readonly && f.conditions?.length && this.checkFieldConditions(f) && this.checkFieldDocumentsConditions(f))
}, },
stepFields () { stepFields () {
return this.fields.filter((f) => !f.readonly).reduce((acc, f) => { const verificationFields = []
const sortedFields = this.fields.reduce((acc, f) => {
if (f.type === 'verification') {
verificationFields.push(f)
} else if (!f.readonly) {
acc.push(f)
}
return acc
}, [])
if (this.orderAsOnPage) {
const fieldAreasIndex = {}
const attachmentUuids = Object.keys(this.attachmentConditionsIndex)
const sortArea = (aArea, bArea) => {
if (aArea.attachment_uuid === bArea.attachment_uuid) {
if (aArea.page === bArea.page) {
if (Math.abs(aArea.y - bArea.y) < 0.01) {
if (aArea.x === bArea.x) {
return 0
} else {
return aArea.x - bArea.x
}
} else {
return aArea.y - bArea.y
}
} else {
return aArea.page - bArea.page
}
} else {
return attachmentUuids.indexOf(aArea.attachment_uuid) - attachmentUuids.indexOf(bArea.attachment_uuid)
}
}
sortedFields.sort((aField, bField) => {
const aArea = (fieldAreasIndex[aField.uuid] ||= [...(aField.areas || [])].sort(sortArea)[0])
const bArea = (fieldAreasIndex[bField.uuid] ||= [...(bField.areas || [])].sort(sortArea)[0])
return sortArea(aArea, bArea)
})
}
if (verificationFields.length) {
sortedFields.push(verificationFields.pop())
}
return sortedFields.reduce((acc, f) => {
const prevStep = acc[acc.length - 1] const prevStep = acc[acc.length - 1]
if (this.checkFieldConditions(f)) { if (this.checkFieldConditions(f) && this.checkFieldDocumentsConditions(f)) {
if (f.type === 'checkbox' && Array.isArray(prevStep) && prevStep[0].type === 'checkbox' && !f.description) { if (f.type === 'checkbox' && Array.isArray(prevStep) && prevStep[0].type === 'checkbox' && !f.description) {
prevStep.push(f) prevStep.push(f)
} else { } else {
@@ -871,7 +1037,7 @@ export default {
}, []) }, [])
}, },
formulaFields () { formulaFields () {
return this.fields.filter((f) => f.preferences?.formula && f.type !== 'payment') return this.fields.filter((f) => f.preferences?.formula && f.type !== 'payment' && this.checkFieldConditions(f) && this.checkFieldDocumentsConditions(f))
}, },
attachmentsIndex () { attachmentsIndex () {
return this.attachments.reduce((acc, a) => { return this.attachments.reduce((acc, a) => {
@@ -892,6 +1058,23 @@ export default {
if (isEmpty(value) && this.currentStep > 0) { if (isEmpty(value) && this.currentStep > 0) {
this.currentStep -= 1 this.currentStep -= 1
} }
},
attachmentConditionsIndex: {
deep: true,
immediate: true,
handler (value) {
this.$nextTick(() => {
const root = this.$root.$el.parentNode.getRootNode()
for (const key in value) {
const doc = root.querySelector(`[id="document-${key}"`)
if (doc) {
doc.classList.toggle('hidden', !value[key])
}
}
})
}
} }
}, },
beforeUnmount () { beforeUnmount () {
@@ -939,7 +1122,9 @@ export default {
this.minimizeForm() this.minimizeForm()
} }
if (/iPhone|iPad|iPod/i.test(navigator.userAgent)) { const isMobile = 'ontouchstart' in window && navigator.maxTouchPoints > 0 && /AppleWebKit|android/i.test(navigator.userAgent)
if (isMobile || /iPhone|iPad|iPod/i.test(navigator.userAgent)) {
this.$nextTick(() => { this.$nextTick(() => {
const root = this.$root.$el.parentNode.getRootNode() const root = this.$root.$el.parentNode.getRootNode()
const scrollbox = root.getElementById('scrollbox') const scrollbox = root.getElementById('scrollbox')
@@ -973,33 +1158,59 @@ export default {
onOrientationChange (event) { onOrientationChange (event) {
this.orientation = event.target.type this.orientation = event.target.type
}, },
checkFieldDocumentsConditions (field) {
if (field.areas?.length) {
return field.areas.some((area) => {
return this.attachmentConditionsIndex[area.attachment_uuid]
})
} else {
return true
}
},
checkFieldConditions (field) { checkFieldConditions (field) {
if (field.conditions?.length) { if (field.conditions?.length) {
return field.conditions.reduce((acc, c) => { const result = field.conditions.reduce((acc, cond) => {
const field = this.fieldsUuidIndex[c.field_uuid] if (cond.operation === 'or') {
acc.push(acc.pop() || this.checkFieldCondition(cond))
if (['not_empty', 'checked', 'equal', 'contains'].includes(c.action) && field && !this.checkFieldConditions(field)) {
return false
}
if (['empty', 'unchecked'].includes(c.action)) {
return acc && isEmpty(this.values[c.field_uuid])
} else if (['not_empty', 'checked'].includes(c.action)) {
return acc && !isEmpty(this.values[c.field_uuid])
} else if (['equal', 'contains'].includes(c.action) && field) {
const option = field.options.find((o) => o.uuid === c.value)
const values = [this.values[c.field_uuid]].flat()
return acc && values.includes(this.optionValue(option, field.options.indexOf(option)))
} else if (['not_equal', 'does_not_contain'].includes(c.action) && field) {
const option = field.options.find((o) => o.uuid === c.value)
const values = [this.values[c.field_uuid]].flat()
return acc && !values.includes(this.optionValue(option, field.options.indexOf(option)))
} else { } else {
return acc acc.push(this.checkFieldCondition(cond))
} }
}, true)
return acc
}, [])
return !result.includes(false)
} else {
return true
}
},
checkFieldCondition (condition) {
const field = this.fieldsUuidIndex[condition.field_uuid]
if (['not_empty', 'checked', 'equal', 'contains'].includes(condition.action) && field && !this.checkFieldConditions(field)) {
return false
}
const defaultValue = !field || isEmpty(field.default_value) ? null : field.default_value
if (['empty', 'unchecked'].includes(condition.action)) {
return isEmpty(this.values[condition.field_uuid] ?? defaultValue)
} else if (['not_empty', 'checked'].includes(condition.action)) {
return !isEmpty(this.values[condition.field_uuid] ?? defaultValue)
} else if (['equal', 'contains'].includes(condition.action) && field) {
if (field.options) {
const option = field.options.find((o) => o.uuid === condition.value)
const values = [this.values[condition.field_uuid] ?? defaultValue].flat()
return values.includes(this.optionValue(option, field.options.indexOf(option)))
} else {
return [this.values[condition.field_uuid] ?? defaultValue].flat().includes(condition.value)
}
} else if (['not_equal', 'does_not_contain'].includes(condition.action) && field) {
const option = field.options.find((o) => o.uuid === condition.value)
const values = [this.values[condition.field_uuid] ?? defaultValue].flat()
return !values.includes(this.optionValue(option, field.options.indexOf(option)))
} else { } else {
return true return true
} }
@@ -1090,7 +1301,9 @@ export default {
if (!this.isCompleted) { if (!this.isCompleted) {
if (scrollToArea) { if (scrollToArea) {
this.scrollIntoField(this.currentField) this.$nextTick(() => {
setTimeout(() => this.scrollIntoField(this.currentField), 1)
})
} }
this.enableScrollIntoField = false this.enableScrollIntoField = false
@@ -1107,7 +1320,7 @@ export default {
const currentFieldUuids = this.currentStepFields.map((f) => f.uuid) const currentFieldUuids = this.currentStepFields.map((f) => f.uuid)
const currentFieldType = this.currentField.type const currentFieldType = this.currentField.type
if (!formData && !this.$refs.form.checkValidity()) { if (!formData && !this.$refs.form.checkValidity() && currentFieldUuids.every((fieldUuid) => isEmpty(this.submittedValues[fieldUuid]) || !isEmpty(this.values[fieldUuid]))) {
return return
} }
@@ -1146,18 +1359,24 @@ export default {
scrollIntoArea (area) { scrollIntoArea (area) {
return this.$refs.areas.scrollIntoArea(area) return this.$refs.areas.scrollIntoArea(area)
}, },
async submitStep () { async submitStep (e) {
this.isSubmitting = true this.isSubmitting = true
const forceComplete = e?.submitter?.getAttribute('name') === 'completed'
if (forceComplete) {
this.isSubmittingComplete = true
}
const submitStep = this.currentStep 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 ? this.$refs.currentStep.submit
: () => Promise.resolve({}) : () => Promise.resolve({})
stepPromise().then(async () => { stepPromise().then(async () => {
const emptyRequiredField = this.stepFields.find((fields, index) => { const emptyRequiredField = this.stepFields.find((fields, index) => {
if (index >= submitStep) { if (forceComplete ? index === submitStep : index >= submitStep) {
return false return false
} }
@@ -1167,9 +1386,9 @@ export default {
}) })
const formData = new FormData(this.$refs.form) const formData = new FormData(this.$refs.form)
const isLastStep = submitStep === this.stepFields.length - 1 const isLastStep = (submitStep === this.stepFields.length - 1) || forceComplete
if (isLastStep && !emptyRequiredField && !this.inviteSubmitters.length) { if (isLastStep && !emptyRequiredField && !this.inviteSubmitters.length && !this.optionalInviteSubmitters.length) {
formData.append('completed', 'true') formData.append('completed', 'true')
} }
@@ -1185,14 +1404,32 @@ export default {
if (response.status === 422 || response.status === 500) { if (response.status === 422 || response.status === 500) {
const data = await response.json() const data = await response.json()
const i18nError = data.error ? this.t(data.error.replace(/\s+/g, '_').toLowerCase()) : '' if (data.field_uuid) {
const field = this.fieldsUuidIndex[data.field_uuid]
alert(i18nError !== data.error ? i18nError : (data.error || this.t('value_is_invalid'))) if (field) {
const step = this.stepFields.findIndex((fields) => fields.includes(field))
if (step !== -1) {
this.goToStep(step, this.autoscrollFields)
this.showFillAllRequiredFields = true
}
}
return Promise.reject(new Error('Required field: ' + data.field_uuid))
} else if (data.error) {
const i18nKey = data.error.replace(/\s+/g, '_').toLowerCase()
alert(this.t(i18nKey) !== i18nKey ? this.t(i18nKey) : data.error)
} else {
alert(this.t('value_is_invalid'))
}
return Promise.reject(new Error(data.error)) return Promise.reject(new Error(data.error))
} }
const nextStep = (isLastStep && emptyRequiredField) || this.stepFields[submitStep + 1] const nextStep = (isLastStep && emptyRequiredField) || (forceComplete ? null : this.stepFields[submitStep + 1])
if (nextStep) { if (nextStep) {
if (this.alwaysMinimize) { if (this.alwaysMinimize) {
@@ -1204,7 +1441,7 @@ export default {
if (emptyRequiredField === nextStep) { if (emptyRequiredField === nextStep) {
this.showFillAllRequiredFields = true this.showFillAllRequiredFields = true
} }
} else if (this.inviteSubmitters.length) { } else if (this.inviteSubmitters.length || this.optionalInviteSubmitters.length) {
this.isInvite = true this.isInvite = true
} else { } else {
this.performComplete(response) this.performComplete(response)
@@ -1213,15 +1450,13 @@ export default {
console.error(error) console.error(error)
}).finally(() => { }).finally(() => {
this.isSubmitting = false this.isSubmitting = false
this.isSubmittingComplete = false
}) })
}).catch(error => { }).catch(error => {
if (error?.message === 'Image too small') { console.log(error)
alert(this.t('signature_is_too_small_please_redraw'))
} else {
console.log(error)
}
}).finally(() => { }).finally(() => {
this.isSubmitting = false this.isSubmitting = false
this.isSubmittingComplete = false
}) })
}, },
minimizeForm () { minimizeForm () {
@@ -38,6 +38,11 @@ export default {
required: false, required: false,
default: () => [] default: () => []
}, },
readonlyValues: {
type: Object,
required: false,
default: () => ({})
},
values: { values: {
type: Object, type: Object,
required: false, required: false,
@@ -87,7 +92,7 @@ export default {
}, },
calculateFormula (field) { calculateFormula (field) {
const transformedFormula = field.preferences.formula.replace(/{{(.*?)}}/g, (match, uuid) => { const transformedFormula = field.preferences.formula.replace(/{{(.*?)}}/g, (match, uuid) => {
return this.values[uuid] || 0.0 return this.readonlyValues[uuid] || this.values[uuid] || 0.0
}) })
return this.math.evaluate(transformedFormula.toLowerCase()) return this.math.evaluate(transformedFormula.toLowerCase())
+103 -26
View File
@@ -1,5 +1,9 @@
const en = { const en = {
complete_all_required_fields_to_proceed_with_identity_verification: 'Complete all required fields to proceed with identity verification.',
verify_id: 'Verify ID',
identity_verification: 'Identity verification',
complete: 'Complete', complete: 'Complete',
fill_all_required_fields_to_complete: 'Fill all required fields to complete',
sign_and_complete: 'Sign and Complete', sign_and_complete: 'Sign and Complete',
text: 'Text', text: 'Text',
by_clicking_you_agree_to_the: 'By clicking "{button}", you agree to the', by_clicking_you_agree_to_the: 'By clicking "{button}", you agree to the',
@@ -66,7 +70,7 @@ const en = {
documents_have_been_signed: 'Documents have been signed!', documents_have_been_signed: 'Documents have been signed!',
create_a_free_account: 'Create a Free Account', create_a_free_account: 'Create a Free Account',
powered_by: 'Powered by', powered_by: 'Powered by',
please_check_the_box_to_continue: 'Please check the box to continue', please_check_the_box_to_continue: 'Please check the box to continue.',
open_source_documents_software: 'open source documents software', open_source_documents_software: 'open source documents software',
verified_phone_number: 'Verify Phone Number', verified_phone_number: 'Verify Phone Number',
use_international_format: 'Use international format: +1xxx', use_international_format: 'Use international format: +1xxx',
@@ -88,11 +92,17 @@ const en = {
pay_with_strip: 'Pay with Stripe', pay_with_strip: 'Pay with Stripe',
reupload: 'Reupload', reupload: 'Reupload',
upload: 'Upload', upload: 'Upload',
files: 'Files' files: 'Files',
signature_is_too_small_or_simple_please_redraw: 'Signature is too small or simple. Please redraw.',
wait_countdown_seconds: 'Wait {countdown} seconds'
} }
const es = { const es = {
complete_all_required_fields_to_proceed_with_identity_verification: 'Complete todos los campos requeridos para continuar con la verificación de identidad.',
verify_id: 'Verificar ID',
identity_verification: 'Verificación de identidad',
complete: 'Completar', complete: 'Completar',
fill_all_required_fields_to_complete: 'Complete todos los campos requeridos para finalizar',
sign_and_complete: 'Firmar y Completar', sign_and_complete: 'Firmar y Completar',
invite: 'Invitar', invite: 'Invitar',
email: 'Correo electrónico', email: 'Correo electrónico',
@@ -158,7 +168,7 @@ const es = {
documents_have_been_signed: '¡Los documentos han sido firmados!', documents_have_been_signed: '¡Los documentos han sido firmados!',
create_a_free_account: 'Crear una Cuenta Gratuita', create_a_free_account: 'Crear una Cuenta Gratuita',
powered_by: 'Desarrollado por', powered_by: 'Desarrollado por',
please_check_the_box_to_continue: 'Por favor marque la casilla para continuar', please_check_the_box_to_continue: 'Por favor marque la casilla para continuar.',
open_source_documents_software: 'software de documentos de código abierto', open_source_documents_software: 'software de documentos de código abierto',
verified_phone_number: 'Verificar número de teléfono', verified_phone_number: 'Verificar número de teléfono',
use_international_format: 'Usar formato internacional: +1xxx', use_international_format: 'Usar formato internacional: +1xxx',
@@ -180,11 +190,17 @@ const es = {
pay_with_strip: 'Pagar con Stripe', pay_with_strip: 'Pagar con Stripe',
reupload: 'Volver a subir', reupload: 'Volver a subir',
upload: 'Subir', upload: 'Subir',
files: 'Archivos' files: 'Archivos',
signature_is_too_small_or_simple_please_redraw: 'La firma es demasiado pequeña o simple. Por favor, vuelve a dibujarla.',
wait_countdown_seconds: 'Espera {countdown} segundos'
} }
const it = { const it = {
complete_all_required_fields_to_proceed_with_identity_verification: "Compila tutti i campi obbligatori per procedere con la verifica dell'identità.",
verify_id: 'Verifica ID',
identity_verification: "Verifica dell'identità",
complete: 'Completa', complete: 'Completa',
fill_all_required_fields_to_complete: 'Compila tutti i campi obbligatori per completare',
sign_and_complete: 'Firma e Completa', sign_and_complete: 'Firma e Completa',
invite: 'Invita', invite: 'Invita',
email: 'Email', email: 'Email',
@@ -250,7 +266,7 @@ const it = {
documents_have_been_signed: 'I documenti sono stati firmati!', documents_have_been_signed: 'I documenti sono stati firmati!',
create_a_free_account: 'Crea un Account Gratuito', create_a_free_account: 'Crea un Account Gratuito',
powered_by: 'Desarrollado por', powered_by: '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', open_source_documents_software: 'software di documenti open source',
verified_phone_number: 'Verifica numero di telefono', verified_phone_number: 'Verifica numero di telefono',
use_international_format: 'Usa formato internazionale: +1xxx', use_international_format: 'Usa formato internazionale: +1xxx',
@@ -272,11 +288,17 @@ const it = {
pay_with_strip: 'Paga con Stripe', pay_with_strip: 'Paga con Stripe',
reupload: 'Ricarica', reupload: 'Ricarica',
upload: 'Carica', upload: 'Carica',
files: 'File' files: 'File',
signature_is_too_small_or_simple_please_redraw: 'La firma è troppo piccola o semplice. Ridisegnala, per favore.',
wait_countdown_seconds: 'Attendi {countdown} secondi'
} }
const de = { const de = {
complete_all_required_fields_to_proceed_with_identity_verification: 'Vervollständigen Sie alle erforderlichen Felder, um mit der Identitätsverifizierung fortzufahren.',
verify_id: 'ID überprüfen',
identity_verification: 'Identitätsüberprüfung',
complete: 'Abschließen', complete: 'Abschließen',
fill_all_required_fields_to_complete: 'Alle erforderlichen Felder ausfüllen, um abzuschließen',
sign_and_complete: 'Signieren und Abschließen', sign_and_complete: 'Signieren und Abschließen',
invite: 'Einladen', invite: 'Einladen',
email: 'E-Mail', email: 'E-Mail',
@@ -342,7 +364,7 @@ const de = {
documents_have_been_signed: 'Dokumente wurden unterschrieben!', documents_have_been_signed: 'Dokumente wurden unterschrieben!',
create_a_free_account: 'Kostenloses Konto erstellen', create_a_free_account: 'Kostenloses Konto erstellen',
powered_by: 'Bereitgestellt von', powered_by: 'Bereitgestellt von',
please_check_the_box_to_continue: 'Bitte setzen Sie das Häkchen, um fortzufahren', please_check_the_box_to_continue: 'Bitte setzen Sie das Häkchen, um fortzufahren.',
open_source_documents_software: 'Open-Source-Dokumentensoftware', open_source_documents_software: 'Open-Source-Dokumentensoftware',
verified_phone_number: 'Telefonnummer überprüfen', verified_phone_number: 'Telefonnummer überprüfen',
use_international_format: 'Internationales Format verwenden: +1xxx', use_international_format: 'Internationales Format verwenden: +1xxx',
@@ -364,11 +386,17 @@ const de = {
pay_with_strip: 'Mit Stripe bezahlen', pay_with_strip: 'Mit Stripe bezahlen',
reupload: 'Erneut hochladen', reupload: 'Erneut hochladen',
upload: 'Hochladen', upload: 'Hochladen',
files: 'Dateien' files: 'Dateien',
signature_is_too_small_or_simple_please_redraw: 'Die Unterschrift ist zu klein oder zu einfach. Bitte erneut zeichnen.',
wait_countdown_seconds: 'Warte {countdown} Sekunden'
} }
const fr = { const fr = {
complete_all_required_fields_to_proceed_with_identity_verification: "Veuillez remplir tous les champs obligatoires pour continuer la vérification de l'identité.",
verif_id: "Vérification de l'ID",
verif_identite: "Vérification de l'identité",
complete: 'Terminer', complete: 'Terminer',
fill_all_required_fields_to_complete: 'Veuillez remplir tous les champs obligatoires pour compléter',
sign_and_complete: 'Signer et Terminer', sign_and_complete: 'Signer et Terminer',
invite: 'Inviter', invite: 'Inviter',
email: 'Courriel', email: 'Courriel',
@@ -434,7 +462,7 @@ const fr = {
documents_have_been_signed: 'Les documents ont été signés!', documents_have_been_signed: 'Les documents ont été signés!',
create_a_free_account: 'Créer un Compte Gratuit', create_a_free_account: 'Créer un Compte Gratuit',
powered_by: 'Propulsé par', powered_by: 'Propulsé par',
please_check_the_box_to_continue: 'Veuillez cocher la case pour continuer', please_check_the_box_to_continue: 'Veuillez cocher la case pour continuer.',
open_source_documents_software: 'logiciel de documents open source', open_source_documents_software: 'logiciel de documents open source',
verified_phone_number: 'Vérifier le numéro de téléphone', verified_phone_number: 'Vérifier le numéro de téléphone',
use_international_format: 'Utiliser le format international : +1xxx', use_international_format: 'Utiliser le format international : +1xxx',
@@ -456,11 +484,17 @@ const fr = {
pay_with_strip: 'Paiement avec Stripe', pay_with_strip: 'Paiement avec Stripe',
reupload: 'Recharger', reupload: 'Recharger',
upload: 'Télécharger', upload: 'Télécharger',
files: 'Fichiers' files: 'Fichiers',
signature_is_too_small_or_simple_please_redraw: 'La signature est trop petite ou trop simple. Veuillez la redessiner.',
wait_countdown_seconds: 'Attendez {countdown} secondes'
} }
const pl = { const pl = {
complete_all_required_fields_to_proceed_with_identity_verification: 'Uzupełnij wszystkie wymagane pola, aby kontynuować weryfikację tożsamości.',
verify_id: 'Zweryfikuj ID',
identity_verification: 'Weryfikacja tożsamości',
complete: 'Zakończ', complete: 'Zakończ',
fill_all_required_fields_to_complete: 'Uzupełnij wszystkie wymagane pola, aby zakończyć',
sign_and_complete: 'Podpisz i zakończ', sign_and_complete: 'Podpisz i zakończ',
invite: 'Zaproś', invite: 'Zaproś',
email: 'E-mail', email: 'E-mail',
@@ -526,7 +560,7 @@ const pl = {
documents_have_been_signed: 'Dokumenty zostały podpisane!', documents_have_been_signed: 'Dokumenty zostały podpisane!',
create_a_free_account: 'Utwórz darmowe konto', create_a_free_account: 'Utwórz darmowe konto',
powered_by: 'Napędzany przez', powered_by: 'Napędzany przez',
please_check_the_box_to_continue: 'Proszę zaznaczyć pole, aby kontynuować', please_check_the_box_to_continue: 'Proszę zaznaczyć pole, aby kontynuować.',
open_source_documents_software: 'oprogramowanie do dokumentów open source', open_source_documents_software: 'oprogramowanie do dokumentów open source',
verified_phone_number: 'Zweryfikuj numer telefonu', verified_phone_number: 'Zweryfikuj numer telefonu',
use_international_format: 'Użyj międzynarodowego formatu: +1xxx', use_international_format: 'Użyj międzynarodowego formatu: +1xxx',
@@ -548,11 +582,16 @@ const pl = {
pay_with_strip: 'Płatność za pomocą Stripe', pay_with_strip: 'Płatność za pomocą Stripe',
reupload: 'Ponowne przesłanie', reupload: 'Ponowne przesłanie',
upload: 'Przesyłanie', upload: 'Przesyłanie',
files: 'Pliki' files: 'Pliki',
signature_is_too_small_or_simple_please_redraw: 'Podpis jest zbyt mały lub zbyt prosty. Proszę narysować go ponownie.'
} }
const uk = { const uk = {
complete_all_required_fields_to_proceed_with_identity_verification: "Заповніть всі обов'язкові поля, щоб продовжити перевірку особи.",
verify_id: 'Підтвердження ідентичності',
identity_verification: 'Ідентифікація особи',
complete: 'Завершити', complete: 'Завершити',
fill_all_required_fields_to_complete: "Заповніть всі обов'язкові поля для завершення",
sign_and_complete: 'Підписати і завершити', sign_and_complete: 'Підписати і завершити',
invite: 'Запросити', invite: 'Запросити',
email: 'Електронна пошта', email: 'Електронна пошта',
@@ -618,7 +657,7 @@ const uk = {
documents_have_been_signed: 'Документи були підписані!', documents_have_been_signed: 'Документи були підписані!',
create_a_free_account: 'Створити безкоштовний обліковий запис', create_a_free_account: 'Створити безкоштовний обліковий запис',
powered_by: 'Працює на базі', powered_by: 'Працює на базі',
please_check_the_box_to_continue: 'Будь ласка, позначте прапорець, щоб продовжити', please_check_the_box_to_continue: 'Будь ласка, позначте прапорець, щоб продовжити.',
open_source_documents_software: 'відкритий програмний засіб для документів', open_source_documents_software: 'відкритий програмний засіб для документів',
verified_phone_number: 'Підтвердіть номер телефону', verified_phone_number: 'Підтвердіть номер телефону',
use_international_format: 'Використовуйте міжнародний формат: +1xxx', use_international_format: 'Використовуйте міжнародний формат: +1xxx',
@@ -640,11 +679,17 @@ const uk = {
pay_with_strip: 'Сплатити за допомогою Stripe', pay_with_strip: 'Сплатити за допомогою Stripe',
reupload: 'Перезавантажити', reupload: 'Перезавантажити',
upload: 'Завантажити', upload: 'Завантажити',
files: 'Файли' files: 'Файли',
signature_is_too_small_or_simple_please_redraw: 'Підпис занадто маленький або надто простий. Будь ласка, перемалюйте.',
wait_countdown_seconds: 'Зачекайте {countdown} секунд'
} }
const cs = { const cs = {
complete_all_required_fields_to_proceed_with_identity_verification: 'Vyplňte všechna povinná pole, abyste mohli pokračovat v ověření identity.',
verify_id: 'Ověřit ID',
identity_verification: 'Ověření identity',
complete: 'Dokončit', complete: 'Dokončit',
fill_all_required_fields_to_complete: 'Please complete all mandatory fields',
sign_and_complete: 'Podepsat a dokončit', sign_and_complete: 'Podepsat a dokončit',
invite: 'Pozvat', invite: 'Pozvat',
email: 'E-mail', email: 'E-mail',
@@ -710,7 +755,7 @@ const cs = {
documents_have_been_signed: 'Dokumenty byly podepsány!', documents_have_been_signed: 'Dokumenty byly podepsány!',
create_a_free_account: 'Vytvořit bezplatný účet', create_a_free_account: 'Vytvořit bezplatný účet',
powered_by: 'Poháněno', powered_by: 'Poháněno',
please_check_the_box_to_continue: 'Prosím, zaškrtněte políčko pro pokračování', please_check_the_box_to_continue: 'Prosím, zaškrtněte políčko pro pokračování.',
open_source_documents_software: 'open source software pro dokumenty', open_source_documents_software: 'open source software pro dokumenty',
verified_phone_number: 'Ověřte telefonní číslo', verified_phone_number: 'Ověřte telefonní číslo',
use_international_format: 'Použijte mezinárodní formát: +1xxx', use_international_format: 'Použijte mezinárodní formát: +1xxx',
@@ -732,11 +777,17 @@ const cs = {
pay_with_strip: 'Zaplacení přes Stripe', pay_with_strip: 'Zaplacení přes Stripe',
reupload: 'Znovu nahrát', reupload: 'Znovu nahrát',
upload: 'Nahrát', upload: 'Nahrát',
files: 'Soubory' files: 'Soubory',
signature_is_too_small_or_simple_please_redraw: 'Podpis je příliš malý nebo jednoduchý. Nakreslete jej prosím znovu.',
wait_countdown_seconds: 'Počkejte {countdown} sekund'
} }
const pt = { const pt = {
complete_all_required_fields_to_proceed_with_identity_verification: 'Preencha todos os campos obrigatórios para prosseguir com a verificação de identidade.',
verify_id: 'Verificar ID',
identity_verification: 'Verificação de identidade',
complete: 'Completar', complete: 'Completar',
fill_all_required_fields_to_complete: 'Preencher todos os campos obrigatórios para concluir',
sign_and_complete: 'Assinar e Completar', sign_and_complete: 'Assinar e Completar',
invite: 'Convidar', invite: 'Convidar',
email: 'E-mail', email: 'E-mail',
@@ -802,7 +853,7 @@ const pt = {
documents_have_been_signed: 'Os documentos foram assinados!', documents_have_been_signed: 'Os documentos foram assinados!',
create_a_free_account: 'Criar uma Conta Gratuita', create_a_free_account: 'Criar uma Conta Gratuita',
powered_by: 'Desenvolvido por', powered_by: 'Desenvolvido por',
please_check_the_box_to_continue: 'Por favor, marque a caixa para continuar', please_check_the_box_to_continue: 'Por favor, marque a caixa para continuar.',
open_source_documents_software: 'software de documentos de código aberto', open_source_documents_software: 'software de documentos de código aberto',
verified_phone_number: 'Verificar Número de Telefone', verified_phone_number: 'Verificar Número de Telefone',
use_international_format: 'Use formato internacional: +1xxx', use_international_format: 'Use formato internacional: +1xxx',
@@ -824,11 +875,17 @@ const pt = {
pay_with_strip: 'Pagar com Stripe', pay_with_strip: 'Pagar com Stripe',
reupload: 'Reenviar', reupload: 'Reenviar',
upload: 'Carregar', upload: 'Carregar',
files: 'Arquivos' files: 'Arquivos',
signature_is_too_small_or_simple_please_redraw: 'A assinatura é muito pequena ou simples. Por favor, redesenhe.',
wait_countdown_seconds: 'Aguarde {countdown} segundos'
} }
const he = { const he = {
complete_all_required_fields_to_proceed_with_identity_verification: 'מלא את כל השדות הנדרשים כדי להמשיך עם אימות זהות.',
verify_id: 'אמת מזהה',
identity_verification: 'אימות זהות',
complete: 'השלם', complete: 'השלם',
fill_all_required_fields_to_complete: 'נא למלא את כל השדות הנדרשים להשלמה',
sign_and_complete: 'חתום והשלם', sign_and_complete: 'חתום והשלם',
invite: 'הזמן', invite: 'הזמן',
email: 'דוא"ל', email: 'דוא"ל',
@@ -895,7 +952,7 @@ const he = {
documents_have_been_signed: 'המסמכים נחתמו!', documents_have_been_signed: 'המסמכים נחתמו!',
create_a_free_account: 'צור חשבון חינם', create_a_free_account: 'צור חשבון חינם',
powered_by: 'מופעל על ידי', powered_by: 'מופעל על ידי',
please_check_the_box_to_continue: 'אנא סמן את התיבה כדי להמשיך', please_check_the_box_to_continue: 'אנא סמן את התיבה כדי להמשיך.',
open_source_documents_software: 'תוכנה פתוחה למסמכים', open_source_documents_software: 'תוכנה פתוחה למסמכים',
verified_phone_number: 'אימות מספר טלפון', verified_phone_number: 'אימות מספר טלפון',
use_international_format: 'השתמש בפורמט בינלאומי: +1xxx', use_international_format: 'השתמש בפורמט בינלאומי: +1xxx',
@@ -917,11 +974,17 @@ const he = {
pay_with_strip: 'שלם עם סטרייפ', pay_with_strip: 'שלם עם סטרייפ',
reupload: 'העלה שוב', reupload: 'העלה שוב',
upload: 'העלאה', upload: 'העלאה',
files: 'קבצים' files: 'קבצים',
signature_is_too_small_or_simple_please_redraw: 'החתימה קטנה או פשוטה מדי. אנא חתום מחדש.',
wait_countdown_seconds: 'המתן {countdown} שניות'
} }
const nl = { const nl = {
complete_all_required_fields_to_proceed_with_identity_verification: 'Vul alle verplichte velden in om door te gaan met de identiteitsverificatie.',
verify_id: 'Verifiëren ID',
identity_verification: 'Identiteitsverificatie',
complete: 'Voltooien', complete: 'Voltooien',
fill_all_required_fields_to_complete: 'Vul alle verplichte velden in om te voltooien',
sign_and_complete: 'Ondertekenen en voltooien', sign_and_complete: 'Ondertekenen en voltooien',
invite: 'Uitnodigen', invite: 'Uitnodigen',
email: 'E-mail', email: 'E-mail',
@@ -988,7 +1051,7 @@ const nl = {
documents_have_been_signed: 'De documenten zijn ondertekend!', documents_have_been_signed: 'De documenten zijn ondertekend!',
create_a_free_account: 'Maak een gratis account aan', create_a_free_account: 'Maak een gratis account aan',
powered_by: 'Aangedreven door', powered_by: 'Aangedreven door',
please_check_the_box_to_continue: 'Vink het vakje aan om door te gaan', please_check_the_box_to_continue: 'Vink het vakje aan om door te gaan.',
open_source_documents_software: 'Open source documenten software', open_source_documents_software: 'Open source documenten software',
verified_phone_number: 'Geverifieerd telefoonnummer', verified_phone_number: 'Geverifieerd telefoonnummer',
use_international_format: 'Gebruik internationaal formaat: +1xxx', use_international_format: 'Gebruik internationaal formaat: +1xxx',
@@ -1010,11 +1073,17 @@ const nl = {
pay_with_strip: 'Betalen met Stripe', pay_with_strip: 'Betalen met Stripe',
reupload: 'Opnieuw uploaden', reupload: 'Opnieuw uploaden',
upload: 'Uploaden', upload: 'Uploaden',
files: 'Bestanden' files: 'Bestanden',
signature_is_too_small_or_simple_please_redraw: 'De handtekening is te klein of te eenvoudig. Teken opnieuw.',
wait_countdown_seconds: 'Wacht {countdown} seconden'
} }
const ar = { const ar = {
complete_all_required_fields_to_proceed_with_identity_verification: 'أكمل جميع الحقول المطلوبة للمتابعة في التحقق من الهوية.',
verify_id: 'تحقق من الهوية',
identity_verification: 'التحقق من الهوية',
complete: 'اكتمال', complete: 'اكتمال',
fill_all_required_fields_to_complete: 'يرجى ملء جميع الحقول المطلوبة لإكمال',
sign_and_complete: 'التوقيع والاكتمال', sign_and_complete: 'التوقيع والاكتمال',
invite: 'دعوة', invite: 'دعوة',
email: 'البريد الإلكتروني', email: 'البريد الإلكتروني',
@@ -1080,7 +1149,7 @@ const ar = {
documents_have_been_signed: 'تم توقيع الوثائق!', documents_have_been_signed: 'تم توقيع الوثائق!',
create_a_free_account: 'إنشاء حساب مجاني', create_a_free_account: 'إنشاء حساب مجاني',
powered_by: 'مدعوم من', powered_by: 'مدعوم من',
please_check_the_box_to_continue: 'الرجاء التحقق من الخانة للمتابعة', please_check_the_box_to_continue: 'الرجاء التحقق من الخانة للمتابعة.',
open_source_documents_software: 'برنامج وثائق مفتوح المصدر', open_source_documents_software: 'برنامج وثائق مفتوح المصدر',
verified_phone_number: 'تحقق من رقم الهاتف', verified_phone_number: 'تحقق من رقم الهاتف',
use_international_format: 'استخدم الشكل الدولي: +1xxx', use_international_format: 'استخدم الشكل الدولي: +1xxx',
@@ -1102,11 +1171,17 @@ const ar = {
pay_with_strip: 'الدفع بواسطة Stripe', pay_with_strip: 'الدفع بواسطة Stripe',
reupload: 'إعادة التحميل', reupload: 'إعادة التحميل',
upload: 'تحميل', upload: 'تحميل',
files: 'الملفات' files: 'الملفات',
signature_is_too_small_or_simple_please_redraw: 'التوقيع صغير جدًا أو بسيط جدًا. يرجى إعادة رسمه.',
wait_countdown_seconds: 'انتظر {countdown} ثانية'
} }
const ko = { const ko = {
complete_all_required_fields_to_proceed_with_identity_verification: '신원 확인을 진행하려면 모든 필수 필드를 작성하십시오.',
verify_id: '아이디 확인',
identity_verification: '신원 확인',
complete: '완료', complete: '완료',
fill_all_required_fields_to_complete: '모든 필수 필드를 작성하여 완료하세요',
sign_and_complete: '서명하고 완료하기', sign_and_complete: '서명하고 완료하기',
invite: '초대하기', invite: '초대하기',
email: '이메일', email: '이메일',
@@ -1171,7 +1246,7 @@ const ko = {
documents_have_been_signed: '문서가 서명되었습니다!', documents_have_been_signed: '문서가 서명되었습니다!',
create_a_free_account: '무료 계정 생성', create_a_free_account: '무료 계정 생성',
powered_by: '구동', powered_by: '구동',
please_check_the_box_to_continue: '계속하려면 확인란을 선택하십시오', please_check_the_box_to_continue: '계속하려면 확인란을 선택하십시오.',
open_source_documents_software: '오픈 소스 문서 소프트웨어', open_source_documents_software: '오픈 소스 문서 소프트웨어',
verified_phone_number: '전화번호 확인됨', verified_phone_number: '전화번호 확인됨',
use_international_format: '국제 포맷 사용: +1xxx', use_international_format: '국제 포맷 사용: +1xxx',
@@ -1193,7 +1268,9 @@ const ko = {
pay_with_strip: '스트라이프로 결제', pay_with_strip: '스트라이프로 결제',
reupload: '다시 업로드', reupload: '다시 업로드',
upload: '업로드', upload: '업로드',
files: '파일' files: '파일',
signature_is_too_small_or_simple_please_redraw: '서명이 너무 작거나 단순합니다. 다시 그려주세요.',
wait_countdown_seconds: '{countdown}초 기다리세요'
} }
const i18n = { en, es, it, de, fr, pl, uk, cs, pt, he, nl, ar, ko } const i18n = { en, es, it, de, fr, pl, uk, cs, pt, he, nl, ar, ko }
+17 -7
View File
@@ -1,11 +1,21 @@
<template> <template>
<div v-if="modelValue"> <div v-if="modelValue">
<div class="flex justify-between items-center w-full mb-2"> <div class="flex justify-between items-end w-full mb-3.5 md:mb-4">
<label <label
class="label text-2xl" v-if="showFieldNames"
>{{ showFieldNames && field.name ? field.name : t('image') }}</label> :for="field.uuid"
class="label text-xl sm:text-2xl py-0 field-name-label"
>
<MarkdownContent
v-if="field.title"
:string="field.title"
/>
<template v-else>
{{ field.name || t('image') }}
</template>
</label>
<button <button
class="btn btn-outline btn-sm" class="btn btn-outline btn-sm reupload-button"
@click.prevent="remove" @click.prevent="remove"
> >
<IconReload :width="16" /> <IconReload :width="16" />
@@ -15,7 +25,7 @@
<div> <div>
<img <img
:src="attachmentsIndex[modelValue].url" :src="attachmentsIndex[modelValue].url"
class="h-52 border border-base-300 rounded mx-auto" class="h-52 border border-base-300 rounded mx-auto uploaded-image-preview"
> >
</div> </div>
<input <input
@@ -30,12 +40,12 @@
<div <div
v-if="field.description" v-if="field.description"
dir="auto" dir="auto"
class="mb-3 px-1" class="mb-3 px-1 field-description-text"
> >
<MarkdownContent :string="field.description" /> <MarkdownContent :string="field.description" />
</div> </div>
<FileDropzone <FileDropzone
:message="`${t('upload')} ${field.name || t('image')}${field.required ? '' : ` (${t('optional')})`}`" :message="`${t('upload')} ${(field.title || field.name) || t('image')}${field.required ? '' : ` (${t('optional')})`}`"
:submitter-slug="submitterSlug" :submitter-slug="submitterSlug"
:dry-run="dryRun" :dry-run="dryRun"
:accept="'image/*'" :accept="'image/*'"
@@ -1,21 +1,22 @@
<template> <template>
<div dir="auto"> <div dir="auto">
<div <div
class="flex justify-between items-center w-full" class="flex justify-between items-end w-full mb-3.5 md:mb-4"
:class="{ 'mb-2': !field.description }" :class="{ 'mb-2': !field.description }"
> >
<label <label
class="label text-2xl" v-if="showFieldNames"
class="label text-xl sm:text-2xl py-0 field-name-label"
> >
<MarkdownContent <MarkdownContent
v-if="field.title" v-if="field.title"
:string="field.title" :string="field.title"
/> />
<template v-else> <template v-else>
{{ showFieldNames && field.name ? field.name : t('initials') }} {{ field.name || t('initials') }}
</template> </template>
</label> </label>
<div class="space-x-2 flex"> <div class="space-x-2 flex flex-none">
<span <span
v-if="isDrawInitials" v-if="isDrawInitials"
class="tooltip" class="tooltip"
@@ -24,7 +25,7 @@
<a <a
id="type_text_button" id="type_text_button"
href="#" href="#"
class="btn btn-outline font-medium btn-sm" class="btn btn-outline font-medium btn-sm type-text-button"
@click.prevent="toggleTextInput" @click.prevent="toggleTextInput"
> >
<IconTextSize :width="16" /> <IconTextSize :width="16" />
@@ -41,7 +42,7 @@
<a <a
id="type_text_button" id="type_text_button"
href="#" href="#"
class="btn btn-outline font-medium btn-sm" class="btn btn-outline font-medium btn-sm type-text-button"
@click.prevent="toggleTextInput" @click.prevent="toggleTextInput"
> >
<IconSignature :width="16" /> <IconSignature :width="16" />
@@ -54,9 +55,7 @@
class="tooltip" class="tooltip"
:data-tip="t('click_to_upload')" :data-tip="t('click_to_upload')"
> >
<label <label class="btn btn-outline btn-sm font-medium inline-flex flex-nowrap upload-image-button">
class="btn btn-outline btn-sm font-medium inline-flex flex-nowrap"
>
<IconUpload :width="16" /> <IconUpload :width="16" />
<input <input
:key="uploadImageInputKey" :key="uploadImageInputKey"
@@ -73,7 +72,7 @@
<a <a
v-if="modelValue || computedPreviousValue" v-if="modelValue || computedPreviousValue"
href="#" href="#"
class="btn font-medium btn-outline btn-sm" class="btn font-medium btn-outline btn-sm clear-canvas-button"
@click.prevent="remove" @click.prevent="remove"
> >
<IconReload :width="16" /> <IconReload :width="16" />
@@ -82,7 +81,7 @@
<a <a
v-else v-else
href="#" href="#"
class="btn font-medium btn-outline btn-sm" class="btn font-medium btn-outline btn-sm clear-canvas-button"
@click.prevent="clear" @click.prevent="clear"
> >
<IconReload :width="16" /> <IconReload :width="16" />
@@ -104,7 +103,7 @@
<div <div
v-if="field.description" v-if="field.description"
dir="auto" dir="auto"
class="mb-3 px-1" class="mb-3 px-1 field-description-text"
> >
<MarkdownContent :string="field.description" /> <MarkdownContent :string="field.description" />
</div> </div>
@@ -119,11 +118,17 @@
:src="attachmentsIndex[modelValue || computedPreviousValue].url" :src="attachmentsIndex[modelValue || computedPreviousValue].url"
class="mx-auto bg-white border border-base-300 rounded max-h-44" class="mx-auto bg-white border border-base-300 rounded max-h-44"
> >
<canvas <div class="relative">
v-show="!modelValue && !computedPreviousValue" <div
ref="canvas" v-if="!isDrawInitials"
class="bg-white border border-base-300 rounded-2xl w-full" class="absolute top-0 right-0 left-0 bottom-0"
/> />
<canvas
v-show="!modelValue && !computedPreviousValue"
ref="canvas"
class="bg-white border border-base-300 rounded-2xl w-full draw-canvas"
/>
</div>
<input <input
v-if="!isDrawInitials && !modelValue && !computedPreviousValue" v-if="!isDrawInitials && !modelValue && !computedPreviousValue"
id="initials_text_input" id="initials_text_input"
@@ -226,6 +231,10 @@ export default {
if (this.$refs.canvas) { if (this.$refs.canvas) {
this.pad = new SignaturePad(this.$refs.canvas) this.pad = new SignaturePad(this.$refs.canvas)
if (this.field.preferences?.color) {
this.pad.penColor = this.field.preferences.color
}
this.pad.addEventListener('beginStroke', () => { this.pad.addEventListener('beginStroke', () => {
this.isInitialsStarted = true this.isInitialsStarted = true
+10 -5
View File
@@ -12,7 +12,7 @@
:value="authenticityToken" :value="authenticityToken"
> >
<div <div
v-for="(submitter, index) in submitters" v-for="(submitter, index) in [...submitters, ...optionalSubmitters]"
:key="submitter.uuid" :key="submitter.uuid"
:class="{ 'mt-4': index !== 0 }" :class="{ 'mt-4': index !== 0 }"
> >
@@ -26,7 +26,7 @@
dir="auto" dir="auto"
class="label text-2xl" class="label text-2xl"
> >
{{ t('invite') }} {{ submitter.name }} {{ t('invite') }} {{ submitter.name }} <template v-if="!submitters.includes(submitter)">({{ t('optional') }})</template>
</label> </label>
<input <input
:id="submitter.uuid" :id="submitter.uuid"
@@ -34,13 +34,13 @@
class="base-input !text-2xl w-full" class="base-input !text-2xl w-full"
:placeholder="t('email')" :placeholder="t('email')"
type="email" type="email"
required :required="submitters.includes(submitter)"
autofocus="true" autofocus="true"
name="submission[submitters][][email]" name="submission[submitters][][email]"
> >
</div> </div>
<div <div
class="mt-6 md:mt-8" class="mt-4 md:mt-6"
> >
<button <button
type="submit" type="submit"
@@ -53,7 +53,7 @@
class="mr-1 animate-spin" class="mr-1 animate-spin"
/> />
<span> <span>
{{ t('submit') }} {{ t('complete') }}
</span><span </span><span
v-if="isSubmitting" v-if="isSubmitting"
class="w-6 flex justify-start mr-1" class="w-6 flex justify-start mr-1"
@@ -78,6 +78,11 @@ export default {
type: Array, type: Array,
required: true required: true
}, },
optionalSubmitters: {
type: Array,
required: false,
default: () => []
},
url: { url: {
type: String, type: String,
required: true required: true
@@ -3,18 +3,24 @@
v-if="showFieldNames && (field.name || field.title)" v-if="showFieldNames && (field.name || field.title)"
:for="field.uuid" :for="field.uuid"
dir="auto" dir="auto"
class="label text-2xl" class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5 field-name-label"
:class="{ 'mb-2': !field.description }" :class="{ 'mb-2': !field.description }"
><MarkdownContent >
v-if="field.title" <MarkdownContent
:string="field.title" v-if="field.title"
/> :string="field.title"
/>
<template v-else>{{ field.name }}</template> <template v-else>{{ field.name }}</template>
<template v-if="!field.required">
<span :class="{ 'hidden sm:inline': (field.title || field.name).length > 20 }">
({{ t('optional') }})
</span>
</template>
</label> </label>
<div <div
v-if="field.description" v-if="field.description"
dir="auto" dir="auto"
class="mb-3 px-1" class="mb-3 px-1 field-description-text"
> >
<MarkdownContent :string="field.description" /> <MarkdownContent :string="field.description" />
</div> </div>
@@ -44,7 +50,7 @@
> >
<label <label
:for="option.uuid" :for="option.uuid"
class="flex items-center space-x-3" class="flex items-center space-x-3 checkbox-label"
@click="scrollIntoField(field)" @click="scrollIntoField(field)"
> >
<input <input
+10 -7
View File
@@ -3,15 +3,18 @@
v-if="showFieldNames && (field.name || field.title)" v-if="showFieldNames && (field.name || field.title)"
:for="field.uuid" :for="field.uuid"
dir="auto" dir="auto"
class="label text-2xl" class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5 field-name-label"
:class="{ 'mb-2': !field.description }" :class="{ 'mb-2': !field.description }"
><MarkdownContent >
v-if="field.title" <MarkdownContent
:string="field.title" v-if="field.title"
/> :string="field.title"
/>
<template v-else>{{ field.name }}</template> <template v-else>{{ field.name }}</template>
<template v-if="!field.required"> <template v-if="!field.required">
({{ t('optional') }}) <span :class="{ 'hidden sm:inline': (field.title || field.name).length > 20 }">
({{ t('optional') }})
</span>
</template> </template>
</label> </label>
<div <div
@@ -21,7 +24,7 @@
<div <div
v-if="field.description" v-if="field.description"
dir="auto" dir="auto"
class="mb-3 px-1" class="mb-3 px-1 field-description-text"
> >
<MarkdownContent :string="field.description" /> <MarkdownContent :string="field.description" />
</div> </div>
@@ -1,8 +1,7 @@
<template> <template>
<label <label
v-if="!modelValue && !sessionId" v-if="!modelValue && !sessionId"
:for="field.uuid" class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5 field-name-label"
class="label text-2xl mb-2"
> >
<MarkdownContent <MarkdownContent
v-if="field.title" v-if="field.title"
@@ -35,7 +34,7 @@
<button <button
v-if="sessionId" v-if="sessionId"
disabled disabled
class="base-button w-full" class="base-button w-full modal-save-button"
> >
<IconLoader <IconLoader
width="22" width="22"
File diff suppressed because it is too large. Load diff
+60 -21
View File
@@ -1,8 +1,9 @@
<template> <template>
<div> <div>
<label <label
v-if="showFieldNames"
:for="isCodeSent ? 'one_time_code' : field.uuid" :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 }" :class="{ 'mb-2': !field.description }"
> >
<MarkdownContent <MarkdownContent
@@ -10,16 +11,13 @@
:string="field.title" :string="field.title"
/> />
<template v-else> <template v-else>
{{ showFieldNames && field.name ? field.name : t('verified_phone_number') }} {{ field.name || t('verified_phone_number') }}
<template v-if="!field.required">
({{ t('optional') }})
</template>
</template> </template>
</label> </label>
<div <div
v-if="field.description" v-if="field.description"
dir="auto" dir="auto"
class="mb-3 px-1" class="mb-3 px-1 field-description-text"
> >
<MarkdownContent :string="field.description" /> <MarkdownContent :string="field.description" />
</div> </div>
@@ -47,14 +45,21 @@
<a <a
v-if="!defaultValue" v-if="!defaultValue"
href="#" href="#"
class="link" class="link change-phone-number-link"
@click.prevent="isCodeSent = false" @click.prevent="isCodeSent = false"
> >
{{ t('change_phone_number') }} {{ t('change_phone_number') }}
</a> </a>
<a <span
href="#" v-if="resendCodeCountdown > 0"
class="link" class="link"
>
{{ t('wait_countdown_seconds').replace('{countdown}', resendCodeCountdown) }}
</span>
<a
v-else
href="#"
class="link resend-code-link"
@click.prevent="resendCode" @click.prevent="resendCode"
> >
{{ isResendLoading ? t('sending') : t('resend_code') }} {{ isResendLoading ? t('sending') : t('resend_code') }}
@@ -63,13 +68,17 @@
</div> </div>
<div <div
v-show="!isCodeSent" 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
<div class="btn bg-base-200 border border-neutral-300 text-2xl whitespace-nowrap font-normal rounded-l-full"> 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 }} {{ selectedCountry.flag }} +{{ selectedCountry.dial }}
</div> </div>
<select <select
id="country_code_select"
class="absolute top-0 bottom-0 right-0 left-0 opacity-0 w-full h-full cursor-pointer" class="absolute top-0 bottom-0 right-0 left-0 opacity-0 w-full h-full cursor-pointer"
:disabled="!!defaultValue" :disabled="!!defaultValue"
@change="onCountrySelect(countries.find((country) => country.flag === $event.target.value))" @change="onCountrySelect(countries.find((country) => country.flag === $event.target.value))"
@@ -169,6 +178,8 @@ export default {
data () { data () {
return { return {
isCodeSent: false, isCodeSent: false,
codeSentAt: null,
resendCodeCountdown: 0,
isResendLoading: false, isResendLoading: false,
phoneValue: this.modelValue || this.defaultValue || '', phoneValue: this.modelValue || this.defaultValue || '',
selectedCountry: {} selectedCountry: {}
@@ -176,8 +187,8 @@ export default {
}, },
computed: { computed: {
countries () { countries () {
return phoneData.map(([name, dial, flag, tz]) => { return phoneData.map(([iso, name, dial, flag, tz]) => {
return { name, dial, flag, tz } return { iso, name, dial, flag, tz }
}) })
}, },
countriesDialIndex () { countriesDialIndex () {
@@ -216,6 +227,11 @@ export default {
this.selectedCountry = this.countries.find((country) => country.tz.includes(tz)) || this.countries[0] this.selectedCountry = this.countries.find((country) => country.tz.includes(tz)) || this.countries[0]
} }
}, },
beforeUnmount () {
if (this.interval) {
clearInterval(this.interval)
}
},
methods: { methods: {
emitSubmit: throttle(function (e) { emitSubmit: throttle(function (e) {
this.$emit('submit') this.$emit('submit')
@@ -242,13 +258,28 @@ export default {
} }
}, },
resendCode () { resendCode () {
this.isResendLoading = true if (this.codeSentAt && Date.now() - this.codeSentAt < 15000) {
this.startResendCodeCountdown()
} else {
this.isResendLoading = true
this.sendVerificationCode().finally(() => { this.sendVerificationCode().then(() => {
alert(this.t('verification_code_has_been_resent')) 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 () { sendVerificationCode () {
return fetch(this.baseUrl + '/api/send_phone_verification_code', { return fetch(this.baseUrl + '/api/send_phone_verification_code', {
@@ -260,12 +291,20 @@ export default {
}), }),
headers: { 'Content-Type': 'application/json' } headers: { 'Content-Type': 'application/json' }
}).then(async (resp) => { }).then(async (resp) => {
if (resp.status === 422) { if ([422, 429].includes(resp.status)) {
const data = await resp.json() 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)) return Promise.reject(new Error(data.error))
} else if (resp.ok) {
this.codeSentAt = Date.now()
return resp
} }
}) })
}, },
@@ -4,30 +4,31 @@
class="relative" class="relative"
> >
<div <div
class="flex justify-between items-center w-full" class="flex justify-between items-end w-full mb-3.5 md:mb-4"
:class="{ 'mb-2': !field.description }" :class="{ 'mb-2': !field.description }"
> >
<label <label
class="label text-2xl" v-if="showFieldNames"
class="label text-xl sm:text-2xl py-0 field-name-label"
> >
<MarkdownContent <MarkdownContent
v-if="field.title" v-if="field.title"
:string="field.title" :string="field.title"
/> />
<template v-else> <template v-else>
{{ showFieldNames && field.name ? field.name : t('signature') }} {{ field.name || t('signature') }}
</template> </template>
</label> </label>
<div class="space-x-2 flex"> <div class="space-x-2 flex flex-none">
<span <span
v-if="isTextSignature && field.preferences?.format !== 'typed'" v-if="isTextSignature && format !== 'typed' && format !== 'upload'"
class="tooltip" class="tooltip"
:data-tip="t('draw_signature')" :data-tip="t('draw_signature')"
> >
<a <a
id="type_text_button" id="type_text_button"
href="#" href="#"
class="btn btn-outline btn-sm font-medium" class="btn btn-outline btn-sm font-medium type-text-button"
@click.prevent="[toggleTextInput(), hideQr()]" @click.prevent="[toggleTextInput(), hideQr()]"
> >
<IconSignature :width="16" /> <IconSignature :width="16" />
@@ -37,14 +38,15 @@
</a> </a>
</span> </span>
<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="tooltip ml-2"
:class="{ 'hidden sm:inline': modelValue || computedPreviousValue }"
:data-tip="t('type_text')" :data-tip="t('type_text')"
> >
<a <a
id="type_text_button" id="type_text_button"
href="#" 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()]" @click.prevent="[toggleTextInput(), hideQr()]"
> >
<IconTextSize :width="16" /> <IconTextSize :width="16" />
@@ -54,13 +56,12 @@
</a> </a>
</span> </span>
<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="tooltip"
:class="{ 'hidden sm:inline': modelValue || computedPreviousValue }"
:data-tip="t('take_photo')" :data-tip="t('take_photo')"
> >
<label <label class="btn btn-outline btn-sm font-medium inline-flex flex-nowrap upload-image-button">
class="btn btn-outline btn-sm font-medium inline-flex flex-nowrap"
>
<IconCamera :width="16" /> <IconCamera :width="16" />
<input <input
:key="uploadImageInputKey" :key="uploadImageInputKey"
@@ -77,14 +78,14 @@
<a <a
v-if="modelValue || computedPreviousValue" v-if="modelValue || computedPreviousValue"
href="#" href="#"
class="btn btn-outline btn-sm font-medium" class="btn btn-outline btn-sm font-medium reupload-button"
@click.prevent="remove" @click.prevent="remove"
> >
<IconReload :width="16" /> <IconReload :width="16" />
{{ t('redraw') }} {{ t(format === 'upload' ? 'reupload' : 'redraw') }}
</a> </a>
<span <span
v-if="withQrButton && !modelValue && !computedPreviousValue && field.preferences?.format !== 'typed'" v-if="withQrButton && !modelValue && !computedPreviousValue && format !== 'typed' && format !== 'upload'"
class=" tooltip" class=" tooltip"
:data-tip="t('drawn_signature_on_a_touchscreen_device')" :data-tip="t('drawn_signature_on_a_touchscreen_device')"
> >
@@ -116,7 +117,7 @@
<div <div
v-if="field.description" v-if="field.description"
dir="auto" dir="auto"
class="mb-3 px-1" class="mb-3 px-1 field-description-text"
> >
<MarkdownContent :string="field.description" /> <MarkdownContent :string="field.description" />
</div> </div>
@@ -131,7 +132,18 @@
:src="attachmentsIndex[modelValue || computedPreviousValue].url" :src="attachmentsIndex[modelValue || computedPreviousValue].url"
class="mx-auto bg-white border border-base-300 rounded max-h-44" class="mx-auto bg-white border border-base-300 rounded max-h-44"
> >
<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 <div
v-if="!modelValue && !computedPreviousValue && !isShowQr && !isTextSignature && isSignatureStarted" v-if="!modelValue && !computedPreviousValue && !isShowQr && !isTextSignature && isSignatureStarted"
class="absolute top-0.5 right-0.5" class="absolute top-0.5 right-0.5"
@@ -145,11 +157,15 @@
{{ t('clear') }} {{ t('clear') }}
</a> </a>
</div> </div>
<div
v-if="isTextSignature"
class="absolute top-0 right-0 left-0 bottom-0"
/>
<canvas <canvas
v-show="!modelValue && !computedPreviousValue" v-show="!modelValue && !computedPreviousValue"
ref="canvas" ref="canvas"
style="padding: 1px; 0" 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 <div
v-if="isShowQr" v-if="isShowQr"
@@ -255,7 +271,7 @@
class="text-base-content/60 text-xs text-center w-full mt-1" class="text-base-content/60 text-xs text-center w-full mt-1"
> >
{{ t('by_clicking_you_agree_to_the').replace('{button}', buttonText.charAt(0).toUpperCase() + buttonText.slice(1)) }} <a {{ t('by_clicking_you_agree_to_the').replace('{button}', buttonText.charAt(0).toUpperCase() + buttonText.slice(1)) }} <a
href="https://www.docuseal.co/esign-disclosure" href="https://www.docuseal.com/esign-disclosure"
target="_blank" target="_blank"
> >
<span class="inline md:hidden"> <span class="inline md:hidden">
@@ -276,8 +292,10 @@
<script> <script>
import { IconReload, IconCamera, IconSignature, IconTextSize, IconArrowsDiagonalMinimize2, IconQrcode, IconX } from '@tabler/icons-vue' import { IconReload, IconCamera, IconSignature, IconTextSize, IconArrowsDiagonalMinimize2, IconQrcode, IconX } from '@tabler/icons-vue'
import { cropCanvasAndExportToPNG } from './crop_canvas' import { cropCanvasAndExportToPNG } from './crop_canvas'
import { isValidSignatureCanvas } from './validate_signature'
import SignaturePad from 'signature_pad' import SignaturePad from 'signature_pad'
import AppearsOn from './appears_on' import AppearsOn from './appears_on'
import FileDropzone from './dropzone'
import MarkdownContent from './markdown_content' import MarkdownContent from './markdown_content'
import { v4 } from 'uuid' import { v4 } from 'uuid'
@@ -290,6 +308,7 @@ export default {
components: { components: {
AppearsOn, AppearsOn,
IconReload, IconReload,
FileDropzone,
IconCamera, IconCamera,
IconQrcode, IconQrcode,
MarkdownContent, MarkdownContent,
@@ -384,6 +403,9 @@ export default {
submitterSlug () { submitterSlug () {
return this.submitter.slug return this.submitter.slug
}, },
format () {
return this.field.preferences?.format
},
defaultReasons () { defaultReasons () {
return { return {
[this.t('approved_by')]: this.t('approved'), [this.t('approved_by')]: this.t('approved'),
@@ -419,6 +441,10 @@ export default {
if (this.$refs.canvas) { if (this.$refs.canvas) {
this.pad = new SignaturePad(this.$refs.canvas) this.pad = new SignaturePad(this.$refs.canvas)
if (this.field.preferences?.color) {
this.pad.penColor = this.field.preferences.color
}
this.pad.addEventListener('endStroke', () => { this.pad.addEventListener('endStroke', () => {
this.isSignatureStarted = true this.isSignatureStarted = true
@@ -573,6 +599,7 @@ export default {
}, },
drawImage (event) { drawImage (event) {
this.remove() this.remove()
this.clear()
this.isSignatureStarted = true this.isSignatureStarted = true
this.drawOnCanvas(event.target.files[0], this.$refs.canvas) this.drawOnCanvas(event.target.files[0], this.$refs.canvas)
@@ -655,8 +682,14 @@ export default {
return Promise.resolve({}) return Promise.resolve({})
} }
return new Promise((resolve, reject) => { if (this.isSignatureStarted && this.pad.toData().length > 0 && !isValidSignatureCanvas(this.pad.toData())) {
cropCanvasAndExportToPNG(this.$refs.canvas, { errorOnTooSmall: true }).then(async (blob) => { 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' }) const file = new File([blob], 'signature.png', { type: 'image/png' })
if (this.dryRun) { if (this.dryRun) {
@@ -692,12 +725,6 @@ export default {
return resolve(attachment) 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)" v-if="showFieldNames && (field.name || field.title)"
:for="field.uuid" :for="field.uuid"
dir="auto" dir="auto"
class="label text-2xl" class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5 field-name-label"
:class="{ 'mb-2': !field.description }" :class="{ 'mb-2': !field.description }"
><MarkdownContent >
v-if="field.title" <MarkdownContent
:string="field.title" v-if="field.title"
/> :string="field.title"
/>
<template v-else>{{ field.name }}</template> <template v-else>{{ field.name }}</template>
<template v-if="!field.required"> <template v-if="!field.required">
({{ t('optional') }}) <span :class="{ 'hidden sm:inline': (field.title || field.name).length > 20 }">
({{ t('optional') }})
</span>
</template> </template>
</label> </label>
<div <div
@@ -21,7 +24,7 @@
<div <div
v-if="field.description" v-if="field.description"
dir="auto" dir="auto"
class="mb-3 px-1" class="mb-3 px-1 field-description-text"
> >
<MarkdownContent :string="field.description" /> <MarkdownContent :string="field.description" />
</div> </div>
@@ -64,7 +67,7 @@
> >
<a <a
href="#" href="#"
class="btn btn-ghost btn-circle btn-sm" class="btn btn-ghost btn-circle btn-sm toggle-multiline-text-button"
@click.prevent="toggleTextArea" @click.prevent="toggleTextArea"
> >
<IconAlignBoxLeftTop /> <IconAlignBoxLeftTop />
@@ -114,7 +117,9 @@ export default {
const area = this.field.areas?.[0] const area = this.field.areas?.[0]
if (area) { 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 { } else {
return null 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>
+100 -53
View File
@@ -1,7 +1,8 @@
<template> <template>
<div <div
class="absolute overflow-visible group" class="absolute overflow-visible group field-area-container"
:style="positionStyle" :style="positionStyle"
:class="{ 'z-[1]': isMoved || isDragged }"
@pointerdown.stop @pointerdown.stop
@mousedown="startMouseMove" @mousedown="startMouseMove"
@touchstart="startTouchDrag" @touchstart="startTouchDrag"
@@ -9,7 +10,7 @@
<div <div
v-if="isSelected || isDraw" v-if="isSelected || isDraw"
class="top-0 bottom-0 right-0 left-0 absolute border border-1.5 pointer-events-none" 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 <div
v-if="field.type === 'cells' && (isSelected || isDraw)" v-if="field.type === 'cells' && (isSelected || isDraw)"
@@ -19,7 +20,7 @@
v-for="(cellW, index) in cells" v-for="(cellW, index) in cells"
:key="index" :key="index"
class="absolute top-0 bottom-0 border-r" 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) + '%' }" :style="{ left: (cellW / area.w * 100) + '%' }"
> >
<span <span
@@ -32,7 +33,7 @@
</div> </div>
<div <div
v-if="field?.type && (isSelected || isNameFocus)" 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" style="top: -25px; height: 25px"
@mousedown.stop @mousedown.stop
@pointerdown.stop @pointerdown.stop
@@ -40,7 +41,7 @@
<FieldSubmitter <FieldSubmitter
v-if="field.type != 'heading'" v-if="field.type != 'heading'"
v-model="field.submitter_uuid" v-model="field.submitter_uuid"
class="border-r" class="border-r roles-dropdown"
:compact="true" :compact="true"
:editable="editable && (!defaultField || defaultField.role !== submitter?.name)" :editable="editable && (!defaultField || defaultField.role !== submitter?.name)"
:allow-add-new="!defaultSubmitters.length" :allow-add-new="!defaultSubmitters.length"
@@ -69,7 +70,7 @@
@keydown.enter.prevent="onNameEnter" @keydown.enter.prevent="onNameEnter"
@focus="onNameFocus" @focus="onNameFocus"
@blur="onNameBlur" @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 <div
v-if="isSettingsFocus || (isValueInput && field.type !== 'heading') || (isNameFocus && !['checkbox', 'phone'].includes(field.type))" v-if="isSettingsFocus || (isValueInput && field.type !== 'heading') || (isNameFocus && !['checkbox', 'phone'].includes(field.type))"
class="flex items-center ml-1.5" class="flex items-center ml-1.5"
@@ -107,7 +108,7 @@
>{{ t('editable') }}</label> >{{ t('editable') }}</label>
<span <span
v-if="field.type !== 'payment' && !isValueInput" v-if="field.type !== 'payment' && !isValueInput"
class="dropdown dropdown-end" class="dropdown dropdown-end field-area-settings-dropdown"
@mouseenter="renderDropdown = true" @mouseenter="renderDropdown = true"
@touchstart="renderDropdown = true" @touchstart="renderDropdown = true"
> >
@@ -140,6 +141,7 @@
:with-required="false" :with-required="false"
:with-areas="false" :with-areas="false"
@click-formula="isShowFormulaModal = true" @click-formula="isShowFormulaModal = true"
@click-font="isShowFontModal = true"
@click-description="isShowDescriptionModal = true" @click-description="isShowDescriptionModal = true"
@click-condition="isShowConditionsModal = true" @click-condition="isShowConditionsModal = true"
@scroll-to="[selectedAreaRef.value = $event, $emit('scroll-to', $event)]" @scroll-to="[selectedAreaRef.value = $event, $emit('scroll-to', $event)]"
@@ -158,9 +160,9 @@
</div> </div>
<div <div
ref="touchValueTarget" ref="touchValueTarget"
class="flex items-center h-full w-full" class="flex items-center h-full w-full field-area"
dir="auto" 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" @click="focusValueInput"
> >
<span <span
@@ -175,6 +177,7 @@
<div <div
ref="textContainer" ref="textContainer"
class="flex items-center px-0.5" class="flex items-center px-0.5"
:style="{ color: field.preferences?.color }"
:class="{ 'w-full h-full': ['cells', 'checkbox'].includes(field.type) }" :class="{ 'w-full h-full': ['cells', 'checkbox'].includes(field.type) }"
> >
<IconCheck <IconCheck
@@ -210,7 +213,7 @@
:contenteditable="isValueInput" :contenteditable="isValueInput"
class="whitespace-pre-wrap outline-none empty:before:content-[attr(placeholder)] before:text-gray-400" class="whitespace-pre-wrap outline-none empty:before:content-[attr(placeholder)] before:text-gray-400"
:class="{ 'cursor-text': isValueInput }" :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" @blur="onDefaultValueBlur"
@paste.prevent="onPaste" @paste.prevent="onPaste"
@keydown.enter="onDefaultValueEnter" @keydown.enter="onDefaultValueEnter"
@@ -236,7 +239,7 @@
/> />
<span <span
v-if="field?.type && editable" 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" @mousedown.stop="startResize"
@touchstart="startTouchResize" @touchstart="startTouchResize"
/> />
@@ -251,12 +254,23 @@
@close="isShowFormulaModal = false" @close="isShowFormulaModal = false"
/> />
</Teleport> </Teleport>
<Teleport
v-if="isShowFontModal"
:to="modalContainerEl"
>
<FontModal
:field="field"
:editable="editable && !defaultField"
:build-default-name="buildDefaultName"
@close="isShowFontModal = false"
/>
</Teleport>
<Teleport <Teleport
v-if="isShowConditionsModal" v-if="isShowConditionsModal"
:to="modalContainerEl" :to="modalContainerEl"
> >
<ConditionsModal <ConditionsModal
:field="field" :item="field"
:build-default-name="buildDefaultName" :build-default-name="buildDefaultName"
@close="isShowConditionsModal = false" @close="isShowConditionsModal = false"
/> />
@@ -281,6 +295,7 @@ import FieldType from './field_type'
import Field from './field' import Field from './field'
import FieldSettings from './field_settings' import FieldSettings from './field_settings'
import FormulaModal from './formula_modal' import FormulaModal from './formula_modal'
import FontModal from './font_modal'
import ConditionsModal from './conditions_modal' import ConditionsModal from './conditions_modal'
import DescriptionModal from './description_modal' import DescriptionModal from './description_modal'
import { IconX, IconCheck, IconDotsVertical } from '@tabler/icons-vue' import { IconX, IconCheck, IconDotsVertical } from '@tabler/icons-vue'
@@ -293,6 +308,7 @@ export default {
IconCheck, IconCheck,
FieldSettings, FieldSettings,
FormulaModal, FormulaModal,
FontModal,
IconDotsVertical, IconDotsVertical,
DescriptionModal, DescriptionModal,
ConditionsModal, ConditionsModal,
@@ -315,6 +331,11 @@ export default {
required: false, required: false,
default: false default: false
}, },
maxPage: {
type: Number,
required: false,
default: null
},
defaultField: { defaultField: {
type: Object, type: Object,
required: false, required: false,
@@ -345,6 +366,7 @@ export default {
data () { data () {
return { return {
isShowFormulaModal: false, isShowFormulaModal: false,
isShowFontModal: false,
isShowConditionsModal: false, isShowConditionsModal: false,
isContenteditable: false, isContenteditable: false,
isSettingsFocus: false, isSettingsFocus: false,
@@ -354,6 +376,7 @@ export default {
isMoved: false, isMoved: false,
renderDropdown: false, renderDropdown: false,
isNameFocus: false, isNameFocus: false,
isHeadingSelected: false,
textOverflowChars: 0, textOverflowChars: 0,
dragFrom: { x: 0, y: 0 } dragFrom: { x: 0, y: 0 }
} }
@@ -370,7 +393,7 @@ export default {
} }
}, },
isValueInput () { 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 () { modalContainerEl () {
return this.$el.getRootNode().querySelector('#docuseal_modal_container') return this.$el.getRootNode().querySelector('#docuseal_modal_container')
@@ -378,11 +401,19 @@ export default {
defaultName () { defaultName () {
return this.buildDefaultName(this.field, this.template.fields) return this.buildDefaultName(this.field, this.template.fields)
}, },
alignClasses () { fontClasses () {
if (!this.field.preferences) {
return {}
}
return { return {
center: 'justify-center', 'justify-center': this.field.preferences.align === 'center',
left: 'justify-start', 'justify-start': this.field.preferences.align === 'left',
right: 'justify-end' '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 () { optionIndexText () {
@@ -413,16 +444,6 @@ export default {
}, },
borderColors () { borderColors () {
return [ 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-red-500/80',
'border-sky-500/80', 'border-sky-500/80',
'border-emerald-500/80', 'border-emerald-500/80',
@@ -437,16 +458,6 @@ export default {
}, },
bgColors () { bgColors () {
return [ 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-red-100',
'bg-sky-100', 'bg-sky-100',
'bg-emerald-100', 'bg-emerald-100',
@@ -492,13 +503,17 @@ export default {
methods: { methods: {
buildDefaultName: Field.methods.buildDefaultName, buildDefaultName: Field.methods.buildDefaultName,
closeDropdown () { closeDropdown () {
document.activeElement.blur() this.$el.getRootNode().activeElement.blur()
}, },
maybeToggleDefaultValue () { maybeToggleDefaultValue () {
if (!this.editable) {
return
}
if (['text', 'number'].includes(this.field.type)) { if (['text', 'number'].includes(this.field.type)) {
this.isContenteditable = true this.isContenteditable = true
this.$nextTick(() => this.focusValueInput()) this.focusValueInput()
} else if (this.field.type === 'checkbox') { } else if (this.field.type === 'checkbox') {
this.field.readonly = !this.field.readonly this.field.readonly = !this.field.readonly
this.field.default_value === true ? delete this.field.default_value : this.field.default_value = true this.field.default_value === true ? delete this.field.default_value : this.field.default_value = true
@@ -520,20 +535,28 @@ export default {
} }
}, },
focusValueInput (e) { focusValueInput (e) {
if (this.$refs.defaultValue !== document.activeElement) { this.$nextTick(() => {
this.$refs.defaultValue.focus() if (this.$refs.defaultValue && this.$refs.defaultValue !== document.activeElement) {
this.$refs.defaultValue.focus()
if (this.$refs.defaultValue.innerText.length && this.$refs.defaultValue !== e?.target) { if (this.$refs.defaultValue.innerText.length && this.$refs.defaultValue !== e?.target) {
window.getSelection().collapse( window.getSelection().collapse(
this.$refs.defaultValue.firstChild, this.$refs.defaultValue.firstChild,
this.$refs.defaultValue.innerText.length this.$refs.defaultValue.innerText.length
) )
}
} }
} })
}, },
formatNumber (number, format) { formatNumber (number, format) {
if (format === 'comma') { if (format === 'comma') {
return new Intl.NumberFormat('en-US').format(number) return new Intl.NumberFormat('en-US').format(number)
} else if (format === 'usd') {
return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(number)
} else if (format === 'gbp') {
return new Intl.NumberFormat('en-GB', { style: 'currency', currency: 'GBP', minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(number)
} else if (format === 'eur') {
return new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR', minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(number)
} else if (format === 'dot') { } else if (format === 'dot') {
return new Intl.NumberFormat('de-DE').format(number) return new Intl.NumberFormat('de-DE').format(number)
} else if (format === 'space') { } else if (format === 'space') {
@@ -639,6 +662,7 @@ export default {
const text = this.$refs.defaultValue.innerText.trim() const text = this.$refs.defaultValue.innerText.trim()
this.isContenteditable = false this.isContenteditable = false
this.isHeadingSelected = false
if (text) { if (text) {
if (this.field.type === 'number') { if (this.field.type === 'number') {
@@ -719,12 +743,18 @@ export default {
const rect = page.getBoundingClientRect() 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.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 () { stopTouchDrag () {
this.$el.getRootNode().removeEventListener('touchmove', this.touchDrag) this.$el.getRootNode().removeEventListener('touchmove', this.touchDrag)
this.$el.getRootNode().removeEventListener('touchend', this.stopTouchDrag) this.$el.getRootNode().removeEventListener('touchend', this.stopTouchDrag)
this.maybeChangeAreaPage(this.area)
if (this.isDragged) { if (this.isDragged) {
this.save() this.save()
} }
@@ -750,10 +780,6 @@ export default {
this.selectedAreaRef.value = this.area 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.dragFrom = { x: rect.left - e.clientX, y: rect.top - e.clientY }
this.$el.getRootNode().addEventListener('mousemove', this.mouseMove) this.$el.getRootNode().addEventListener('mousemove', this.mouseMove)
@@ -772,21 +798,42 @@ export default {
const rect = page.getBoundingClientRect() 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.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) { stopMouseMove (e) {
this.$el.getRootNode().removeEventListener('mousemove', this.mouseMove) this.$el.getRootNode().removeEventListener('mousemove', this.mouseMove)
this.$el.getRootNode().removeEventListener('mouseup', this.stopMouseMove) this.$el.getRootNode().removeEventListener('mouseup', this.stopMouseMove)
this.maybeChangeAreaPage(this.area)
if (this.isMoved) { if (this.isMoved) {
this.save() this.save()
} }
if (this.field.type === 'heading') {
this.isHeadingSelected = !this.isMoved
this.focusValueInput()
}
this.isDragged = false this.isDragged = false
this.isMoved = false this.isMoved = false
this.$emit('stop-drag') 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 () { stopDrag () {
this.$el.getRootNode().removeEventListener('mousemove', this.drag) this.$el.getRootNode().removeEventListener('mousemove', this.drag)
this.$el.getRootNode().removeEventListener('mouseup', this.stopDrag) this.$el.getRootNode().removeEventListener('mouseup', this.stopDrag)
+209 -40
View File
@@ -1,10 +1,11 @@
<template> <template>
<div <div
style="max-width: 1600px" 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 <div
v-if="pendingFieldAttachmentUuids.length" v-if="pendingFieldAttachmentUuids.length && editable"
class="top-1.5 sticky h-0 z-20 max-w-2xl mx-auto" 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"> <div class="alert border-base-content/30 py-2 px-2.5">
@@ -31,7 +32,7 @@
<div <div
v-if="$slots.buttons || withTitle" v-if="$slots.buttons || withTitle"
id="title_container" 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 }" :class="{ sticky: withStickySubmitters || isBreakpointLg }"
:style="{ backgroundColor }" :style="{ backgroundColor }"
> >
@@ -46,7 +47,7 @@
v-if="withTitle" v-if="withTitle"
:model-value="template.name" :model-value="template.name"
:editable="editable" :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" :icon-stroke-width="2.3"
@update:model-value="updateName" @update:model-value="updateName"
/> />
@@ -162,8 +163,8 @@
</div> </div>
<div <div
id="main_container" id="main_container"
class="flex" class="flex main-container"
:class="$slots.buttons || withTitle ? 'md:max-h-[calc(100%_-_60px)]' : 'md:max-h-[100%]'" :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 <div
v-if="withDocumentsList" v-if="withDocumentsList"
@@ -178,6 +179,7 @@
:with-arrows="template.schema.length > 1" :with-arrows="template.schema.length > 1"
:item="item" :item="item"
:document="sortedDocuments[index]" :document="sortedDocuments[index]"
:data-document-uuid="item.attachment_uuid"
:accept-file-types="acceptFileTypes" :accept-file-types="acceptFileTypes"
:with-replace-button="withUploadButton" :with-replace-button="withUploadButton"
:editable="editable" :editable="editable"
@@ -186,6 +188,7 @@
@remove="onDocumentRemove" @remove="onDocumentRemove"
@replace="onDocumentReplace" @replace="onDocumentReplace"
@up="moveDocument(item, -1)" @up="moveDocument(item, -1)"
@reorder="reorderFields"
@down="moveDocument(item, 1)" @down="moveDocument(item, 1)"
@change="save" @change="save"
/> />
@@ -202,7 +205,7 @@
<button <button
v-if="sortedDocuments.length && editable && withAddPageButton" v-if="sortedDocuments.length && editable && withAddPageButton"
id="add_blank_page_button" id="add_blank_page_button"
class="btn btn-outline w-full" class="btn btn-outline w-full add-blank-page-button"
@click.prevent="addBlankPage" @click.prevent="addBlankPage"
> >
<IconInnerShadowTop <IconInnerShadowTop
@@ -219,7 +222,8 @@
</div> </div>
<div <div
id="pages_container" 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 <div
ref="documents" ref="documents"
@@ -235,7 +239,7 @@
<button <button
v-if="withAddPageButton" v-if="withAddPageButton"
id="add_blank_page_button" 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" @click.prevent="addBlankPage"
> >
<IconInnerShadowTop <IconInnerShadowTop
@@ -262,7 +266,8 @@
:is-drag="!!dragField" :is-drag="!!dragField"
:input-mode="inputMode" :input-mode="inputMode"
:default-fields="[...defaultRequiredFields, ...defaultFields]" :default-fields="[...defaultRequiredFields, ...defaultFields]"
:allow-draw="!onlyDefinedFields" :allow-draw="!onlyDefinedFields || drawField"
:data-document-uuid="document.uuid"
:default-submitters="defaultSubmitters" :default-submitters="defaultSubmitters"
:with-field-placeholder="withFieldPlaceholder" :with-field-placeholder="withFieldPlaceholder"
:draw-field="drawField" :draw-field="drawField"
@@ -302,7 +307,7 @@
<button <button
v-if="withAddPageButton" v-if="withAddPageButton"
id="add_blank_page_button" 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" @click.prevent="addBlankPage"
> >
<IconInnerShadowTop <IconInnerShadowTop
@@ -320,9 +325,9 @@
</div> </div>
</div> </div>
<div <div
v-if="withFieldsList" v-if="withFieldsList && !isMobile"
id="fields_list_container" 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'" :class="drawField ? 'overflow-hidden' : 'overflow-y-auto overflow-x-hidden'"
> >
<div <div
@@ -330,13 +335,13 @@
class="sticky inset-0 h-full z-20" class="sticky inset-0 h-full z-20"
:style="{ backgroundColor }" :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> <p>
{{ t('draw_field_on_the_document') }} {{ t('draw_field_on_the_document') }}
</p> </p>
<div> <div>
<button <button
class="base-button" class="base-button cancel-draw-button"
@click="clearDrawField" @click="clearDrawField"
> >
{{ t('cancel') }} {{ t('cancel') }}
@@ -361,7 +366,9 @@
:with-help="withHelp" :with-help="withHelp"
:default-submitters="defaultSubmitters" :default-submitters="defaultSubmitters"
:draw-field-type="drawFieldType" :draw-field-type="drawFieldType"
:with-fields-search="withFieldsSearch"
:default-fields="[...defaultRequiredFields, ...defaultFields]" :default-fields="[...defaultRequiredFields, ...defaultFields]"
:template="template"
:default-required-fields="defaultRequiredFields" :default-required-fields="defaultRequiredFields"
:field-types="fieldTypes" :field-types="fieldTypes"
:with-sticky-submitters="withStickySubmitters" :with-sticky-submitters="withStickySubmitters"
@@ -378,14 +385,14 @@
</div> </div>
</div> </div>
</div> </div>
<div class="sticky bottom-0"> <div class="sticky bottom-0 z-10">
<MobileDrawField <MobileDrawField
v-if="drawField && isBreakpointLg" v-if="drawField && (isBreakpointLg || isMobile)"
:draw-field="drawField" :draw-field="drawField"
:fields="template.fields" :fields="template.fields"
:submitters="template.submitters" :submitters="template.submitters"
:selected-submitter="selectedSubmitter" :selected-submitter="selectedSubmitter"
class="md:hidden" :class="{ 'md:hidden': !isMobile }"
:editable="editable" :editable="editable"
@cancel="[drawField = null, drawOption = null]" @cancel="[drawField = null, drawOption = null]"
@change-submitter="[selectedSubmitter = $event, drawField.submitter_uuid = $event.uuid]" @change-submitter="[selectedSubmitter = $event, drawField.submitter_uuid = $event.uuid]"
@@ -396,11 +403,15 @@
:default-fields="[...defaultRequiredFields, ...defaultFields]" :default-fields="[...defaultRequiredFields, ...defaultFields]"
:default-required-fields="defaultRequiredFields" :default-required-fields="defaultRequiredFields"
:field-types="fieldTypes" :field-types="fieldTypes"
:class="{ 'md:hidden': !isMobile }"
:selected-submitter="selectedSubmitter" :selected-submitter="selectedSubmitter"
@select="startFieldDraw($event)" @select="startFieldDraw($event)"
/> />
</div> </div>
<div id="docuseal_modal_container" /> <div
id="docuseal_modal_container"
class="modal-container"
/>
</div> </div>
</template> </template>
@@ -415,6 +426,7 @@ import Contenteditable from './contenteditable'
import DocumentPreview from './preview' import DocumentPreview from './preview'
import DocumentControls from './controls' import DocumentControls from './controls'
import MobileFields from './mobile_fields' import MobileFields from './mobile_fields'
import FieldSubmitter from './field_submitter'
import { IconPlus, IconUsersPlus, IconDeviceFloppy, IconChevronDown, IconEye, IconWritingSign, IconInnerShadowTop, IconInfoCircle, IconAdjustments } from '@tabler/icons-vue' import { IconPlus, IconUsersPlus, IconDeviceFloppy, IconChevronDown, IconEye, IconWritingSign, IconInnerShadowTop, IconInfoCircle, IconAdjustments } from '@tabler/icons-vue'
import { v4 } from 'uuid' import { v4 } from 'uuid'
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
@@ -454,6 +466,7 @@ export default {
fieldTypes: this.fieldTypes, fieldTypes: this.fieldTypes,
backgroundColor: this.backgroundColor, backgroundColor: this.backgroundColor,
withPhone: this.withPhone, withPhone: this.withPhone,
withVerification: this.withVerification,
withPayment: this.withPayment, withPayment: this.withPayment,
isPaymentConnected: this.isPaymentConnected, isPaymentConnected: this.isPaymentConnected,
withFormula: this.withFormula, withFormula: this.withFormula,
@@ -558,6 +571,11 @@ export default {
required: false, required: false,
default: () => [] default: () => []
}, },
defineSubmitters: {
type: Array,
required: false,
default: () => []
},
acceptFileTypes: { acceptFileTypes: {
type: String, type: String,
required: false, required: false,
@@ -609,6 +627,11 @@ export default {
required: false, required: false,
default: true default: true
}, },
withFieldsSearch: {
type: Boolean,
required: false,
default: null
},
withFieldsList: { withFieldsList: {
type: Boolean, type: Boolean,
required: false, required: false,
@@ -629,6 +652,11 @@ export default {
required: false, required: false,
default: false default: false
}, },
withVerification: {
type: Boolean,
required: false,
default: null
},
withPayment: { withPayment: {
type: Boolean, type: Boolean,
required: false, required: false,
@@ -677,11 +705,17 @@ export default {
} }
}, },
computed: { computed: {
submitterDefaultNames: FieldSubmitter.computed.names,
selectedAreaRef: () => ref(), selectedAreaRef: () => ref(),
fieldsDragFieldRef: () => ref(), fieldsDragFieldRef: () => ref(),
language () { language () {
return this.locale.split('-')[0].toLowerCase() 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 () { defaultDateFormat () {
const isUsBrowser = Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US') 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)$/) const isUsTimezone = new Intl.DateTimeFormat('en-US', { timeZoneName: 'short' }).format(new Date()).match(/\s(?:CST|CDT|PST|PDT|EST|EDT)$/)
@@ -749,6 +783,22 @@ 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 (defineSubmittersUuids.filter(Boolean).length || existingSubmittersUuids.filter(Boolean).length) {
submitter.uuid = defineSubmittersUuids[index] || existingSubmittersUuids[index] || submitter.uuid || v4()
} else {
submitter.uuid ||= v4()
}
})
this.selectedSubmitter = this.template.submitters[0] this.selectedSubmitter = this.template.submitters[0]
}, },
mounted () { mounted () {
@@ -786,6 +836,89 @@ export default {
this.documentRefs = [] this.documentRefs = []
}, },
methods: { 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 () { closeDropdown () {
document.activeElement.blur() document.activeElement.blur()
}, },
@@ -998,6 +1131,22 @@ export default {
if (!field.areas.length) { if (!field.areas.length) {
this.template.fields.splice(this.template.fields.indexOf(field), 1) 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() this.save()
@@ -1056,7 +1205,7 @@ export default {
} else if (type === 'image') { } else if (type === 'image') {
area.w = pageMask.clientWidth / 5 / pageMask.clientWidth area.w = pageMask.clientWidth / 5 / pageMask.clientWidth
area.h = (pageMask.clientWidth / 5 / pageMask.clientWidth) * (pageMask.clientWidth / pageMask.clientHeight) 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.w = pageMask.clientWidth / 5 / pageMask.clientWidth
area.h = (pageMask.clientWidth / 5 / pageMask.clientWidth) * (pageMask.clientWidth / pageMask.clientHeight) / 2 area.h = (pageMask.clientWidth / 5 / pageMask.clientWidth) * (pageMask.clientWidth / pageMask.clientHeight) / 2
} else if (type === 'initials') { } else if (type === 'initials') {
@@ -1170,6 +1319,10 @@ export default {
} }
}, },
onDropfield (area) { onDropfield (area) {
if (!this.editable) {
return
}
const field = this.fieldsDragFieldRef.value || { const field = this.fieldsDragFieldRef.value || {
name: '', name: '',
uuid: v4(), uuid: v4(),
@@ -1180,7 +1333,11 @@ export default {
if (!this.fieldsDragFieldRef.value) { if (!this.fieldsDragFieldRef.value) {
if (['select', 'multiple', 'radio'].includes(field.type)) { 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)) { if (['stamp', 'heading'].includes(field.type)) {
@@ -1188,9 +1345,8 @@ export default {
} }
if (field.type === 'date') { if (field.type === 'date') {
field.preferences = { field.preferences ||= {}
format: this.defaultDateFormat field.preferences.format ||= this.defaultDateFormat
}
} }
} }
@@ -1220,7 +1376,7 @@ export default {
w: area.maskW / 5 / area.maskW, w: area.maskW / 5 / area.maskW,
h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH) 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 = { baseArea = {
w: area.maskW / 5 / area.maskW, w: area.maskW / 5 / area.maskW,
h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH) / 2 h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH) / 2
@@ -1251,11 +1407,20 @@ export default {
const lastArea = field.areas[field.areas.length - 1] const lastArea = field.areas[field.areas.length - 1]
if (lastArea) { if (lastArea) {
fieldArea.x -= lastArea.w / 2
fieldArea.w = lastArea.w fieldArea.w = lastArea.w
fieldArea.h = lastArea.h 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) field.areas.push(fieldArea)
this.selectedAreaRef.value = fieldArea this.selectedAreaRef.value = fieldArea
@@ -1273,6 +1438,8 @@ export default {
const documentRef = this.documentRefs.find((e) => e.document.uuid === area.attachment_uuid) 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) const areaRef = documentRef.pageRefs[area.page].areaRefs.find((ref) => ref.area === this.selectedAreaRef.value)
areaRef.isHeadingSelected = true
areaRef.focusValueInput() areaRef.focusValueInput()
}) })
} }
@@ -1323,7 +1490,9 @@ export default {
} }
this.$nextTick(() => { 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]) this.scrollIntoDocument(data.schema[0])
}) })
@@ -1358,29 +1527,29 @@ export default {
onDocumentRemove (item) { onDocumentRemove (item) {
if (window.confirm(this.t('are_you_sure_'))) { if (window.confirm(this.t('are_you_sure_'))) {
this.template.schema.splice(this.template.schema.indexOf(item), 1) this.template.schema.splice(this.template.schema.indexOf(item), 1)
}
const removedFieldUuids = [] const removedFieldUuids = []
this.template.fields.forEach((field) => { this.template.fields.forEach((field) => {
[...(field.areas || [])].forEach((area) => { [...(field.areas || [])].forEach((area) => {
if (area.attachment_uuid === item.attachment_uuid) { if (area.attachment_uuid === item.attachment_uuid) {
field.areas.splice(field.areas.indexOf(area), 1) field.areas.splice(field.areas.indexOf(area), 1)
removedFieldUuids.push(field.uuid) removedFieldUuids.push(field.uuid)
} }
})
}) })
})
this.template.fields = this.template.fields =
this.template.fields.filter((f) => !removedFieldUuids.includes(f.uuid) || f.areas?.length) this.template.fields.filter((f) => !removedFieldUuids.includes(f.uuid) || f.areas?.length)
this.save() this.save()
}
}, },
onDocumentReplace (data) { onDocumentReplace (data) {
const { replaceSchemaItem, schema, documents } = 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) this.template.documents.push(...documents)
if (data.fields) { 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="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"> <div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
<span> <span class="modal-title">
{{ t('condition') }} - {{ field.name || buildDefaultName(field, template.fields) }} {{ t('condition') }} - {{ item.name || buildDefaultName(item, template.fields) }}
</span> </span>
<a <a
href="#" href="#"
class="text-xl" class="text-xl modal-close-button"
@click.prevent="$emit('close')" @click.prevent="$emit('close')"
>&times;</a> >&times;</a>
</div> </div>
@@ -23,25 +23,36 @@
class="bg-base-300 rounded-xl py-2 px-3 text-center" class="bg-base-300 rounded-xl py-2 px-3 text-center"
> >
<a <a
href="https://www.docuseal.co/pricing" href="https://www.docuseal.com/pricing"
target="_blank" target="_blank"
class="link" class="link"
>{{ t('available_in_pro') }}</a> >{{ t('available_in_pro') }}</a>
</div> </div>
<form @submit.prevent="validateSaveAndClose"> <form @submit.prevent="validateSaveAndClose">
<div class="my-4 space-y-5"> <div class="my-4">
<div <div
v-for="(condition, cindex) in conditions" v-for="(condition, cindex) in conditions"
:key="cindex" :key="cindex"
class="space-y-4 relative" class="space-y-4 relative"
> >
<div <div
v-if="conditions.length > 1" v-if="cindex > 0"
class="flex justify-between border-b mx-1 -mb-1 pb-1" 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 }} {{ t('condition') }} {{ cindex + 1 }}
</span> </label>
<a <a
href="#" href="#"
class="link text-sm" class="link text-sm"
@@ -120,19 +131,17 @@
class="inline float-right link text-right mb-3 px-2" class="inline float-right link text-right mb-3 px-2"
@click.prevent="conditions.push({})" @click.prevent="conditions.push({})"
> + {{ t('add_condition') }}</a> > + {{ t('add_condition') }}</a>
<button <button class="base-button w-full mt-2 modal-save-button">
class="base-button w-full mt-2"
>
{{ t('save') }} {{ t('save') }}
</button> </button>
</form> </form>
<div <div
v-if="field.conditions?.[0]?.field_uuid" v-if="item.conditions?.[0]?.field_uuid"
class="text-center w-full mt-4" class="text-center w-full mt-4"
> >
<button <button
class="link" class="link"
@click="[conditions = [], delete field.conditions, validateSaveAndClose()]" @click="[conditions = [], delete item.conditions, validateSaveAndClose()]"
> >
{{ t('remove_condition') }} {{ t('remove_condition') }}
</button> </button>
@@ -147,7 +156,7 @@ export default {
name: 'ConditionModal', name: 'ConditionModal',
inject: ['t', 'save', 'template', 'withConditions'], inject: ['t', 'save', 'template', 'withConditions'],
props: { props: {
field: { item: {
type: Object, type: Object,
required: true required: true
}, },
@@ -159,22 +168,26 @@ export default {
emits: ['close'], emits: ['close'],
data () { data () {
return { 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: { computed: {
fields () { fields () {
return this.template.fields.reduce((acc, f) => { if (this.item.submitter_uuid) {
if (f !== this.field && f.submitter_uuid === this.field.submitter_uuid) { return this.template.fields.reduce((acc, f) => {
acc.push(f) if (f !== this.item) {
} acc.push(f)
}
return acc return acc
}, []) }, [])
} else {
return this.template.fields
}
} }
}, },
created () { created () {
this.field.conditions ||= [] this.item.conditions ||= []
}, },
methods: { methods: {
conditionField (condition) { conditionField (condition) {
@@ -208,9 +221,9 @@ export default {
} }
if (this.conditions.find((f) => f.field_uuid)) { if (this.conditions.find((f) => f.field_uuid)) {
this.field.conditions = this.conditions this.item.conditions = this.conditions
} else { } else {
delete this.field.conditions delete this.item.conditions
} }
this.save() this.save()
+4 -4
View File
@@ -1,7 +1,7 @@
<template> <template>
<div class="flex space-x-2"> <div class="flex space-x-2">
<Contenteditable <Contenteditable
class="w-full block mr-6" class="w-full block mr-6 document-preview-name"
:model-value="item.name" :model-value="item.name"
:icon-width="16" :icon-width="16"
@update:model-value="onUpdateName" @update:model-value="onUpdateName"
@@ -15,7 +15,7 @@
/> />
<button <button
v-if="withArrows" 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" style="width: 24px; height: 24px"
@click.stop="$emit('up', item)" @click.stop="$emit('up', item)"
> >
@@ -23,14 +23,14 @@
</button> </button>
<button <button
v-if="withArrows" 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" style="width: 24px; height: 24px"
@click.stop="$emit('down', item)" @click.stop="$emit('down', item)"
> >
&darr; &darr;
</button> </button>
<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" style="width: 24px; height: 24px"
@click.stop="$emit('remove', item)" @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="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"> <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) }} {{ field.name || buildDefaultName(field, template.fields) }}
</span> </span>
<a <a
href="#" href="#"
class="text-xl" class="text-xl modal-close-button"
@click.prevent="$emit('close')" @click.prevent="$emit('close')"
>&times;</a> >&times;</a>
</div> </div>
@@ -55,9 +55,7 @@
> >
</div> </div>
</div> </div>
<button <button class="base-button w-full mt-4 modal-save-button">
class="base-button w-full mt-4"
>
{{ t('save') }} {{ t('save') }}
</button> </button>
</form> </form>
@@ -7,6 +7,7 @@
:input-mode="inputMode" :input-mode="inputMode"
:number="index" :number="index"
:editable="editable" :editable="editable"
:data-page="index"
:areas="areasIndex[index]" :areas="areasIndex[index]"
:allow-draw="allowDraw" :allow-draw="allowDraw"
:is-drag="isDrag" :is-drag="isDrag"
@@ -16,6 +17,7 @@
:draw-field="drawField" :draw-field="drawField"
:draw-field-type="drawFieldType" :draw-field-type="drawFieldType"
:selected-submitter="selectedSubmitter" :selected-submitter="selectedSubmitter"
:total-pages="sortedPreviewImages.length"
:image="image" :image="image"
@drop-field="$emit('drop-field', {...$event, attachment_uuid: document.uuid })" @drop-field="$emit('drop-field', {...$event, attachment_uuid: document.uuid })"
@remove-area="$emit('remove-area', $event)" @remove-area="$emit('remove-area', $event)"
+26 -11
View File
@@ -3,7 +3,7 @@
class="list-field group mb-2" class="list-field group mb-2"
> >
<div <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 }" :style="{ backgroundColor: backgroundColor }"
> >
<div class="flex items-center justify-between relative group/contenteditable-container"> <div class="flex items-center justify-between relative group/contenteditable-container">
@@ -23,7 +23,7 @@
/> />
<Contenteditable <Contenteditable
ref="name" 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'" :editable="editable && !defaultField && field.type != 'heading'"
:icon-inline="true" :icon-inline="true"
:icon-width="18" :icon-width="18"
@@ -98,7 +98,7 @@
/> />
<span <span
v-else-if="field.type !== 'heading'" v-else-if="field.type !== 'heading'"
class="dropdown dropdown-end" class="dropdown dropdown-end field-settings-dropdown"
@mouseenter="renderDropdown = true" @mouseenter="renderDropdown = true"
@touchstart="renderDropdown = true" @touchstart="renderDropdown = true"
> >
@@ -127,6 +127,7 @@
:editable="editable" :editable="editable"
:background-color="dropdownBgColor" :background-color="dropdownBgColor"
@click-formula="isShowFormulaModal = true" @click-formula="isShowFormulaModal = true"
@click-font="isShowFontModal = true"
@click-description="isShowDescriptionModal = true" @click-description="isShowDescriptionModal = true"
@click-condition="isShowConditionsModal = true" @click-condition="isShowConditionsModal = true"
@set-draw="$emit('set-draw', $event)" @set-draw="$emit('set-draw', $event)"
@@ -136,7 +137,7 @@
</ul> </ul>
</span> </span>
<button <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')" :title="t('remove')"
@click="$emit('remove', field)" @click="$emit('remove', field)"
> >
@@ -192,14 +193,14 @@
class="w-full input input-primary input-xs text-sm bg-transparent" class="w-full input input-primary input-xs text-sm bg-transparent"
:placeholder="`${t('option')} ${index + 1}`" :placeholder="`${t('option')} ${index + 1}`"
type="text" type="text"
:readonly="!editable" :readonly="!editable || defaultField"
required required
dir="auto" dir="auto"
@focus="maybeFocusOnOptionArea(option)" @focus="maybeFocusOnOptionArea(option)"
@blur="save" @blur="save"
> >
<button <button
v-if="editable" v-if="editable && !defaultField"
class="text-sm w-3.5" class="text-sm w-3.5"
tabindex="-1" tabindex="-1"
@click="removeOption(option)" @click="removeOption(option)"
@@ -208,11 +209,11 @@
</button> </button>
</div> </div>
<div <div
v-if="field.options && !editable" v-if="field.options && (!editable || defaultField)"
class="pb-1" class="pb-1"
/> />
<button <button
v-else-if="field.options && editable" v-else-if="field.options && editable && !defaultField"
class="text-center text-sm w-full pb-1" class="text-center text-sm w-full pb-1"
@click="addOption" @click="addOption"
> >
@@ -231,12 +232,23 @@
@close="isShowFormulaModal = false" @close="isShowFormulaModal = false"
/> />
</Teleport> </Teleport>
<Teleport
v-if="isShowFontModal"
:to="modalContainerEl"
>
<FontModal
:field="field"
:editable="editable && !defaultField"
:build-default-name="buildDefaultName"
@close="isShowFontModal = false"
/>
</Teleport>
<Teleport <Teleport
v-if="isShowConditionsModal" v-if="isShowConditionsModal"
:to="modalContainerEl" :to="modalContainerEl"
> >
<ConditionsModal <ConditionsModal
:field="field" :item="field"
:build-default-name="buildDefaultName" :build-default-name="buildDefaultName"
@close="isShowConditionsModal = false" @close="isShowConditionsModal = false"
/> />
@@ -261,6 +273,7 @@ import FieldType from './field_type'
import PaymentSettings from './payment_settings' import PaymentSettings from './payment_settings'
import FieldSettings from './field_settings' import FieldSettings from './field_settings'
import FormulaModal from './formula_modal' import FormulaModal from './formula_modal'
import FontModal from './font_modal'
import ConditionsModal from './conditions_modal' import ConditionsModal from './conditions_modal'
import DescriptionModal from './description_modal' import DescriptionModal from './description_modal'
import { IconRouteAltLeft, IconMathFunction, IconNewSection, IconTrashX, IconSettings } from '@tabler/icons-vue' import { IconRouteAltLeft, IconMathFunction, IconNewSection, IconTrashX, IconSettings } from '@tabler/icons-vue'
@@ -275,6 +288,7 @@ export default {
PaymentSettings, PaymentSettings,
IconNewSection, IconNewSection,
FormulaModal, FormulaModal,
FontModal,
DescriptionModal, DescriptionModal,
ConditionsModal, ConditionsModal,
IconRouteAltLeft, IconRouteAltLeft,
@@ -305,6 +319,7 @@ export default {
isNameFocus: false, isNameFocus: false,
showPaymentModal: false, showPaymentModal: false,
isShowFormulaModal: false, isShowFormulaModal: false,
isShowFontModal: false,
isShowConditionsModal: false, isShowConditionsModal: false,
isShowDescriptionModal: false, isShowDescriptionModal: false,
renderDropdown: false renderDropdown: false
@@ -365,7 +380,7 @@ export default {
} else { } else {
const typeIndex = fields.filter((f) => f.type === field.type).indexOf(field) 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}` return `${this.fieldNames[field.type]} ${typeIndex + 1}`
} else { } else {
return `${this.fieldLabels[field.type]} ${typeIndex + 1}` return `${this.fieldLabels[field.type]} ${typeIndex + 1}`
@@ -392,7 +407,7 @@ export default {
return this.sortedAreas[0] && this.$emit('scroll-to', this.sortedAreas[0]) return this.sortedAreas[0] && this.$emit('scroll-to', this.sortedAreas[0])
}, },
closeDropdown () { closeDropdown () {
document.activeElement.blur() this.$el.getRootNode().activeElement.blur()
}, },
addOption () { addOption () {
this.field.options.push({ value: '', uuid: v4() }) this.field.options.push({ value: '', uuid: v4() })
@@ -27,7 +27,34 @@
</label> </label>
</div> </div>
<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" class="py-1.5 px-1 relative"
@click.stop @click.stop
> >
@@ -36,7 +63,7 @@
@change="[field.preferences ||= {}, field.preferences.align = $event.target.value, save()]" @change="[field.preferences ||= {}, field.preferences.align = $event.target.value, save()]"
> >
<option <option
v-for="value in ['left', 'right', 'center']" v-for="value in ['left', 'right', field.type === 'cells' ? null : 'center'].filter(Boolean)"
:key="value" :key="value"
:selected="field.preferences?.align ? value === field.preferences.align : value === 'left'" :selected="field.preferences?.align ? value === field.preferences.align : value === 'left'"
:value="value" :value="value"
@@ -212,16 +239,12 @@
{{ t('any') }} {{ t('any') }}
</option> </option>
<option <option
value="drawn" v-for="type in ['drawn', 'typed', 'drawn_or_typed', 'upload']"
:selected="field.preferences?.format === 'drawn'" :key="type"
:value="type"
:selected="field.preferences?.format === type"
> >
{{ t('drawn') }} {{ t(type) }}
</option>
<option
value="typed"
:selected="field.preferences?.format === 'typed'"
>
{{ t('typed') }}
</option> </option>
</select> </select>
<label <label
@@ -233,14 +256,14 @@
</label> </label>
</div> </div>
<li <li
v-if="withRequired && field.type != 'phone' && field.type != 'stamp'" v-if="withRequired && field.type !== 'phone' && field.type !== 'stamp' && field.type !== 'verification'"
@click.stop @click.stop
> >
<label class="cursor-pointer py-1.5"> <label class="cursor-pointer py-1.5">
<input <input
v-model="field.required" v-model="field.required"
type="checkbox" type="checkbox"
:disabled="!editable || defaultField" :disabled="!editable || (defaultField && [true, false].includes(defaultField.required))"
class="toggle toggle-xs" class="toggle toggle-xs"
@update:model-value="save" @update:model-value="save"
> >
@@ -307,6 +330,19 @@
v-if="field.type != 'stamp'" v-if="field.type != 'stamp'"
class="pb-0.5 mt-0.5" 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 <li
v-if="field.type != 'stamp'" v-if="field.type != 'stamp'"
> >
@@ -404,7 +440,7 @@
</template> </template>
<script> <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 { export default {
name: 'FieldSettings', name: 'FieldSettings',
@@ -415,6 +451,7 @@ export default {
IconRouteAltLeft, IconRouteAltLeft,
IconCopy, IconCopy,
IconNewSection, IconNewSection,
IconTypography,
IconX IconX
}, },
inject: ['template', 'save', 't'], inject: ['template', 'save', 't'],
@@ -449,7 +486,7 @@ export default {
default: true 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 () { data () {
return { return {
} }
@@ -465,6 +502,9 @@ export default {
numberFormats () { numberFormats () {
return [ return [
'none', 'none',
'usd',
'eur',
'gbp',
'comma', 'comma',
'dot', 'dot',
'space' 'space'
@@ -545,6 +585,12 @@ export default {
formatNumber (number, format) { formatNumber (number, format) {
if (format === 'comma') { if (format === 'comma') {
return new Intl.NumberFormat('en-US').format(number) return new Intl.NumberFormat('en-US').format(number)
} else if (format === 'usd') {
return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(number)
} else if (format === 'gbp') {
return new Intl.NumberFormat('en-GB', { style: 'currency', currency: 'GBP', minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(number)
} else if (format === 'eur') {
return new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR', minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(number)
} else if (format === 'dot') { } else if (format === 'dot') {
return new Intl.NumberFormat('de-DE').format(number) return new Intl.NumberFormat('de-DE').format(number)
} else if (format === 'space') { } else if (format === 'space') {
@@ -5,11 +5,11 @@
@touchstart="renderDropdown = true" @touchstart="renderDropdown = true"
> >
<div class="flex space-x-2 items-end"> <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"> <div class="flex items-center space-x-2">
<span <span
class="w-3 h-3 flex-shrink-0 rounded-full" class="w-3 h-3 flex-shrink-0 rounded-full"
:class="colors[submitters.indexOf(selectedSubmitter)]" :class="colors[submitters.indexOf(selectedSubmitter) % colors.length]"
/> />
<Contenteditable <Contenteditable
v-model="selectedSubmitter.name" v-model="selectedSubmitter.name"
@@ -22,7 +22,7 @@
/> />
</div> </div>
</div> </div>
<div class="dropdown dropdown-top dropdown-end"> <div class="dropdown dropdown-top dropdown-end roles-dropdown-mobile">
<label <label
tabindex="0" tabindex="0"
class="bg-base-100 cursor-pointer rounded-md p-2 border border-base-300 w-full flex justify-center" 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="py-1 flex items-center">
<span <span
class="rounded-full w-3 h-3 ml-1 mr-3" class="rounded-full w-3 h-3 ml-1 mr-3"
:class="colors[index]" :class="colors[index % colors.length]"
/> />
<span> <span>
{{ submitter.name }} {{ submitter.name }}
@@ -79,7 +79,7 @@
:stroke-width="1.6" :stroke-width="1.6"
/> />
<span class="py-1"> <span class="py-1">
{{ t('add') }} {{ names[submitters.length] }} {{ t('add') }} {{ names[lastPartyIndex] }}
</span> </span>
</a> </a>
</li> </li>
@@ -101,7 +101,7 @@
> >
<button <button
class="mx-1 w-3 h-3 rounded-full" class="mx-1 w-3 h-3 rounded-full"
:class="colors[submitters.indexOf(selectedSubmitter)]" :class="colors[submitters.indexOf(selectedSubmitter) % colors.length]"
/> />
</label> </label>
<label <label
@@ -113,7 +113,7 @@
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<span <span
class="w-3 h-3 rounded-full" class="w-3 h-3 rounded-full"
:class="colors[submitters.indexOf(selectedSubmitter)]" :class="colors[submitters.indexOf(selectedSubmitter) % colors.length]"
/> />
<Contenteditable <Contenteditable
v-model="selectedSubmitter.name" v-model="selectedSubmitter.name"
@@ -143,6 +143,7 @@
<li <li
v-for="(submitter, index) in submitters" v-for="(submitter, index) in submitters"
:key="submitter.uuid" :key="submitter.uuid"
class="w-full"
> >
<a <a
href="#" href="#"
@@ -153,7 +154,7 @@
<span class="py-1 flex items-center"> <span class="py-1 flex items-center">
<span <span
class="rounded-full w-3 h-3 ml-1 mr-3" class="rounded-full w-3 h-3 ml-1 mr-3"
:class="colors[index]" :class="colors[index % colors.length]"
/> />
<span> <span>
{{ submitter.name }} {{ submitter.name }}
@@ -191,7 +192,10 @@
</div> </div>
</a> </a>
</li> </li>
<li v-if="submitters.length < names.length && editable && allowAddNew"> <li
v-if="submitters.length < names.length && editable && allowAddNew"
class="w-full"
>
<a <a
href="#" href="#"
class="flex px-2" class="flex px-2"
@@ -202,7 +206,7 @@
:stroke-width="1.6" :stroke-width="1.6"
/> />
<span class="py-1"> <span class="py-1">
{{ t('add') }} {{ names[submitters.length] }} {{ t('add') }} {{ names[lastPartyIndex] }}
</span> </span>
</a> </a>
</li> </li>
@@ -215,6 +219,14 @@ import { IconUserPlus, IconTrashX, IconPlus, IconChevronUp, IconChevronDown } fr
import Contenteditable from './contenteditable' import Contenteditable from './contenteditable'
import { v4 } from 'uuid' 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 { export default {
name: 'FieldSubmitter', name: 'FieldSubmitter',
components: { components: {
@@ -275,16 +287,6 @@ export default {
computed: { computed: {
colors () { colors () {
return [ 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-red-500',
'bg-sky-500', 'bg-sky-500',
'bg-emerald-500', 'bg-emerald-500',
@@ -298,6 +300,14 @@ export default {
] ]
}, },
names () { names () {
const generatedNames = []
for (let i = 21; i < 101; i++) {
const suffix = getOrdinalSuffix(i)
generatedNames.push(`${i}${suffix} ${this.t('party')}`)
}
return [ return [
this.t('first_party'), this.t('first_party'),
this.t('second_party'), this.t('second_party'),
@@ -318,9 +328,19 @@ export default {
this.t('seventeenth_party'), this.t('seventeenth_party'),
this.t('eighteenth_party'), this.t('eighteenth_party'),
this.t('nineteenth_party'), this.t('nineteenth_party'),
this.t('twentieth_party') this.t('twentieth_party'),
...generatedNames
] ]
}, },
lastPartyIndex () {
const index = Math.max(...this.submitters.map((s) => this.names.indexOf(s.name)))
if (index !== -1) {
return index + 1
} else {
return this.submitters.length
}
},
selectedSubmitter () { selectedSubmitter () {
return this.submitters.find((e) => e.uuid === this.modelValue) return this.submitters.find((e) => e.uuid === this.modelValue)
} }
@@ -353,7 +373,7 @@ export default {
}, },
addSubmitter () { addSubmitter () {
const newSubmitter = { const newSubmitter = {
name: this.names[this.submitters.length], name: this.names[this.lastPartyIndex],
uuid: v4() uuid: v4()
} }
@@ -363,7 +383,7 @@ export default {
this.$emit('new-submitter', newSubmitter) this.$emit('new-submitter', newSubmitter)
}, },
closeDropdown () { closeDropdown () {
document.activeElement.blur() this.$el.getRootNode().activeElement.blur()
} }
} }
} }
+11 -8
View File
@@ -1,6 +1,6 @@
<template> <template>
<span <span
class="dropdown" class="dropdown field-types-dropdown"
@mouseenter="renderDropdown = true" @mouseenter="renderDropdown = true"
@touchstart="renderDropdown = true" @touchstart="renderDropdown = true"
> >
@@ -30,7 +30,7 @@
v-for="(icon, type) in fieldIconsSorted" v-for="(icon, type) in fieldIconsSorted"
:key="type" :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 <a
href="#" href="#"
class="text-sm py-1 px-2" class="text-sm py-1 px-2"
@@ -51,11 +51,11 @@
</template> </template>
<script> <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 { export default {
name: 'FiledTypeDropdown', name: 'FiledTypeDropdown',
inject: ['withPhone', 'withPayment', 't', 'fieldTypes'], inject: ['withPhone', 'withPayment', 'withVerification', 't', 'fieldTypes'],
props: { props: {
modelValue: { modelValue: {
type: String, type: String,
@@ -111,7 +111,8 @@ export default {
cells: this.t('cells'), cells: this.t('cells'),
stamp: this.t('stamp'), stamp: this.t('stamp'),
payment: this.t('payment'), payment: this.t('payment'),
phone: this.t('phone') phone: this.t('phone'),
verification: this.t('verify_id')
} }
}, },
fieldLabels () { fieldLabels () {
@@ -130,7 +131,8 @@ export default {
cells: this.t('cells_field'), cells: this.t('cells_field'),
stamp: this.t('stamp_field'), stamp: this.t('stamp_field'),
payment: this.t('payment_field'), payment: this.t('payment_field'),
phone: this.t('phone_field') phone: this.t('phone_field'),
verification: this.t('verify_id')
} }
}, },
fieldIcons () { fieldIcons () {
@@ -150,7 +152,8 @@ export default {
cells: IconColumns3, cells: IconColumns3,
stamp: IconRubberStamp, stamp: IconRubberStamp,
payment: IconCreditCard, payment: IconCreditCard,
phone: IconPhoneCheck phone: IconPhoneCheck,
verification: IconId
} }
}, },
fieldIconsSorted () { fieldIconsSorted () {
@@ -167,7 +170,7 @@ export default {
}, },
methods: { methods: {
closeDropdown () { closeDropdown () {
document.activeElement.blur() this.$el.getRootNode().activeElement.blur()
} }
} }
} }
+67 -13
View File
@@ -2,10 +2,10 @@
<div :class="withStickySubmitters ? 'sticky top-0 z-[1]' : ''"> <div :class="withStickySubmitters ? 'sticky top-0 z-[1]' : ''">
<FieldSubmitter <FieldSubmitter
:model-value="selectedSubmitter.uuid" :model-value="selectedSubmitter.uuid"
class="roles-dropdown w-full rounded-lg" class="roles-dropdown w-full rounded-lg roles-dropdown"
:style="withStickySubmitters ? { backgroundColor } : {}" :style="withStickySubmitters ? { backgroundColor } : {}"
:submitters="submitters" :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" :editable="editable && !defaultSubmitters.length"
@new-submitter="save" @new-submitter="save"
@remove="removeSubmitter" @remove="removeSubmitter"
@@ -26,7 +26,7 @@
:field="field" :field="field"
:type-index="fields.filter((f) => f.type === field.type).indexOf(field)" :type-index="fields.filter((f) => f.type === field.type).indexOf(field)"
:editable="editable && (!fieldsDragFieldRef.value || fieldsDragFieldRef.value !== field)" :editable="editable && (!fieldsDragFieldRef.value || fieldsDragFieldRef.value !== field)"
:default-field="defaultFields.find((f) => f.name === field.name)" :default-field="defaultFieldsIndex[field.name]"
:draggable="editable" :draggable="editable"
@dragstart="fieldsDragFieldRef.value = field" @dragstart="fieldsDragFieldRef.value = field"
@dragend="fieldsDragFieldRef.value = null" @dragend="fieldsDragFieldRef.value = null"
@@ -74,7 +74,7 @@
<div <div
:style="{ backgroundColor }" :style="{ backgroundColor }"
draggable="true" 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 })" @dragstart="onDragstart({ type: 'text', ...field })"
@dragend="$emit('drag-end')" @dragend="$emit('drag-end')"
> >
@@ -104,21 +104,21 @@
</div> </div>
<div <div
v-if="editable && !onlyDefinedFields" 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 <template
v-for="(icon, type) in fieldIconsSorted" v-for="(icon, type) in fieldIconsSorted"
:key="type" :key="type"
> >
<button <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" 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 }" :style="{ backgroundColor }"
:class="drawFieldType === type ? 'border-base-content/40' : 'border-base-300 hover:border-base-content/20'" :class="drawFieldType === type ? 'border-base-content/40' : 'border-base-300 hover:border-base-content/20'"
@dragstart="onDragstart({ type: type })" @dragstart="onDragstart({ type: type })"
@dragend="$emit('drag-end')" @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 <div
class="flex items-console transition-all cursor-grab h-full absolute left-0" class="flex items-console transition-all cursor-grab h-full absolute left-0"
@@ -140,9 +140,35 @@
:data-tip="t('unlock_sms_verified_phone_number_field_with_paid_plan_use_text_field_for_phone_numbers_without_verification')" :data-tip="t('unlock_sms_verified_phone_number_field_with_paid_plan_use_text_field_for_phone_numbers_without_verification')"
> >
<a <a
href="https://www.docuseal.co/pricing" href="https://www.docuseal.com/pricing"
target="_blank" 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 }" :style="{ backgroundColor }"
> >
<div class="w-0 absolute left-0"> <div class="w-0 absolute left-0">
@@ -197,12 +223,21 @@ export default {
IconDrag, IconDrag,
IconLock IconLock
}, },
inject: ['save', 'backgroundColor', 'withPhone', 'withPayment', 't', 'fieldsDragFieldRef'], inject: ['save', 'backgroundColor', 'withPhone', 'withVerification', 'withPayment', 't', 'fieldsDragFieldRef'],
props: { props: {
fields: { fields: {
type: Array, type: Array,
required: true required: true
}, },
withFieldsSearch: {
type: Boolean,
required: false,
default: null
},
template: {
type: Object,
required: true
},
withHelp: { withHelp: {
type: Boolean, type: Boolean,
required: false, required: false,
@@ -267,7 +302,18 @@ export default {
fieldNames: FieldType.computed.fieldNames, fieldNames: FieldType.computed.fieldNames,
fieldIcons: FieldType.computed.fieldIcons, fieldIcons: FieldType.computed.fieldIcons,
isShowFieldSearch () { 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 () { fieldIconsSorted () {
if (this.fieldTypes.length) { if (this.fieldTypes.length) {
@@ -290,7 +336,7 @@ export default {
}, },
filteredSubmitterDefaultFields () { filteredSubmitterDefaultFields () {
if (this.defaultFieldsSearch) { 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 { } else {
return this.submitterDefaultFields 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() this.save()
} }
} }
Loaded 100 of 322 files, more files were not shown because too many files have changed in this diff. Show more