Compare commits

..

217 Commits

Author SHA1 Message Date
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
Alex Turchyn 05d578d88f Merge from docusealco/wip 2024-10-28 14:44:22 +02:00
Pete Matsyburka 8773b30b10 adjust auto migration 2024-10-27 14:26:52 +02:00
Pete Matsyburka 1b636c5878 fix completed submitters 2024-10-26 19:46:22 +03:00
Pete Matsyburka 46e948ab99 download combined completed 2024-10-26 19:30:23 +03:00
Pete Matsyburka 889103aa5e use com 2024-10-26 09:54:41 +03:00
Pete Matsyburka 558a14a5f2 fix email validate 2024-10-25 22:17:23 +03:00
Pete Matsyburka 72bbdb38bf mask key 2024-10-25 16:17:57 +03:00
Pete Matsyburka 3975a03cb4 adjust from email 2024-10-24 23:32:36 +03:00
Pete Matsyburka e398327fb7 refactor 2024-10-24 20:27:29 +03:00
Oleksandr Turchyn 9266694d65 add integration users page 2024-10-24 20:09:57 +03:00
Pete Matsyburka 350c4b1c2a do not submit invalid field value 2024-10-24 18:45:08 +03:00
Pete Matsyburka c3a933d87d fix fields detection 2024-10-24 17:56:02 +03:00
Pete Matsyburka 970e53a5a3 remove log 2024-10-24 12:30:14 +03:00
Pete Matsyburka 520bac2ec7 add decline account preferences toggle 2024-10-24 12:15:32 +03:00
Pete Matsyburka e1eddd31c4 fix field condition 2024-10-24 12:08:10 +03:00
Oleksandr Turchyn 3c616a824c show error for cross-environment API key usage 2024-10-24 12:07:58 +03:00
Pete Matsyburka 90d86b58c2 adjust migration 2024-10-23 22:57:33 +03:00
Pete Matsyburka 4332b4ebb8 fix specs 2024-10-23 12:01:55 +03:00
Oleksandr Turchyn e2f930f6f7 add completed submitters and documents 2024-10-23 12:01:43 +03:00
Pete Matsyburka c303159c63 adjust email interceptor 2024-10-23 10:46:53 +03:00
Pete Matsyburka c1ce2bec00 fix update name 2024-10-23 10:45:30 +03:00
Oleksandr Turchyn 3d2046f227 add validation for submitters count 2024-10-22 22:27:11 +03:00
Pete Matsyburka babaf1aa48 adjust error message 2024-10-22 18:18:14 +03:00
Pete Matsyburka 3f03d343ca Revert "dry run email validation"
This reverts commit 910490528d.
2024-10-22 17:41:25 +03:00
Pete Matsyburka 110cdb5123 fix specs 2024-10-22 15:56:59 +03:00
Pete Matsyburka 440d1114be fix email validation 2024-10-22 09:58:07 +03:00
Pete Matsyburka 9032517d4e smtp settings 2024-10-21 23:40:56 +03:00
Pete Matsyburka 7c71fefc47 optimize query 2024-10-21 14:34:11 +03:00
Pete Matsyburka e8bd8ff76f fix cdn url 2024-10-21 12:33:36 +03:00
Pete Matsyburka fed420cae1 Merge pull request #39 from docusealco/wip 2024-10-21 12:15:46 +03:00
Pete Matsyburka 910490528d dry run email validation 2024-10-20 15:44:24 +03:00
Alex Turchyn ab810be250 validate email format 2024-10-20 15:42:20 +03:00
Alex Turchyn 178ee47093 add more specs 2024-10-20 15:26:14 +03:00
Alex Turchyn 6f90147a11 validate the uniqueness of submitter roles 2024-10-20 15:26:04 +03:00
Pete Matsyburka 3ebaacbb7a locale date format 2024-10-20 09:48:39 +03:00
Pete Matsyburka 64b39b53a2 fix com from 2024-10-19 10:09:53 +03:00
Pete Matsyburka 2948910c25 fix com selector 2024-10-19 09:41:13 +03:00
Pete Matsyburka 1d20c79cf4 com host 2024-10-18 23:15:55 +03:00
Pete Matsyburka 48f3b13f3e adjust smtp from 2024-10-18 23:07:40 +03:00
Pete Matsyburka 1fc1e9af16 fix font size 2024-10-18 17:48:31 +03:00
Pete Matsyburka ce70016603 update rails 2024-10-16 23:58:37 +03:00
Pete Matsyburka e0e79e343d update email 2024-10-16 12:16:20 +03:00
Pete Matsyburka 6568f81ee9 adjust pdf generation 2024-10-15 20:58:08 +03:00
Oleksandr Turchyn 386f130d7a allow to paste date in date field 2024-10-15 19:31:25 +03:00
Pete Matsyburka 3ee41975fd add documents preview endpoint 2024-10-15 19:31:24 +03:00
Pete Matsyburka d9f314898b with style false 2024-10-14 12:28:26 +03:00
Pete Matsyburka 4fc7412cac fix ios 18 heif images 2024-10-14 12:28:26 +03:00
Pete Matsyburka 9adbfc52c2 fix signing date format 2024-10-14 12:28:26 +03:00
Pete Matsyburka 1ad8d9196a optimize phone step 2024-10-14 12:28:26 +03:00
Oleksandr Turchyn e9172392b5 add the phone code selector 2024-10-14 12:28:26 +03:00
Pete Matsyburka d6d94de328 fix submit form step 2024-10-14 12:28:26 +03:00
Alex Turchyn 77261f457c fix selector colors 2024-10-14 12:28:26 +03:00
Pete Matsyburka c258915031 fix z index 2024-10-14 12:28:26 +03:00
Pete Matsyburka c1db8bdd3b rename job 2024-10-14 12:28:26 +03:00
Pete Matsyburka c700484ce8 add reuse signature config 2024-10-14 12:28:26 +03:00
Pete Matsyburka b62bd200ed fix layout 2024-10-14 12:28:26 +03:00
Pete Matsyburka a164fe8dd8 fix sign 2024-10-14 12:28:26 +03:00
Pete Matsyburka bb40e872b3 fix custom message 2024-10-14 12:28:26 +03:00
Pete Matsyburka bab80e9488 fix image preview format 2024-10-14 12:28:26 +03:00
Pete Matsyburka d090d79ebf process images async 2024-10-14 12:28:26 +03:00
Pete Matsyburka 2299ba4e29 fix clear button 2024-10-14 12:28:26 +03:00
Pete Matsyburka d6474f0e6e adjust submitter edit 2024-10-14 12:28:26 +03:00
Oleksandr Turchyn e0a297cf81 add ability to edit submitter details 2024-10-14 12:28:26 +03:00
Pete Matsyburka 40f5fa4217 add recurrent payment 2024-10-14 12:28:26 +03:00
Oleksandr Turchyn 94e552d170 improve translations 2024-10-07 15:30:39 +03:00
Pete Matsyburka cf1b0a4c16 add noverify smtp option 2024-10-07 15:30:39 +03:00
Pete Matsyburka 9aa0a8ecbf skip autocorrect tld 2024-10-07 15:30:39 +03:00
Pete Matsyburka fd4bef1706 fix i18n 2024-10-07 15:30:39 +03:00
Pete Matsyburka d32b0e6d20 add completed id 2024-10-07 15:30:39 +03:00
Pete Matsyburka 4a846be214 fix js 2024-10-07 15:30:39 +03:00
Pete Matsyburka 3dd3877ea9 update deps 2024-10-07 15:30:39 +03:00
Pete Matsyburka b4dba0d809 fix smtp without password 2024-10-07 15:30:39 +03:00
Pete Matsyburka dea80a82d8 fix pagination 2024-10-07 15:30:39 +03:00
Pete Matsyburka eefa994724 rails 7.2 2024-10-07 15:30:39 +03:00
Pete Matsyburka 35cce24539 adjust audit values 2024-10-07 15:30:39 +03:00
Pete Matsyburka e6c8203959 fix variables 2024-10-07 15:30:39 +03:00
Oleksandr Turchyn 9479771fc8 fix i18n 2024-10-07 15:30:39 +03:00
Pete Matsyburka 711427521c fix file url performance 2024-10-07 15:30:39 +03:00
Alex Turchyn 5dae742361 add I18n invitation email 2024-10-07 15:30:39 +03:00
Pete Matsyburka d8ccd4beee i18n signature 2024-10-07 15:30:39 +03:00
Pete Matsyburka c532a0115d remove audit log file links 2024-10-07 15:30:39 +03:00
Alex Turchyn c9c3364764 add matcher 2024-10-07 15:30:39 +03:00
Alex Turchyn 1ff23d7c40 add I18n Audit log 2024-10-07 15:30:39 +03:00
Pete Matsyburka 95e9b34033 fix sign field name 2024-10-07 15:30:39 +03:00
Pete Matsyburka 6b42dcf91b adjust combined pdf sign reason 2024-10-07 15:30:39 +03:00
Pete Matsyburka 28962c949b configure audit log values 2024-10-07 15:30:39 +03:00
Pete Matsyburka 4ecbbf8410 update rubocop 2024-09-30 12:13:07 +03:00
Pete Matsyburka 990b35be18 i18n emails 2024-09-30 12:13:07 +03:00
Pete Matsyburka 8002764855 update readme 2024-09-30 12:13:07 +03:00
Pete Matsyburka b80982658c fix i18n 2024-09-30 12:13:07 +03:00
Pete Matsyburka 443ac23c29 fix account form 2024-09-30 12:13:07 +03:00
Pete Matsyburka 37bb987e3e fix i18n 2024-09-30 12:13:07 +03:00
Alex Turchyn c7b08e8d30 add languages 2024-09-30 12:13:07 +03:00
Pete Matsyburka dd502e8cef remove unused code 2024-09-30 12:13:07 +03:00
Pete Matsyburka a554182e57 fix html modal 2024-09-30 12:13:07 +03:00
Pete Matsyburka 0c039ee64f fix tempalte clone roles 2024-09-30 12:13:07 +03:00
Pete Matsyburka c6b004f107 fix submitter name 2024-09-30 12:13:07 +03:00
Pete Matsyburka 2fcb08d27d await for completion when order enforced 2024-09-30 12:13:07 +03:00
Pete Matsyburka 75fe5ce381 fix custom certs 2024-09-30 12:13:07 +03:00
Pete Matsyburka fd7d0eaf53 enforce order toggle 2024-09-30 12:13:07 +03:00
Pete Matsyburka 7e537e4232 adjust signing certs 2024-09-30 12:13:07 +03:00
Pete Matsyburka a892975010 adjust download documents 2024-09-30 12:13:07 +03:00
Alex Turchyn 05ec92186b fix i18n 2024-09-30 12:13:07 +03:00
Oleksandr Turchyn 8b035f80b3 fix i18n typos 2024-09-23 11:45:07 +03:00
Pete Matsyburka c717293306 fix form preview 2024-09-23 11:45:07 +03:00
Pete Matsyburka b3f3cf2aa6 fix label 2024-09-23 11:45:07 +03:00
Pete Matsyburka 7406334f77 adjust i18n 2024-09-23 11:45:07 +03:00
Pete Matsyburka 3e1350a221 upgrade puma 2024-09-23 11:45:07 +03:00
Pete Matsyburka 961e0a1c89 fix i18n 2024-09-23 11:45:07 +03:00
Pete Matsyburka 2ce9bbecb9 fix cells field 2024-09-23 11:45:07 +03:00
Pete Matsyburka 704da3a2ec adjust email edit button 2024-09-23 11:45:07 +03:00
Pete Matsyburka 2f530ccfca i18n emails 2024-09-23 11:45:07 +03:00
Pete Matsyburka e6c20f5ff6 adjust submit button 2024-09-23 11:45:07 +03:00
Pete Matsyburka d267978437 set emails copy duration 2024-09-23 11:45:07 +03:00
Oleksandr Turchyn e9d728fa4a add i18n 2024-09-23 11:45:07 +03:00
Pete Matsyburka 3c60fb4ee6 adjust show continue 2024-09-23 11:45:07 +03:00
Oleksandr Turchyn f1e494b669 adjust wording 2024-09-23 11:45:07 +03:00
Pete Matsyburka f7a8e4102b fix scroll 2024-09-23 11:45:07 +03:00
dependabot[bot] 311c2b0e34 Bump devise-two-factor in the bundler group across 1 directory
Bumps the bundler group with 1 update in the / directory: [devise-two-factor](https://github.com/tinfoil/devise-two-factor).


Updates `devise-two-factor` from 5.0.0 to 6.0.0
- [Changelog](https://github.com/devise-two-factor/devise-two-factor/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tinfoil/devise-two-factor/compare/v5.0.0...v6.0.0)

---
updated-dependencies:
- dependency-name: devise-two-factor
  dependency-type: direct:production
  dependency-group: bundler
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-23 11:45:07 +03:00
Pete Matsyburka 5dd59f4f7c optimize image 2024-09-23 11:45:07 +03:00
Pete Matsyburka 571f5d8221 set font style 2024-09-23 11:45:07 +03:00
Pete Matsyburka 136b0eb5ed fix typo 2024-09-23 11:45:07 +03:00
345 changed files with 14881 additions and 3026 deletions
+5 -5
View File
@@ -51,22 +51,22 @@ Metrics/AbcSize:
- spec/**/*
Metrics/ModuleLength:
Max: 500
Max: 1000
Metrics/ClassLength:
Max: 500
Max: 1000
RSpec/NestedGroups:
Max: 6
RSpec/MultipleExpectations:
Max: 20
Max: 25
RSpec/ExampleLength:
Max: 40
Max: 50
RSpec/MultipleMemoizedHelpers:
Max: 6
Max: 9
Metrics/BlockNesting:
Max: 4
+2 -2
View File
@@ -41,7 +41,7 @@ ENV OPENSSL_CONF=/app/openssl_legacy.cnf
WORKDIR /app
RUN apk add --no-cache sqlite-dev libpq-dev mariadb-dev vips-dev vips-poppler poppler-utils redis vips-heif gcompat ttf-freefont && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf
RUN echo '@edge https://dl-cdn.alpinelinux.org/alpine/edge/community' >> /etc/apk/repositories && apk add --no-cache sqlite-dev libpq-dev mariadb-dev vips-dev vips-poppler poppler-utils redis libheif@edge vips-heif gcompat ttf-freefont && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf
RUN echo $'.include = /etc/ssl/openssl.cnf\n\
\n\
@@ -57,7 +57,7 @@ activate = 1' >> /app/openssl_legacy.cnf
COPY ./Gemfile ./Gemfile.lock ./
RUN apk add --no-cache build-base && bundle install && apk del build-base && rm -rf ~/.bundle /usr/local/bundle/cache && ruby -e "puts Dir['/usr/local/bundle/**/{spec,rdoc,resources/shared,resources/collation,resources/locales}']" | xargs rm -rf
RUN apk add --no-cache build-base && bundle install && apk del --no-cache build-base && rm -rf ~/.bundle /usr/local/bundle/cache && ruby -e "puts Dir['/usr/local/bundle/**/{spec,rdoc,resources/shared,resources/collation,resources/locales}']" | xargs rm -rf
COPY ./bin ./bin
COPY ./app ./app
+1 -2
View File
@@ -23,7 +23,6 @@ gem 'image_processing'
gem 'jwt'
gem 'lograge'
gem 'mysql2', require: false
gem 'net-smtp', '0.4.0'
gem 'oj'
gem 'pagy'
gem 'pg', require: false
@@ -40,7 +39,7 @@ gem 'ruby-vips'
gem 'rubyXL'
gem 'shakapacker'
gem 'sidekiq'
gem 'sqlite3', require: false, force_ruby_platform: true
gem 'sqlite3', require: false
gem 'strip_attributes'
gem 'turbo-rails'
gem 'twitter_cldr', require: false
+192 -189
View File
@@ -1,105 +1,101 @@
GEM
remote: https://rubygems.org/
specs:
actioncable (7.1.3.4)
actionpack (= 7.1.3.4)
activesupport (= 7.1.3.4)
actioncable (7.2.1.1)
actionpack (= 7.2.1.1)
activesupport (= 7.2.1.1)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6)
actionmailbox (7.1.3.4)
actionpack (= 7.1.3.4)
activejob (= 7.1.3.4)
activerecord (= 7.1.3.4)
activestorage (= 7.1.3.4)
activesupport (= 7.1.3.4)
mail (>= 2.7.1)
net-imap
net-pop
net-smtp
actionmailer (7.1.3.4)
actionpack (= 7.1.3.4)
actionview (= 7.1.3.4)
activejob (= 7.1.3.4)
activesupport (= 7.1.3.4)
mail (~> 2.5, >= 2.5.4)
net-imap
net-pop
net-smtp
actionmailbox (7.2.1.1)
actionpack (= 7.2.1.1)
activejob (= 7.2.1.1)
activerecord (= 7.2.1.1)
activestorage (= 7.2.1.1)
activesupport (= 7.2.1.1)
mail (>= 2.8.0)
actionmailer (7.2.1.1)
actionpack (= 7.2.1.1)
actionview (= 7.2.1.1)
activejob (= 7.2.1.1)
activesupport (= 7.2.1.1)
mail (>= 2.8.0)
rails-dom-testing (~> 2.2)
actionpack (7.1.3.4)
actionview (= 7.1.3.4)
activesupport (= 7.1.3.4)
actionpack (7.2.1.1)
actionview (= 7.2.1.1)
activesupport (= 7.2.1.1)
nokogiri (>= 1.8.5)
racc
rack (>= 2.2.4)
rack (>= 2.2.4, < 3.2)
rack-session (>= 1.0.1)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
actiontext (7.1.3.4)
actionpack (= 7.1.3.4)
activerecord (= 7.1.3.4)
activestorage (= 7.1.3.4)
activesupport (= 7.1.3.4)
useragent (~> 0.16)
actiontext (7.2.1.1)
actionpack (= 7.2.1.1)
activerecord (= 7.2.1.1)
activestorage (= 7.2.1.1)
activesupport (= 7.2.1.1)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
actionview (7.1.3.4)
activesupport (= 7.1.3.4)
actionview (7.2.1.1)
activesupport (= 7.2.1.1)
builder (~> 3.1)
erubi (~> 1.11)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
activejob (7.1.3.4)
activesupport (= 7.1.3.4)
activejob (7.2.1.1)
activesupport (= 7.2.1.1)
globalid (>= 0.3.6)
activemodel (7.1.3.4)
activesupport (= 7.1.3.4)
activerecord (7.1.3.4)
activemodel (= 7.1.3.4)
activesupport (= 7.1.3.4)
activemodel (7.2.1.1)
activesupport (= 7.2.1.1)
activerecord (7.2.1.1)
activemodel (= 7.2.1.1)
activesupport (= 7.2.1.1)
timeout (>= 0.4.0)
activestorage (7.1.3.4)
actionpack (= 7.1.3.4)
activejob (= 7.1.3.4)
activerecord (= 7.1.3.4)
activesupport (= 7.1.3.4)
activestorage (7.2.1.1)
actionpack (= 7.2.1.1)
activejob (= 7.2.1.1)
activerecord (= 7.2.1.1)
activesupport (= 7.2.1.1)
marcel (~> 1.0)
activesupport (7.1.3.4)
activesupport (7.2.1.1)
base64
bigdecimal
concurrent-ruby (~> 1.0, >= 1.0.2)
concurrent-ruby (~> 1.0, >= 1.3.1)
connection_pool (>= 2.2.5)
drb
i18n (>= 1.6, < 2)
logger (>= 1.4.2)
minitest (>= 5.1)
mutex_m
tzinfo (~> 2.0)
addressable (2.8.6)
public_suffix (>= 2.0.2, < 6.0)
securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5)
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
annotate (3.2.0)
activerecord (>= 3.2, < 8.0)
rake (>= 10.4, < 14.0)
arabic-letter-connector (0.1.1)
ast (2.4.2)
aws-eventstream (1.3.0)
aws-partitions (1.913.0)
aws-sdk-core (3.191.6)
aws-partitions (1.985.0)
aws-sdk-core (3.209.1)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.8)
aws-sigv4 (~> 1.9)
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.79.0)
aws-sdk-core (~> 3, >= 3.191.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.146.1)
aws-sdk-core (~> 3, >= 3.191.0)
aws-sdk-kms (1.94.0)
aws-sdk-core (~> 3, >= 3.207.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.167.0)
aws-sdk-core (~> 3, >= 3.207.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.8)
aws-sdk-secretsmanager (1.91.0)
aws-sdk-core (~> 3, >= 3.191.0)
aws-sigv4 (~> 1.1)
aws-sigv4 (1.8.0)
aws-sigv4 (~> 1.5)
aws-sdk-secretsmanager (1.108.0)
aws-sdk-core (~> 3, >= 3.207.0)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.10.0)
aws-eventstream (~> 1, >= 1.0.2)
azure-storage-blob (2.0.3)
azure-storage-common (~> 2.0)
@@ -120,14 +116,14 @@ GEM
smart_properties
bigdecimal (3.1.8)
bindex (0.8.1)
bootsnap (1.18.3)
bootsnap (1.18.4)
msgpack (~> 1.2)
builder (3.3.0)
bullet (7.1.6)
bullet (7.2.0)
activesupport (>= 3.0.0)
uniform_notifier (~> 1.11)
camertron-eprun (1.1.1)
cancancan (3.5.0)
cancancan (3.6.1)
capybara (3.40.0)
addressable
matrix
@@ -137,7 +133,8 @@ GEM
rack-test (>= 0.6.3)
regexp_parser (>= 1.5, < 3.0)
xpath (~> 3.2)
childprocess (5.0.0)
childprocess (5.1.0)
logger (~> 1.5)
chunky_png (1.4.0)
cldr-plurals-runtime-rb (1.1.0)
cmdparse (3.0.7)
@@ -148,12 +145,12 @@ GEM
bigdecimal
rexml
crass (1.0.6)
css_parser (1.17.1)
css_parser (1.19.0)
addressable
csv (3.3.0)
cuprite (0.15)
cuprite (0.15.1)
capybara (~> 3.0)
ferrum (~> 0.14.0)
ferrum (~> 0.15.0)
date (3.3.4)
debug (1.9.2)
irb (~> 1.10)
@@ -165,7 +162,7 @@ GEM
railties (>= 4.1.0)
responders
warden (~> 1.2.3)
devise-two-factor (5.0.0)
devise-two-factor (6.0.0)
activesupport (~> 7.0)
devise (~> 4.0)
railties (~> 7.0)
@@ -173,26 +170,26 @@ GEM
diff-lcs (1.5.1)
digest-crc (0.6.5)
rake (>= 12.0.0, < 14.0.0)
docile (1.4.0)
dotenv (3.1.0)
docile (1.4.1)
dotenv (3.1.4)
drb (2.2.1)
email_typo (0.2.3)
erb_lint (0.5.0)
erb_lint (0.6.0)
activesupport
better_html (>= 2.0.1)
parser (>= 2.7.1.4)
rainbow
rubocop
rubocop (>= 1)
smart_properties
erubi (1.13.0)
factory_bot (6.4.6)
factory_bot (6.5.0)
activesupport (>= 5.0.0)
factory_bot_rails (6.4.3)
factory_bot (~> 6.4)
railties (>= 5.0.0)
faker (3.3.1)
faker (3.4.2)
i18n (>= 1.8.11, < 2)
faraday (1.10.3)
faraday (1.10.4)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
@@ -212,93 +209,96 @@ GEM
faraday-httpclient (1.0.1)
faraday-multipart (1.0.4)
multipart-post (~> 2)
faraday-net_http (1.0.1)
faraday-net_http (1.0.2)
faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0)
faraday-rack (1.0.0)
faraday-retry (1.0.3)
faraday_middleware (1.2.0)
faraday_middleware (1.2.1)
faraday (~> 1.0)
ferrum (0.14)
ferrum (0.15)
addressable (~> 2.5)
concurrent-ruby (~> 1.1)
webrick (~> 1.7)
websocket-driver (>= 0.6, < 0.8)
ffi (1.16.3)
websocket-driver (~> 0.7)
ffi (1.17.0)
ffi (1.17.0-aarch64-linux-gnu)
ffi (1.17.0-x86_64-linux-gnu)
geom2d (0.4.1)
globalid (1.2.1)
activesupport (>= 6.1)
google-apis-core (0.14.1)
google-apis-core (0.15.1)
addressable (~> 2.5, >= 2.5.1)
googleauth (~> 1.9)
httpclient (>= 2.8.1, < 3.a)
httpclient (>= 2.8.3, < 3.a)
mini_mime (~> 1.0)
mutex_m
representable (~> 3.0)
retriable (>= 2.0, < 4.a)
rexml
google-apis-iamcredentials_v1 (0.20.0)
google-apis-core (>= 0.14.0, < 2.a)
google-apis-storage_v1 (0.37.0)
google-apis-core (>= 0.14.0, < 2.a)
google-cloud-core (1.7.0)
google-apis-iamcredentials_v1 (0.21.0)
google-apis-core (>= 0.15.0, < 2.a)
google-apis-storage_v1 (0.46.0)
google-apis-core (>= 0.15.0, < 2.a)
google-cloud-core (1.7.1)
google-cloud-env (>= 1.0, < 3.a)
google-cloud-errors (~> 1.0)
google-cloud-env (2.1.1)
google-cloud-env (2.2.1)
faraday (>= 1.0, < 3.a)
google-cloud-errors (1.4.0)
google-cloud-storage (1.49.0)
google-cloud-storage (1.52.0)
addressable (~> 2.8)
digest-crc (~> 0.4)
google-apis-core (~> 0.13)
google-apis-iamcredentials_v1 (~> 0.18)
google-apis-storage_v1 (~> 0.33)
google-apis-storage_v1 (~> 0.38)
google-cloud-core (~> 1.6)
googleauth (~> 1.9)
mini_mime (~> 1.0)
googleauth (1.11.0)
googleauth (1.11.1)
faraday (>= 1.0, < 3.a)
google-cloud-env (~> 2.1)
jwt (>= 1.4, < 3.0)
multi_json (~> 1.11)
os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a)
hashdiff (1.1.0)
hexapdf (0.42.0)
hashdiff (1.1.1)
hexapdf (0.47.0)
cmdparse (~> 3.0, >= 3.0.3)
geom2d (~> 0.4, >= 0.4.1)
openssl (>= 2.2.1)
htmlentities (4.3.4)
httpclient (2.8.3)
i18n (1.14.5)
i18n (1.14.6)
concurrent-ruby (~> 1.0)
image_processing (1.12.2)
image_processing (1.13.0)
mini_magick (>= 4.9.5, < 5)
ruby-vips (>= 2.0.17, < 3)
io-console (0.7.2)
irb (1.13.1)
irb (1.14.1)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
jmespath (1.6.2)
json (2.7.2)
jwt (2.8.1)
jwt (2.9.3)
base64
language_server-protocol (3.17.0.3)
launchy (3.0.0)
launchy (3.0.1)
addressable (~> 2.8)
childprocess (~> 5.0)
letter_opener (1.10.0)
launchy (>= 2.2, < 4)
letter_opener_web (2.0.0)
actionmailer (>= 5.2)
letter_opener (~> 1.7)
railties (>= 5.2)
letter_opener_web (3.0.0)
actionmailer (>= 6.1)
letter_opener (~> 1.9)
railties (>= 6.1)
rexml
logger (1.6.1)
lograge (0.14.0)
actionpack (>= 4)
activesupport (>= 4)
railties (>= 4)
request_store (~> 1.0)
loofah (2.22.0)
loofah (2.23.1)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
mail (2.8.1)
@@ -309,49 +309,51 @@ GEM
marcel (1.0.4)
matrix (0.4.2)
method_source (1.1.0)
mini_magick (4.12.0)
mini_magick (4.13.2)
mini_mime (1.1.5)
mini_portile2 (2.8.7)
minitest (5.24.1)
msgpack (1.7.2)
mini_portile2 (2.8.8)
minitest (5.25.1)
msgpack (1.7.3)
multi_json (1.15.0)
multipart-post (2.4.0)
multipart-post (2.4.1)
mutex_m (0.2.0)
mysql2 (0.5.6)
net-http-persistent (4.0.2)
net-http-persistent (4.0.4)
connection_pool (~> 2.2)
net-imap (0.4.14)
net-imap (0.4.17)
date
net-protocol
net-pop (0.1.2)
net-protocol
net-protocol (0.2.2)
timeout
net-smtp (0.4.0)
net-smtp (0.5.0)
net-protocol
nio4r (2.7.1)
nokogiri (1.16.7)
nio4r (2.7.3)
nokogiri (1.16.8)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
nokogiri (1.16.7-aarch64-linux)
nokogiri (1.16.8-aarch64-linux)
racc (~> 1.4)
nokogiri (1.16.7-x86_64-linux)
nokogiri (1.16.8-x86_64-linux)
racc (~> 1.4)
oj (3.16.3)
oj (3.16.6)
bigdecimal (>= 3.0)
ostruct (>= 0.2)
openssl (3.2.0)
orm_adapter (0.5.0)
os (1.1.4)
ostruct (0.6.0)
package_json (0.1.0)
pagy (8.1.2)
parallel (1.25.1)
parser (3.3.4.0)
pagy (9.1.0)
parallel (1.26.3)
parser (3.3.5.0)
ast (~> 2.4.1)
racc
pg (1.5.6)
premailer (1.23.0)
pg (1.5.8)
premailer (1.27.0)
addressable
css_parser (>= 1.12.0)
css_parser (>= 1.19.0)
htmlentities (>= 4.0.0)
premailer-rails (1.12.0)
actionmailer (>= 3)
@@ -362,15 +364,15 @@ GEM
pry (0.14.2)
coderay (~> 1.1)
method_source (~> 1.0)
pry-rails (0.3.9)
pry (>= 0.10.4)
pry-rails (0.3.11)
pry (>= 0.13.0)
psych (5.1.2)
stringio
public_suffix (5.0.5)
puma (6.4.2)
public_suffix (6.0.1)
puma (6.4.3)
nio4r (~> 2.0)
racc (1.8.1)
rack (3.1.7)
rack (3.1.8)
rack-proxy (0.7.7)
rack
rack-session (2.0.0)
@@ -380,27 +382,27 @@ GEM
rackup (2.1.0)
rack (>= 3)
webrick (~> 1.8)
rails (7.1.3.4)
actioncable (= 7.1.3.4)
actionmailbox (= 7.1.3.4)
actionmailer (= 7.1.3.4)
actionpack (= 7.1.3.4)
actiontext (= 7.1.3.4)
actionview (= 7.1.3.4)
activejob (= 7.1.3.4)
activemodel (= 7.1.3.4)
activerecord (= 7.1.3.4)
activestorage (= 7.1.3.4)
activesupport (= 7.1.3.4)
rails (7.2.1.1)
actioncable (= 7.2.1.1)
actionmailbox (= 7.2.1.1)
actionmailer (= 7.2.1.1)
actionpack (= 7.2.1.1)
actiontext (= 7.2.1.1)
actionview (= 7.2.1.1)
activejob (= 7.2.1.1)
activemodel (= 7.2.1.1)
activerecord (= 7.2.1.1)
activestorage (= 7.2.1.1)
activesupport (= 7.2.1.1)
bundler (>= 1.15.0)
railties (= 7.1.3.4)
railties (= 7.2.1.1)
rails-dom-testing (2.2.0)
activesupport (>= 5.0.0)
minitest
nokogiri (>= 1.6)
rails-html-sanitizer (1.6.0)
rails-html-sanitizer (1.6.1)
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)
i18n (>= 0.7, < 2)
railties (>= 6.0.0, < 8)
@@ -408,100 +410,101 @@ GEM
actionview (> 3.1)
activesupport (> 3.1)
railties (> 3.1)
railties (7.1.3.4)
actionpack (= 7.1.3.4)
activesupport (= 7.1.3.4)
irb
railties (7.2.1.1)
actionpack (= 7.2.1.1)
activesupport (= 7.2.1.1)
irb (~> 1.13)
rackup (>= 1.0.0)
rake (>= 12.2)
thor (~> 1.0, >= 1.2.2)
zeitwerk (~> 2.6)
rainbow (3.1.1)
rake (13.2.1)
rdoc (6.6.3.1)
rdoc (6.7.0)
psych (>= 4.0.0)
redis-client (0.22.2)
connection_pool
regexp_parser (2.9.2)
reline (0.5.7)
reline (0.5.10)
io-console (~> 0.5)
representable (3.2.0)
declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
request_store (1.6.0)
request_store (1.7.0)
rack (>= 1.4)
responders (3.1.1)
actionpack (>= 5.2)
railties (>= 5.2)
retriable (3.1.2)
rexml (3.3.6)
strscan
rexml (3.3.9)
rotp (6.3.0)
rqrcode (2.2.0)
chunky_png (~> 1.0)
rqrcode_core (~> 1.0)
rqrcode_core (1.2.0)
rspec-core (3.13.0)
rspec-core (3.13.1)
rspec-support (~> 3.13.0)
rspec-expectations (3.13.0)
rspec-expectations (3.13.3)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-mocks (3.13.0)
rspec-mocks (3.13.2)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-rails (6.1.2)
actionpack (>= 6.1)
activesupport (>= 6.1)
railties (>= 6.1)
rspec-rails (7.0.1)
actionpack (>= 7.0)
activesupport (>= 7.0)
railties (>= 7.0)
rspec-core (~> 3.13)
rspec-expectations (~> 3.13)
rspec-mocks (~> 3.13)
rspec-support (~> 3.13)
rspec-support (3.13.1)
rubocop (1.65.0)
rubocop (1.66.1)
json (~> 2.3)
language_server-protocol (>= 3.17.0)
parallel (~> 1.10)
parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 2.4, < 3.0)
rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.31.1, < 2.0)
rubocop-ast (>= 1.32.2, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.31.3)
rubocop-ast (1.32.3)
parser (>= 3.3.1.0)
rubocop-performance (1.21.0)
rubocop-performance (1.22.1)
rubocop (>= 1.48.1, < 2.0)
rubocop-ast (>= 1.31.1, < 2.0)
rubocop-rails (2.24.1)
rubocop-rails (2.26.2)
activesupport (>= 4.2.0)
rack (>= 1.1)
rubocop (>= 1.33.0, < 2.0)
rubocop (>= 1.52.0, < 2.0)
rubocop-ast (>= 1.31.1, < 2.0)
rubocop-rspec (3.0.3)
rubocop-rspec (3.1.0)
rubocop (~> 1.61)
ruby-progressbar (1.13.0)
ruby-vips (2.2.1)
ruby-vips (2.2.2)
ffi (~> 1.12)
logger
ruby2_keywords (0.0.5)
rubyXL (3.4.27)
nokogiri (>= 1.10.8)
rubyzip (>= 1.3.0)
rubyzip (2.3.2)
securerandom (0.3.1)
semantic_range (3.0.0)
shakapacker (8.0.0)
shakapacker (8.0.2)
activesupport (>= 5.2)
package_json
rack-proxy (>= 0.6.1)
railties (>= 5.2)
semantic_range (>= 2.3.0)
sidekiq (7.2.4)
sidekiq (7.3.2)
concurrent-ruby (< 2)
connection_pool (>= 2.3.0)
logger
rack (>= 2.2.4)
redis-client (>= 0.19.0)
redis-client (>= 0.22.2)
signet (0.19.0)
addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a)
@@ -511,33 +514,34 @@ GEM
docile (~> 1.1)
simplecov-html (~> 0.11)
simplecov_json_formatter (~> 0.1)
simplecov-html (0.12.3)
simplecov-html (0.13.1)
simplecov_json_formatter (0.1.4)
smart_properties (1.17.0)
sqlite3 (1.7.3)
sqlite3 (2.1.0)
mini_portile2 (~> 2.8.0)
stringio (3.1.0)
sqlite3 (2.1.0-aarch64-linux-gnu)
sqlite3 (2.1.0-x86_64-linux-gnu)
stringio (3.1.1)
strip_attributes (1.13.0)
activemodel (>= 3.0, < 8.0)
strscan (3.1.0)
thor (1.3.1)
thor (1.3.2)
timeout (0.4.1)
trailblazer-option (0.1.2)
turbo-rails (2.0.5)
turbo-rails (2.0.10)
actionpack (>= 6.0.0)
activejob (>= 6.0.0)
railties (>= 6.0.0)
twitter_cldr (6.12.0)
twitter_cldr (6.12.1)
camertron-eprun
cldr-plurals-runtime-rb (~> 1.1)
tzinfo
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
tzinfo-data (1.2024.1)
tzinfo-data (1.2024.2)
tzinfo (>= 1.0.0)
uber (0.1.0)
unicode-display_width (2.5.0)
unicode-display_width (2.6.0)
uniform_notifier (1.16.0)
useragent (0.16.10)
warden (1.2.9)
rack (>= 2.0.9)
web-console (4.2.1)
@@ -545,17 +549,17 @@ GEM
activemodel (>= 6.0.0)
bindex (>= 0.4.0)
railties (>= 6.0.0)
webmock (3.23.0)
webmock (3.24.0)
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
webrick (1.8.1)
webrick (1.8.2)
websocket-driver (0.7.6)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
xpath (3.2.0)
nokogiri (~> 1.8)
zeitwerk (2.6.14)
zeitwerk (2.7.0)
PLATFORMS
aarch64-linux
@@ -592,7 +596,6 @@ DEPENDENCIES
letter_opener_web
lograge
mysql2
net-smtp (= 0.4.0)
oj
pagy
pg
+14 -14
View File
@@ -1,6 +1,6 @@
<h1 align="center" style="border-bottom: none">
<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" />
<br>
</a>
@@ -25,12 +25,12 @@
DocuSeal is an open source platform that provides secure and efficient digital document signing and processing. Create PDF forms to have them filled and signed online on any device with an easy-to-use, mobile-optimized web tool.
</p>
<h2 align="center">
<a href="https://demo.docuseal.co">✨ Live Demo</a>
<a href="https://demo.docuseal.tech">✨ Live Demo</a>
<span>|</span>
<a href="https://docuseal.co/sign_up">☁️ Try in Cloud</a>
<a href="https://docuseal.com/sign_up">☁️ Try in Cloud</a>
</h2>
[![Demo](https://github.com/docusealco/docuseal/assets/5418788/d8703ea3-361a-423f-8bfe-eff1bd9dbe14)](https://demo.docuseal.co)
[![Demo](https://github.com/docusealco/docuseal/assets/5418788/d8703ea3-361a-423f-8bfe-eff1bd9dbe14)](https://demo.docuseal.tech)
## Features
- PDF form fields builder (WYSIWYG)
@@ -42,7 +42,7 @@ DocuSeal is an open source platform that provides secure and efficient digital d
- PDF signature verification
- Users management
- Mobile-optimized
- Signing available in 13 languages
- 6 UI languages with signing available in 13 languages
- API and Webhooks for integrations
- Easy to deploy in minutes
@@ -54,11 +54,11 @@ DocuSeal is an open source platform that provides secure and efficient digital d
- Conditional fields and formulas
- Bulk send with CSV, XLSX spreadsheet import
- SSO / SAML
- Template creation with HTML API ([Guide](https://www.docuseal.co/guides/create-pdf-document-fillable-form-with-html-api))
- Template creation with PDF or DOCX and field tags API ([Guide](https://www.docuseal.co/guides/use-embedded-text-field-tags-in-the-pdf-to-create-a-fillable-form))
- Embedded signing form ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue), [Angular](https://github.com/docusealco/docuseal-angular) or [JavaScript](https://www.docuseal.co/docs/embedded))
- Embedded document form builder ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue), [Angular](https://github.com/docusealco/docuseal-angular) or [JavaScript](https://www.docuseal.co/docs/embedded))
- [Learn more](https://www.docuseal.co/pricing)
- 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.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.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.com/docs/embedded))
- [Learn more](https://www.docuseal.com/pricing)
## 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.
[Book a Meeting](https://www.docuseal.co/contact)
[Book a Meeting](https://www.docuseal.com/contact)
## License
@@ -102,6 +102,6 @@ Unless otherwise noted, all files © 2023 DocuSeal LLC.
## Tools
- [Signature Maker](https://www.docuseal.co/online-signature)
- [Sign Document Online](https://www.docuseal.co/sign-documents-online)
- [Fill PDF Online](https://www.docuseal.co/fill-pdf)
- [Signature Maker](https://www.docuseal.com/online-signature)
- [Sign Document Online](https://www.docuseal.com/sign-documents-online)
- [Fill PDF Online](https://www.docuseal.com/fill-pdf)
+1 -1
View File
@@ -1,6 +1,6 @@
# Reporting a Vulnerability
If you come across any security concern or vulnarability, please report the information via email to security@docuseal.co instead of opening a GitHub issue. We will promptly respond and will collaborate with you to validate the issue, and resolve it ASAP.
If you come across any security concern or vulnarability, please report the information via email to security@docuseal.com instead of opening a GitHub issue. We will promptly respond and will collaborate with you to validate the issue, and resolve it ASAP.
**We have a bug bounty program to reward security researchers.**
@@ -8,6 +8,7 @@ class AccountConfigsController < ApplicationController
AccountConfig::ALLOW_TYPED_SIGNATURE,
AccountConfig::FORCE_MFA,
AccountConfig::ALLOW_TO_RESUBMIT,
AccountConfig::ALLOW_TO_DECLINE_KEY,
AccountConfig::FORM_PREFILL_SIGNATURE_KEY,
AccountConfig::ESIGNING_PREFERENCE_KEY,
AccountConfig::FORM_WITH_CONFETTI_KEY,
@@ -16,7 +17,8 @@ class AccountConfigsController < ApplicationController
AccountConfig::FLATTEN_RESULT_PDF_KEY,
AccountConfig::WITH_SIGNATURE_ID,
AccountConfig::COMBINE_PDF_RESULT_KEY,
AccountConfig::REQUIRE_SIGNING_REASON_KEY
AccountConfig::REQUIRE_SIGNING_REASON_KEY,
AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY
].freeze
InvalidKey = Class.new(StandardError)
+23 -18
View File
@@ -4,11 +4,11 @@ class AccountsController < ApplicationController
LOCALE_OPTIONS = {
'en-US' => 'English (United States)',
'en-GB' => 'English (United Kingdom)',
'fr-FR' => 'French (France)',
'es-ES' => 'Spanish (Spain)',
'pt-PT' => 'Portuguese (Portugal)',
'de-DE' => 'German (Germany)',
'it-IT' => 'Italian (Italy)'
'fr-FR' => 'Français',
'es-ES' => 'Español',
'pt-PT' => 'Português',
'de-DE' => 'Deutsch',
'it-IT' => 'Italiano'
}.freeze
before_action :load_account
@@ -19,21 +19,25 @@ class AccountsController < ApplicationController
def update
current_account.update!(account_params)
@encrypted_config = EncryptedConfig.find_or_initialize_by(account: current_account,
key: EncryptedConfig::APP_URL_KEY)
@encrypted_config.assign_attributes(app_url_params)
unless Docuseal.multitenant?
@encrypted_config = EncryptedConfig.find_or_initialize_by(account: current_account,
key: EncryptedConfig::APP_URL_KEY)
@encrypted_config.assign_attributes(app_url_params)
unless URI.parse(@encrypted_config.value.to_s).class.in?([URI::HTTP, URI::HTTPS])
@encrypted_config.errors.add(:value, 'should be a valid URL')
unless URI.parse(@encrypted_config.value.to_s).class.in?([URI::HTTP, URI::HTTPS])
@encrypted_config.errors.add(:value, I18n.t('should_be_a_valid_url'))
return render :show, status: :unprocessable_entity
return render :show, status: :unprocessable_entity
end
@encrypted_config.save!
Docuseal.refresh_default_url_options!
end
@encrypted_config.save!
Docuseal.refresh_default_url_options!
redirect_to settings_account_path, notice: 'Account information has been updated'
with_locale do
redirect_to settings_account_path, notice: I18n.t('account_information_has_been_updated')
end
rescue ActiveRecord::RecordInvalid
render :show, status: :unprocessable_entity
end
@@ -43,11 +47,12 @@ class AccountsController < ApplicationController
true_user.update!(locked_at: Time.current)
# rubocop:disable Layout/LineLength
render turbo_stream: turbo_stream.replace(
:account_delete_button,
html: helpers.tag.p('Your account removal request will be processed within 2 weeks. ' \
'Please contact us if you want to keep your account.')
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'))
)
# rubocop:enable Layout/LineLength
end
private
@@ -38,12 +38,12 @@ module Api
private
def authorization_check!(blob)
is_authorized =
blob.attachments.all? do |a|
a.name.in?(%w[logo preview_images]) ||
(current_user && a.record.account.id == current_user.account_id) ||
!a.record.account.account_configs.find_or_initialize_by(key: AccountConfig::DOWNLOAD_LINKS_AUTH_KEY).value
end
attachment = blob.attachments.take
is_authorized = attachment.name.in?(%w[logo preview_images]) ||
(current_user && attachment.record.account.id == current_user.account_id) ||
!attachment.record.account.account_configs
.find_or_initialize_by(key: AccountConfig::DOWNLOAD_LINKS_AUTH_KEY).value
return if is_authorized
+29 -2
View File
@@ -25,9 +25,9 @@ module Api
render json: { error: 'Too many requests' }, status: :too_many_requests
end
if Rails.env.production?
unless Rails.env.development?
rescue_from CanCan::AccessDenied do |e|
render json: { error: e.message }, status: :forbidden
render json: { error: access_denied_error_message(e) }, status: :forbidden
end
rescue_from JSON::ParserError do |e|
@@ -39,6 +39,33 @@ module Api
private
def access_denied_error_message(error)
return 'Not authorized' if request.headers['X-Auth-Token'].blank?
return 'Not authorized' unless error.subject.is_a?(ActiveRecord::Base)
return 'Not authorized' unless error.subject.respond_to?(:account_id)
linked_account_record_exists =
if current_user.account.testing?
current_user.account.linked_account_accounts.where(account_type: 'testing')
.exists?(account_id: error.subject.account_id)
else
current_user.account.testing_accounts.exists?(id: error.subject.account_id)
end
return 'Not authorized' unless linked_account_record_exists
object_name = error.subject.model_name.human
id = error.subject.id
if current_user.account.testing?
"#{object_name} #{id} not found using testing API key; Use production API key to " \
"access production #{object_name.downcase.pluralize}."
else
"#{object_name} #{id} not found using production API key; Use testing API key to " \
"access testing #{object_name.downcase.pluralize}."
end
end
def paginate(relation, field: :id)
result = relation.order(field => :desc)
.limit([params.fetch(:limit, DEFAULT_LIMIT).to_i, MAX_LIMIT].min)
@@ -0,0 +1,45 @@
# frozen_string_literal: true
module Api
class SubmissionDocumentsController < ApiBaseController
load_and_authorize_resource :submission
def index
documents =
if @submission.submitters.all?(&:completed_at?)
last_submitter = @submission.submitters.max_by(&:completed_at)
if last_submitter.documents_attachments.blank?
last_submitter.documents_attachments = Submissions::EnsureResultGenerated.call(submitter)
end
last_submitter.documents_attachments
else
values_hash = Submissions::GeneratePreviewAttachments.build_values_hash(@submission)
if @submission.preview_documents.present? &&
@submission.preview_documents.all? { |s| s.metadata['values_hash'] == values_hash }
@submission.preview_documents
else
ApplicationRecord.no_touching do
@submission.preview_documents.each(&:destroy)
end
Submissions::GeneratePreviewAttachments.call(@submission, values_hash:)
end
end
ActiveRecord::Associations::Preloader.new(
records: documents,
associations: [:blob]
).call
render json: {
id: @submission.id,
documents: documents.map do |attachment|
{ name: attachment.filename.base, url: ActiveStorage::Blob.proxy_url(attachment.blob) }
end
}
end
end
end
+15 -6
View File
@@ -67,20 +67,26 @@ module Api
submissions = create_submissions(@template, params)
submissions.each do |submission|
SendSubmissionCreatedWebhookRequestJob.perform_async({ 'submission_id' => submission.id })
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
Submissions.send_signature_requests(submissions)
submissions.each do |submission|
if submission.submitters.all?(&:completed_at?) && submission.submitters.last
ProcessSubmitterCompletionJob.perform_async({ 'submitter_id' => submission.submitters.last.id })
submission.submitters.each do |submitter|
next unless submitter.completed_at?
ProcessSubmitterCompletionJob.perform_async('submitter_id' => submitter.id, 'send_invitation_email' => false)
end
end
render json: build_create_json(submissions)
rescue Submitters::NormalizeValues::BaseError, DownloadUtils::UnableToDownload => e
rescue Submitters::NormalizeValues::BaseError, Submissions::CreateFromSubmitters::BaseError,
DownloadUtils::UnableToDownload => e
Rollbar.warning(e) if defined?(Rollbar)
render json: { error: e.message }, status: :unprocessable_entity
@@ -92,7 +98,10 @@ module Api
else
@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
render json: @submission.as_json(only: %i[id archived_at])
@@ -13,7 +13,10 @@ module Api
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: {}
end
+2 -1
View File
@@ -68,7 +68,7 @@ module Api
end
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]
Submitters.send_signature_requests([@submitter])
end
@@ -120,6 +120,7 @@ module Api
end&.dig('uuid')
submitter.email = Submissions.normalize_email(attrs[:email]) if attrs.key?(:email)
submitter.name = attrs[:name] if attrs.key?(:name)
if attrs.key?(:phone)
submitter.phone = attrs[:phone].to_s.gsub(/[^0-9+]/, '')
@@ -25,7 +25,10 @@ module Api
schema_documents = Templates::CloneAttachments.call(template: cloned_template, original_template: @template)
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)
end
+7 -4
View File
@@ -65,7 +65,10 @@ module Api
@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])
end
@@ -84,7 +87,7 @@ module Api
def filter_templates(templates, params)
templates = Templates.search(templates, params[:q])
templates = params[:archived] ? templates.archived : templates.active
templates = params[:archived].in?(['true', true]) ? templates.archived : templates.active
templates = templates.where(external_id: params[:application_key]) if params[:application_key].present?
templates = templates.where(external_id: params[:external_id]) if params[:external_id].present?
templates = templates.joins(:folder).where(folder: { name: params[:folder] }) if params[:folder].present?
@@ -97,12 +100,12 @@ module Api
:name,
:external_id,
{
submitters: [%i[name uuid is_requester invite_by_uuid invite_by_uuid linked_to_uuid email]],
submitters: [%i[name uuid is_requester invite_by_uuid linked_to_uuid email]],
fields: [[:uuid, :submitter_uuid, :name, :type,
:required, :readonly, :default_value,
:title, :description,
{ preferences: {},
conditions: [%i[field_uuid value action]],
conditions: [%i[field_uuid value action operation]],
options: [%i[value uuid]],
validation: %i[message pattern],
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]]
+1 -4
View File
@@ -20,10 +20,7 @@ module Api
pdf = HexaPDF::Document.new(io: StringIO.new(file))
trusted_certs = Accounts.load_trusted_certs(current_account)
is_checksum_found = ActiveStorage::Attachment.joins(:blob)
.where(name: 'documents', record_type: 'Submitter')
.exists?(blob: { checksum: Digest::MD5.base64digest(file) })
is_checksum_found = CompletedDocument.exists?(sha256: Base64.urlsafe_encode64(Digest::SHA256.digest(file)))
render json: {
checksum_status: is_checksum_found ? 'verified' : 'not_found',
+1 -1
View File
@@ -12,6 +12,6 @@ class ApiSettingsController < ApplicationController
current_user.access_token.save!
redirect_back(fallback_location: settings_api_index_path, notice: 'API token as been updated.')
redirect_back(fallback_location: settings_api_index_path, notice: I18n.t('api_token_has_been_updated'))
end
end
+29 -1
View File
@@ -8,12 +8,14 @@ class ApplicationController < ActionController::Base
check_authorization unless: :devise_controller?
around_action :with_locale
before_action :sign_in_for_demo, if: -> { Docuseal.demo? }
before_action :maybe_redirect_to_setup, unless: :signed_in?
before_action :authenticate_user!, unless: :devise_controller?
helper_method :button_title,
:current_account,
:form_link_host,
:svg_icon
impersonates :user, with: ->(uuid) { User.find_by(uuid:) }
@@ -37,6 +39,10 @@ class ApplicationController < ActionController::Base
end
def default_url_options
if request.domain == 'docuseal.com'
return { host: 'docuseal.com', protocol: ENV['FORCE_SSL'].present? ? 'https' : 'http' }
end
Docuseal.default_url_options
end
@@ -51,7 +57,18 @@ class ApplicationController < ActionController::Base
private
def with_locale(&)
return yield unless current_account
locale = params[:lang].presence if Rails.env.development?
locale ||= current_account.locale
I18n.with_locale(locale, &)
end
def with_browser_locale(&)
return yield if I18n.locale != :'en-US' && I18n.locale != :en
locale = params[:lang].presence
locale ||= request.env['HTTP_ACCEPT_LANGUAGE'].to_s[BROWSER_LOCALE_REGEXP].to_s
@@ -79,7 +96,8 @@ class ApplicationController < ActionController::Base
redirect_to setup_index_path unless User.exists?
end
def button_title(title: 'Submit', disabled_with: 'Submitting', title_class: '', icon: nil, icon_disabled: nil)
def button_title(title: I18n.t('submit'), disabled_with: I18n.t('submitting'), title_class: '', icon: nil,
icon_disabled: nil)
render_to_string(partial: 'shared/button_title',
locals: { title:, disabled_with:, title_class:, icon:, icon_disabled: })
end
@@ -87,4 +105,14 @@ class ApplicationController < ActionController::Base
def svg_icon(icon_name, class: '')
render_to_string(partial: "icons/#{icon_name}", locals: { class: })
end
def form_link_host
Docuseal.default_url_options[:host]
end
def maybe_redirect_com
return if request.domain != 'docuseal.co'
redirect_to request.url.gsub('.co/', '.com/'), allow_other_host: true, status: :moved_permanently
end
end
+1 -1
View File
@@ -33,7 +33,7 @@ class DashboardController < ApplicationController
account_id: current_user.account_id,
key: AccountConfig::FORCE_MFA)
redirect_to mfa_setup_path, notice: 'Setup 2FA to continue'
redirect_to mfa_setup_path, notice: I18n.t('setup_2fa_to_continue')
end
def maybe_render_landing
@@ -9,9 +9,9 @@ class EmailSmtpSettingsController < ApplicationController
def create
if @encrypted_config.update(email_configs)
SettingsMailer.smtp_successful_setup(@encrypted_config.value['from_email']).deliver_now!
SettingsMailer.smtp_successful_setup(@encrypted_config.value['from_email'] || current_user.email).deliver_now!
redirect_to settings_email_index_path, notice: 'Changes have been saved'
redirect_to settings_email_index_path, notice: I18n.t('changes_have_been_saved')
else
render :index, status: :unprocessable_entity
end
+1 -1
View File
@@ -2,7 +2,7 @@
class ErrorsController < ActionController::Base
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 = [
'/templates/html',
+18 -10
View File
@@ -11,6 +11,8 @@ class EsignSettingsController < ApplicationController
end
end
prepend_before_action :maybe_redirect_com, only: %i[show]
before_action :load_encrypted_config
authorize_resource :encrypted_config, parent: false, only: %i[new create]
authorize_resource :encrypted_config, only: %i[update destroy show]
@@ -21,9 +23,9 @@ class EsignSettingsController < ApplicationController
default_pkcs = GenerateCertificate.load_pkcs(cert_data) if cert_data['cert'].present?
custom_pkcs_list = (cert_data['custom'] || []).map do |e|
{ 'pkcs' => OpenSSL::PKCS12.new(Base64.urlsafe_decode64(e['data']), e['password'].to_s),
'name' => e['name'],
'status' => e['status'] }
pkcs = e['data'].present? ? OpenSSL::PKCS12.new(Base64.urlsafe_decode64(e['data']), e['password'].to_s) : nil
{ 'pkcs' => pkcs, 'name' => e['name'], 'status' => e['status'] }
end
@pkcs_list = [
@@ -48,7 +50,7 @@ class EsignSettingsController < ApplicationController
if (@encrypted_config.value && @encrypted_config.value['custom']&.any? { |e| e['name'] == @cert_record.name }) ||
@cert_record.name == DEFAULT_CERT_NAME
@cert_record.errors.add(:name, 'already exists')
@cert_record.errors.add(:name, I18n.t('already_exists'))
return render turbo_stream: turbo_stream.replace(:modal, template: 'esign_settings/new'),
status: :unprocessable_entity
@@ -56,7 +58,7 @@ class EsignSettingsController < ApplicationController
save_new_cert!(@encrypted_config, @cert_record)
redirect_to settings_esign_path, notice: 'Certificate has been successfully added!'
redirect_to settings_esign_path, notice: I18n.t('certificate_has_been_successfully_added')
rescue OpenSSL::PKCS12::PKCS12Error => e
Rollbar.error(e) if defined?(Rollbar)
@@ -66,14 +68,20 @@ class EsignSettingsController < ApplicationController
end
def update
@encrypted_config.value['custom'].each { |e| e['status'] = 'validate' }
@encrypted_config.value['custom'].to_a.each { |e| e['status'] = 'validate' }
custom_cert_data = @encrypted_config.value['custom'].find { |e| e['name'] == params[:name] }
custom_cert_data['status'] = 'default' if custom_cert_data
custom_cert_data = @encrypted_config.value['custom'].to_a.find { |e| e['name'] == params[:name] }
if custom_cert_data
custom_cert_data['status'] = 'default'
elsif params[:name] == Docuseal::AATL_CERT_NAME
@encrypted_config.value['custom'] ||= []
@encrypted_config.value['custom'] << { 'name' => params[:name], 'status' => 'default' }
end
@encrypted_config.save!
redirect_to settings_esign_path, notice: 'Default certificate has been selected'
redirect_to settings_esign_path, notice: I18n.t('default_certificate_has_been_selected')
end
def destroy
@@ -81,7 +89,7 @@ class EsignSettingsController < ApplicationController
@encrypted_config.save!
redirect_to settings_esign_path, notice: 'Certificate has been removed'
redirect_to settings_esign_path, notice: I18n.t('certificate_has_been_removed')
end
private
+5 -5
View File
@@ -18,11 +18,11 @@ class MfaSetupController < ApplicationController
current_user.otp_required_for_login = true
current_user.save!
redirect_to settings_profile_index_path, notice: '2FA has been configured'
redirect_to settings_profile_index_path, notice: I18n.t('2fa_has_been_configured')
else
@provision_url = current_user.otp_provisioning_uri(current_user.email, issuer: Docuseal.product_name)
@error_message = 'Code is invalid'
@error_message = I18n.t('code_is_invalid')
render turbo_stream: turbo_stream.replace(:mfa_form, partial: 'mfa_setup/form'), status: :unprocessable_entity
end
@@ -32,9 +32,9 @@ class MfaSetupController < ApplicationController
if current_user.validate_and_consume_otp!(params[:otp_attempt])
current_user.update!(otp_required_for_login: false, otp_secret: nil)
redirect_to settings_profile_index_path, notice: '2FA has been removed'
redirect_to settings_profile_index_path, notice: I18n.t('2fa_has_been_removed')
else
@error_message = 'Code is invalid'
@error_message = I18n.t('code_is_invalid')
render turbo_stream: turbo_stream.replace(:modal, template: 'mfa_setup/edit'), status: :unprocessable_entity
end
@@ -43,7 +43,7 @@ class MfaSetupController < ApplicationController
private
def set_provision_url
return redirect_to root_path, alert: '2FA has been set up already' if current_user.otp_required_for_login
return redirect_to root_path, alert: I18n.t('2fa_has_been_set_up_already') if current_user.otp_required_for_login
current_user.otp_secret ||= User.generate_otp_secret
@@ -13,9 +13,9 @@ class NotificationsSettingsController < ApplicationController
def create
if @account_config.value.present? ? @account_config.save : @account_config.delete
redirect_back fallback_location: settings_notifications_path, notice: 'Changes have been saved'
redirect_back fallback_location: settings_notifications_path, notice: I18n.t('changes_have_been_saved')
else
redirect_back fallback_location: settings_notifications_path, alert: 'Unable to save'
redirect_back fallback_location: settings_notifications_path, alert: I18n.t('unable_to_save')
end
end
@@ -6,7 +6,8 @@ class PersonalizationSettingsController < ApplicationController
AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY,
AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY,
AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY,
AccountConfig::FORM_COMPLETED_MESSAGE_KEY
AccountConfig::FORM_COMPLETED_MESSAGE_KEY,
*(Docuseal.multitenant? ? [] : [AccountConfig::POLICY_LINKS_KEY])
].freeze
InvalidKey = Class.new(StandardError)
@@ -30,7 +31,7 @@ class PersonalizationSettingsController < ApplicationController
@account_config.save!
end
redirect_back(fallback_location: settings_personalization_path, notice: 'Settings have been saved.')
redirect_back(fallback_location: settings_personalization_path, notice: I18n.t('settings_have_been_saved'))
end
private
@@ -12,7 +12,8 @@ class PreviewDocumentPageController < ActionController::API
return head :not_found unless attachment
preview_image = attachment.preview_images.joins(:blob).find_by(blob: { filename: "#{params[:id]}#{FORMAT}" })
preview_image = attachment.preview_images.joins(:blob)
.find_by(blob: { filename: ["#{params[:id]}.png", "#{params[:id]}.jpg"] })
return redirect_to preview_image.url, allow_other_host: true if preview_image
+2 -2
View File
@@ -9,7 +9,7 @@ class ProfileController < ApplicationController
def update_contact
if current_user.update(contact_params)
redirect_to settings_profile_index_path, notice: 'Contact information has been updated'
redirect_to settings_profile_index_path, notice: I18n.t('contact_information_has_been_update')
else
render :index, status: :unprocessable_entity
end
@@ -18,7 +18,7 @@ class ProfileController < ApplicationController
def update_password
if current_user.update(password_params)
bypass_sign_in(current_user)
redirect_to settings_profile_index_path, notice: 'Password has been changed'
redirect_to settings_profile_index_path, notice: I18n.t('password_has_been_changed')
else
render :index, status: :unprocessable_entity
end
@@ -7,6 +7,8 @@ class SendSubmissionEmailController < ApplicationController
skip_before_action :verify_authenticity_token
skip_authorization_check
SEND_DURATION = 30.minutes
def success; end
def create
@@ -23,7 +25,10 @@ class SendSubmissionEmailController < ApplicationController
RateLimit.call("send-email-#{@submitter.id}", limit: 2, ttl: 5.minutes)
SubmitterMailer.documents_copy_email(@submitter, sig: true).deliver_later!
unless EmailEvent.exists?(tag: :submitter_documents_copy, email: @submitter.email, emailable: @submitter,
event_type: :send, created_at: SEND_DURATION.ago..Time.current)
SubmitterMailer.documents_copy_email(@submitter, sig: true).deliver_later!
end
respond_to do |f|
f.html { redirect_to success_send_submission_email_index_path }
+1 -1
View File
@@ -12,7 +12,7 @@ class SessionsController < Devise::SessionsController
Rollbar.warning('Sign in new user') if defined?(Rollbar)
return redirect_to new_registration_path(sign_up: true, user: sign_in_params.slice(:email)),
notice: 'Create a new account'
notice: I18n.t('create_a_new_account')
end
if User.exists?(email:, otp_required_for_login: true) && sign_in_params[:otp_attempt].blank?
+4 -4
View File
@@ -21,7 +21,7 @@ class SetupController < ApplicationController
@encrypted_config = EncryptedConfig.new(encrypted_config_params)
unless URI.parse(encrypted_config_params[:value].to_s).class.in?([URI::HTTP, URI::HTTPS])
@encrypted_config.errors.add(:value, 'should be a valid URL')
@encrypted_config.errors.add(:value, I18n.t('should_be_a_valid_url'))
return render :index, status: :unprocessable_entity
end
@@ -56,7 +56,7 @@ class SetupController < ApplicationController
def account_params
return {} unless params[:account]
params.require(:account).permit(:name, :timezone)
params.require(:account).permit(:name, :timezone, :locale)
end
def encrypted_config_params
@@ -66,10 +66,10 @@ class SetupController < ApplicationController
end
def redirect_to_root_if_signed
redirect_to root_path, notice: 'You are already signed in'
redirect_to root_path, notice: I18n.t('you_are_already_signed_in')
end
def ensure_first_user_not_created!
redirect_to new_user_session_path, notice: 'Please sign in.' if User.exists?
redirect_to new_user_session_path, notice: I18n.t('please_sign_in') if User.exists?
end
end
+6 -2
View File
@@ -7,6 +7,7 @@ class StartFormController < ApplicationController
skip_authorization_check
around_action :with_browser_locale, only: %i[show completed]
before_action :maybe_redirect_com, only: %i[show completed]
before_action :load_template
def show
@@ -24,7 +25,7 @@ class StartFormController < ApplicationController
redirect_to start_form_completed_path(@template.slug, email: submitter_params[:email])
else
if filter_undefined_submitters(@template).size > 1 && @submitter.new_record?
@error_message = 'Not found'
@error_message = I18n.t('not_found')
return render :show
end
@@ -37,7 +38,10 @@ class StartFormController < ApplicationController
if @submitter.save
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
redirect_to submit_form_path(@submitter.slug)
@@ -11,7 +11,7 @@ class StorageSettingsController < ApplicationController
if @encrypted_config.update(storage_configs)
LoadActiveStorageConfigs.reload
redirect_to settings_storage_index_path, notice: 'Changes have been saved'
redirect_to settings_storage_index_path, notice: I18n.t('changes_have_been_saved')
else
render :index, status: :unprocessable_entity
end
@@ -8,8 +8,18 @@ class SubmissionsArchivedController < ApplicationController
@submissions = @submissions.where.not(archived_at: nil)
.or(@submissions.where.not(templates: { archived_at: nil }))
.preload(:created_by_user, template: :author)
@submissions = Submissions.search(@submissions, params[:q], search_template: true)
@pagy, @submissions = pagy(@submissions.preload(:submitters).order(id: :desc))
@submissions = @submissions.preload(:template_accesses) unless current_user.role.in?(%w[admin superadmin])
@submissions = Submissions.search(@submissions, params[:q], search_template: true)
@submissions = Submissions::Filter.call(@submissions, current_user, params)
@submissions = if params[:completed_at_from].present? || params[:completed_at_to].present?
@submissions.order(Submitter.arel_table[:completed_at].maximum.desc)
else
@submissions.order(id: :desc)
end
@pagy, @submissions = pagy(@submissions.preload(submitters: :start_form_submission_events))
end
end
+26 -7
View File
@@ -6,9 +6,18 @@ class SubmissionsController < ApplicationController
load_and_authorize_resource :submission, only: %i[show destroy]
prepend_before_action :maybe_redirect_com, only: %i[show]
def show
@submission = Submissions.preload_with_pages(@submission)
unless @submission.submitters.all?(&:completed_at?)
ActiveRecord::Associations::Preloader.new(
records: [@submission],
associations: [submitters: :start_form_submission_events]
).call
end
render :show, layout: 'plain'
end
@@ -43,13 +52,11 @@ class SubmissionsController < ApplicationController
params: params.merge('send_completed_email' => true))
end
submissions.each do |submission|
SendSubmissionCreatedWebhookRequestJob.perform_async({ 'submission_id' => submission.id })
end
enqueue_submission_created_webhooks(@template, submissions)
Submissions.send_signature_requests(submissions)
redirect_to template_path(@template), notice: 'New recipients have been added'
redirect_to template_path(@template), notice: I18n.t('new_recipients_have_been_added')
end
def destroy
@@ -57,13 +64,16 @@ class SubmissionsController < ApplicationController
if params[:permanently].present?
@submission.destroy!
'Submission has been removed'
I18n.t('submission_has_been_removed')
else
@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
'Submission has been archived'
I18n.t('submission_has_been_archived')
end
redirect_back(fallback_location: template_path(@submission.template), notice:)
@@ -78,6 +88,15 @@ class SubmissionsController < ApplicationController
template.save!
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
params.permit(submission: { submitters: [%i[uuid email phone name]] })
end
@@ -10,11 +10,20 @@ class SubmissionsDashboardController < ApplicationController
.where(templates: { archived_at: nil })
.preload(:created_by_user, template: :author)
@submissions = @submissions.preload(:template_accesses) unless current_user.role.in?(%w[admin superadmin])
@submissions = Submissions.search(@submissions, params[:q], search_template: true)
@submissions = Submissions::Filter.call(@submissions, current_user, params)
@submissions = @submissions.pending if params[:status] == 'pending'
@submissions = @submissions.completed if params[:status] == 'completed'
@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))
end
end
@@ -53,8 +53,15 @@ class SubmissionsDownloadController < ApplicationController
end
def build_urls(submitter)
filename_format = AccountConfig.find_or_initialize_by(account_id: submitter.account_id,
key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value
Submitters.select_attachments_for_download(submitter).map do |attachment|
ActiveStorage::Blob.proxy_url(attachment.blob, expires_at: FILES_TTL.from_now.to_i)
ActiveStorage::Blob.proxy_url(
attachment.blob,
expires_at: FILES_TTL.from_now.to_i,
filename: Submitters.build_document_filename(submitter, attachment.blob, filename_format)
)
end
end
@@ -65,6 +72,13 @@ class SubmissionsDownloadController < ApplicationController
attachment = submitter.submission.combined_document_attachment
attachment ||= Submissions::GenerateCombinedAttachment.call(submitter)
ActiveStorage::Blob.proxy_url(attachment.blob, expires_at: FILES_TTL.from_now.to_i)
filename_format = AccountConfig.find_or_initialize_by(account_id: submitter.account_id,
key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value
ActiveStorage::Blob.proxy_url(
attachment.blob,
expires_at: FILES_TTL.from_now.to_i,
filename: Submitters.build_document_filename(submitter, attachment.blob, filename_format)
)
end
end
@@ -0,0 +1,17 @@
# frozen_string_literal: true
class SubmissionsFiltersController < ApplicationController
ALLOWED_NAMES = %w[
author
completed_at
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_authorization_check
prepend_before_action :maybe_redirect_com, only: %i[show completed]
TTL = 40.minutes
def show
@@ -18,8 +20,8 @@ class SubmissionsPreviewController < ApplicationController
@submission ||= Submission.find_by!(slug: params[:slug])
if !@submission.submitters.all?(&:completed_at?) && current_user.blank?
raise ActionController::RoutingError, 'Not Found'
if @submission.account.archived_at? || (!@submission.submitters.all?(&:completed_at?) && current_user.blank?)
raise ActionController::RoutingError, I18n.t('not_found')
end
if !submission_valid_ttl?(@submission) && !signature_valid
@@ -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
+23 -8
View File
@@ -12,17 +12,22 @@ class SubmitFormController < ApplicationController
def show
@submitter = Submitter.find_by!(slug: params[:slug])
submission = @submitter.submission
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
return render :archived if @submitter.submission.template.archived_at? || @submitter.submission.archived_at?
return render :expired if @submitter.submission.expired?
return render :archived if submission.template.archived_at? ||
submission.archived_at? ||
@submitter.account.archived_at?
return render :expired if submission.expired?
return render :declined if @submitter.declined_at?
return render :awaiting if submission.template.preferences['submitters_order'] == 'preserved' &&
!Submitters.current_submitter_order?(@submitter)
Submitters.preload_with_pages(@submitter)
Submitters::MaybeUpdateDefaultValues.call(@submitter, current_user)
@attachments_index = ActiveStorage::Attachment.where(record: @submitter.submission.submitters, name: :attachments)
.preload(:blob).index_by(&:uuid)
@attachments_index = build_attachments_index(submission)
@form_configs = Submitters::FormConfigs.call(@submitter, CONFIG_KEYS)
@@ -46,18 +51,21 @@ class SubmitFormController < ApplicationController
submitter = Submitter.find_by!(slug: params[:slug])
if submitter.completed_at?
return render json: { error: 'Form has been completed already.' }, status: :unprocessable_entity
return render json: { error: I18n.t('form_has_been_completed_already') }, status: :unprocessable_entity
end
if submitter.template.archived_at? || submitter.submission.archived_at?
return render json: { error: 'Form has been archived.' }, status: :unprocessable_entity
return render json: { error: I18n.t('form_has_been_archived') }, status: :unprocessable_entity
end
if submitter.submission.expired?
return render json: { error: 'Form has been expired.' }, status: :unprocessable_entity
return render json: { error: I18n.t('form_has_been_expired') }, status: :unprocessable_entity
end
return render json: { error: 'Form has been declined.' }, status: :unprocessable_entity if submitter.declined_at?
if submitter.declined_at?
return render json: { error: I18n.t('form_has_been_declined') },
status: :unprocessable_entity
end
Submitters::SubmitValues.call(submitter, params, request)
@@ -71,4 +79,11 @@ class SubmitFormController < ApplicationController
end
def success; end
private
def build_attachments_index(submission)
ActiveStorage::Attachment.where(record: submission.submitters, name: :attachments)
.preload(:blob).index_by(&:uuid)
end
end
@@ -21,8 +21,14 @@ class SubmitFormDeclineController < ApplicationController
user = submitter.submission.created_by_user || submitter.template.author
SubmitterMailer.declined_email(submitter, user).deliver_later!
SendFormDeclinedWebhookRequestJob.perform_async('submitter_id' => submitter.id)
if user.user_configs.find_by(key: UserConfig::RECEIVE_DECLINED_EMAIL)&.value != false
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)
end
@@ -9,7 +9,8 @@ class SubmittersAutocompleteController < ApplicationController
def index
submitters = search_submitters(@submitters)
values = submitters.limit(LIMIT).group(SELECT_COLUMNS.join(', ')).pluck(SELECT_COLUMNS.join(', '))
arel_columns = SELECT_COLUMNS.map { |col| Submitter.arel_table[col] }
values = submitters.limit(LIMIT).group(arel_columns).pluck(arel_columns)
attrs = values.map { |row| SELECT_COLUMNS.zip(row).to_h }
attrs = attrs.uniq { |e| e[params[:field]] } if params[:field].present?
+80
View File
@@ -0,0 +1,80 @@
# frozen_string_literal: true
class SubmittersController < ApplicationController
load_and_authorize_resource :submitter, only: %i[edit update]
def edit
@submitter_email_message =
if @submitter.preferences['email_message_uuid'].present?
@submitter.account.email_messages.find_by(uuid: @submitter.preferences['email_message_uuid'])
end
end
def update
submission = @submitter.submission
if @submitter.submission_events.exists?(event_type: 'start_form') || submission.archived_at? || submission.expired?
return redirect_back fallback_location: submission_path(submission), alert: I18n.t('submitter_cannot_be_updated')
end
if submitter_params.values.all?(&:blank?)
return redirect_back fallback_location: submission_path(submission),
alert: I18n.t('at_least_one_field_must_be_filled')
end
if params[:is_custom_message] != '1'
params.delete(:subject)
params.delete(:body)
end
submitter_preferences = Submitters.normalize_preferences(@submitter.account, current_user, params)
if submitter_preferences.key?('email_message_uuid')
@submitter.preferences['email_message_uuid'] = submitter_preferences['email_message_uuid']
end
assign_submitter_attrs(@submitter, submitter_params)
if @submitter.save
maybe_resend_email_sms(@submitter, params)
redirect_back fallback_location: submission_path(submission), notice: I18n.t('changes_have_been_saved')
else
redirect_back fallback_location: submission_path(submission), alert: I18n.t('unable_to_save')
end
end
private
def maybe_resend_email_sms(submitter, params)
if params[:send_email] == '1' && submitter.email.present?
is_sent_recently = Docuseal.multitenant? &&
EmailEvent.exists?(email: submitter.email,
tag: 'submitter_invitation',
emailable: submitter,
event_type: 'send',
created_at: 4.hours.ago..Time.current)
SendSubmitterInvitationEmailJob.perform_async('submitter_id' => submitter.id) unless is_sent_recently
end
return if submitter.phone.blank?
return unless params[:send_sms] == '1'
SendSubmitterInvitationSmsJob.perform_async('submitter_id' => submitter.id)
end
def assign_submitter_attrs(submitter, attrs)
submitter.phone = attrs[:phone].to_s.gsub(/[^0-9+]/, '') if attrs.key?(:phone)
submitter.email = Submissions.normalize_email(attrs[:email]) if attrs.key?(:email)
submitter.name = attrs[:name] if attrs.key?(:name)
submitter
end
def submitter_params
params.require(:submitter).permit(:email, :name, :phone).transform_values(&:strip)
end
end
@@ -6,11 +6,11 @@ class SubmittersSendEmailController < ApplicationController
def create
if Docuseal.multitenant? && SubmissionEvent.exists?(submitter: @submitter,
event_type: 'send_email',
created_at: 24.hours.ago..Time.current)
created_at: 10.hours.ago..Time.current)
Rollbar.warning("Already sent: #{@submitter.id}") if defined?(Rollbar)
return redirect_back(fallback_location: submission_path(@submitter.submission),
alert: 'Email has been sent already.')
alert: I18n.t('email_has_been_sent_already'))
end
SendSubmitterInvitationEmailJob.perform_async('submitter_id' => @submitter.id)
@@ -18,6 +18,6 @@ class SubmittersSendEmailController < ApplicationController
@submitter.sent_at ||= Time.current
@submitter.save!
redirect_back(fallback_location: submission_path(@submitter.submission), notice: 'Email has been sent')
redirect_back(fallback_location: submission_path(@submitter.submission), notice: I18n.t('email_has_been_sent'))
end
end
@@ -5,7 +5,7 @@ class TemplateDocumentsController < ApplicationController
def create
if params[:blobs].blank? && params[:files].blank?
return render json: { error: 'File is missing' }, status: :unprocessable_entity
return render json: { error: I18n.t('file_is_missing') }, status: :unprocessable_entity
end
old_fields_hash = @template.fields.hash
@@ -28,6 +28,6 @@ class TemplateDocumentsController < ApplicationController
)
}
rescue Templates::CreateAttachments::PdfEncrypted
render json: { error: 'PDF encrypted' }, status: :unprocessable_entity
render json: { error: 'PDF encrypted', status: 'pdf_encrypted' }, status: :unprocessable_entity
end
end
@@ -4,10 +4,10 @@ class TemplateFoldersController < ApplicationController
load_and_authorize_resource :template_folder
def show
@templates = @template_folder.templates.active.preload(:author).order(id: :desc)
@templates = @template_folder.templates.active.preload(:author, :template_accesses).order(id: :desc)
@templates = Templates.search(@templates, params[:q])
@pagy, @templates = pagy(@templates, items: 12)
@pagy, @templates = pagy(@templates, limit: 12)
end
def edit; end
@@ -15,9 +15,9 @@ class TemplateFoldersController < ApplicationController
def update
if @template_folder != current_account.default_template_folder &&
@template_folder.update(template_folder_params)
redirect_to folder_path(@template_folder), notice: 'Folder name has been updated'
redirect_to folder_path(@template_folder), notice: I18n.t('folder_name_has_been_updated')
else
redirect_to folder_path(@template_folder), alert: 'Unable to rename folder'
redirect_to folder_path(@template_folder), alert: I18n.t('unable_to_rename_folder')
end
end
@@ -4,9 +4,9 @@ class TemplatesArchivedController < ApplicationController
load_and_authorize_resource :template, parent: false
def index
@templates = @templates.where.not(archived_at: nil).preload(:author, :folder).order(id: :desc)
@templates = @templates.where.not(archived_at: nil).preload(:author, :folder, :template_accesses).order(id: :desc)
@templates = Templates.search(@templates, params[:q])
@pagy, @templates = pagy(@templates, items: 12)
@pagy, @templates = pagy(@templates, limit: 12)
end
end
@@ -7,8 +7,15 @@ class TemplatesArchivedSubmissionsController < ApplicationController
def index
@submissions = @submissions.where.not(archived_at: nil)
@submissions = Submissions.search(@submissions, params[:q], search_values: true)
@submissions = Submissions::Filter.call(@submissions, current_user, params)
@pagy, @submissions = pagy(@submissions.preload(:submitters).order(id: :desc))
@submissions = if params[:completed_at_from].present? || params[:completed_at_to].present?
@submissions.order(Submitter.arel_table[:completed_at].maximum.desc)
else
@submissions.order(id: :desc)
end
@pagy, @submissions = pagy(@submissions.preload(submitters: :start_form_submission_events))
rescue ActiveRecord::RecordNotFound
redirect_to root_path
end
+34 -10
View File
@@ -9,19 +9,28 @@ class TemplatesController < ApplicationController
submissions = @template.submissions.accessible_by(current_ability)
submissions = submissions.active if @template.archived_at.blank?
submissions = Submissions.search(submissions, params[:q], search_values: true)
submissions = Submissions::Filter.call(submissions, current_user, params)
@base_submissions = submissions
submissions = submissions.pending if params[:status] == 'pending'
submissions = submissions.completed if params[:status] == 'completed'
@pagy, @submissions = pagy(submissions.preload(:submitters).order(id: :desc))
submissions = if params[:completed_at_from].present? || params[:completed_at_to].present?
submissions.order(Submitter.arel_table[:completed_at].maximum.desc)
else
submissions.order(id: :desc)
end
submissions = submissions.preload(:template_accesses) unless current_user.role.in?(%w[admin superadmin])
@pagy, @submissions = pagy(submissions.preload(submitters: :start_form_submission_events))
rescue ActiveRecord::RecordNotFound
redirect_to root_path
end
def new
@template.name = "#{@base_template.name} (Clone)" if @base_template
@template.name = "#{@base_template.name} (#{I18n.t('clone')})" if @base_template
end
def edit
@@ -66,7 +75,7 @@ class TemplatesController < ApplicationController
if @template.save
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)
else
@@ -77,7 +86,7 @@ class TemplatesController < ApplicationController
def update
@template.update!(template_params)
SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => @template.id)
enqueue_template_updated_webhooks(@template)
head :ok
end
@@ -87,11 +96,11 @@ class TemplatesController < ApplicationController
if params[:permanently].present?
@template.destroy!
'Template has been removed.'
I18n.t('template_has_been_removed')
else
@template.update!(archived_at: Time.current)
'Template has been archived.'
I18n.t('template_has_been_archived')
end
redirect_back(fallback_location: root_path, notice:)
@@ -102,13 +111,13 @@ class TemplatesController < ApplicationController
def template_params
params.require(:template).permit(
: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]],
fields: [[:uuid, :submitter_uuid, :name, :type,
:required, :readonly, :default_value,
:title, :description,
{ preferences: {},
conditions: [%i[field_uuid value action]],
conditions: [%i[field_uuid value action operation]],
options: [%i[value uuid]],
validation: %i[message pattern],
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]] }
@@ -116,14 +125,29 @@ class TemplatesController < ApplicationController
end
def authorized_clone_account_id?(account_id)
true_user.account_id.to_s == account_id.to_s || true_user.account.linked_accounts.exists?(id: account_id)
true_user.account_id.to_s == account_id.to_s ||
true_user.account.linked_accounts.accessible_by(current_ability).exists?(id: account_id)
end
def maybe_redirect_to_template(template)
if template.account == current_account
redirect_to(edit_template_path(@template))
else
redirect_back(fallback_location: root_path, notice: 'Template has been cloned')
redirect_back(fallback_location: root_path, notice: I18n.t('template_has_been_cloned'))
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
@@ -23,14 +23,14 @@ class TemplatesDashboardController < ApplicationController
@template_folders = @template_folders.reject { |e| e.name == TemplateFolder::DEFAULT_NAME }
@templates = filter_templates(@templates)
items =
limit =
if @template_folders.size < 4
TEMPLATES_PER_PAGE
else
(@template_folders.size < 7 ? 9 : 6)
end
@pagy, @templates = pagy(@templates, items:)
@pagy, @templates = pagy(@templates, limit:)
end
end
@@ -45,7 +45,7 @@ class TemplatesDashboardController < ApplicationController
end
def filter_templates(templates)
rel = templates.active.preload(:author).order(id: :desc)
rel = templates.active.preload(:author, :template_accesses).order(id: :desc)
if params[:q].blank?
if Docuseal.multitenant? && !current_account.testing?
@@ -9,9 +9,9 @@ class TemplatesFoldersController < ApplicationController
@template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:name])
if @template.save
redirect_back(fallback_location: template_path(@template), notice: 'Document template has been moved')
redirect_back(fallback_location: template_path(@template), notice: I18n.t('document_template_has_been_moved'))
else
redirect_back(fallback_location: template_path(@template), notice: 'Unable to move template into folder')
redirect_back(fallback_location: template_path(@template), notice: I18n.t('unable_to_move_template_into_folder'))
end
end
@@ -22,8 +22,10 @@ class TemplatesPreferencesController < ApplicationController
preferences: %i[bcc_completed request_email_subject request_email_body
documents_copy_email_subject documents_copy_email_body
documents_copy_email_enabled documents_copy_email_attach_audit
documents_copy_email_attach_documents
completed_notification_email_attach_documents
completed_redirect_url
submitters_order
completed_notification_email_subject completed_notification_email_body
completed_notification_email_enabled completed_notification_email_attach_audit] +
[completed_message: %i[title body]]
@@ -6,6 +6,6 @@ class TemplatesRestoreController < ApplicationController
def create
@template.update!(archived_at: nil)
redirect_to template_path(@template), notice: 'Template has been unarchived'
redirect_to template_path(@template), notice: I18n.t('template_has_been_unarchived')
end
end
@@ -23,7 +23,7 @@ class TemplatesUploadsController < ApplicationController
@template.update!(schema:)
SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => @template.id)
enqueue_template_created_webhooks(@template)
redirect_to edit_template_path(@template)
rescue Templates::CreateAttachments::PdfEncrypted
@@ -33,7 +33,7 @@ class TemplatesUploadsController < ApplicationController
raise if Rails.env.local?
redirect_to root_path, alert: 'Unable to upload file'
redirect_to root_path, alert: I18n.t('unable_to_update_file')
end
private
@@ -55,14 +55,22 @@ class TemplatesUploadsController < ApplicationController
tempfile.write(DownloadUtils.call(params[:url]).body)
tempfile.rewind
filename = URI.decode_www_form_component(params[:filename]) if params[:filename].present?
filename ||= File.basename(URI.decode_www_form_component(params[:url]))
file = ActionDispatch::Http::UploadedFile.new(
tempfile:,
filename: File.basename(
URI.decode_www_form_component(params[:filename].presence || params[:url]), '.*'
),
filename:,
type: Marcel::MimeType.for(tempfile)
)
{ files: [file] }
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
@@ -4,9 +4,8 @@ class TestingApiSettingsController < ApplicationController
def index
authorize!(:manage, current_user.access_token)
@webhook_config =
current_account.encrypted_configs.find_or_initialize_by(key: EncryptedConfig::WEBHOOK_URL_KEY)
@webhook_url = current_account.webhook_urls.first_or_initialize
authorize!(:manage, @webhook_config)
authorize!(:manage, @webhook_url)
end
end
@@ -14,12 +14,12 @@ class TimestampServerController < ApplicationController
test_timeserver_url(@encrypted_config.value) if @encrypted_config.value.present?
if @encrypted_config.value.present? ? @encrypted_config.save : @encrypted_config.delete
redirect_back fallback_location: settings_notifications_path, notice: 'Changes have been saved'
redirect_back fallback_location: settings_notifications_path, notice: I18n.t('changes_have_been_saved')
else
redirect_back fallback_location: settings_notifications_path, alert: 'Unable to save'
redirect_back fallback_location: settings_notifications_path, alert: I18n.t('unable_to_save')
end
rescue SocketError, TimestampError, OpenSSL::Timestamp::TimestampError
redirect_back fallback_location: settings_notifications_path, alert: 'Invalid Timeserver'
redirect_back fallback_location: settings_notifications_path, alert: t('invalid_timeserver')
end
private
+4 -4
View File
@@ -9,7 +9,7 @@ class UserInitialsController < ApplicationController
def update
file = params[:file]
return redirect_to settings_profile_index_path, notice: 'Unable to save initials' if file.blank?
return redirect_to settings_profile_index_path, notice: I18n.t('unable_to_save_initials') if file.blank?
blob = ActiveStorage::Blob.create_and_upload!(io: file.open,
filename: file.original_filename,
@@ -22,16 +22,16 @@ class UserInitialsController < ApplicationController
)
if @user_config.update(value: attachment.uuid)
redirect_to settings_profile_index_path, notice: 'Initials has been saved'
redirect_to settings_profile_index_path, notice: I18n.t('initials_has_been_saved')
else
redirect_to settings_profile_index_path, notice: 'Unable to save initials'
redirect_to settings_profile_index_path, notice: I18n.t('unable_to_save_initials')
end
end
def destroy
@user_config.destroy
redirect_to settings_profile_index_path, notice: 'Initials has been removed'
redirect_to settings_profile_index_path, notice: I18n.t('initials_has_been_removed')
end
private
@@ -9,7 +9,7 @@ class UserSignaturesController < ApplicationController
def update
file = params[:file]
return redirect_to settings_profile_index_path, notice: 'Unable to save signature' if file.blank?
return redirect_to settings_profile_index_path, notice: I18n.t('unable_to_save_signature') if file.blank?
blob = ActiveStorage::Blob.create_and_upload!(io: file.open,
filename: file.original_filename,
@@ -22,16 +22,16 @@ class UserSignaturesController < ApplicationController
)
if @user_config.update(value: attachment.uuid)
redirect_to settings_profile_index_path, notice: 'Signature has been saved'
redirect_to settings_profile_index_path, notice: I18n.t('signature_has_been_saved')
else
redirect_to settings_profile_index_path, notice: 'Unable to save signature'
redirect_to settings_profile_index_path, notice: I18n.t('unable_to_save_signature')
end
end
def destroy
@user_config.destroy
redirect_to settings_profile_index_path, notice: 'Signature has been removed'
redirect_to settings_profile_index_path, notice: I18n.t('signature_has_been_removed')
end
private
+11 -7
View File
@@ -9,9 +9,11 @@ class UsersController < ApplicationController
def index
@users =
if params[:status] == 'archived'
@users.archived
@users.archived.where.not(role: 'integration')
elsif params[:status] == 'integration'
@users.active.where(role: 'integration')
else
@users.active
@users.active.where.not(role: 'integration')
end
@pagy, @users = pagy(@users.where(account: current_account).order(id: :desc))
@@ -32,17 +34,19 @@ class UsersController < ApplicationController
@user = existing_user
end
@user.role = User::ADMIN_ROLE unless role_valid?(@user.role)
if @user.save
UserMailer.invitation_email(@user).deliver_later!
redirect_back fallback_location: settings_users_path, notice: 'User has been invited'
redirect_back fallback_location: settings_users_path, notice: I18n.t('user_has_been_invited')
else
render turbo_stream: turbo_stream.replace(:modal, template: 'users/new'), status: :unprocessable_entity
end
end
def update
return redirect_to settings_users_path, notice: '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.delete(:role) if !role_valid?(attrs[:role]) || current_user == @user
@@ -56,7 +60,7 @@ class UsersController < ApplicationController
end
if @user.update(attrs)
redirect_back fallback_location: settings_users_path, notice: 'User has been updated'
redirect_back fallback_location: settings_users_path, notice: I18n.t('user_has_been_updated')
else
render turbo_stream: turbo_stream.replace(:modal, template: 'users/edit'), status: :unprocessable_entity
end
@@ -64,12 +68,12 @@ class UsersController < ApplicationController
def destroy
if Docuseal.demo? || @user.id == current_user.id
return redirect_to settings_users_path, notice: 'Unable to remove user'
return redirect_to settings_users_path, notice: I18n.t('unable_to_remove_user')
end
@user.update!(archived_at: Time.current)
redirect_back fallback_location: settings_users_path, notice: 'User has been removed'
redirect_back fallback_location: settings_users_path, notice: I18n.t('user_has_been_removed')
end
private
@@ -14,6 +14,6 @@ class VerifyPdfSignatureController < ApplicationController
render turbo_stream: turbo_stream.replace('result', partial: 'result',
locals: { pdfs:, files: params[:files], trusted_certs: })
rescue HexaPDF::MalformedPDFError
render turbo_stream: turbo_stream.replace('result', html: helpers.tag.div('Invalid PDF', id: 'result'))
render turbo_stream: turbo_stream.replace('result', html: helpers.tag.div(I18n.t('invalid_pdf'), id: 'result'))
end
end
@@ -1,37 +1,22 @@
# frozen_string_literal: true
class WebhookPreferencesController < ApplicationController
EVENTS = %w[
form.viewed
form.started
form.completed
form.declined
template.created
template.updated
submission.created
submission.archived
].freeze
load_and_authorize_resource :webhook_url, parent: false
before_action :load_account_config
authorize_resource :account_config, parent: false
def update
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
@account_config.value[account_config_params[:event]] = account_config_params[:value] == '1'
@account_config.save!
@webhook_url.save!
head :ok
end
private
def load_account_config
@account_config =
current_account.account_configs.find_or_initialize_by(key: AccountConfig::WEBHOOK_PREFERENCES_KEY)
@account_config.value ||= {}
end
def account_config_params
params.permit(:event, :value)
def webhook_preferences_params
params.require(:webhook_url).permit(events: {})
end
end
+9 -16
View File
@@ -1,29 +1,22 @@
# frozen_string_literal: true
class WebhookSecretController < ApplicationController
before_action :load_encrypted_config
authorize_resource :encrypted_config, parent: false
load_and_authorize_resource :webhook_url, parent: false
def index; end
def show; end
def create
@encrypted_config.assign_attributes(value: {
encrypted_config_params[:key] => encrypted_config_params[:value]
def update
@webhook_url.update!(secret: {
webhook_secret_params[:key] => webhook_secret_params[:value]
}.compact_blank)
@encrypted_config.value.present? ? @encrypted_config.save! : @encrypted_config.delete
redirect_back(fallback_location: settings_webhooks_path, notice: 'Webhook Secret has been saved.')
redirect_back(fallback_location: settings_webhook_path(@webhook_url),
notice: I18n.t('webhook_secret_has_been_saved'))
end
private
def load_encrypted_config
@encrypted_config =
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, {})
def webhook_secret_params
params.require(:webhook_url).permit(secret: %i[key value]).fetch(:secret, {})
end
end
+38 -14
View File
@@ -1,36 +1,60 @@
# frozen_string_literal: true
class WebhookSettingsController < ApplicationController
before_action :load_encrypted_config
authorize_resource :encrypted_config, parent: false
load_and_authorize_resource :webhook_url, parent: false, only: %i[index show new create update destroy]
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 new; end
def create
@encrypted_config.assign_attributes(encrypted_config_params)
@webhook_url.save!
@encrypted_config.value.present? ? @encrypted_config.save! : @encrypted_config.delete
redirect_back(fallback_location: settings_webhooks_path, notice: 'Webhook URL has been saved.')
redirect_to settings_webhooks_path, notice: I18n.t('webhook_url_has_been_saved')
end
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
SendFormCompletedWebhookRequestJob.perform_async({ 'submitter_id' => submitter.id,
'encrypted_config_id' => @encrypted_config.id })
if submitter.blank? || @webhook_url.blank?
return redirect_back(fallback_location: settings_webhooks_path,
alert: I18n.t('unable_to_resend_webhook_request'))
end
redirect_back(fallback_location: settings_webhooks_path, notice: 'Webhook request has been sent.')
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'))
end
private
def load_encrypted_config
@encrypted_config =
current_account.encrypted_configs.find_or_initialize_by(key: EncryptedConfig::WEBHOOK_URL_KEY)
def create_params
params.require(:webhook_url).permit(:url, events: []).reverse_merge(events: [])
end
def encrypted_config_params
params.require(:encrypted_config).permit(:value)
def update_params
params.require(:webhook_url).permit(:url)
end
end
+7 -1
View File
@@ -29,6 +29,8 @@ import SearchInput from './elements/search_input'
import ToggleAttribute from './elements/toggle_attribute'
import LinkedInput from './elements/linked_input'
import CheckboxGroup from './elements/checkbox_group'
import MaskedInput from './elements/masked_input'
import SetDateButton from './elements/set_date_button'
import * as TurboInstantClick from './lib/turbo_instant_click'
@@ -95,6 +97,8 @@ safeRegisterElement('search-input', SearchInput)
safeRegisterElement('toggle-attribute', ToggleAttribute)
safeRegisterElement('linked-input', LinkedInput)
safeRegisterElement('checkbox-group', CheckboxGroup)
safeRegisterElement('masked-input', MaskedInput)
safeRegisterElement('set-date-button', SetDateButton)
safeRegisterElement('template-builder', class extends HTMLElement {
connectedCallback () {
@@ -109,6 +113,7 @@ safeRegisterElement('template-builder', class extends HTMLElement {
backgroundColor: '#faf7f5',
locale: this.dataset.locale,
withPhone: this.dataset.withPhone === 'true',
withVerification: ['true', 'false'].includes(this.dataset.withVerification) ? this.dataset.withVerification === 'true' : null,
withLogo: this.dataset.withLogo !== 'false',
editable: this.dataset.editable !== 'false',
authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content,
@@ -150,7 +155,8 @@ safeRegisterElement('import-list', class extends HTMLElement {
this.app = createApp(ImportList, {
template: JSON.parse(this.dataset.template),
multitenant: this.dataset.multitenant === 'true',
authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content
authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content,
i18n: JSON.parse(this.dataset.i18n || '{}')
})
this.app.mount(this.appElem)
+17 -8
View File
@@ -51,10 +51,6 @@ button[disabled] .enabled {
@apply input input-bordered bg-white;
}
.base-input-slim {
@apply input input-bordered bg-white h-10;
}
.base-textarea {
@apply textarea textarea-bordered bg-white rounded-3xl;
}
@@ -101,6 +97,23 @@ button[disabled] .enabled {
bottom: auto;
}
.tooltip-bottom-start:before {
transform: translateX(-31%);
top: var(--tooltip-offset);
left: 100%;
right: auto;
bottom: auto;
}
.tooltip-bottom-start:after {
transform: translateX(-25%);
border-color: transparent transparent var(--tooltip-color) transparent;
top: var(--tooltip-tail-offset);
left: 50%;
right: auto;
bottom: auto;
}
.autocomplete {
background: white;
z-index: 1000;
@@ -134,7 +147,3 @@ button[disabled] .enabled {
outline-offset: 3px;
outline-color: hsl(var(--bc) / 0.2);
}
select:required:invalid {
color: gray !important;
}
+4
View File
@@ -12,6 +12,10 @@ window.customElements.define('draw-signature', class extends HTMLElement {
this.pad = new SignaturePad(this.canvas)
if (this.dataset.color) {
this.pad.penColor = this.dataset.color
}
this.pad.addEventListener('endStroke', () => {
this.updateSubmitButtonVisibility()
})
+18
View File
@@ -0,0 +1,18 @@
export default class extends HTMLElement {
connectedCallback () {
const maskedToken = this.input.value
this.input.addEventListener('focus', () => {
this.input.value = this.dataset.token
this.input.select()
})
this.input.addEventListener('focusout', () => {
this.input.value = maskedToken
})
}
get input () {
return this.querySelector('input')
}
}
@@ -0,0 +1,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)
}
}
+3
View File
@@ -15,18 +15,21 @@ safeRegisterElement('submission-form', class extends HTMLElement {
this.app = createApp(Form, {
submitter: JSON.parse(this.dataset.submitter),
inviteSubmitters: JSON.parse(this.dataset.inviteSubmitters),
schema: JSON.parse(this.dataset.schema),
canSendEmail: this.dataset.canSendEmail === 'true',
previousSignatureValue: this.dataset.previousSignatureValue,
goToLast: this.dataset.goToLast === 'true',
isDemo: this.dataset.isDemo === 'true',
attribution: this.dataset.attribution !== 'false',
scrollPadding: this.dataset.scrollPadding || '-80px',
language: this.dataset.language,
dryRun: this.dataset.dryRun === 'true',
expand: ['true', 'false'].includes(this.dataset.expand) ? this.dataset.expand === 'true' : null,
withSignatureId: this.dataset.withSignatureId === 'true',
requireSigningReason: this.dataset.requireSigningReason === 'true',
withConfetti: this.dataset.withConfetti !== 'false',
withDisclosure: this.dataset.withDisclosure === 'true',
reuseSignature: this.dataset.reuseSignature !== 'false',
withTypedSignature: this.dataset.withTypedSignature !== 'false',
authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content,
rememberSignature: this.dataset.rememberSignature === 'true',
-4
View File
@@ -31,10 +31,6 @@ button[disabled] .enabled {
display: none;
}
select:required:invalid {
@apply text-gray-300;
}
.input-bordered {
@apply border-base-content/20;
}
+14 -8
View File
@@ -3,7 +3,7 @@
class="field-area flex absolute lg:text-base -outline-offset-1"
dir="auto"
:style="computedStyle"
: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, 'field-area-active outline-red-500 outline-dashed outline-2 z-10': isActive && submittable, 'bg-opacity-40': (isActive || isValueSet) && submittable }"
: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 }"
>
<div
v-if="(!withFieldPlaceholder || !field.name || field.type === 'cells') && !isActive && !isValueSet && field.type !== 'checkbox' && submittable && !area.option_uuid"
@@ -29,17 +29,16 @@
{{ optionValue(option) }}
</template>
<template v-else>
{{ field.name || fieldNames[field.type] }}
{{ field.title || field.name || fieldNames[field.type] }}
<template v-if="field.type === 'checkbox' && !field.name">
{{ fieldIndex + 1 }}
</template>
<template v-else-if="!field.required && field.type !== 'checkbox'">
(optional)
({{ t('optional') }})
</template>
</template>
</div>
<div
v-if="isActive"
ref="scrollToElem"
class="absolute"
:style="{ top: scrollPadding }"
@@ -210,7 +209,7 @@
</template>
<script>
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconCheck, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp, IconSquareNumber1 } from '@tabler/icons-vue'
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconCheck, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp, IconSquareNumber1, IconId } from '@tabler/icons-vue'
export default {
name: 'FieldArea',
@@ -312,7 +311,8 @@ export default {
cells: this.t('cells'),
stamp: this.t('stamp'),
payment: this.t('payment'),
phone: this.t('phone')
phone: this.t('phone'),
verification: this.t('verify_id')
}
},
alignClasses () {
@@ -341,7 +341,8 @@ export default {
cells: IconColumns3,
multiple: IconChecks,
phone: IconPhoneCheck,
payment: IconCreditCard
payment: IconCreditCard,
verification: IconId
}
},
image () {
@@ -405,7 +406,12 @@ export default {
}
if (this.field.preferences?.font_size) {
style.fontSize = this.field.preferences.font_size + 'pt'
style.fontSize = `clamp(4pt, 1.6vw, ${parseInt(this.field.preferences.font_size) * 1.23}pt)`
style.lineHeight = `clamp(6pt, 2.0vw, ${parseInt(this.field.preferences.font_size) * 1.23 + 3}pt)`
}
if (this.field.preferences?.color) {
style.color = this.field.preferences.color
}
return style
+10 -2
View File
@@ -132,10 +132,18 @@ export default {
this.scrollInContainer(areaRef.$el)
} else {
const targetRect = areaRef.$refs.scrollToElem.getBoundingClientRect()
const root = this.$root.$el?.parentNode?.classList?.contains('ds') ? this.$root.$el : document.body
const rootRect = root.getBoundingClientRect()
const scrollEl = this.scrollEl || window
let rootRect = {}
if (this.scrollEl === document.documentElement) {
rootRect = this.scrollEl.getBoundingClientRect()
} else {
const root = this.$root.$el?.parentNode?.classList?.contains('ds') ? this.$root.$el : document.body
rootRect = root.getBoundingClientRect()
}
scrollEl.scrollTo({ top: targetRect.top - rootRect.top, behavior: 'smooth' })
}
+16 -5
View File
@@ -1,5 +1,6 @@
<template>
<div
id="form_completed"
class="mx-auto max-w-md flex flex-col"
dir="auto"
>
@@ -10,7 +11,7 @@
:height="30"
/>
<span>
{{ completedMessage.title || 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>
</div>
<div
@@ -75,7 +76,7 @@
</a>
<a
v-if="isDemo"
href="https://docuseal.co/sign_up"
href="https://docuseal.com/sign_up"
class="white-button flex items-center space-x-1 w-full"
>
<IconLogin />
@@ -88,9 +89,9 @@
v-if="attribution"
class="text-center mt-4"
>
{{ t('signed_with') }}
{{ t('powered_by') }}
<a
href="https://www.docuseal.co/start"
href="https://www.docuseal.com/start"
target="_blank"
class="underline"
>DocuSeal</a> - {{ t('open_source_documents_software') }}
@@ -129,6 +130,16 @@ export default {
required: false,
default: true
},
hasSignatureFields: {
type: Boolean,
required: false,
default: false
},
hasMultipleDocuments: {
type: Boolean,
required: false,
default: false
},
withDownloadButton: {
type: Boolean,
required: false,
@@ -212,7 +223,7 @@ export default {
this.downloadUrls(urls)
}
} else {
alert('Failed to download files')
alert(this.t('failed_to_download_files'))
}
})
},
@@ -37,6 +37,7 @@
<AppearsOn :field="field" />
<div class="text-center">
<input
:id="field.uuid"
ref="input"
v-model="value"
class="base-input !text-2xl text-center w-full"
@@ -45,6 +46,7 @@
:name="`values[${field.uuid}]`"
@keydown.enter="onEnter"
@focus="$emit('focus')"
@paste="onPaste"
>
</div>
</div>
@@ -98,6 +100,25 @@ export default {
this.$emit('submit')
}
},
onPaste (e) {
e.preventDefault()
let pasteData = e.clipboardData.getData('text').trim()
if (pasteData.match(/^\d{2}\.\d{2}\.\d{4}$/)) {
pasteData = pasteData.split('.').reverse().join('-')
}
const parsedDate = new Date(pasteData)
if (!isNaN(parsedDate)) {
const inputEl = this.$refs.input
inputEl.valueAsDate = new Date(parsedDate.getTime() - parsedDate.getTimezoneOffset() * 60000)
inputEl.dispatchEvent(new Event('input', { bubbles: true }))
}
},
setCurrentDate () {
const inputEl = this.$refs.input
@@ -1,5 +1,6 @@
<template>
<div
id="dropzone"
class="flex h-32 w-full"
@dragover.prevent
@drop.prevent="onDropFiles"
+249 -59
View File
@@ -11,12 +11,13 @@
:with-label="!isAnonymousChecboxes && showFieldNames"
:current-step="currentStepFields"
:scroll-padding="scrollPadding"
@focus-step="[saveStep(), currentField.type !== 'checkbox' ? isFormVisible = true : '', goToStep($event, false, true)]"
@focus-step="[saveStep(), goToStep($event, false, true), currentField.type !== 'checkbox' ? isFormVisible = true : '']"
/>
<FieldAreas
:steps="readonlyConditionalFields.map((e) => [e])"
:values="readonlyConditionalFields.reduce((acc, f) => { acc[f.uuid] = f.default_value; return acc }, {})"
:values="readonlyConditionalFields.reduce((acc, f) => { acc[f.uuid] = (values[f.uuid] || f.default_value); return acc }, {})"
:submitter="submitter"
:attachments-index="attachmentsIndex"
:submittable="false"
/>
<FormulaFieldAreas
@@ -24,6 +25,44 @@
:fields="formulaFields"
: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
v-if="!isFormVisible"
id="expand_form_button"
@@ -38,8 +77,11 @@
<template v-else-if="alwaysMinimize">
{{ t('next') }}
</template>
<template v-else-if="isShowContinue">
{{ t('continue') }}
</template>
<template v-else>
{{ t('submit_form') }}
{{ t('start_now') }}
</template>
<IconArrowsDiagonal
class="absolute right-0 mr-4"
@@ -59,7 +101,7 @@
id="minimize_form_button"
class="absolute right-0 mr-2 mt-2 top-0 hidden md:block"
:title="t('minimize')"
@click.prevent="isFormVisible = false"
@click.prevent="minimizeForm"
>
<IconArrowsDiagonalMinimize2
:width="20"
@@ -71,6 +113,7 @@
>
<form
v-if="!isCompleted && !isInvite"
id="steps_form"
ref="form"
:action="submitPath"
method="post"
@@ -112,7 +155,7 @@
v-model="values[currentField.uuid]"
:show-field-names="showFieldNames"
:field="currentField"
@submit="submitStep"
@submit="!isSubmitting && submitStep()"
@focus="scrollIntoField(currentField)"
/>
<div v-else-if="currentField.type === 'select'">
@@ -149,6 +192,7 @@
dir="auto"
:required="currentField.required"
class="select base-input !text-2xl w-full text-center font-normal"
:class="{ 'text-gray-300': !values[currentField.uuid] }"
:name="`values[${currentField.uuid}]`"
@change="values[currentField.uuid] = $event.target.value"
@focus="scrollIntoField(currentField)"
@@ -156,6 +200,7 @@
<option
value=""
:selected="!values[currentField.uuid]"
class="text-gray-300"
>
{{ t('select_your_option') }}
</option>
@@ -164,6 +209,7 @@
:key="option.uuid"
:selected="values[currentField.uuid] == option.value"
:value="option.value"
class="text-base-content"
>
{{ option.value }}
</option>
@@ -201,7 +247,7 @@
<span
@click="scrollIntoField(currentField)"
>
{{ t('complete_hightlighted_checkboxes_and_click') }} <span class="font-semibold">{{ stepFields.length === currentStep + 1 ? t('submit') : t('next') }}</span>.
{{ t('complete_hightlighted_checkboxes_and_click') }} <span class="font-semibold">{{ submitButtonText }}</span>.
</span>
</div>
<div
@@ -265,7 +311,7 @@
>
<template v-if="isAnonymousChecboxes || !showFieldNames">
<span class="text-xl">
{{ t('complete_hightlighted_checkboxes_and_click') }} <span class="font-semibold">{{ stepFields.length === currentStep + 1 ? t('submit') : t('next') }}</span>.
{{ t('complete_hightlighted_checkboxes_and_click') }} <span class="font-semibold">{{ submitButtonText }}</span>.
</span>
<input
v-for="field in currentStepFields"
@@ -340,7 +386,7 @@
:remember-signature="rememberSignature"
:attachments-index="attachmentsIndex"
:require-signing-reason="requireSigningReason"
:button-text="buttonText"
:button-text="submitButtonText"
:dry-run="dryRun"
:with-disclosure="withDisclosure"
:with-qr-button="withQrButton"
@@ -349,7 +395,7 @@
@update:reason="values[currentField.preferences?.reason_field_uuid] = $event"
@attached="attachments.push($event)"
@start="scrollIntoField(currentField)"
@minimize="isFormVisible = false"
@minimize="minimizeForm"
/>
<InitialsStep
v-else-if="currentField.type === 'initials'"
@@ -365,7 +411,7 @@
@attached="attachments.push($event)"
@start="scrollIntoField(currentField)"
@focus="scrollIntoField(currentField)"
@minimize="isFormVisible = false"
@minimize="minimizeForm"
/>
<AttachmentStep
v-else-if="currentField.type === 'file'"
@@ -389,7 +435,7 @@
:default-value="submitter.phone"
:submitter-slug="submitterSlug"
@focus="scrollIntoField(currentField)"
@submit="submitStep"
@submit="!isSubmitting && submitStep()"
/>
<PaymentStep
v-else-if="currentField.type === 'payment'"
@@ -401,11 +447,24 @@
:values="values"
@attached="attachments.push($event)"
@focus="scrollIntoField(currentField)"
@submit="submitStep"
@submit="!isSubmitting && submitStep()"
/>
<VerificationStep
v-else-if="currentField.type === 'verification'"
ref="currentStep"
:key="currentField.uuid"
:locale="language?.toLowerCase() || browserLanguage"
:submitter="submitter"
:empty-value-required-step="emptyValueRequiredStep"
:field="currentField"
:submitter-slug="submitterSlug"
:values="values"
@focus="scrollIntoField(currentField)"
@submit="!isSubmitting && submitStep()"
/>
</div>
<div
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'"
>
<button
@@ -421,7 +480,7 @@
class="mr-1 animate-spin"
/>
<span>
{{ buttonText }}
{{ submitButtonText }}
</span><span
v-if="isSubmitting"
class="w-6 flex justify-start mr-1"
@@ -449,6 +508,8 @@
v-else
:is-demo="isDemo"
:attribution="attribution"
:has-signature-fields="stepFields.some((fields) => fields.some((f) => ['signature', 'initials'].includes(f.type)))"
:has-multiple-documents="hasMultipleDocuments"
:completed-button="completedRedirectUrl ? {} : completedButton"
:completed-message="completedRedirectUrl ? {} : completedMessage"
:with-send-copy-button="withSendCopyButton && !completedRedirectUrl"
@@ -490,6 +551,7 @@ import AttachmentStep from './attachment_step'
import MultiSelectStep from './multi_select_step'
import PhoneStep from './phone_step'
import PaymentStep from './payment_step'
import VerificationStep from './verification_step'
import TextStep from './text_step'
import NumberStep from './number_step'
import DateStep from './date_step'
@@ -532,6 +594,7 @@ export default {
IconWritingSign,
AttachmentStep,
InitialsStep,
VerificationStep,
InviteForm,
MultiSelectStep,
IconInnerShadowTop,
@@ -584,6 +647,16 @@ export default {
required: false,
default: false
},
completeButtonToRef: {
type: Object,
required: false,
default: null
},
schema: {
type: Array,
required: false,
default: () => []
},
attachments: {
type: Array,
required: false,
@@ -759,10 +832,12 @@ export default {
isFormVisible: this.expand !== false,
showFillAllRequiredFields: false,
currentStep: 0,
isShowContinue: false,
enableScrollIntoField: true,
phoneVerifiedValues: {},
orientation: screen?.orientation?.type,
isSubmitting: false,
isSubmittingComplete: false,
submittedValues: {},
recalculateButtonDisabledKey: ''
}
@@ -771,9 +846,37 @@ export default {
isMobile () {
return /android|iphone|ipad/i.test(navigator.userAgent)
},
buttonText () {
if (this.alwaysMinimize || this.stepFields.length === this.currentStep + 1) {
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 () {
if (this.alwaysMinimize) {
return this.t('submit')
} else if (this.stepFields.length === this.currentStep + 1) {
if (this.currentField.type === 'signature') {
return this.t('sign_and_complete')
} else {
return this.t('complete')
}
} else {
return this.t('next')
}
@@ -781,6 +884,18 @@ export default {
alwaysMinimize () {
return this.minimize || (this.orientation?.includes('landscape') && this.isMobile && parseInt(window.innerHeight) < 550)
},
hasMultipleDocuments () {
return Object.keys(
this.stepFields.reduce((acc, fields) => {
fields.forEach((f) => {
f.areas?.forEach((a) => {
acc[a.attachment_uuid] = 1
})
})
return acc
}, {})
).filter(Boolean).length > 1
},
currentStepFields () {
return this.stepFields[this.currentStep] || []
},
@@ -826,13 +941,29 @@ export default {
return this.currentStepFields[0]
},
readonlyConditionalFields () {
return this.fields.filter((f) => f.readonly && f.conditions?.length && this.checkFieldConditions(f))
return this.fields.filter((f) => f.readonly && f.conditions?.length && this.checkFieldConditions(f) && this.checkFieldDocumentsConditions(f))
},
stepFields () {
return this.fields.filter((f) => !f.readonly).reduce((acc, f) => {
const verificationFields = []
const sortedFields = this.fields.reduce((acc, f) => {
if (f.type === 'verification') {
verificationFields.push(f)
} else if (!f.readonly) {
acc.push(f)
}
return acc
}, [])
if (verificationFields.length) {
sortedFields.push(verificationFields.pop())
}
return sortedFields.reduce((acc, f) => {
const prevStep = acc[acc.length - 1]
if (this.checkFieldConditions(f)) {
if (this.checkFieldConditions(f) && this.checkFieldDocumentsConditions(f)) {
if (f.type === 'checkbox' && Array.isArray(prevStep) && prevStep[0].type === 'checkbox' && !f.description) {
prevStep.push(f)
} else {
@@ -865,6 +996,23 @@ export default {
if (isEmpty(value) && this.currentStep > 0) {
this.currentStep -= 1
}
},
attachmentConditionsIndex: {
deep: true,
immediate: true,
handler (value) {
this.$nextTick(() => {
const root = this.$root.$el.parentNode.getRootNode()
for (const key in value) {
const doc = root.querySelector(`[id="document-${key}"`)
if (doc) {
doc.classList.toggle('hidden', !value[key])
}
}
})
}
}
},
beforeUnmount () {
@@ -873,7 +1021,7 @@ export default {
mounted () {
this.submittedValues = JSON.parse(JSON.stringify(this.values))
screen?.orientation.addEventListener('change', this.onOrientationChange)
screen?.orientation?.addEventListener('change', this.onOrientationChange)
this.fields.forEach((field) => {
if (field.default_value && !field.readonly) {
@@ -905,11 +1053,11 @@ export default {
}
if (document.body?.clientWidth >= 768 && this.expand !== true && ['signature', 'initials', 'file', 'image'].includes(this.currentField?.type)) {
this.isFormVisible = false
this.minimizeForm()
}
if (this.alwaysMinimize) {
this.isFormVisible = false
this.minimizeForm()
}
if (/iPhone|iPad|iPod/i.test(navigator.userAgent)) {
@@ -946,33 +1094,57 @@ export default {
onOrientationChange (event) {
this.orientation = event.target.type
},
checkFieldDocumentsConditions (field) {
if (field.areas?.length) {
return field.areas.some((area) => {
return this.attachmentConditionsIndex[area.attachment_uuid]
})
} else {
return true
}
},
checkFieldConditions (field) {
if (field.conditions?.length) {
return field.conditions.reduce((acc, c) => {
const field = this.fieldsUuidIndex[c.field_uuid]
if (['not_empty', 'checked', 'equal', 'contains'].includes(c.action) && field && !this.checkFieldConditions(field)) {
return false
}
if (['empty', 'unchecked'].includes(c.action)) {
return acc && isEmpty(this.values[c.field_uuid])
} else if (['not_empty', 'checked'].includes(c.action)) {
return acc && !isEmpty(this.values[c.field_uuid])
} else if (['equal', 'contains'].includes(c.action) && field) {
const option = field.options.find((o) => o.uuid === c.value)
const values = [this.values[c.field_uuid]].flat()
return acc && values.includes(this.optionValue(option, field.options.indexOf(option)))
} else if (['not_equal', 'does_not_contain'].includes(c.action) && field) {
const option = field.options.find((o) => o.uuid === c.value)
const values = [this.values[c.field_uuid]].flat()
return acc && !values.includes(this.optionValue(option, field.options.indexOf(option)))
const result = field.conditions.reduce((acc, cond) => {
if (cond.operation === 'or') {
acc.push(acc.pop() || this.checkFieldCondition(cond))
} else {
return acc
acc.push(this.checkFieldCondition(cond))
}
}, true)
return acc
}, [])
return !result.includes(false)
} else {
return true
}
},
checkFieldCondition (condition) {
const field = this.fieldsUuidIndex[condition.field_uuid]
if (['not_empty', 'checked', 'equal', 'contains'].includes(condition.action) && field && !this.checkFieldConditions(field)) {
return false
}
if (['empty', 'unchecked'].includes(condition.action)) {
return isEmpty(this.values[condition.field_uuid])
} else if (['not_empty', 'checked'].includes(condition.action)) {
return !isEmpty(this.values[condition.field_uuid])
} 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]].flat()
return values.includes(this.optionValue(option, field.options.indexOf(option)))
} else {
return [this.values[condition.field_uuid]].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]].flat()
return !values.includes(this.optionValue(option, field.options.indexOf(option)))
} else {
return true
}
@@ -1080,6 +1252,10 @@ export default {
const currentFieldUuids = this.currentStepFields.map((f) => f.uuid)
const currentFieldType = this.currentField.type
if (!formData && !this.$refs.form.checkValidity()) {
return
}
if (this.dryRun) {
currentFieldUuids.forEach((fieldUuid) => {
this.submittedValues[fieldUuid] = this.values[fieldUuid]
@@ -1115,16 +1291,24 @@ export default {
scrollIntoArea (area) {
return this.$refs.areas.scrollIntoArea(area)
},
async submitStep () {
async submitStep (e) {
this.isSubmitting = true
const stepPromise = ['signature', 'phone', 'initials', 'payment'].includes(this.currentField.type)
const forceComplete = e?.submitter?.getAttribute('name') === 'completed'
if (forceComplete) {
this.isSubmittingComplete = true
}
const submitStep = this.currentStep
const stepPromise = ['signature', 'phone', 'initials', 'payment', 'verification'].includes(this.currentField.type)
? this.$refs.currentStep.submit
: () => Promise.resolve({})
stepPromise().then(async () => {
const emptyRequiredField = this.stepFields.find((fields, index) => {
if (index >= this.currentStep) {
if (forceComplete ? index === submitStep : index >= submitStep) {
return false
}
@@ -1134,7 +1318,7 @@ export default {
})
const formData = new FormData(this.$refs.form)
const isLastStep = this.currentStep === this.stepFields.length - 1
const isLastStep = (submitStep === this.stepFields.length - 1) || forceComplete
if (isLastStep && !emptyRequiredField && !this.inviteSubmitters.length) {
formData.append('completed', 'true')
@@ -1152,22 +1336,22 @@ export default {
if (response.status === 422 || response.status === 500) {
const data = await response.json()
const i18nError = data.error ? this.t(data.error.replace(/\s+/g, '_').toLowerCase()) : ''
if (data.error) {
const i18nKey = data.error.replace(/\s+/g, '_').toLowerCase()
alert(i18nError !== data.error ? i18nError : (data.error || this.t('value_is_invalid')))
alert(this.t(i18nKey) !== i18nKey ? this.t(i18nKey) : data.error)
} else {
alert(this.t('value_is_invalid'))
}
return Promise.reject(new Error(data.error))
}
if (isLastStep) {
this.isSecondWalkthrough = true
}
const nextStep = (isLastStep && emptyRequiredField) || this.stepFields[this.currentStep + 1]
const nextStep = (isLastStep && emptyRequiredField) || (forceComplete ? null : this.stepFields[submitStep + 1])
if (nextStep) {
if (this.alwaysMinimize) {
this.isFormVisible = false
this.minimizeForm()
}
this.goToStep(this.stepFields.indexOf(nextStep), this.autoscrollFields)
@@ -1184,21 +1368,27 @@ export default {
console.error(error)
}).finally(() => {
this.isSubmitting = false
this.isSubmittingComplete = false
})
}).catch(error => {
if (error?.message === 'Image too small') {
alert('Signature is too small - please redraw.')
alert(this.t('signature_is_too_small_please_redraw'))
} else {
console.log(error)
}
}).finally(() => {
this.isSubmitting = false
this.isSubmittingComplete = false
})
},
minimizeForm () {
this.isFormVisible = false
this.isShowContinue = true
},
async performComplete (resp) {
this.isCompleted = true
if (resp) {
if (resp?.text) {
const respData = await resp.text()
if (respData) {
+183 -53
View File
@@ -1,4 +1,10 @@
const en = {
complete_all_required_fields_to_proceed_with_identity_verification: 'Complete all required fields to proceed with identity verification.',
verify_id: 'Verify ID',
identity_verification: 'Identity verification',
complete: 'Complete',
fill_all_required_fields_to_complete: 'Fill all required fields to complete',
sign_and_complete: 'Sign and Complete',
text: 'Text',
by_clicking_you_agree_to_the: 'By clicking "{button}", you agree to the',
electronic_signature_disclosure: 'Electronic Signature Disclosure',
@@ -38,7 +44,8 @@ const en = {
minimize: 'Minimize',
payment: 'Payment',
phone: 'Phone',
submit_form: 'Submit Form',
start_now: 'Start Now',
continue: 'Continue',
sign_now: 'Sign Now',
type_here_: 'Type here...',
optional: 'optional',
@@ -59,9 +66,11 @@ const en = {
type_signature_here: 'Type signature here',
type_initial_here: 'Type initials here',
form_has_been_completed: 'Form has been completed!',
document_has_been_signed: 'Document has been signed!',
documents_have_been_signed: 'Documents have been signed!',
create_a_free_account: 'Create a Free Account',
signed_with: 'Signed with',
please_check_the_box_to_continue: 'Please check the box to continue',
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: 'Verify Phone Number',
use_international_format: 'Use international format: +1xxx',
@@ -83,10 +92,17 @@ const en = {
pay_with_strip: 'Pay with Stripe',
reupload: 'Reupload',
upload: 'Upload',
files: 'Files'
files: 'Files',
signature_is_too_small_please_redraw: 'Signature is too small. Please redraw.'
}
const es = {
complete_all_required_fields_to_proceed_with_identity_verification: 'Complete todos los campos requeridos para continuar con la verificación de identidad.',
verify_id: 'Verificar ID',
identity_verification: 'Verificación de identidad',
complete: 'Completar',
fill_all_required_fields_to_complete: 'Complete todos los campos requeridos para finalizar',
sign_and_complete: 'Firmar y Completar',
invite: 'Invitar',
email: 'Correo electrónico',
approved: 'Aprobado',
@@ -126,7 +142,8 @@ const es = {
payment: 'Pago',
phone: 'Teléfono',
take_photo: 'Tomar foto',
submit_form: 'Enviar Formulario',
start_now: 'Comenzar ahora',
continue: 'Continuar',
sign_now: 'Firmar ahora',
type_here_: 'Escribe aquí...',
optional: 'opcional',
@@ -146,9 +163,11 @@ const es = {
type_signature_here: 'Escribe la firma aquí',
type_initial_here: 'Escribe las iniciales aquí',
form_has_been_completed: '¡El formulario ha sido completado!',
document_has_been_signed: '¡El documento ha sido firmado!',
documents_have_been_signed: '¡Los documentos han sido firmados!',
create_a_free_account: 'Crear una Cuenta Gratuita',
signed_with: 'Firmado con',
please_check_the_box_to_continue: 'Por favor marque la casilla para continuar',
powered_by: 'Desarrollado por',
please_check_the_box_to_continue: 'Por favor marque la casilla para continuar.',
open_source_documents_software: 'software de documentos de código abierto',
verified_phone_number: 'Verificar número de teléfono',
use_international_format: 'Usar formato internacional: +1xxx',
@@ -170,10 +189,17 @@ const es = {
pay_with_strip: 'Pagar con Stripe',
reupload: 'Volver a subir',
upload: 'Subir',
files: 'Archivos'
files: 'Archivos',
signature_is_too_small_please_redraw: 'La firma es demasiado pequeña. Por favor, dibújala de nuevo.'
}
const it = {
complete_all_required_fields_to_proceed_with_identity_verification: "Compila tutti i campi obbligatori per procedere con la verifica dell'identità.",
verify_id: 'Verifica ID',
identity_verification: "Verifica dell'identità",
complete: 'Completa',
fill_all_required_fields_to_complete: 'Compila tutti i campi obbligatori per completare',
sign_and_complete: 'Firma e Completa',
invite: 'Invita',
email: 'Email',
approved: 'Approvato',
@@ -204,7 +230,7 @@ const it = {
pay: 'Pagamento',
file: 'File',
select: 'Seleziona',
checkbox: 'Casella di controllo',
checkbox: 'Checkbox',
multiple: 'Multiplo',
radio: 'Radio',
cells: 'Celle',
@@ -212,7 +238,8 @@ const it = {
payment: 'Pagamento',
number_phone_is_invalid: '{number} telefono non è valido',
phone: 'Telefono',
submit_form: 'Invia Modulo',
start_now: 'Inizia ora',
continue: 'Continua',
sign_now: 'Firma ora',
type_here_: 'Digita qui...',
optional: 'opzionale',
@@ -233,9 +260,11 @@ const it = {
type_signature_here: 'Scrivi la firma qui',
type_initial_here: 'Scrivi le iniziali qui',
form_has_been_completed: 'Il modulo è stato completato!',
document_has_been_signed: 'Il documento è stato firmato!',
documents_have_been_signed: 'I documenti sono stati firmati!',
create_a_free_account: 'Crea un Account Gratuito',
signed_with: 'Firmato con',
please_check_the_box_to_continue: 'Si prega di spuntare la casella per continuare',
powered_by: 'Desarrollado por',
please_check_the_box_to_continue: 'Si prega di spuntare la casella per continuare.',
open_source_documents_software: 'software di documenti open source',
verified_phone_number: 'Verifica numero di telefono',
use_international_format: 'Usa formato internazionale: +1xxx',
@@ -257,10 +286,17 @@ const it = {
pay_with_strip: 'Paga con Stripe',
reupload: 'Ricarica',
upload: 'Carica',
files: 'File'
files: 'File',
signature_is_too_small_please_redraw: 'La firma è troppo piccola. Ridisegnala per favore.'
}
const de = {
complete_all_required_fields_to_proceed_with_identity_verification: 'Vervollständigen Sie alle erforderlichen Felder, um mit der Identitätsverifizierung fortzufahren.',
verify_id: 'ID überprüfen',
identity_verification: 'Identitätsüberprüfung',
complete: 'Abschließen',
fill_all_required_fields_to_complete: 'Alle erforderlichen Felder ausfüllen, um abzuschließen',
sign_and_complete: 'Signieren und Abschließen',
invite: 'Einladen',
email: 'E-Mail',
approved: 'Genehmigt',
@@ -299,7 +335,8 @@ const de = {
payment: 'Zahlung',
number_phone_is_invalid: '{number} Telefonnummer ist ungültig',
phone: 'Telefon',
submit_form: 'Formular absenden',
start_now: 'Jetzt starten',
continue: 'Fortsetzen',
sign_now: 'Jetzt unterschreiben',
type_here_: 'Hier eingeben...',
optional: 'optional',
@@ -320,9 +357,11 @@ const de = {
type_signature_here: 'Unterschrift hier eingeben',
type_initial_here: 'Initialen hier eingeben',
form_has_been_completed: 'Formular wurde ausgefüllt!',
document_has_been_signed: 'Dokument wurde unterschrieben!',
documents_have_been_signed: 'Dokumente wurden unterschrieben!',
create_a_free_account: 'Kostenloses Konto erstellen',
signed_with: 'Unterschrieben mit',
please_check_the_box_to_continue: 'Bitte setzen Sie das Häkchen, um fortzufahren',
powered_by: 'Bereitgestellt von',
please_check_the_box_to_continue: 'Bitte setzen Sie das Häkchen, um fortzufahren.',
open_source_documents_software: 'Open-Source-Dokumentensoftware',
verified_phone_number: 'Telefonnummer überprüfen',
use_international_format: 'Internationales Format verwenden: +1xxx',
@@ -344,10 +383,17 @@ const de = {
pay_with_strip: 'Mit Stripe bezahlen',
reupload: 'Erneut hochladen',
upload: 'Hochladen',
files: 'Dateien'
files: 'Dateien',
signature_is_too_small_please_redraw: 'Die Unterschrift ist zu klein. Bitte erneut zeichnen.'
}
const fr = {
complete_all_required_fields_to_proceed_with_identity_verification: "Veuillez remplir tous les champs obligatoires pour continuer la vérification de l'identité.",
verif_id: "Vérification de l'ID",
verif_identite: "Vérification de l'identité",
complete: 'Terminer',
fill_all_required_fields_to_complete: 'Veuillez remplir tous les champs obligatoires pour compléter',
sign_and_complete: 'Signer et Terminer',
invite: 'Inviter',
email: 'Courriel',
approved: 'Approuvé',
@@ -386,7 +432,8 @@ const fr = {
payment: 'Paiement',
number_phone_is_invalid: '{number} téléphone est invalide',
phone: 'Téléphone',
submit_form: 'Envoyer le Formulaire',
start_now: 'Commencer maintenant',
continue: 'Continuer',
sign_now: 'Signer maintenant',
type_here_: 'Tapez ici...',
optional: 'facultatif',
@@ -407,9 +454,11 @@ const fr = {
type_signature_here: 'Tapez la signature ici',
type_initial_here: 'Tapez les initiales ici',
form_has_been_completed: 'Le formulaire a été complété !',
document_has_been_signed: 'Le document a été signé!',
documents_have_been_signed: 'Les documents ont été signés!',
create_a_free_account: 'Créer un Compte Gratuit',
signed_with: 'Signé avec',
please_check_the_box_to_continue: 'Veuillez cocher la case pour continuer',
powered_by: 'Propulsé par',
please_check_the_box_to_continue: 'Veuillez cocher la case pour continuer.',
open_source_documents_software: 'logiciel de documents open source',
verified_phone_number: 'Vérifier le numéro de téléphone',
use_international_format: 'Utiliser le format international : +1xxx',
@@ -431,10 +480,17 @@ const fr = {
pay_with_strip: 'Paiement avec Stripe',
reupload: 'Recharger',
upload: 'Télécharger',
files: 'Fichiers'
files: 'Fichiers',
signature_is_too_small_please_redraw: 'La signature est trop petite. Veuillez la redessiner.'
}
const pl = {
complete_all_required_fields_to_proceed_with_identity_verification: 'Uzupełnij wszystkie wymagane pola, aby kontynuować weryfikację tożsamości.',
verify_id: 'Zweryfikuj ID',
identity_verification: 'Weryfikacja tożsamości',
complete: 'Zakończ',
fill_all_required_fields_to_complete: 'Uzupełnij wszystkie wymagane pola, aby zakończyć',
sign_and_complete: 'Podpisz i zakończ',
invite: 'Zaproś',
email: 'E-mail',
approved: 'Zaakceptowany',
@@ -473,7 +529,8 @@ const pl = {
payment: 'Płatność',
number_phone_is_invalid: '{number} telefon jest nieprawidłowy',
phone: 'Telefon',
submit_form: 'Wyślij Formularz',
start_now: 'Zacznij teraz',
continue: 'Kontynuuj',
sign_now: 'Podpisz teraz',
type_here_: 'Wpisz tutaj...',
optional: 'opcjonalny',
@@ -494,9 +551,11 @@ const pl = {
type_signature_here: 'Wpisz podpis tutaj',
type_initial_here: 'Wpisz inicjały tutaj',
form_has_been_completed: 'Formularz został wypełniony!',
document_has_been_signed: 'Dokument został podpisany!',
documents_have_been_signed: 'Dokumenty zostały podpisane!',
create_a_free_account: 'Utwórz darmowe konto',
signed_with: 'Podpisane za pomocą',
please_check_the_box_to_continue: 'Proszę zaznaczyć pole, aby kontynuować',
powered_by: 'Napędzany przez',
please_check_the_box_to_continue: 'Proszę zaznaczyć pole, aby kontynuować.',
open_source_documents_software: 'oprogramowanie do dokumentów open source',
verified_phone_number: 'Zweryfikuj numer telefonu',
use_international_format: 'Użyj międzynarodowego formatu: +1xxx',
@@ -518,10 +577,17 @@ const pl = {
pay_with_strip: 'Płatność za pomocą Stripe',
reupload: 'Ponowne przesłanie',
upload: 'Przesyłanie',
files: 'Pliki'
files: 'Pliki',
signature_is_too_small_please_redraw: 'Podpis jest zbyt mały. Proszę narysować go ponownie.'
}
const uk = {
complete_all_required_fields_to_proceed_with_identity_verification: "Заповніть всі обов'язкові поля, щоб продовжити перевірку особи.",
verify_id: 'Підтвердження ідентичності',
identity_verification: 'Ідентифікація особи',
complete: 'Завершити',
fill_all_required_fields_to_complete: "Заповніть всі обов'язкові поля для завершення",
sign_and_complete: 'Підписати і завершити',
invite: 'Запросити',
email: 'Електронна пошта',
approved: 'Затверджено',
@@ -560,7 +626,8 @@ const uk = {
payment: 'Платіж',
number_phone_is_invalid: '{number} телефон недійсний',
phone: 'Телефон',
submit_form: 'Надіслати Форму',
start_now: 'Почати зараз',
continue: 'Продовжити',
sign_now: 'Підписати зараз',
type_here_: 'Введіть тут',
optional: 'необов’язково',
@@ -581,9 +648,11 @@ const uk = {
type_signature_here: 'Введіть підпис тут',
type_initial_here: 'Введіть ініціали тут',
form_has_been_completed: 'Форму заповнено!',
document_has_been_signed: 'Документ був підписаний!',
documents_have_been_signed: 'Документи були підписані!',
create_a_free_account: 'Створити безкоштовний обліковий запис',
signed_with: ідписано за допомогою',
please_check_the_box_to_continue: 'Будь ласка, позначте прапорець, щоб продовжити',
powered_by: рацює на базі',
please_check_the_box_to_continue: 'Будь ласка, позначте прапорець, щоб продовжити.',
open_source_documents_software: 'відкритий програмний засіб для документів',
verified_phone_number: 'Підтвердіть номер телефону',
use_international_format: 'Використовуйте міжнародний формат: +1xxx',
@@ -605,10 +674,17 @@ const uk = {
pay_with_strip: 'Сплатити за допомогою Stripe',
reupload: 'Перезавантажити',
upload: 'Завантажити',
files: 'Файли'
files: 'Файли',
signature_is_too_small_please_redraw: 'Підпис занадто малий. Будь ласка, перемалюйте його.'
}
const cs = {
complete_all_required_fields_to_proceed_with_identity_verification: 'Vyplňte všechna povinná pole, abyste mohli pokračovat v ověření identity.',
verify_id: 'Ověřit ID',
identity_verification: 'Ověření identity',
complete: 'Dokončit',
fill_all_required_fields_to_complete: 'Please complete all mandatory fields',
sign_and_complete: 'Podepsat a dokončit',
invite: 'Pozvat',
email: 'E-mail',
approved: 'Schváleno',
@@ -647,7 +723,8 @@ const cs = {
payment: 'Platba',
phone: 'Telefon',
number_phone_is_invalid: '{number} telefon je neplatný',
submit_form: 'Odeslat formulář',
start_now: 'Začít nyní',
continue: 'Pokračovat',
sign_now: 'Podepsat nyní',
type_here_: 'Zadejte zde',
optional: 'volitelné',
@@ -668,9 +745,11 @@ const cs = {
type_signature_here: 'Sem zadejte podpis',
type_initial_here: 'Sem zadejte iniciály',
form_has_been_completed: 'Formulář byl dokončen!',
document_has_been_signed: 'Dokument byl podepsán!',
documents_have_been_signed: 'Dokumenty byly podepsány!',
create_a_free_account: 'Vytvořit bezplatný účet',
signed_with: 'Podepsáno pomocí',
please_check_the_box_to_continue: 'Prosím, zaškrtněte políčko pro pokračování',
powered_by: 'Poháněno',
please_check_the_box_to_continue: 'Prosím, zaškrtněte políčko pro pokračování.',
open_source_documents_software: 'open source software pro dokumenty',
verified_phone_number: 'Ověřte telefonní číslo',
use_international_format: 'Použijte mezinárodní formát: +1xxx',
@@ -692,10 +771,17 @@ const cs = {
pay_with_strip: 'Zaplacení přes Stripe',
reupload: 'Znovu nahrát',
upload: 'Nahrát',
files: 'Soubory'
files: 'Soubory',
signature_is_too_small_please_redraw: 'Podpis je příliš malý. Prosím, překreslete ho.'
}
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',
fill_all_required_fields_to_complete: 'Preencher todos os campos obrigatórios para concluir',
sign_and_complete: 'Assinar e Completar',
invite: 'Convidar',
email: 'E-mail',
approved: 'Aprovado',
@@ -734,7 +820,8 @@ const pt = {
payment: 'Pagamento',
number_phone_is_invalid: '{number} telefone é inválido',
phone: 'Telefone',
submit_form: 'Enviar Formulário',
start_now: 'Começar agora',
continue: 'Continuar',
sign_now: 'Assinar agora',
type_here_: 'Digite aqui',
optional: 'opcional',
@@ -755,9 +842,11 @@ const pt = {
type_signature_here: 'Digite a assinatura aqui',
type_initial_here: 'Digite as iniciais aqui',
form_has_been_completed: 'O formulário foi concluído!',
document_has_been_signed: 'O documento foi assinado!',
documents_have_been_signed: 'Os documentos foram assinados!',
create_a_free_account: 'Criar uma Conta Gratuita',
signed_with: 'Assinado com',
please_check_the_box_to_continue: 'Por favor, marque a caixa para continuar',
powered_by: 'Desenvolvido por',
please_check_the_box_to_continue: 'Por favor, marque a caixa para continuar.',
open_source_documents_software: 'software de documentos de código aberto',
verified_phone_number: 'Verificar Número de Telefone',
use_international_format: 'Use formato internacional: +1xxx',
@@ -779,10 +868,17 @@ const pt = {
pay_with_strip: 'Pagar com Stripe',
reupload: 'Reenviar',
upload: 'Carregar',
files: 'Arquivos'
files: 'Arquivos',
signature_is_too_small_please_redraw: 'A assinatura é muito pequena. Por favor, redesenhe-a.'
}
const he = {
complete_all_required_fields_to_proceed_with_identity_verification: 'מלא את כל השדות הנדרשים כדי להמשיך עם אימות זהות.',
verify_id: 'אמת מזהה',
identity_verification: 'אימות זהות',
complete: 'השלם',
fill_all_required_fields_to_complete: 'נא למלא את כל השדות הנדרשים להשלמה',
sign_and_complete: 'חתום והשלם',
invite: 'הזמן',
email: 'דוא"ל',
approved: 'מאושר',
@@ -821,7 +917,8 @@ const he = {
payment: 'תשלום',
phone: 'טלפון',
number_phone_is_invalid: '{number} טלפון אינו חוקי',
submit_form: 'שלח טופס',
start_now: 'התחל עכשיו',
continue: 'המשך',
sign_now: 'חתום כעת',
type_here_: 'הקלד כאן',
optional: 'אופציונלי',
@@ -843,9 +940,11 @@ const he = {
type_signature_here: 'הקלד חתימה כאן',
type_initial_here: 'הקלד ציוני ראשי תיבות כאן',
form_has_been_completed: 'הטופס הושלם',
document_has_been_signed: 'המסמך נחתם!',
documents_have_been_signed: 'המסמכים נחתמו!',
create_a_free_account: 'צור חשבון חינם',
signed_with: 'חתום באמצעות',
please_check_the_box_to_continue: 'אנא סמן את התיבה כדי להמשיך',
powered_by: 'מופעל על ידי',
please_check_the_box_to_continue: 'אנא סמן את התיבה כדי להמשיך.',
open_source_documents_software: 'תוכנה פתוחה למסמכים',
verified_phone_number: 'אימות מספר טלפון',
use_international_format: 'השתמש בפורמט בינלאומי: +1xxx',
@@ -867,10 +966,17 @@ const he = {
pay_with_strip: 'שלם עם סטרייפ',
reupload: 'העלה שוב',
upload: 'העלאה',
files: 'קבצים'
files: 'קבצים',
signature_is_too_small_please_redraw: 'החתימה קטנה מדי. אנא צייר מחדש.'
}
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',
fill_all_required_fields_to_complete: 'Vul alle verplichte velden in om te voltooien',
sign_and_complete: 'Ondertekenen en voltooien',
invite: 'Uitnodigen',
email: 'E-mail',
approved: 'Goedgekeurd',
@@ -910,7 +1016,8 @@ const nl = {
payment: 'Betaling',
phone: 'Telefoon',
number_phone_is_invalid: '{number} telefoon is ongeldig',
submit_form: 'Formulier verzenden',
start_now: 'Nu beginnen',
continue: 'Doorgaan',
sign_now: 'Nu ondertekenen',
type_here_: 'Typ hier...',
optional: 'Optioneel',
@@ -931,9 +1038,11 @@ const nl = {
type_signature_here: 'Typ hier uw handtekening',
type_initial_here: 'Typ hier uw initialen',
form_has_been_completed: 'Formulier is voltooid!',
document_has_been_signed: 'Document is ondertekend!',
documents_have_been_signed: 'De documenten zijn ondertekend!',
create_a_free_account: 'Maak een gratis account aan',
signed_with: 'Ondertekend met',
please_check_the_box_to_continue: 'Vink het vakje aan om door te gaan',
powered_by: 'Aangedreven door',
please_check_the_box_to_continue: 'Vink het vakje aan om door te gaan.',
open_source_documents_software: 'Open source documenten software',
verified_phone_number: 'Geverifieerd telefoonnummer',
use_international_format: 'Gebruik internationaal formaat: +1xxx',
@@ -955,10 +1064,17 @@ const nl = {
pay_with_strip: 'Betalen met Stripe',
reupload: 'Opnieuw uploaden',
upload: 'Uploaden',
files: 'Bestanden'
files: 'Bestanden',
signature_is_too_small_please_redraw: 'De handtekening is te klein. Teken deze opnieuw, alstublieft.'
}
const ar = {
complete_all_required_fields_to_proceed_with_identity_verification: 'أكمل جميع الحقول المطلوبة للمتابعة في التحقق من الهوية.',
verify_id: 'تحقق من الهوية',
identity_verification: 'التحقق من الهوية',
complete: 'اكتمال',
fill_all_required_fields_to_complete: 'يرجى ملء جميع الحقول المطلوبة لإكمال',
sign_and_complete: 'التوقيع والاكتمال',
invite: 'دعوة',
email: 'البريد الإلكتروني',
approved: 'موافق عليه',
@@ -997,7 +1113,8 @@ const ar = {
payment: 'الدفع',
phone: 'هاتف',
number_phone_is_invalid: '{number} هاتف غير صالح',
submit_form: 'إرسال النموذج',
start_now: 'ابدأ الآن',
continue: 'متابعة',
sign_now: 'وقع الآن',
type_here_: 'اكتب هنا',
optional: 'اختياري',
@@ -1018,9 +1135,11 @@ const ar = {
type_signature_here: 'اكتب التوقيع هنا',
type_initial_here: 'اكتب الاختصارات هنا',
form_has_been_completed: 'تم إكمال النموذج!',
document_has_been_signed: 'تم توقيع الوثيقة!',
documents_have_been_signed: 'تم توقيع الوثائق!',
create_a_free_account: 'إنشاء حساب مجاني',
signed_with: 'تم التوقيع بواسطة',
please_check_the_box_to_continue: 'الرجاء التحقق من الخانة للمتابعة',
powered_by: 'مدعوم من',
please_check_the_box_to_continue: 'الرجاء التحقق من الخانة للمتابعة.',
open_source_documents_software: 'برنامج وثائق مفتوح المصدر',
verified_phone_number: 'تحقق من رقم الهاتف',
use_international_format: 'استخدم الشكل الدولي: +1xxx',
@@ -1042,10 +1161,17 @@ const ar = {
pay_with_strip: 'الدفع بواسطة Stripe',
reupload: 'إعادة التحميل',
upload: 'تحميل',
files: 'الملفات'
files: 'الملفات',
signature_is_too_small_please_redraw: 'التوقيع صغير جدًا. يرجى إعادة الرسم.'
}
const ko = {
complete_all_required_fields_to_proceed_with_identity_verification: '신원 확인을 진행하려면 모든 필수 필드를 작성하십시오.',
verify_id: '아이디 확인',
identity_verification: '신원 확인',
complete: '완료',
fill_all_required_fields_to_complete: '모든 필수 필드를 작성하여 완료하세요',
sign_and_complete: '서명하고 완료하기',
invite: '초대하기',
email: '이메일',
approved: '승인됨',
@@ -1083,7 +1209,8 @@ const ko = {
payment: '결제',
phone: '전화',
number_phone_is_invalid: '{number} 전화번호가 유효하지 않습니다',
submit_form: '양식 제출',
start_now: '지금 시작하기',
continue: '계속하다',
sign_now: '지금 서명',
type_here_: '여기에 입력...',
optional: '선택 사항',
@@ -1104,9 +1231,11 @@ const ko = {
type_signature_here: '여기에 서명 입력',
type_initial_here: '여기에 이니셜 입력',
form_has_been_completed: '양식이 완료되었습니다!',
document_has_been_signed: '문서가 서명되었습니다!',
documents_have_been_signed: '문서가 서명되었습니다!',
create_a_free_account: '무료 계정 생성',
signed_with: '다음으로 서명:',
please_check_the_box_to_continue: '계속하려면 확인란을 선택하십시오',
powered_by: '구동',
please_check_the_box_to_continue: '계속하려면 확인란을 선택하십시오.',
open_source_documents_software: '오픈 소스 문서 소프트웨어',
verified_phone_number: '전화번호 확인됨',
use_international_format: '국제 포맷 사용: +1xxx',
@@ -1128,7 +1257,8 @@ const ko = {
pay_with_strip: '스트라이프로 결제',
reupload: '다시 업로드',
upload: '업로드',
files: '파일'
files: '파일',
signature_is_too_small_please_redraw: '서명이 너무 작습니다. 다시 그려주세요.'
}
const i18n = { en, es, it, de, fr, pl, uk, cs, pt, he, nl, ar, ko }
@@ -226,6 +226,10 @@ export default {
if (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.isInitialsStarted = true
@@ -1,7 +1,6 @@
<template>
<label
v-if="!modelValue && !sessionId"
:for="field.uuid"
class="label text-2xl mb-2"
>
<MarkdownContent
File diff suppressed because it is too large Load Diff
+110 -21
View File
@@ -61,30 +61,59 @@
</a>
</div>
</div>
<input
<div
v-show="!isCodeSent"
:id="field.uuid"
ref="phone"
:value="modelValue || defaultValue"
:readonly="!!defaultValue"
class="base-input !text-2xl w-full"
autocomplete="tel"
pattern="^\+[0-9\s\-]+$"
type="tel"
inputmode="tel"
:required="field.required"
placeholder="+1 234 567-8900"
:name="`values[${field.uuid}]`"
@invalid="$event.target.value ? $event.target.setCustomValidity(`${t('use_international_format')}...`) : ''"
@input="[$event.target.setCustomValidity(''), $emit('update:model-value', $event.target.value)]"
@focus="$emit('focus')"
class="flex w-full rounded-full outline-neutral-content outline-2 outline-offset-2 focus-within:outline"
>
<div
id="country_code"
class="relative inline-block"
>
<div class="btn bg-base-200 border border-neutral-300 text-2xl whitespace-nowrap font-normal rounded-l-full">
{{ selectedCountry.flag }} +{{ selectedCountry.dial }}
</div>
<select
id="country_code_select"
class="absolute top-0 bottom-0 right-0 left-0 opacity-0 w-full h-full cursor-pointer"
:disabled="!!defaultValue"
@change="onCountrySelect(countries.find((country) => country.flag === $event.target.value))"
>
<option
v-for="(country, index) in countries"
:key="index"
:value="country.flag"
>
{{ country.flag }} {{ country.name }}
</option>
</select>
</div>
<input
:name="`values[${field.uuid}]`"
:value="fullInternationalPhoneValue"
hidden
>
<input
:id="field.uuid"
ref="phone"
:value="phoneValue"
:readonly="!!defaultValue"
class="base-input !text-2xl !rounded-l-none !border-l-0 !outline-none w-full"
autocomplete="tel"
type="tel"
inputmode="tel"
:required="field.required"
placeholder="234 567-8900"
@input="onPhoneInput"
@focus="$emit('focus')"
>
</div>
</div>
</div>
</template>
<script>
import MarkdownContent from './markdown_content'
import phoneData from './phone_data'
function throttle (func, delay) {
let lastCallTime = 0
@@ -144,13 +173,73 @@ export default {
data () {
return {
isCodeSent: false,
isResendLoading: false
isResendLoading: false,
phoneValue: this.modelValue || this.defaultValue || '',
selectedCountry: {}
}
},
computed: {
countries () {
return phoneData.map(([iso, name, dial, flag, tz]) => {
return { iso, name, dial, flag, tz }
})
},
countriesDialIndex () {
return this.countries.reduce((acc, item) => {
acc[item.dial] ||= item
return acc
}, {})
},
dialCodesRegexp () {
const dialCodes = this.countries.map((country) => country.dial).sort((a, b) => b.length - a.length)
return new RegExp(`^\\+(${dialCodes.join('|')})`)
},
detectedPhoneValueDialCode () {
return (this.phoneValue || '').replace(/[^\d+]/g, '').match(this.dialCodesRegexp)?.[1]
},
fullInternationalPhoneValue () {
if (this.detectedPhoneValueDialCode) {
return this.phoneValue
} else if (this.phoneValue) {
return ['+', this.selectedCountry.dial, this.phoneValue].filter(Boolean).join('')
} else {
return ''
}
}
},
mounted () {
const browserTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone
if (this.detectedPhoneValueDialCode) {
this.selectedCountry = this.countriesDialIndex[this.detectedPhoneValueDialCode]
} else if (browserTimeZone) {
const tz = browserTimeZone.split('/')[1]
this.selectedCountry = this.countries.find((country) => country.tz.includes(tz)) || this.countries[0]
}
},
methods: {
emitSubmit: throttle(function (e) {
this.$emit('submit')
}, 1000),
onCountrySelect (country) {
if (this.selectedCountry.flag !== country.flag) {
this.phoneValue = this.phoneValue.replace(`+${this.selectedCountry.dial}`, `+${country.dial}`)
}
this.selectedCountry = country
this.$refs.phone.focus()
},
onPhoneInput (e) {
this.phoneValue = e.target.value
if (this.detectedPhoneValueDialCode) {
this.selectedCountry = this.countriesDialIndex[this.detectedPhoneValueDialCode]
}
},
onInputCode (e) {
if (e.target.value.length === 6) {
this.emitSubmit()
@@ -171,14 +260,14 @@ export default {
body: JSON.stringify({
submitter_slug: this.submitterSlug,
locale: this.locale,
phone: this.$refs.phone.value
phone: this.fullInternationalPhoneValue
}),
headers: { 'Content-Type': 'application/json' }
}).then(async (resp) => {
if (resp.status === 422) {
const data = await resp.json()
alert(this.t('number_phone_is_invalid').replace('{number}', this.$refs.phone.value))
alert(this.t('number_phone_is_invalid').replace('{number}', this.fullInternationalPhoneValue))
return Promise.reject(new Error(data.error))
}
@@ -189,13 +278,13 @@ export default {
return Promise.resolve({})
}
if (!this.$refs.phone.value.toString().startsWith('+')) {
if (!this.fullInternationalPhoneValue.toString().startsWith('+')) {
alert(this.t('use_international_format'))
return Promise.reject(new Error('phone invalid'))
} else if (!this.isCodeSent) {
return this.sendVerificationCode().then(() => {
this.$emit('update:model-value', this.$refs.phone.value)
this.$emit('update:model-value', this.fullInternationalPhoneValue)
this.isCodeSent = true
@@ -145,6 +145,10 @@
{{ t('clear') }}
</a>
</div>
<div
v-if="isTextSignature"
class="absolute top-0 right-0 left-0 bottom-0"
/>
<canvas
v-show="!modelValue && !computedPreviousValue"
ref="canvas"
@@ -197,6 +201,7 @@
<select
v-if="requireSigningReason && !isOtherReason"
class="select base-input !text-2xl w-full mt-6 text-center"
:class="{ 'text-gray-300': !reason }"
required
:name="`values[${field.preferences.reason_field_uuid}]`"
@change="$event.target.value === 'other' ? [reason = '', isOtherReason = true] : $emit('update:reason', $event.target.value)"
@@ -205,6 +210,7 @@
value=""
disabled
:selected="!reason"
class="text-gray-300"
>
{{ t('select_a_reason') }}
</option>
@@ -213,10 +219,14 @@
:key="option"
:value="option"
:selected="reason === option"
class="text-base-content"
>
{{ label }}
</option>
<option value="other">
<option
value="other"
class="text-base-content"
>
{{ t('other') }}
</option>
</select>
@@ -249,7 +259,7 @@
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
href="https://www.docuseal.co/esign-disclosure"
href="https://www.docuseal.com/esign-disclosure"
target="_blank"
>
<span class="inline md:hidden">
@@ -413,6 +423,10 @@ export default {
if (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.isSignatureStarted = true
+3 -1
View File
@@ -114,7 +114,9 @@ export default {
const area = this.field.areas?.[0]
if (area) {
return parseInt(area.w / area.cell_w) + 1
const num = area.w / area.cell_w
return (num % 1) > 0.2 ? parseInt(num) + 1 : parseInt(num)
} else {
return null
}
@@ -0,0 +1,169 @@
<template>
<label
class="label text-2xl mb-2"
>
<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"
>
<MarkdownContent :string="field.description" />
</div>
<div
v-if="emptyValueRequiredStep && emptyValueRequiredStep[0] !== field"
class="px-1"
>
{{ 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
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,
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
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()
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>
+4 -2
View File
@@ -175,6 +175,7 @@
<div
ref="textContainer"
class="flex items-center px-0.5"
:style="{ color: field.preferences?.color }"
:class="{ 'w-full h-full': ['cells', 'checkbox'].includes(field.type) }"
>
<IconCheck
@@ -236,7 +237,7 @@
/>
<span
v-if="field?.type && editable"
class="h-4 w-4 md:h-2.5 md:w-2.5 -right-1 rounded-full -bottom-1 border-gray-400 bg-white shadow-md border absolute cursor-nwse-resize"
class="h-4 w-4 lg:h-2.5 lg:w-2.5 -right-1 rounded-full -bottom-1 border-gray-400 bg-white shadow-md border absolute cursor-nwse-resize"
@mousedown.stop="startResize"
@touchstart="startTouchResize"
/>
@@ -256,7 +257,7 @@
:to="modalContainerEl"
>
<ConditionsModal
:field="field"
:item="field"
:build-default-name="buildDefaultName"
@close="isShowConditionsModal = false"
/>
@@ -360,6 +361,7 @@ export default {
},
computed: {
fieldNames: FieldType.computed.fieldNames,
fieldLabels: FieldType.computed.fieldLabels,
fieldIcons: FieldType.computed.fieldIcons,
isDefaultValuePresent () {
if (this.field?.type === 'radio' && this.field?.areas?.length > 1) {
+75 -21
View File
@@ -1,10 +1,11 @@
<template>
<div
style="max-width: 1600px"
class="mx-auto pl-3 md:pl-4 h-full"
class="mx-auto pl-3 h-full"
:class="isMobile ? 'pl-4' : 'md:pl-4'"
>
<div
v-if="pendingFieldAttachmentUuids.length"
v-if="pendingFieldAttachmentUuids.length && editable"
class="top-1.5 sticky h-0 z-20 max-w-2xl mx-auto"
>
<div class="alert border-base-content/30 py-2 px-2.5">
@@ -131,7 +132,7 @@
class="flex items-center justify-center space-x-2"
>
<IconEye class="w-6 h-6 flex-shrink-0" />
<span class="whitespace-nowrap">Save and Preview</span>
<span class="whitespace-nowrap">{{ t('save_and_preview') }}</span>
</a>
</li>
<li>
@@ -142,7 +143,7 @@
@click="closeDropdown"
>
<IconAdjustments class="w-6 h-6 flex-shrink-0" />
<span class="whitespace-nowrap">Preferences</span>
<span class="whitespace-nowrap">{{ t('preferences') }}</span>
</a>
</li>
</ul>
@@ -163,7 +164,7 @@
<div
id="main_container"
class="flex"
: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
v-if="withDocumentsList"
@@ -219,7 +220,8 @@
</div>
<div
id="pages_container"
class="w-full overflow-y-hidden md:overflow-y-auto overflow-x-hidden mt-0.5 pt-0.5"
class="w-full overflow-y-hidden overflow-x-hidden mt-0.5 pt-0.5"
:class="isMobile ? 'overflow-y-auto' : 'md:overflow-y-auto'"
>
<div
ref="documents"
@@ -320,7 +322,7 @@
</div>
</div>
<div
v-if="withFieldsList"
v-if="withFieldsList && !isMobile"
id="fields_list_container"
class="relative w-80 flex-none mt-1 pr-4 pl-0.5 hidden md:block"
:class="drawField ? 'overflow-hidden' : 'overflow-y-auto overflow-x-hidden'"
@@ -332,7 +334,7 @@
>
<div class="bg-base-200 rounded-lg p-5 text-center space-y-4">
<p>
{{ t('draw_field_on_the_document').replace('{field}', drawField?.name || '') }}
{{ t('draw_field_on_the_document') }}
</p>
<div>
<button
@@ -362,6 +364,7 @@
:default-submitters="defaultSubmitters"
:draw-field-type="drawFieldType"
:default-fields="[...defaultRequiredFields, ...defaultFields]"
:template="template"
:default-required-fields="defaultRequiredFields"
:field-types="fieldTypes"
:with-sticky-submitters="withStickySubmitters"
@@ -378,14 +381,14 @@
</div>
</div>
</div>
<div class="sticky bottom-0">
<div class="sticky bottom-0 z-10">
<MobileDrawField
v-if="drawField && isBreakpointLg"
v-if="drawField && (isBreakpointLg || isMobile)"
:draw-field="drawField"
:fields="template.fields"
:submitters="template.submitters"
:selected-submitter="selectedSubmitter"
class="md:hidden"
:class="{ 'md:hidden': !isMobile }"
:editable="editable"
@cancel="[drawField = null, drawOption = null]"
@change-submitter="[selectedSubmitter = $event, drawField.submitter_uuid = $event.uuid]"
@@ -396,6 +399,7 @@
:default-fields="[...defaultRequiredFields, ...defaultFields]"
:default-required-fields="defaultRequiredFields"
:field-types="fieldTypes"
:class="{ 'md:hidden': !isMobile }"
:selected-submitter="selectedSubmitter"
@select="startFieldDraw($event)"
/>
@@ -415,10 +419,11 @@ import Contenteditable from './contenteditable'
import DocumentPreview from './preview'
import DocumentControls from './controls'
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 { v4 } from 'uuid'
import { ref, computed } from 'vue'
import { en as i18nEn } from './i18n'
import * as i18n from './i18n'
export default {
name: 'TemplateBuilder',
@@ -454,6 +459,7 @@ export default {
fieldTypes: this.fieldTypes,
backgroundColor: this.backgroundColor,
withPhone: this.withPhone,
withVerification: this.withVerification,
withPayment: this.withPayment,
isPaymentConnected: this.isPaymentConnected,
withFormula: this.withFormula,
@@ -558,6 +564,11 @@ export default {
required: false,
default: () => []
},
defineSubmitters: {
type: Array,
required: false,
default: () => []
},
acceptFileTypes: {
type: String,
required: false,
@@ -629,6 +640,11 @@ export default {
required: false,
default: false
},
withVerification: {
type: Boolean,
required: false,
default: null
},
withPayment: {
type: Boolean,
required: false,
@@ -677,8 +693,15 @@ export default {
}
},
computed: {
submitterDefaultNames: FieldSubmitter.computed.names,
selectedAreaRef: () => ref(),
fieldsDragFieldRef: () => ref(),
language () {
return this.locale.split('-')[0].toLowerCase()
},
isMobile () {
return /android|iphone|ipad/i.test(navigator.userAgent)
},
defaultDateFormat () {
const isUsBrowser = Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US')
const isUsTimezone = new Intl.DateTimeFormat('en-US', { timeZoneName: 'short' }).format(new Date()).match(/\s(?:CST|CDT|PST|PDT|EST|EDT)$/)
@@ -690,6 +713,7 @@ export default {
'de-DE': 'DD.MM.YYYY',
'fr-FR': 'DD/MM/YYYY',
'it-IT': 'DD/MM/YYYY',
'en-GB': 'DD/MM/YYYY',
'es-ES': 'DD/MM/YYYY'
}
},
@@ -745,6 +769,18 @@ export default {
}
})
this.defineSubmitters.forEach((name, index) => {
const submitter = (this.template.submitters[index] ||= {})
submitter.name = name || this.submitterDefaultNames[index]
if (existingSubmittersUuids.filter(Boolean).length) {
submitter.uuid = existingSubmittersUuids[index] || submitter.uuid || v4()
} else {
submitter.uuid ||= v4()
}
})
this.selectedSubmitter = this.template.submitters[0]
},
mounted () {
@@ -786,7 +822,7 @@ export default {
document.activeElement.blur()
},
t (key) {
return this.i18n[key] || i18nEn[key] || key
return this.i18n[key] || i18n[this.language]?.[key] || i18n.en[key] || key
},
removePendingFields () {
this.template.fields = this.template.fields.filter((f) => {
@@ -994,6 +1030,22 @@ export default {
if (!field.areas.length) {
this.template.fields.splice(this.template.fields.indexOf(field), 1)
this.template.fields.forEach((f) => {
(f.conditions || []).forEach((c) => {
if (c.field_uuid === field.uuid) {
f.conditions.splice(f.conditions.indexOf(c), 1)
}
})
})
this.template.schema.forEach((item) => {
(item.conditions || []).forEach((c) => {
if (c.field_uuid === field.uuid) {
item.conditions.splice(item.conditions.indexOf(c), 1)
}
})
})
}
this.save()
@@ -1052,7 +1104,7 @@ export default {
} else if (type === 'image') {
area.w = pageMask.clientWidth / 5 / pageMask.clientWidth
area.h = (pageMask.clientWidth / 5 / pageMask.clientWidth) * (pageMask.clientWidth / pageMask.clientHeight)
} else if (type === 'signature' || type === 'stamp') {
} else if (type === 'signature' || type === 'stamp' || type === 'verification') {
area.w = pageMask.clientWidth / 5 / pageMask.clientWidth
area.h = (pageMask.clientWidth / 5 / pageMask.clientWidth) * (pageMask.clientWidth / pageMask.clientHeight) / 2
} else if (type === 'initials') {
@@ -1216,7 +1268,7 @@ export default {
w: area.maskW / 5 / area.maskW,
h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH)
}
} else if (field.type === 'signature' || field.type === 'stamp') {
} else if (field.type === 'signature' || field.type === 'stamp' || field.type === 'verification') {
baseArea = {
w: area.maskW / 5 / area.maskW,
h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH) / 2
@@ -1319,7 +1371,9 @@ export default {
}
this.$nextTick(() => {
this.$refs.previews.scrollTop = this.$refs.previews.scrollHeight
if (this.$refs.previews) {
this.$refs.previews.scrollTop = this.$refs.previews.scrollHeight
}
this.scrollIntoDocument(data.schema[0])
})
@@ -1352,7 +1406,7 @@ export default {
this.save()
},
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)
}
@@ -1451,7 +1505,7 @@ export default {
if (!this.template.fields.length) {
e.preventDefault()
alert('Please draw fields to prepare the document.')
alert(this.t('please_draw_fields_to_prepare_the_document'))
} else {
const submitterWithoutFields =
this.template.submitters.find((submitter) => !this.template.fields.some((f) => f.submitter_uuid === submitter.uuid))
@@ -1459,7 +1513,7 @@ export default {
if (submitterWithoutFields) {
e.preventDefault()
alert(`Please add fields for the ${submitterWithoutFields.name}. Or, remove the ${submitterWithoutFields.name} if not needed.`)
alert(this.t('please_add_fields_for_the_submitter_name_or_remove_the_submitter_name_if_not_needed').replaceAll('{submitter_name}', submitterWithoutFields.name))
}
}
},
@@ -1475,13 +1529,13 @@ export default {
}
if (!this.template.fields.length) {
alert('Please draw fields to prepare the document.')
alert(this.t('please_draw_fields_to_prepare_the_document'))
} else {
const submitterWithoutFields =
this.template.submitters.find((submitter) => !this.template.fields.some((f) => f.submitter_uuid === submitter.uuid))
if (submitterWithoutFields) {
alert(`Please add fields for the ${submitterWithoutFields.name}. Or, remove the ${submitterWithoutFields.name} if not needed.`)
alert(this.t('please_add_fields_for_the_submitter_name_or_remove_the_submitter_name_if_not_needed').replaceAll('{submitter_name}', submitterWithoutFields.name))
} else {
this.isSaving = true
@@ -9,7 +9,7 @@
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl">
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
<span>
{{ t('condition') }} - {{ field.name || buildDefaultName(field, template.fields) }}
{{ t('condition') }} - {{ item.name || buildDefaultName(item, template.fields) }}
</span>
<a
href="#"
@@ -23,21 +23,32 @@
class="bg-base-300 rounded-xl py-2 px-3 text-center"
>
<a
href="https://www.docuseal.co/pricing"
href="https://www.docuseal.com/pricing"
target="_blank"
class="link"
>Available in Pro</a>
>{{ t('available_in_pro') }}</a>
</div>
<form @submit.prevent="validateSaveAndClose">
<div class="my-4 space-y-5">
<div class="my-4">
<div
v-for="(condition, cindex) in conditions"
:key="cindex"
class="space-y-4 relative"
>
<div
v-if="cindex > 0"
class="divider -mb-2 mx-1"
>
<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 border-b mx-1 -mb-1 pb-1"
class="flex justify-between mx-1"
>
<span class="text-sm">
{{ t('condition') }} {{ cindex + 1 }}
@@ -50,6 +61,7 @@
</div>
<select
class="select select-bordered select-sm w-full bg-white h-11 pl-4 text-base font-normal"
:class="{ 'text-gray-300': !condition.field_uuid }"
required
@change="[
condition.field_uuid = $event.target.value,
@@ -68,6 +80,7 @@
v-for="f in fields"
:key="f.uuid"
:value="f.uuid"
class="text-base-content"
:selected="condition.field_uuid === f.uuid"
>
{{ f.name || buildDefaultName(f, template.fields) }}
@@ -88,8 +101,9 @@
</option>
</select>
<select
v-if="conditionField(condition)?.options?.length"
v-if="['radio', 'select', 'multiple'].includes(conditionField(condition)?.type) && conditionField(condition)?.options"
class="select select-bordered select-sm w-full bg-white h-11 pl-4 text-base font-normal"
:class="{ 'text-gray-300': !condition.value }"
required
@change="condition.value = $event.target.value"
>
@@ -105,6 +119,7 @@
:key="option.uuid"
:value="option.uuid"
:selected="condition.value === option.uuid"
class="text-base-content"
>
{{ option.value || `${t('option')} ${index + 1}` }}
</option>
@@ -123,12 +138,12 @@
</button>
</form>
<div
v-if="field.conditions?.[0]?.field_uuid"
v-if="item.conditions?.[0]?.field_uuid"
class="text-center w-full mt-4"
>
<button
class="link"
@click="[conditions = [], delete field.conditions, validateSaveAndClose()]"
@click="[conditions = [], delete item.conditions, validateSaveAndClose()]"
>
{{ t('remove_condition') }}
</button>
@@ -143,7 +158,7 @@ export default {
name: 'ConditionModal',
inject: ['t', 'save', 'template', 'withConditions'],
props: {
field: {
item: {
type: Object,
required: true
},
@@ -155,22 +170,26 @@ export default {
emits: ['close'],
data () {
return {
conditions: this.field.conditions?.[0] ? JSON.parse(JSON.stringify(this.field.conditions)) : [{}]
conditions: this.item.conditions?.[0] ? JSON.parse(JSON.stringify(this.item.conditions)) : [{}]
}
},
computed: {
fields () {
return this.template.fields.reduce((acc, f) => {
if (f !== this.field && f.submitter_uuid === this.field.submitter_uuid) {
acc.push(f)
}
if (this.item.submitter_uuid) {
return this.template.fields.reduce((acc, f) => {
if (f !== this.item && f.submitter_uuid === this.item.submitter_uuid) {
acc.push(f)
}
return acc
}, [])
return acc
}, [])
} else {
return this.template.fields
}
}
},
created () {
this.field.conditions ||= []
this.item.conditions ||= []
},
methods: {
conditionField (condition) {
@@ -200,13 +219,13 @@ export default {
},
validateSaveAndClose () {
if (!this.withConditions) {
return alert('Available only in Pro')
return alert(this.t('available_only_in_pro'))
}
if (this.conditions.find((f) => f.field_uuid)) {
this.field.conditions = this.conditions
this.item.conditions = this.conditions
} else {
delete this.field.conditions
delete this.item.conditions
}
this.save()
+1 -1
View File
@@ -105,7 +105,7 @@ export default {
this.upload()
} else {
alert('Only PDF and images are supported.')
alert(this.t('only_pdf_and_images_are_supported'))
}
}
}
+4 -4
View File
@@ -236,7 +236,7 @@
:to="modalContainerEl"
>
<ConditionsModal
:field="field"
:item="field"
:build-default-name="buildDefaultName"
@close="isShowConditionsModal = false"
/>
@@ -312,6 +312,7 @@ export default {
},
computed: {
fieldNames: FieldType.computed.fieldNames,
fieldLabels: FieldType.computed.fieldLabels,
dropdownBgColor () {
return ['', null, 'transparent'].includes(this.backgroundColor) ? 'white' : this.backgroundColor
},
@@ -364,11 +365,10 @@ export default {
} else {
const typeIndex = fields.filter((f) => f.type === field.type).indexOf(field)
if (this.field.type === 'heading') {
if (field.type === 'heading') {
return `${this.fieldNames[field.type]} ${typeIndex + 1}`
} else {
const suffix = { multiple: this.t('select'), radio: this.t('group') }[field.type] || this.t('field')
return `${this.fieldNames[field.type]} ${suffix} ${typeIndex + 1}`
return `${this.fieldLabels[field.type]} ${typeIndex + 1}`
}
}
},
@@ -79,7 +79,7 @@
:stroke-width="1.6"
/>
<span class="py-1">
{{ t('add') }} {{ names[submitters.length] }}
{{ t('add') }} {{ names[lastPartyIndex] }}
</span>
</a>
</li>
@@ -202,7 +202,7 @@
:stroke-width="1.6"
/>
<span class="py-1">
{{ t('add') }} {{ names[submitters.length] }}
{{ t('add') }} {{ names[lastPartyIndex] }}
</span>
</a>
</li>
@@ -321,6 +321,15 @@ export default {
this.t('twentieth_party')
]
},
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 () {
return this.submitters.find((e) => e.uuid === this.modelValue)
}
@@ -330,7 +339,7 @@ export default {
this.$emit('update:model-value', submitter.uuid)
},
remove (submitter) {
if (window.confirm(this.t('are_you_sure'))) {
if (window.confirm(this.t('are_you_sure_'))) {
this.$emit('remove', submitter)
}
},
@@ -353,7 +362,7 @@ export default {
},
addSubmitter () {
const newSubmitter = {
name: this.names[this.submitters.length],
name: this.names[this.lastPartyIndex],
uuid: v4()
}
+27 -5
View File
@@ -30,7 +30,7 @@
v-for="(icon, type) in fieldIconsSorted"
:key="type"
>
<li v-if="(fieldTypes.length === 0 || fieldTypes.includes(type)) && (withPhone || type != 'phone') && (withPayment || type != 'payment')">
<li v-if="(fieldTypes.length === 0 || fieldTypes.includes(type)) && (withPhone || type != 'phone') && (withPayment || type != 'payment') && (withVerification || type != 'verification')">
<a
href="#"
class="text-sm py-1 px-2"
@@ -51,11 +51,11 @@
</template>
<script>
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp, IconSquareNumber1, IconHeading } from '@tabler/icons-vue'
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp, IconSquareNumber1, IconHeading, IconId } from '@tabler/icons-vue'
export default {
name: 'FiledTypeDropdown',
inject: ['withPhone', 'withPayment', 't', 'fieldTypes'],
inject: ['withPhone', 'withPayment', 'withVerification', 't', 'fieldTypes'],
props: {
modelValue: {
type: String,
@@ -111,7 +111,28 @@ export default {
cells: this.t('cells'),
stamp: this.t('stamp'),
payment: this.t('payment'),
phone: this.t('phone')
phone: this.t('phone'),
verification: this.t('verify_id')
}
},
fieldLabels () {
return {
text: this.t('text_field'),
signature: this.t('signature_field'),
initials: this.t('initials_field'),
date: this.t('date_field'),
number: this.t('number_field'),
image: this.t('image_field'),
file: this.t('file_field'),
select: this.t('select_field'),
checkbox: this.t('checkbox_field'),
multiple: this.t('multiple_field'),
radio: this.t('radio_field'),
cells: this.t('cells_field'),
stamp: this.t('stamp_field'),
payment: this.t('payment_field'),
phone: this.t('phone_field'),
verification: this.t('verify_id')
}
},
fieldIcons () {
@@ -131,7 +152,8 @@ export default {
cells: IconColumns3,
stamp: IconRubberStamp,
payment: IconCreditCard,
phone: IconPhoneCheck
phone: IconPhoneCheck,
verification: IconId
}
},
fieldIconsSorted () {
+46 -8
View File
@@ -111,14 +111,14 @@
:key="type"
>
<button
v-if="(fieldTypes.length === 0 || fieldTypes.includes(type)) && (withPhone || type != 'phone') && (withPayment || type != 'payment')"
v-if="(fieldTypes.length === 0 || fieldTypes.includes(type)) && (withPhone || type != 'phone') && (withPayment || type != 'payment') && (withVerification || type != 'verification')"
draggable="true"
class="field-type-button group flex items-center justify-center border border-dashed w-full rounded relative"
:style="{ backgroundColor }"
:class="drawFieldType === type ? 'border-base-content/40' : 'border-base-300 hover:border-base-content/20'"
@dragstart="onDragstart({ type: type })"
@dragend="$emit('drag-end')"
@click="['file', 'payment'].includes(type) ? $emit('add-field', type) : $emit('set-draw-type', type)"
@click="['file', 'payment', 'verification'].includes(type) ? $emit('add-field', type) : $emit('set-draw-type', type)"
>
<div
class="flex items-console transition-all cursor-grab h-full absolute left-0"
@@ -137,10 +137,36 @@
v-else-if="type == 'phone' && (fieldTypes.length === 0 || fieldTypes.includes(type))"
class="tooltip tooltip-bottom flex"
:class="{'tooltip-bottom-end': withPayment, 'tooltip-bottom': !withPayment }"
data-tip="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
href="https://www.docuseal.co/pricing"
href="https://www.docuseal.com/pricing"
target="_blank"
class="opacity-50 flex items-center justify-center border border-dashed border-base-300 w-full rounded relative"
: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"
:style="{ backgroundColor }"
@@ -168,13 +194,13 @@
>
<ul class="list-disc list-outside ml-3">
<li>
Draw a text field on the page with a mouse
{{ t('draw_a_text_field_on_the_page_with_a_mouse') }}
</li>
<li>
Drag &amp; drop any other field type on the page
{{ t('drag_and_drop_any_other_field_type_on_the_page') }}
</li>
<li>
Click on the field type above to start drawing it
{{ t('click_on_the_field_type_above_to_start_drawing_it') }}
</li>
</ul>
</div>
@@ -197,12 +223,16 @@ export default {
IconDrag,
IconLock
},
inject: ['save', 'backgroundColor', 'withPhone', 'withPayment', 't', 'fieldsDragFieldRef'],
inject: ['save', 'backgroundColor', 'withPhone', 'withVerification', 'withPayment', 't', 'fieldsDragFieldRef'],
props: {
fields: {
type: Array,
required: true
},
template: {
type: Object,
required: true
},
withHelp: {
type: Boolean,
required: false,
@@ -354,6 +384,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()
}
}
@@ -23,10 +23,10 @@
class="bg-base-300 rounded-xl py-2 px-3 text-center"
>
<a
href="https://www.docuseal.co/pricing"
href="https://www.docuseal.com/pricing"
target="_blank"
class="link"
>Available in Pro</a>
>{{ t('available_in_pro') }}</a>
</div>
<div class="flex-inline mb-2 gap-2 space-y-1">
<button
@@ -195,13 +195,13 @@ export default {
},
validateSaveAndClose () {
if (!this.withFormula) {
return alert('Available only in Pro')
return alert(this.t('available_only_in_pro'))
}
const normalizedFormula = this.normalizeFormula(this.formula)
if (normalizedFormula.includes('FIELD NOT FOUND')) {
alert('Some fields are missing in the formula.')
alert(this.t('some_fields_are_missing_in_the_formula'))
} else {
this.field.preferences.formula = normalizedFormula
+826 -6
View File
@@ -1,5 +1,9 @@
const en = {
verify_id: 'Verify ID',
obtain_qualified_electronic_signature_with_the_trusted_provider_click_to_learn_more: 'Obtain qualified electronic signature (QeS) with the trusted provider. Click to learn more.',
editable: 'Editable',
recurrent: 'Recurrent',
one_off: 'One-off',
search_field: 'Search field',
field_not_found: 'Field not found',
clear: 'Clear',
@@ -29,7 +33,7 @@ const en = {
select_value_: 'Select value...',
remove_condition: 'Remove condition',
add_condition: 'Add condition',
are_you_sure: 'Are you sure?',
are_you_sure_: 'Are you sure?',
sign_yourself: 'Sign Yourself',
set_signing_date: 'Set signing date',
signing_date: 'Signing Date',
@@ -46,7 +50,7 @@ const en = {
drawn: 'Drawn',
formula: 'Formula',
typed: 'Typed',
draw_field_on_the_document: 'Draw {field} field on the document',
draw_field_on_the_document: 'Draw a field on the document',
click_to_upload: 'Click to upload',
or_drag_and_drop_files: 'or drag and drop files',
uploading: 'Uploading',
@@ -101,9 +105,30 @@ const en = {
stamp: 'Stamp',
payment: 'Payment',
phone: 'Phone',
field: 'Field',
group: 'Group',
text_field: 'Text Field',
signature_field: 'Signature Field',
initials_field: 'Initials Field',
date_field: 'Date Field',
number_field: 'Number Field',
image_field: 'Image Field',
file_field: 'File Field',
select_field: 'Select Field',
checkbox_field: 'Checkbox Field',
multiple_field: 'Multiple Select Field',
radio_field: 'Radio Group Field',
cells_field: 'Cells Field',
stamp_field: 'Stamp Field',
payment_field: 'Payment Field',
phone_field: 'Phone Field',
draw_a_text_field_on_the_page_with_a_mouse: 'Draw a text field on the page with a mouse',
drag_and_drop_any_other_field_type_on_the_page: 'Drag & drop any other field type on the page',
click_on_the_field_type_above_to_start_drawing_it: 'Click on the field type above to start drawing it',
please_draw_fields_to_prepare_the_document: 'Please draw fields to prepare the document.',
only_pdf_and_images_are_supported: 'Only PDF and images are supported.',
unlock_sms_verified_phone_number_field_with_paid_plan_use_text_field_for_phone_numbers_without_verification: 'Unlock SMS-verified phone number field with paid plan. Use text field for phone numbers without verification.',
available_only_in_pro: 'Available only in Pro',
failed_to_download_files: 'Failed to download files',
please_add_fields_for_the_submitter_name_or_remove_the_submitter_name_if_not_needed: 'Please add fields for the {submitter_name}. Or, remove the {submitter_name} if not needed.',
draw_field: 'Draw {field} Field',
replace: 'Replace',
uploading_: 'Uploading...',
@@ -117,7 +142,802 @@ const en = {
custom: 'Custom',
numbers_only: 'Numbers only',
letters_only: 'Letters only',
regexp_validation: 'Regexp validation'
regexp_validation: 'Regexp validation',
enter_pdf_password: 'Enter PDF password',
wrong_password: 'Wrong password',
currency: 'Currency',
save_and_preview: 'Save and Preview',
preferences: 'Preferences',
available_in_pro: 'Available in Pro',
some_fields_are_missing_in_the_formula: 'Some fields are missing in the formula.',
learn_more: 'Learn more',
and: 'and',
or: 'or'
}
export { en }
const es = {
verify_id: 'Verificar ID',
obtain_qualified_electronic_signature_with_the_trusted_provider_click_to_learn_more: 'Obtenga una firma electrónica cualificada (QeS) con el proveedor de confianza. Haga clic para obtener más información.',
recurrent: 'Recurrente',
one_off: 'Único',
editable: 'Editable',
search_field: 'Campo de búsqueda',
field_not_found: 'Campo no encontrado',
clear: 'Borrar',
type_value: 'Escriba valor',
align: 'Alinear',
add_all_required_fields_to_continue: 'Agregar todos los campos requeridos para continuar',
uploaded_pdf_contains_form_fields_keep_or_remove_them: 'El PDF cargado tiene campos. ¿Mantenerlos o eliminarlos?',
keep: 'Mantener',
left: 'Izquierda',
heading: 'Encabezado',
validation: 'Validación',
add_blank_page: 'Agregar página en blanco',
right: 'Derecha',
center: 'Centro',
with_logo: 'Con logotipo',
description: 'Descripción',
signing_date: 'Fecha de Firma',
display_title: 'Título de visualización',
unchecked: 'No marcado',
price: 'Precio',
equal: 'Igual',
not_equal: 'No es igual',
contains: 'Contiene',
does_not_contain: 'No contiene',
not_empty: 'No vacío',
empty: 'Vacío',
select_field_: 'Seleccionar campo...',
select_value_: 'Seleccionar valor...',
remove_condition: 'Eliminar condición',
add_condition: 'Agregar condición',
condition: 'Condición',
formula: 'Fórmula',
edit: 'Editar',
settings: 'Configuración',
up: 'Arriba',
down: 'Abajo',
set_signing_date: 'Establecer fecha de firma',
are_you_sure_: '¿Estás seguro?',
sign_yourself: 'Firma tú mismo',
checked: 'Seleccionado',
send: 'Enviar',
remove: 'Eliminar',
save: 'Guardar',
cancel: 'Cancelar',
or_add_field_without_drawing: 'O añadir campo sin dibujar',
draw_field_on_the_document: 'Dibujar un campo en el documento',
click_to_upload: 'Haz clic para cargar',
or_drag_and_drop_files: 'o arrastra y suelta archivos',
uploading: 'Subiendo',
processing_: 'Procesando...',
add_pdf_documents_or_images: 'Agregar documentos PDF o imágenes',
add_documents_or_images: 'Agregar documentos o imágenes',
required: 'Requerido',
default_value: 'Valor predeterminado',
format: 'Formato',
read_only: 'Solo lectura',
page: 'Página',
draw_new_area: 'Dibujar nueva área',
copy_to_all_pages: 'Copiar a todas las páginas',
add_option: 'Agregar opción',
option: 'Opción',
first_party: 'Primera Parte',
second_party: 'Segunda Parte',
third_party: 'Tercera Parte',
fourth_party: 'Cuarta Parte',
fifth_party: 'Quinta Parte',
sixth_party: 'Sexta Parte',
seventh_party: 'Séptima Parte',
eighth_party: 'Octava Parte',
ninth_party: 'Novena Parte',
tenth_party: 'Décima Parte',
eleventh_party: 'Undécimo Partido',
twelfth_party: 'Duodécimo Partido',
thirteenth_party: 'Decimotercer Partido',
fourteenth_party: 'Catorceavo Partido',
fifteenth_party: 'Quinceavo Partido',
sixteenth_party: 'Dieciséisavo Partido',
seventeenth_party: 'Diecisieteavo Partido',
eighteenth_party: 'Dieciochoavo Partido',
nineteenth_party: 'Decimonovena Fiesta',
twentieth_party: 'Vigésima Fiesta',
draw: 'Dibujar',
add: 'Agregar',
text: 'Texto',
signature: 'Firma',
initials: 'Iniciales',
date: 'Fecha',
number: 'Número',
image: 'Imagen',
file: 'Archivo',
select: 'Seleccionar',
checkbox: 'Casilla',
multiple: 'Múltiple',
radio: 'Radio',
cells: 'Celdas',
stamp: 'Sello',
payment: 'Pago',
phone: 'Teléfono',
text_field: 'Campo de Texto',
signature_field: 'Campo de Firma',
initials_field: 'Campo de Iniciales',
date_field: 'Campo de Fecha',
number_field: 'Campo de Número',
image_field: 'Campo de Imagen',
file_field: 'Campo de Archivo',
select_field: 'Campo de Selección',
checkbox_field: 'Campo de Casilla',
multiple_field: 'Campo Múltiple',
radio_field: 'Campo de Grupo Radio',
cells_field: 'Campo de Celdas',
stamp_field: 'Campo de Sello',
payment_field: 'Campo de Pago',
phone_field: 'Campo de Teléfono',
draw_a_text_field_on_the_page_with_a_mouse: 'Dibujar un campo de texto en la página con el mouse',
drag_and_drop_any_other_field_type_on_the_page: 'Arrastra y suelta cualquier otro tipo de campo en la página',
click_on_the_field_type_above_to_start_drawing_it: 'Haz clic en el tipo de campo de arriba para comenzar a dibujarlo',
please_draw_fields_to_prepare_the_document: 'Por favor, dibuja los campos para preparar el documento.',
only_pdf_and_images_are_supported: 'Solo se admiten PDF e imágenes.',
unlock_sms_verified_phone_number_field_with_paid_plan_use_text_field_for_phone_numbers_without_verification: 'Desbloquea el campo de número de teléfono verificado por SMS con un plan pago. Usa el campo de texto para números de teléfono sin verificación.',
available_only_in_pro: 'Disponible solo en Pro',
failed_to_download_files: 'Error al descargar los archivos',
please_add_fields_for_the_submitter_name_or_remove_the_submitter_name_if_not_needed: 'Por favor, añade campos para {submitter_name} o elimina {submitter_name} si no es necesario.',
draw_field: 'Dibujar campo {field}',
replace: 'Reemplazar',
uploading_: 'Subiendo...',
add_document: 'Subir',
any: 'Cualquier',
drawn: 'Dibujado',
typed: 'Escrito',
none: 'Ninguno',
ssn: 'SSN',
ein: 'EIN',
email: 'Correo electrónico',
url: 'URL',
zip: 'ZIP',
custom: 'Personalizado',
numbers_only: 'Solo números',
letters_only: 'Solo letras',
regexp_validation: 'Validación de expresión regular',
enter_pdf_password: 'Ingrese la contraseña del PDF',
wrong_password: 'Contraseña incorrecta',
currency: 'Moneda',
save_and_preview: 'Guardar y previsualizar',
preferences: 'Preferencias',
available_in_pro: 'Disponible en Pro',
some_fields_are_missing_in_the_formula: 'Faltan algunos campos en la fórmula.',
learn_more: 'Aprende más',
and: 'y',
or: 'o'
}
const it = {
verify_id: 'Verifica ID',
obtain_qualified_electronic_signature_with_the_trusted_provider_click_to_learn_more: 'Ottieni una firma elettronica qualificata (QeS) con il fornitore di fiducia. Clicca per saperne di più.',
ricorrente: 'Ricorrente',
una_volta: 'Una volta',
editable: 'Modificabile',
search_field: 'Campo di ricerca',
field_not_found: 'Campo non trovato',
clear: 'Cancella',
align: 'Allinea',
add_all_required_fields_to_continue: 'Aggiungi tutti i campi obbligatori per continuare',
uploaded_pdf_contains_form_fields_keep_or_remove_them: 'Il PDF caricato contiene campi del modulo. Mantenerli o rimuoverli?',
keep: 'Mantieni',
left: 'Sinistra',
heading: 'Intestazione',
validation: 'Validazione',
add_blank_page: 'Aggiungi pagina vuota',
right: 'Destra',
center: 'Centro',
description: 'Descrizione',
display_title: 'Mostra titolo',
with_logo: 'Con logo',
unchecked: 'Non selezionato',
price: 'Prezzo',
type_value: 'Inserisci valore',
equal: 'Uguale',
not_equal: 'Non uguale',
contains: 'Contiene',
does_not_contain: 'Non contiene',
not_empty: 'Non vuoto',
empty: 'Vuoto',
select_field_: 'Seleziona campo...',
select_value_: 'Seleziona valore...',
remove_condition: 'Rimuovi condizione',
add_condition: 'Aggiungi condizione',
are_you_sure_: 'Sei sicuro?',
sign_yourself: 'Firma te stesso',
set_signing_date: 'Imposta data di firma',
signing_date: 'Data di firma',
send: 'Invia',
remove: 'Rimuovi',
edit: 'Modifica',
settings: 'Impostazioni',
up: 'Su',
down: 'Giù',
checked: 'Selezionato',
save: 'Salva',
cancel: 'Annulla',
any: 'Qualsiasi',
drawn: 'Disegnato',
formula: 'Formula',
typed: 'Digitato',
draw_field_on_the_document: 'Disegnare un campo sul documento',
click_to_upload: 'Clicca per caricare',
or_drag_and_drop_files: 'o trascina e rilascia i file',
uploading: 'Caricamento in corso',
processing_: 'Elaborazione...',
add_pdf_documents_or_images: 'Aggiungi documenti PDF o immagini',
add_documents_or_images: 'Aggiungi documenti o immagini',
required: 'Obbligatorio',
default_value: 'Valore predefinito',
format: 'Formato',
read_only: 'Sola lettura',
page: 'Pagina',
draw_new_area: 'Disegna nuova area',
copy_to_all_pages: 'Copia in tutte le pagine',
add_option: 'Aggiungi opzione',
option: 'Opzione',
condition: 'Condizione',
first_party: 'Prima parte',
second_party: 'Seconda parte',
third_party: 'Terza parte',
fourth_party: 'Quarta parte',
fifth_party: 'Quinta parte',
sixth_party: 'Sesta parte',
seventh_party: 'Settima parte',
eighth_party: 'Ottava parte',
ninth_party: 'Nona parte',
tenth_party: 'Decima parte',
eleventh_party: 'Undicesima parte',
twelfth_party: 'Dodicesima parte',
thirteenth_party: 'Tredicesima parte',
fourteenth_party: 'Quattordicesima parte',
fifteenth_party: 'Quindicesima parte',
sixteenth_party: 'Sedicesima parte',
seventeenth_party: 'Diciassettesima parte',
eighteenth_party: 'Diciottesima parte',
nineteenth_party: 'Diciannovesima parte',
twentieth_party: 'Ventesima parte',
draw: 'Disegna',
add: 'Aggiungi',
or_add_field_without_drawing: 'Oppure aggiungi campo senza disegno',
text: 'Testo',
number: 'Numero',
signature: 'Firma',
initials: 'Iniziali',
date: 'Data',
image: 'Immagine',
file: 'File',
select: 'Seleziona',
checkbox: 'Checkbox',
multiple: 'Multiplo',
radio: 'Radio',
cells: 'Celle',
stamp: 'Timbro',
payment: 'Pagamento',
phone: 'Telefono',
text_field: 'Campo di Testo',
signature_field: 'Campo di Firma',
initials_field: 'Campo di Iniziali',
date_field: 'Campo Data',
number_field: 'Campo Numero',
image_field: 'Campo Immagine',
file_field: 'Campo File',
select_field: 'Campo di Selezione',
checkbox_field: 'Campo di Checkbox',
multiple_field: 'Campo Selezione Multipla',
radio_field: 'Campo di Gruppo Radio',
cells_field: 'Campo Celle',
stamp_field: 'Campo Timbro',
payment_field: 'Campo Pagamento',
phone_field: 'Campo Telefono',
draw_a_text_field_on_the_page_with_a_mouse: 'Disegna un campo di testo sulla pagina con il mouse',
drag_and_drop_any_other_field_type_on_the_page: 'Trascina e rilascia qualsiasi altro tipo di campo sulla pagina',
click_on_the_field_type_above_to_start_drawing_it: 'Clicca sul tipo di campo sopra per iniziare a disegnarlo',
please_draw_fields_to_prepare_the_document: 'Per favore, disegna i campi per preparare il documento.',
only_pdf_and_images_are_supported: 'Sono supportati solo PDF e immagini.',
unlock_sms_verified_phone_number_field_with_paid_plan_use_text_field_for_phone_numbers_without_verification: 'Sblocca il campo numero di telefono verificato tramite SMS con un piano a pagamento. Usa il campo di testo per numeri di telefono senza verifica.',
available_only_in_pro: 'Disponibile solo in Pro',
failed_to_download_files: 'Impossibile scaricare i file',
please_add_fields_for_the_submitter_name_or_remove_the_submitter_name_if_not_needed: 'Aggiungi campi per {submitter_name} o rimuovi {submitter_name} se non necessario.',
draw_field: 'Disegna il campo {field}',
replace: 'Sostituisci',
uploading_: 'Caricamento in corso...',
add_document: 'Aggiungi',
none: 'Nessuno',
ssn: 'SSN',
ein: 'EIN',
email: 'Email',
url: 'URL',
zip: 'CAP',
custom: 'Personalizzato',
numbers_only: 'Solo numeri',
letters_only: 'Solo lettere',
regexp_validation: 'Validazione regexp',
enter_pdf_password: 'Inserisci password PDF',
wrong_password: 'Password errata',
currency: 'Valuta',
save_and_preview: 'Salva e Anteprima',
preferences: 'Preferenze',
available_in_pro: 'Disponibile in Pro',
some_fields_are_missing_in_the_formula: 'Alcuni campi mancano nella formula.',
learn_more: 'Scopri di più',
and: 'e',
or: 'o'
}
const pt = {
verify_id: 'Verificar ID',
obtain_qualified_electronic_signature_with_the_trusted_provider_click_to_learn_more: 'Obtenha a assinatura eletrônica qualificada (QeS) com o provedor confiável. Clique para saber mais.',
recurrent: 'Recurrente',
one_off: 'Único',
editable: 'Editável',
search_field: 'Campo de busca',
field_not_found: 'Campo não encontrado',
clear: 'Limpar',
type_value: 'Digite valor',
add_all_required_fields_to_continue: 'Adicione todos os campos obrigatórios para continuar',
uploaded_pdf_contains_form_fields_keep_or_remove_them: 'O PDF carregado contém campos. Manter ou removê-los?',
keep: 'Manter',
align: 'Alinhar',
left: 'Esquerda',
heading: 'Cabeçalho',
validation: 'Validação',
add_blank_page: 'Adicionar página em branco',
right: 'Direita',
center: 'Centro',
with_logo: 'Com logotipo',
description: 'Descrição',
display_title: 'Título de exibição',
signing_date: 'Data da Assinatura',
unchecked: 'Não marcado',
price: 'Preço',
equal: 'Igual',
not_equal: 'Não é igual',
contains: 'Contém',
does_not_contain: 'Não contém',
not_empty: 'Não vazio',
empty: 'Vazio',
add_condition: 'Adicionar condição',
select_field_: 'Selecionar campo...',
select_value_: 'Selecionar valor...',
remove_condition: 'Remover condição',
condition: 'Condição',
formula: 'Fórmula',
edit: 'Editar',
settings: 'Configurações',
up: 'Para cima',
down: 'Para baixo',
set_signing_date: 'Definir data de assinatura',
are_you_sure_: 'Tem certeza?',
sign_yourself: 'Assine você mesmo',
checked: 'Marcado',
send: 'Enviar',
remove: 'Remover',
save: 'Salvar',
cancel: 'Cancelar',
or_add_field_without_drawing: 'Ou adicione campo sem desenhar',
draw_field_on_the_document: 'Desenhar um campo no documento',
click_to_upload: 'Clique para enviar',
or_drag_and_drop_files: 'ou arraste e solte arquivos',
uploading: 'Carregando',
processing_: 'Processando...',
add_pdf_documents_or_images: 'Adicionar documentos PDF ou imagens',
add_documents_or_images: 'Adicionar documentos ou imagens',
required: 'Obrigatório',
default_value: 'Valor padrão',
format: 'Formato',
read_only: 'Somente leitura',
page: 'Página',
draw_new_area: 'Desenhar nova área',
copy_to_all_pages: 'Copiar para todas as páginas',
add_option: 'Adicionar opção',
option: 'Opção',
first_party: 'Primeira Parte',
second_party: 'Segunda Parte',
third_party: 'Terceira Parte',
fourth_party: 'Quarta Parte',
fifth_party: 'Quinta Parte',
sixth_party: 'Sexta Parte',
seventh_party: 'Sétima Parte',
eighth_party: 'Oitava Parte',
ninth_party: 'Nona Parte',
tenth_party: 'Décima Parte',
eleventh_party: 'Décima Primeira Parte',
twelfth_party: 'Décima Segunda Parte',
thirteenth_party: 'Décima Terceira Parte',
fourteenth_party: 'Décima Quarta Parte',
fifteenth_party: 'Décima Quinta Parte',
sixteenth_party: 'Décima Sexta Parte',
seventeenth_party: 'Décima Sétima Parte',
eighteenth_party: 'Décima Oitava Parte',
nineteenth_party: 'Décima Nona Parte',
twentieth_party: 'Vigésima Parte',
draw: 'Desenhar',
add: 'Adicionar',
text: 'Texto',
signature: 'Assinatura',
initials: 'Rúbrica',
date: 'Data',
number: 'Número',
image: 'Imagem',
file: 'Arquivo',
select: 'Selecionar',
checkbox: 'Caixa',
multiple: 'Múltiplo',
radio: 'Rádio',
cells: 'Células',
stamp: 'Carimbo',
payment: 'Pagamento',
phone: 'Telefone',
text_field: 'Campo de Texto',
signature_field: 'Campo de Assinatura',
initials_field: 'Campo de Rúbrica',
date_field: 'Campo de Data',
number_field: 'Campo de Número',
image_field: 'Campo de Imagem',
file_field: 'Campo de Arquivo',
select_field: 'Campo de Seleção',
checkbox_field: 'Campo de Caixa',
multiple_field: 'Campo de Opção Múltipla',
radio_field: 'Campo de Grupo Rádio',
cells_field: 'Campo de Células',
stamp_field: 'Campo de Carimbo',
payment_field: 'Campo de Pagamento',
phone_field: 'Campo de Telefone',
draw_a_text_field_on_the_page_with_a_mouse: 'Desenhar um campo de texto na página com o mouse',
drag_and_drop_any_other_field_type_on_the_page: 'Arraste e solte qualquer outro tipo de campo na página',
click_on_the_field_type_above_to_start_drawing_it: 'Clique no tipo de campo acima para começar a desenhá-lo',
please_draw_fields_to_prepare_the_document: 'Por favor, desenhe os campos para preparar o documento.',
only_pdf_and_images_are_supported: 'Apenas PDFs e imagens são suportados.',
unlock_sms_verified_phone_number_field_with_paid_plan_use_text_field_for_phone_numbers_without_verification: 'Desbloqueie o campo de número de telefone verificado por SMS com um plano pago. Use o campo de texto para números de telefone sem verificação.',
available_only_in_pro: 'Disponível apenas no Pro',
failed_to_download_files: 'Falha ao baixar arquivos',
please_add_fields_for_the_submitter_name_or_remove_the_submitter_name_if_not_needed: 'Adicione campos para {submitter_name} ou remova {submitter_name} se não for necessário.',
draw_field: 'Desenhar campo {field}',
replace: 'Substituir',
uploading_: 'Carregando...',
add_document: 'Enviar',
any: 'Qualquer',
drawn: 'Desenhado',
typed: 'Digitado',
none: 'Nenhum',
ssn: 'SSN',
ein: 'EIN',
email: 'Email',
url: 'URL',
zip: 'ZIP',
custom: 'Personalizado',
numbers_only: 'Somente números',
letters_only: 'Somente letras',
regexp_validation: 'Validação de expressão regular',
enter_pdf_password: 'Digite a senha do PDF',
wrong_password: 'Senha incorreta',
currency: 'Moeda',
save_and_preview: 'Salvar e Pré-visualizar',
preferences: 'Preferências',
available_in_pro: 'Disponível no Pro',
some_fields_are_missing_in_the_formula: 'Faltam alguns campos na fórmula.',
learn_more: 'Saiba mais',
and: 'e',
or: 'ou'
}
const fr = {
verify_id: "Vérifier l'ID",
obtain_qualified_electronic_signature_with_the_trusted_provider_click_to_learn_more: 'Obtenez une signature électronique qualifiée (QeS) avec le fournisseur de confiance. Cliquez pour en savoir plus.',
recurrent: 'Récurrent',
one_off: 'Ponctuel',
editable: 'Éditable',
search_field: 'Champ de recherche',
field_not_found: 'Champ non trouvé',
clear: 'Effacer',
type_value: 'Tapez la valeur',
add_all_required_fields_to_continue: 'Ajoutez tous les champs obligatoires pour continuer',
uploaded_pdf_contains_form_fields_keep_or_remove_them: 'Le PDF téléchargé contient des champs. Les conserver ou les supprimer?',
keep: 'Conserver',
align: 'Aligner',
left: 'Gauche',
heading: 'En-tête',
validation: 'Validation',
add_blank_page: 'Ajouter une page blanche',
right: 'Droite',
add_condition: 'Ajouter une condition',
center: 'Centre',
with_logo: 'Avec logo',
description: 'Description',
display_title: "Titre d'affichage",
unchecked: 'Non coché',
price: 'Prix',
equal: 'Égal',
not_equal: 'Non égal',
contains: 'Contient',
signing_date: 'Date de signature',
does_not_contain: 'Ne contient pas',
not_empty: 'Non vide',
empty: 'Vide',
select_field_: 'Sélectionner un champ...',
select_value_: 'Sélectionner une valeur...',
remove_condition: 'Supprimer la condition',
condition: 'Condition',
formula: 'Formule',
edit: 'Éditer',
settings: 'Paramètres',
up: 'Haut',
down: 'Bas',
set_signing_date: 'Définir la date de signature',
are_you_sure_: 'Êtes-vous sûr?',
sign_yourself: 'Signez-vous',
checked: 'Coché',
send: 'Envoyer',
remove: 'Supprimer',
save: 'Enregistrer',
cancel: 'Annuler',
draw_field_on_the_document: 'Dessiner un champ sur le document',
click_to_upload: 'Cliquez pour télécharger',
or_drag_and_drop_files: 'ou faites glisser-déposer des fichiers',
uploading: 'Téléchargement en cours',
processing_: 'Traitement en cours...',
add_pdf_documents_or_images: 'Ajoutez des documents PDF ou des images',
add_documents_or_images: 'Ajoutez des documents ou des images',
required: 'Requis',
default_value: 'Valeur par défaut',
format: 'Format',
read_only: 'Lecture seule',
page: 'Page',
draw_new_area: 'Dessiner une nouvelle zone',
copy_to_all_pages: 'Copier sur toutes les pages',
add_option: 'Ajouter une option',
option: 'Option',
first_party: 'Première partie',
second_party: 'Deuxième partie',
third_party: 'Troisième partie',
fourth_party: 'Quatrième partie',
fifth_party: 'Cinquième partie',
sixth_party: 'Sixième partie',
seventh_party: 'Septième partie',
eighth_party: 'Huitième partie',
ninth_party: 'Neuvième partie',
tenth_party: 'Dixième partie',
eleventh_party: 'Onzième Parti',
twelfth_party: 'Douzième Parti',
thirteenth_party: 'Treizième Parti',
fourteenth_party: 'Quatorzième Parti',
fifteenth_party: 'Quinzième Parti',
sixteenth_party: 'Seizième Parti',
seventeenth_party: 'Dix-septième Parti',
eighteenth_party: 'Dix-huitième Parti',
nineteenth_party: 'Dix-Neuvième Fête',
twentieth_party: 'Vingtième Fête',
draw: 'Dessiner',
add: 'Ajouter',
or_add_field_without_drawing: 'Ou ajoutez un champ sans dessiner',
text: 'Texte',
signature: 'Signature',
initials: 'Initiales',
date: 'Date',
number: 'Numéro',
image: 'Image',
file: 'Fichier',
select: 'Choisir',
checkbox: 'Coche',
multiple: 'Multiple',
radio: 'Radio',
cells: 'Cellules',
stamp: 'Tampon',
payment: 'Paiement',
phone: 'Téléphone',
text_field: 'Champ de Texte',
signature_field: 'Champ de Signature',
initials_field: "Champ d'Initiales",
date_field: 'Champ de Date',
number_field: 'Champ de Numéro',
image_field: "Champ d'Image",
file_field: 'Champ de Fichier',
select_field: 'Champ de Choix',
checkbox_field: 'Champ de Coche',
multiple_field: 'Champ de Choix Multiple',
radio_field: 'Champ de Groupe Radio',
cells_field: 'Champ de Cellules',
stamp_field: 'Champ de Tampon',
payment_field: 'Champ de Paiement',
phone_field: 'Champ de Téléphone',
draw_a_text_field_on_the_page_with_a_mouse: 'Dessinez un champ texte sur la page avec une souris',
drag_and_drop_any_other_field_type_on_the_page: 'Glissez et déposez tout autre type de champ sur la page',
click_on_the_field_type_above_to_start_drawing_it: 'Cliquez sur le type de champ ci-dessus pour commencer à le dessiner',
please_draw_fields_to_prepare_the_document: 'Veuillez dessiner les champs pour préparer le document.',
only_pdf_and_images_are_supported: 'Seuls les PDF et les images sont pris en charge.',
unlock_sms_verified_phone_number_field_with_paid_plan_use_text_field_for_phone_numbers_without_verification: 'Débloquez le champ de numéro de téléphone vérifié par SMS avec un plan payant. Utilisez un champ texte pour les numéros de téléphone sans vérification.',
available_only_in_pro: 'Disponible uniquement en Pro',
failed_to_download_files: 'Échec du téléchargement des fichiers',
please_add_fields_for_the_submitter_name_or_remove_the_submitter_name_if_not_needed: 'Veuillez ajouter des champs pour {submitter_name} ou retirer {submitter_name} si ce n\'est pas nécessaire.',
draw_field: 'Dessiner le champ {field}',
replace: 'Remplacer',
uploading_: 'Téléchargement en cours...',
add_document: 'Télécharger',
any: 'Tout',
drawn: 'Dessiné',
typed: 'Tapé',
none: 'Aucun',
ssn: 'SSN',
ein: 'EIN',
email: 'E-mail',
url: 'URL',
zip: 'ZIP',
custom: 'Personnalisé',
numbers_only: 'Chiffres uniquement',
letters_only: 'Lettres uniquement',
regexp_validation: 'Validation par expression régulière',
enter_pdf_password: 'Entrez le mot de passe PDF',
wrong_password: 'Mot de passe incorrect',
currency: 'Devise',
save_and_preview: 'Enregistrer et prévisualiser',
preferences: 'Préférences',
available_in_pro: 'Disponible en version Pro',
some_fields_are_missing_in_the_formula: 'Certains champs manquent dans la formule.',
learn_more: 'En savoir plus',
and: 'et',
or: 'ou'
}
const de = {
verify_id: 'ID überprüfen',
obtain_qualified_electronic_signature_with_the_trusted_provider_click_to_learn_more: 'Erhalten Sie eine qualifizierte elektronische Signatur (QeS) beim vertrauenswürdigen Anbieter. Klicken Sie hier, um mehr zu erfahren.',
wiederkehrend: 'Wiederkehrend',
einmalig: 'Einmalig',
editable: 'Bearbeitbar',
search_field: 'Suchfeld',
field_not_found: 'Feld nicht gefunden',
clear: 'Löschen',
type_value: 'Wert eingeben',
add_all_required_fields_to_continue: 'Fügen Sie alle erforderlichen Felder hinzu, um fortzufahren',
uploaded_pdf_contains_form_fields_keep_or_remove_them: 'Das hochgeladene PDF enthält Formularfelder. Behalten oder entfernen?',
keep: 'Behalten',
align: 'Ausrichten',
left: 'Links',
heading: 'Überschrift',
validation: 'Validierung',
add_blank_page: 'Leere Seite hinzufügen',
right: 'Rechts',
add_condition: 'Bedingung hinzufügen',
center: 'Zentriert',
with_logo: 'Mit Logo',
description: 'Beschreibung',
display_title: 'Anzeigetitel',
unchecked: 'Nicht überprüft',
price: 'Preis',
equal: 'Gleich',
not_equal: 'Ungleich',
contains: 'Enthält',
does_not_contain: 'Enthält nicht',
not_empty: 'Nicht leer',
empty: 'Leer',
signing_date: 'Signaturdatum',
select_field_: 'Feld auswählen...',
select_value_: 'Wert auswählen...',
remove_condition: 'Bedingung entfernen',
condition: 'Bedingung',
formula: 'Formel',
edit: 'Bearbeiten',
settings: 'Einstellungen',
up: 'Nach oben',
down: 'Nach unten',
set_signing_date: 'Unterschriftdatum festlegen',
are_you_sure_: 'Bist du sicher?',
sign_yourself: 'Unterschreiben Sie selbst',
checked: 'Ausgewählt',
send: 'Senden',
remove: 'Entfernen',
save: 'Speichern',
cancel: 'Abbrechen',
draw_field_on_the_document: 'Ein Feld auf dem Dokument zeichnen',
click_to_upload: 'Klicken Sie zum Hochladen',
or_drag_and_drop_files: 'oder Dateien hierher ziehen',
uploading: 'Hochladen',
processing_: 'Verarbeitung...',
add_pdf_documents_or_images: 'PDF-Dokumente oder Bilder hinzufügen',
add_documents_or_images: 'Dokumente oder Bilder hinzufügen',
required: 'Erforderlich',
default_value: 'Standardwert',
format: 'Format',
read_only: 'Nur lesen',
page: 'Seite',
draw_new_area: 'Neuen Bereich zeichnen',
copy_to_all_pages: 'Auf alle Seiten kopieren',
add_option: 'Option hinzufügen',
option: 'Option',
first_party: 'Erste Partei',
second_party: 'Zweite Partei',
third_party: 'Dritte Partei',
fourth_party: 'Vierte Partei',
fifth_party: 'Fünfte Partei',
sixth_party: 'Sechste Partei',
seventh_party: 'Siebte Partei',
eighth_party: 'Achte Partei',
ninth_party: 'Neunte Partei',
tenth_party: 'Zehnte Partei',
eleventh_party: 'Elfte Partei',
twelfth_party: 'Zwölfte Partei',
thirteenth_party: 'Dreizehnte Partei',
fourteenth_party: 'Vierzehnte Partei',
fifteenth_party: 'Fünfzehnte Partei',
sixteenth_party: 'Sechzehnte Partei',
seventeenth_party: 'Siebzehnte Partei',
eighteenth_party: 'Achtzehnte Partei',
nineteenth_party: 'Neunzehnte Party',
twentieth_party: 'Zwanzigste Party',
draw: 'Zeichnen',
add: 'Hinzufügen',
or_add_field_without_drawing: 'Oder Feld ohne Zeichnung hinzufügen',
text: 'Text',
signature: 'Unterschrift',
initials: 'Initialen',
date: 'Datum',
number: 'Nummer',
image: 'Bild',
file: 'Datei',
select: 'Auswählen',
checkbox: 'Checkbox',
multiple: 'Mehrere',
radio: 'Radio',
cells: 'Zellen',
stamp: 'Stempel',
payment: 'Zahlung',
phone: 'Telefon',
text_field: 'Textfeld',
signature_field: 'Unterschriftsfeld',
initials_field: 'Initialenfeld',
date_field: 'Datumsfeld',
number_field: 'Zahlenfeld',
image_field: 'Bildfeld',
file_field: 'Dateifeld',
select_field: 'Auswahlfeld',
checkbox_field: 'Checkbox-Feld',
multiple_field: 'Mehrfach-Auswahlfeld',
radio_field: 'Radio-Gruppenfeld',
cells_field: 'Zellenfeld',
stamp_field: 'Stempelfeld',
payment_field: 'Zahlungsfeld',
phone_field: 'Telefonfeld',
draw_a_text_field_on_the_page_with_a_mouse: 'Textfeld auf der Seite mit der Maus zeichnen',
drag_and_drop_any_other_field_type_on_the_page: 'Ziehe und lasse einen anderen Feldtyp auf die Seite fallen',
click_on_the_field_type_above_to_start_drawing_it: 'Klicke auf den Feldtyp oben, um mit dem Zeichnen zu beginnen',
please_draw_fields_to_prepare_the_document: 'Bitte zeichne Felder, um das Dokument vorzubereiten.',
only_pdf_and_images_are_supported: 'Nur PDFs und Bilder werden unterstützt.',
unlock_sms_verified_phone_number_field_with_paid_plan_use_text_field_for_phone_numbers_without_verification: 'Schalte das SMS-verifizierte Telefonnummernfeld mit einem kostenpflichtigen Plan frei. Verwende das Textfeld für Telefonnummern ohne Verifizierung.',
available_only_in_pro: 'Nur in Pro verfügbar',
failed_to_download_files: 'Fehler beim Herunterladen der Dateien',
please_add_fields_for_the_submitter_name_or_remove_the_submitter_name_if_not_needed: 'Bitte füge Felder für {submitter_name} hinzu oder entferne {submitter_name}, falls nicht erforderlich.',
draw_field: 'Feld {field} zeichnen',
replace: 'Ersetzen',
uploading_: 'Hochladen...',
add_document: 'Hochladen',
any: 'Jede',
drawn: 'Gezeichnet',
typed: 'Getippt',
none: 'Keine',
ssn: 'SSN',
ein: 'EIN',
email: 'E-Mail',
url: 'URL',
zip: 'ZIP',
custom: 'Benutzerdefiniert',
numbers_only: 'Nur Zahlen',
letters_only: 'Nur Buchstaben',
regexp_validation: 'Regulärer Ausdruck Überprüfung',
enter_pdf_password: 'Geben Sie das PDF-Passwort ein',
wrong_password: 'Falsches Passwort',
currency: 'Währung',
save_and_preview: 'Speichern und Vorschau',
preferences: 'Einstellungen',
available_in_pro: 'In Pro verfügbar',
some_fields_are_missing_in_the_formula: 'Einige Felder fehlen in der Formel.',
learn_more: 'Erfahren Sie mehr',
and: 'und',
or: 'oder'
}
export { en, es, it, pt, fr, de }
+33 -17
View File
@@ -3,7 +3,7 @@
<div v-if="selectedSheetIndex === null && spreadsheet">
<form @submit.prevent="[selectedSheetIndex = $refs.selectWorksheet.value, buildDefaultMappings()]">
<label class="label">
Select Worksheet
{{ t('select_worksheet') }}
</label>
<select
ref="selectWorksheet"
@@ -18,7 +18,7 @@
</option>
</select>
<button class="base-button mt-4 w-full">
Open
{{ t('open') }}
</button>
</form>
</div>
@@ -36,10 +36,10 @@
</div>
<div class="flex">
<div class="relative w-full py-2 px-2 text-sm">
Recipient field
{{ t('recipient_field') }}
</div>
<div class="relative w-full py-2 pl-4 text-sm">
Spreadsheet column
{{ t('spreadsheet_column') }}
</div>
</div>
<div
@@ -50,6 +50,7 @@
<div class="flex">
<select
class="base-select !select-sm !h-10"
:class="{ '!text-gray-300': !mapping.field_name }"
required
@change="mapping.field_name = $event.target.value"
>
@@ -58,13 +59,14 @@
value=""
:selected="!mapping.field_name"
>
Select Field
{{ t('select_field') }}
</option>
<option
v-for="(field, index) in selectFieldsForSubmitter(submitter)"
:key="index"
:value="field.name"
:selected="mapping.field_name === field.name"
class="text-base-content"
>
{{ field.name }}
</option>
@@ -75,6 +77,7 @@
<div class="w-full relative">
<select
class="base-select !select-sm !h-10"
:class="{ '!text-gray-300': !mapping.column_index && mapping.column_index != 0 }"
required
@change="mapping.column_index = parseInt($event.target.value)"
>
@@ -83,7 +86,7 @@
value=""
:selected="mapping.column_index == null"
>
Select Column
{{ t('select_column') }}
</option>
<template
v-for="(column, index) in columns"
@@ -93,6 +96,7 @@
v-if="column"
:value="index"
:selected="index === mapping.column_index"
class="text-base-content"
>
{{ column }}
</option>
@@ -118,8 +122,8 @@
</div>
<div class="flex items-center pl-1">
<span
class="tooltip tooltip-top"
data-tip="Remove"
class="tooltip tooltip-left"
:data-tip="t('remove')"
>
<button
:disabled="mappings.filter((m) => m.submitter_uuid === submitter.uuid).length < 2"
@@ -138,7 +142,7 @@
@click.prevent="addMapping(submitter)"
>
<IconPlus class="w-4 h-4" />
New Field Mapping
{{ t('new_field_mapping') }}
</button>
</div>
</div>
@@ -152,7 +156,7 @@
<div
class="px-3 border-y py-2 border-base-300 text-center w-full text-sm font-semibold"
>
Total entries: {{ submissionsData.length }}
{{ t('total_entries') }}: {{ submissionsData.length }}
<template v-if="multitenant && submissionsData.length >= 1000">
/ 1000
</template>
@@ -187,10 +191,10 @@
<div
class="font-medium text-lg mb-1"
>
Upload CSV or XLSX Spreadsheet
{{ t('upload_csv_or_xlsx_spreadsheet') }}
</div>
<div class="text-sm">
<span class="font-medium">Click to Upload</span> or drag and drop files.
<span class="font-medium">{{ t('click_to_upload') }}</span> {{ t('or_drag_and_drop_files') }}
</div>
</div>
</div>
@@ -210,11 +214,11 @@
</label>
</div>
<div class="text-center mt-2">
Or <a
{{ t('or') }} <a
:download="`${template.name}.csv`"
:href="`data:text/csv;base64,${csvBase64}`"
class="link font-medium"
>download</a> a spreadsheet to fill and import
>{{ t('download') }}</a> {{ t('a_sample_spreadsheet_to_fill_and_import') }}
</div>
</div>
</div>
@@ -248,6 +252,11 @@ export default {
type: String,
required: false,
default: ''
},
i18n: {
type: Object,
required: false,
default: () => ({})
}
},
data () {
@@ -270,13 +279,17 @@ export default {
this.mappings.forEach((mapping) => {
if (mapping.field_name && mapping.column_index != null) {
submittersIndex[mapping.submitter_uuid] ||= { uuid: mapping.submitter_uuid, fields: [] }
submittersIndex[mapping.submitter_uuid] ||= {
uuid: mapping.submitter_uuid,
role: this.submitters.find((s) => s.uuid === mapping.submitter_uuid).name,
fields: []
}
if (['name', 'email', 'phone', 'external_id'].includes(mapping.field_name.toLowerCase())) {
submittersIndex[mapping.submitter_uuid][mapping.field_name.toLowerCase()] = row[mapping.column_index]
}
const fieldType = this.fieldTypesIndex[mapping.submitter_uuid][mapping.field_name]
const fieldType = this.fieldTypesIndex[mapping.submitter_uuid]?.[mapping.field_name]
if (fieldType && fieldType !== 'phone') {
submittersIndex[mapping.submitter_uuid].fields.push({
@@ -287,7 +300,7 @@ export default {
})
if (Object.keys(submittersIndex).length !== 0) {
submissions.push({ submitters: Object.values(submittersIndex) })
submissions.push({ submitters: this.submitters.map((s) => submittersIndex[s.uuid]).filter(Boolean) })
}
})
@@ -354,6 +367,9 @@ export default {
}
},
methods: {
t (key) {
return this.i18n[key] || key
},
onDropFiles (e) {
this.uploadFile(e.dataTransfer.files[0])
},
@@ -1,6 +1,6 @@
<template>
<div class="absolute text-center w-full bottom-0 pr-3 mb-4">
<span class="w-full bg-base-200 px-4 py-2 rounded-md inline-flex space-x-2 mx-auto items-center justify-between mb-2 z-20 md:hidden">
<span class="w-full bg-base-200 px-4 py-2 rounded-md inline-flex space-x-2 mx-auto items-center justify-between mb-2 z-20">
<div class="flex items-center space-x-2">
<component
:is="fieldIcons[drawField.type]"
@@ -1,6 +1,6 @@
<template>
<span
class="dropdown dropdown-top dropdown-end absolute bottom-4 right-4 z-10 md:hidden"
class="dropdown dropdown-top dropdown-end absolute bottom-4 right-4 z-10"
>
<label
class="btn btn-neutral text-white btn-circle btn-lg group"
@@ -12,9 +12,10 @@
height="28"
/>
<IconX
class="hidden group-focus:inline"
width="28"
height="28"
class="hidden group-focus:inline p-3"
width="64"
height="50"
@click="closeDropdown"
/>
</label>
<ul
@@ -55,7 +56,7 @@
v-for="(icon, type) in fieldIconsSorted"
:key="type"
>
<li v-if="(fieldTypes.length === 0 || fieldTypes.includes(type)) && (withPhone || type != 'phone') && (withPayment || type != 'payment')">
<li v-if="(fieldTypes.length === 0 || fieldTypes.includes(type)) && (withPhone || type != 'phone') && (withPayment || type != 'payment') && (withVerification || type != 'verification')">
<a
href="#"
class="text-sm py-1 px-2"
@@ -85,7 +86,7 @@ export default {
IconPlus,
IconX
},
inject: ['withPhone', 'withPayment', 'backgroundColor', 't'],
inject: ['withPhone', 'withPayment', 'withVerification', 'backgroundColor', 't'],
props: {
modelValue: {
type: String,
@@ -21,6 +21,7 @@
@click="closeDropdown"
>
<div
v-if="!('price_id' in field.preferences)"
class="py-1.5 px-1 relative"
@click.stop
>
@@ -43,7 +44,7 @@
class="absolute -top-1 left-2.5 px-1 h-4"
style="font-size: 8px"
>
Currency
{{ t('currency') }}
</label>
</div>
<div
@@ -58,6 +59,13 @@
class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0"
@blur="save"
>
<input
v-else-if="'price_id' in field.preferences"
v-model="field.preferences.price_id"
placeholder="Price ID: price_XXXXX"
class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0"
@blur="save"
>
<input
v-else
v-model="field.preferences.price"
@@ -72,8 +80,25 @@
class="absolute -top-1 left-2.5 px-1 h-4"
style="font-size: 8px"
>
Price
{{ t('price') }}
</label>
<div class="flex items-center justify-center">
<a
href="#"
class="hover:underline"
style="font-size: 11px"
:class="{'underline': !('price_id' in field.preferences)}"
@click="delete field.preferences.price_id"
>{{ t('one_off') }}</a>
<span class="h-2.5 border-l border-base-content mx-1" />
<a
href="#"
class="hover:underline"
style="font-size: 11px"
:class="{'underline': ('price_id' in field.preferences)}"
@click="field.preferences.price_id ??= ''"
>{{ t('recurrent') }}</a>
</div>
</div>
<div
v-if="!isConnected || isOauthSuccess"
@@ -153,12 +178,15 @@
<a
v-if="!isConnected"
class="block link text-center mt-1"
href="https://www.docuseal.co/blog/accept-payments-and-request-signatures-with-ease"
href="https://www.docuseal.com/blog/accept-payments-and-request-signatures-with-ease"
target="_blank"
data-turbo="false"
>Learn more</a>
>{{ t('learn_more') }}</a>
</div>
<li class="mb-1">
<li
v-if="!('price_id' in field.preferences)"
class="mb-1"
>
<label
class="label-text cursor-pointer text-center w-full flex items-center"
@click="$emit('click-formula')"
+46 -2
View File
@@ -9,14 +9,30 @@
loading="lazy"
>
<div
class="group flex justify-end cursor-pointer top-0 bottom-0 left-0 right-0 absolute p-1"
class="group flex justify-end cursor-pointer top-0 bottom-0 left-0 right-0 absolute p-1 hover:bg-black/10 transition-colors"
@click="$emit('scroll-to', item)"
>
<div
v-if="editable"
class="flex justify-between w-full"
>
<div style="width: 26px" />
<div
style="width: 26px"
class="flex flex-col justify-between group-hover:opacity-100"
:class="{'opacity-0': !item.conditions?.length }"
>
<div>
<button
class="btn border-base-200 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors p-0"
@click.stop="isShowConditionsModal = true"
>
<IconRouteAltLeft
:width="14"
:stroke-width="1.6"
/>
</button>
</div>
</div>
<div class="">
<ReplaceButton
v-if="withReplaceButton"
@@ -73,19 +89,36 @@
/>
</div>
</div>
<Teleport
v-if="isShowConditionsModal"
:to="modalContainerEl"
>
<ConditionsModal
:item="item"
:build-default-name="buildDefaultName"
@close="isShowConditionsModal = false"
/>
</Teleport>
</template>
<script>
import Contenteditable from './contenteditable'
import Upload from './upload'
import { IconRouteAltLeft } from '@tabler/icons-vue'
import ConditionsModal from './conditions_modal'
import ReplaceButton from './replace'
import Field from './field'
import FieldType from './field_type'
export default {
name: 'DocumentPreview',
components: {
Contenteditable,
IconRouteAltLeft,
ConditionsModal,
ReplaceButton
},
inject: ['t'],
props: {
item: {
type: Object,
@@ -121,13 +154,24 @@ export default {
}
},
emits: ['scroll-to', 'change', 'remove', 'up', 'down', 'replace'],
data () {
return {
isShowConditionsModal: false
}
},
computed: {
fieldNames: FieldType.computed.fieldNames,
fieldLabels: FieldType.computed.fieldLabels,
previewImage () {
return [...this.document.preview_images].sort((a, b) => parseInt(a.filename) - parseInt(b.filename))[0]
},
modalContainerEl () {
return this.$el.getRootNode().querySelector('#docuseal_modal_container')
}
},
methods: {
upload: Upload.methods.upload,
buildDefaultName: Field.methods.buildDefaultName,
onUpdateName (value) {
this.item.name = value

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