Compare commits

...
130 Commits
Author SHA1 Message Date
Alex Turchyn ac9a5c9556 Merge from docusealco/wip 2024-12-23 14:04:15 +02:00
Alex Turchyn 20d09f6dff add countdown for resend code 2024-12-23 01:06:05 +02:00
Pete Matsyburka 333a758688 heading value 2024-12-22 20:44:58 +02:00
Pete Matsyburka 2c8b4d5816 fix typo 2024-12-22 18:33:07 +02:00
Pete Matsyburka 9ce822bce8 add user tokens 2024-12-22 11:33:15 +02:00
Pete Matsyburka 0a382a688c remove schema rb 2024-12-22 01:11:25 +02:00
Pete Matsyburka bd3008a35a fix sidekiq 2024-12-22 00:51:38 +02:00
Alex Turchyn e9bd11cb2e update rails to v8.0.1 2024-12-22 00:04:08 +02:00
Pete Matsyburka ca429bc445 adjust default name 2024-12-20 16:00:38 +02:00
Pete Matsyburka 21a0c54843 fix 2024-12-20 13:58:32 +02:00
Pete Matsyburka 2f41d13009 add order fields 2024-12-20 13:58:12 +02:00
Alex Turchyn f69dcf77de set field alignment from PDF form field preferences 2024-12-20 13:58:00 +02:00
Pete Matsyburka 32596d738c optional invite 2024-12-19 17:33:24 +02:00
Pete Matsyburka e7e5d0e776 add signature formats 2024-12-19 00:42:35 +02:00
Alex Turchyn bb3b38e76f Merge from docusealco/wip 2024-12-18 21:30:59 +02:00
Alex Turchyn bc4c21f804 fix CI 2024-12-18 21:11:07 +02:00
Pete Matsyburka 2fb5de4fa9 fix submission schema 2024-12-18 16:53:35 +02:00
Pete Matsyburka 2e58b2e37b disable certs 2024-12-17 23:25:04 +02:00
Alex Turchyn 780f5512f4 Merge from docusealco/wip 2024-12-16 18:24:32 +02:00
Pete Matsyburka 2cf4908d53 fix filter 2024-12-13 23:19:38 +02:00
Pete Matsyburka 4a9b898713 edit required field 2024-12-13 21:24:30 +02:00
Alex Turchyn bb552d13d4 don't raise validation error if the message is empty 2024-12-13 20:55:37 +02:00
Pete Matsyburka 15d06f4fb0 add status filters 2024-12-13 20:46:26 +02:00
Pete Matsyburka 35a2aba887 adjust permanently remove 2024-12-13 13:31:54 +02:00
Pete Matsyburka 48a82b1d49 add import image 2024-12-13 12:48:44 +02:00
Pete Matsyburka b8453e14c4 add version 2024-12-13 12:04:45 +02:00
Pete Matsyburka 29fbdfbe2f cells align 2024-12-13 00:24:29 +02:00
Pete Matsyburka 5340af0bc6 remove reason: 2024-12-12 19:16:24 +02:00
Pete Matsyburka 1881b6c50f add sidekiq queue 2024-12-12 18:38:14 +02:00
Alex Turchyn b6a2aae970 restrict user invites 2024-12-12 14:04:09 +02:00
Pete Matsyburka b317094192 custom field width height 2024-12-11 19:32:29 +02:00
Pete Matsyburka ab1f4cf297 refactor clone 2024-12-10 23:25:53 +02:00
Pete Matsyburka d3033c072a update document name on clone 2024-12-10 22:27:37 +02:00
Pete Matsyburka f380200e03 add clone document params 2024-12-10 17:42:49 +02:00
Pete Matsyburka a4928268ea fix folder templates 2024-12-09 21:48:47 +02:00
Alex Turchyn dd18c51384 Merge from docusealco/wip 2024-12-09 17:25:23 +02:00
Alex Turchyn c2014a1549 fix field drawings on tablets 2024-12-09 14:43:44 +02:00
Pete Matsyburka 46edc7dc37 adjust title 2024-12-09 12:55:02 +02:00
Pete Matsyburka d62d83ef8e fix duplciate email 2024-12-09 01:06:57 +02:00
Pete Matsyburka 79350c8fba fix submission page 2024-12-08 17:50:23 +02:00
Pete Matsyburka 6702dd52b6 update dep 2024-12-08 15:19:39 +02:00
Pete Matsyburka c8b74a2c43 adjust role 2024-12-08 15:12:02 +02:00
Pete Matsyburka 1746920eb5 add template access 2024-12-08 14:52:22 +02:00
Pete Matsyburka bacbc4309c skip heading 2024-12-07 12:59:28 +02:00
Pete Matsyburka 4429d742b3 document conditions 2024-12-07 02:04:50 +02:00
Pete Matsyburka 38b5eccb65 adjust readonly fields 2024-12-06 00:47:40 +02:00
Pete Matsyburka d8126c4a47 uniq submission.submitters 2024-12-05 19:39:10 +02:00
Pete Matsyburka b1c31672ee use tz country code 2024-12-04 17:40:46 +02:00
Pete Matsyburka 975f3a95fe add db search path 2024-12-04 14:00:13 +02:00
Alex Turchyn e3bed5ae67 add iso codes to phone data 2024-12-04 12:16:55 +02:00
Pete Matsyburka 4737b103c0 fix i18n 2024-12-02 20:17:39 +02:00
Alex Turchyn c5178b96d3 Merge from docusealco/wip 2024-12-02 14:30:33 +02:00
Pete Matsyburka 39a2159196 fix values hash 2024-12-01 11:41:19 +02:00
Pete Matsyburka 5724159b7b verification 2024-12-01 11:23:51 +02:00
Pete Matsyburka 05b7cc9fa6 fix link 2024-11-29 22:03:09 +02:00
Pete Matsyburka 18b13b5b1e adjust policy links 2024-11-29 19:49:44 +02:00
Alex Turchyn d25555b9b0 add policy links to personalization settings 2024-11-29 13:52:47 +02:00
Pete Matsyburka eec266ee83 fix archived false api 2024-11-29 13:30:33 +02:00
Alex Turchyn 75683631f0 add logical 'or' to field conditions 2024-11-29 13:28:11 +02:00
Pete Matsyburka 36c64d1de4 fix counters 2024-11-28 14:57:20 +02:00
Pete Matsyburka 2552b24f21 add documents copy attachment toggle 2024-11-27 21:59:45 +02:00
Alex Turchyn e6ca6dfd85 fix missing file format 2024-11-25 19:40:21 +02:00
Alex Turchyn fa2d86b20a Merge from docusealco/wip 2024-11-25 12:49:04 +02:00
Pete Matsyburka 8e212e4586 adjust title size 2024-11-24 13:18:37 +02:00
Pete Matsyburka a59c18f04b adjust filters ui 2024-11-24 13:03:58 +02:00
Pete Matsyburka 1ab0f1c90c fix form error message 2024-11-23 22:21:25 +02:00
Pete Matsyburka 9beaaa0851 adjust filters 2024-11-23 17:38:11 +02:00
Alex Turchyn b86aac86d1 add template submissions filters 2024-11-22 22:23:20 +02:00
Pete Matsyburka c2eb94b571 update url 2024-11-22 12:10:29 +02:00
Pete Matsyburka a377e45a63 use product email url 2024-11-22 10:30:42 +02:00
Pete Matsyburka 58574abc6b fix filename 2024-11-21 23:18:27 +02:00
Pete Matsyburka cbae888971 add file host param 2024-11-21 23:03:30 +02:00
Pete Matsyburka 944e1f31db adjust condition 2024-11-21 18:23:34 +02:00
Pete Matsyburka 711b3b3190 use filename format in email attachments 2024-11-20 21:49:37 +02:00
Pete Matsyburka e7abadcefb add filename format option 2024-11-20 01:12:26 +02:00
Alex Turchyn 18d60f5522 add signing form specs 2024-11-19 21:28:58 +02:00
Pete Matsyburka 6d5c6e086d use email host 2024-11-19 00:05:41 +02:00
Pete Matsyburka 5a48ab0360 configure form link 2024-11-18 21:08:32 +02:00
Pete Matsyburka 599785b6d1 fix number cast empty string 2024-11-17 21:29:20 +02:00
Pete Matsyburka f2fd5fb82b fix default number 2024-11-17 21:15:10 +02:00
Pete Matsyburka f2782bd84e fix form show 2024-11-16 23:06:00 +02:00
Pete Matsyburka e52a721fc9 fix typo 2024-11-16 18:19:25 +02:00
Pete Matsyburka b035123ff9 disable email var 2024-11-16 16:13:30 +02:00
Pete Matsyburka ca8de23082 use account archived at 2024-11-15 22:17:55 +02:00
Pete Matsyburka f514654c40 rails spec hook 2024-11-15 00:30:33 +02:00
Pete Matsyburka 99bc528a6c fix spec 2024-11-14 23:36:03 +02:00
Pete Matsyburka f3c77d0177 fix bulk order 2024-11-14 22:19:22 +02:00
Pete Matsyburka db2b5f35bd fix cells length 2024-11-14 21:15:40 +02:00
Pete Matsyburka 5728381725 fix role 2024-11-14 20:09:29 +02:00
Pete Matsyburka a7affb1a49 add link to export 2024-11-14 20:04:51 +02:00
Pete Matsyburka 21bf5ed9f7 fix fields message 2024-11-14 14:38:28 +02:00
Pete Matsyburka 8ddd4ee916 adjust duration 2024-11-14 09:35:20 +02:00
Pete Matsyburka 0e2fd8ee4f use email host 2024-11-13 17:53:55 +02:00
Pete Matsyburka 27901063ed fix preview 2024-11-12 17:11:09 +02:00
Pete Matsyburka ce31ebde1e add delay 2024-11-11 19:32:27 +02:00
Alex Turchyn 0c2abf211e Merge from docusealco/wip 2024-11-11 17:44:28 +02:00
Pete Matsyburka 8ecaa4fd7a adjust custom font size 2024-11-11 14:25:07 +02:00
Pete Matsyburka 6759c26d86 fix rubocop 2024-11-10 13:11:25 +02:00
Pete Matsyburka 76f1f140ac add field color 2024-11-10 12:48:53 +02:00
Pete Matsyburka ca7ab5aab7 adjust webhooks controller 2024-11-09 11:15:19 +02:00
Alex Turchyn fd3530ac62 update Webhooks UI 2024-11-09 00:41:46 +02:00
Pete Matsyburka 4ea863592b fix form 2024-11-08 22:12:07 +02:00
Pete Matsyburka 3f87d0336d do not use same reply to as to 2024-11-08 13:02:32 +02:00
Pete Matsyburka e251482481 unique party names 2024-11-08 12:15:13 +02:00
Pete Matsyburka c79fca75f3 add complete button 2024-11-07 23:41:49 +02:00
Alex Turchyn 0b0f0b497e fix acroform select field 2024-11-06 20:30:01 +02:00
Pete Matsyburka e9034d8da7 webhook secret priority 2024-11-06 14:06:33 +02:00
Pete Matsyburka 8fbb8b8f6e do not touch updated_at 2024-11-05 22:55:03 +02:00
Alex Turchyn 4635fdd32b remove replace_timestamps helper 2024-11-05 22:34:14 +02:00
Pete Matsyburka 6dd4382446 optimize webhooks query 2024-11-04 16:19:43 +02:00
Alex Turchyn 50acff2e81 Merge from docusealco/wip 2024-11-04 16:13:29 +02:00
Pete Matsyburka cb6d24dfca add canonical urls 2024-11-04 11:37:58 +02:00
Pete Matsyburka ab4a22d8ba fix test links 2024-11-03 13:43:28 +02:00
Pete Matsyburka bc40a15de1 use com 2024-11-03 10:19:59 +02:00
Pete Matsyburka 2542f26d96 adjust webhooks 2024-11-03 09:49:30 +02:00
Alex Turchyn 84edb6dbda use webhook urls instead of encrypted configs 2024-11-02 22:12:41 +02:00
Pete Matsyburka 88776e7316 Merge from docusealco/wip 2024-11-02 14:48:21 +02:00
Pete Matsyburka f669045362 redirect com 2024-11-02 09:48:47 +02:00
Pete Matsyburka 68d3efbdba use com 2024-11-02 09:02:50 +02:00
Pete Matsyburka 22d4833496 allow to unarchive submissions 2024-11-01 21:37:02 +02:00
Pete Matsyburka ff14caa9f7 fix image 2024-11-01 19:01:51 +02:00
Pete Matsyburka 05647135bc add status to export 2024-10-31 17:22:14 +02:00
Pete Matsyburka ae76c2a4a8 disable draw when type 2024-10-31 16:43:43 +02:00
Pete Matsyburka fb2b3d44fe fix bulk 2024-10-31 00:51:21 +02:00
Pete Matsyburka 3868557efd add define submitters 2024-10-30 23:07:10 +02:00
dependabot[bot] 9bef28c8ba Bump rexml from 3.3.8 to 3.3.9 in the bundler group across 1 directory (#50) 2024-10-30 12:12:36 +02:00
Pete Matsyburka f25fd3b03d do not edit if declined 2024-10-29 17:11:44 +02:00
Pete Matsyburka d1d7571ee4 disable decline email 2024-10-29 16:44:07 +02:00
Pete Matsyburka 6a298a068c fix updated templates 2024-10-29 12:11:21 +02:00
Pete Matsyburka d4aef79b68 use tsa fallback url 2024-10-29 08:29:26 +02:00
233 changed files with 6034 additions and 1246 deletions

No files matched your search

+5 -5
View File
@@ -51,25 +51,25 @@ Metrics/AbcSize:
- spec/**/* - spec/**/*
Metrics/ModuleLength: Metrics/ModuleLength:
Max: 500 Max: 1000
Metrics/ClassLength: Metrics/ClassLength:
Max: 500 Max: 1000
RSpec/NestedGroups: RSpec/NestedGroups:
Max: 6 Max: 6
RSpec/MultipleExpectations: RSpec/MultipleExpectations:
Max: 20 Max: 25
RSpec/ExampleLength: RSpec/ExampleLength:
Max: 40 Max: 50
RSpec/MultipleMemoizedHelpers: RSpec/MultipleMemoizedHelpers:
Max: 9 Max: 9
Metrics/BlockNesting: Metrics/BlockNesting:
Max: 4 Max: 5
Rails/I18nLocaleTexts: Rails/I18nLocaleTexts:
Enabled: false Enabled: false
+2 -1
View File
@@ -62,12 +62,13 @@ RUN apk add --no-cache build-base && bundle install && apk del --no-cache build-
COPY ./bin ./bin COPY ./bin ./bin
COPY ./app ./app COPY ./app ./app
COPY ./config ./config COPY ./config ./config
COPY ./db ./db COPY ./db/migrate ./db/migrate
COPY ./log ./log COPY ./log ./log
COPY ./lib ./lib COPY ./lib ./lib
COPY ./public ./public COPY ./public ./public
COPY ./tmp ./tmp COPY ./tmp ./tmp
COPY LICENSE README.md Rakefile config.ru .version ./ COPY LICENSE README.md Rakefile config.ru .version ./
COPY .version ./public/version
COPY --from=fonts /fonts/GoNotoKurrent-Regular.ttf /fonts/GoNotoKurrent-Bold.ttf /fonts/DancingScript-Regular.otf /fonts/OFL.txt /fonts COPY --from=fonts /fonts/GoNotoKurrent-Regular.ttf /fonts/GoNotoKurrent-Bold.ttf /fonts/DancingScript-Regular.otf /fonts/OFL.txt /fonts
COPY --from=fonts /fonts/FreeSans.ttf /usr/share/fonts/freefont COPY --from=fonts /fonts/FreeSans.ttf /usr/share/fonts/freefont
+160 -154
View File
@@ -1,67 +1,67 @@
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
actioncable (7.2.1.1) actioncable (8.0.1)
actionpack (= 7.2.1.1) actionpack (= 8.0.1)
activesupport (= 7.2.1.1) activesupport (= 8.0.1)
nio4r (~> 2.0) nio4r (~> 2.0)
websocket-driver (>= 0.6.1) websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6) zeitwerk (~> 2.6)
actionmailbox (7.2.1.1) actionmailbox (8.0.1)
actionpack (= 7.2.1.1) actionpack (= 8.0.1)
activejob (= 7.2.1.1) activejob (= 8.0.1)
activerecord (= 7.2.1.1) activerecord (= 8.0.1)
activestorage (= 7.2.1.1) activestorage (= 8.0.1)
activesupport (= 7.2.1.1) activesupport (= 8.0.1)
mail (>= 2.8.0) mail (>= 2.8.0)
actionmailer (7.2.1.1) actionmailer (8.0.1)
actionpack (= 7.2.1.1) actionpack (= 8.0.1)
actionview (= 7.2.1.1) actionview (= 8.0.1)
activejob (= 7.2.1.1) activejob (= 8.0.1)
activesupport (= 7.2.1.1) activesupport (= 8.0.1)
mail (>= 2.8.0) mail (>= 2.8.0)
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
actionpack (7.2.1.1) actionpack (8.0.1)
actionview (= 7.2.1.1) actionview (= 8.0.1)
activesupport (= 7.2.1.1) activesupport (= 8.0.1)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
racc rack (>= 2.2.4)
rack (>= 2.2.4, < 3.2)
rack-session (>= 1.0.1) rack-session (>= 1.0.1)
rack-test (>= 0.6.3) rack-test (>= 0.6.3)
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6) rails-html-sanitizer (~> 1.6)
useragent (~> 0.16) useragent (~> 0.16)
actiontext (7.2.1.1) actiontext (8.0.1)
actionpack (= 7.2.1.1) actionpack (= 8.0.1)
activerecord (= 7.2.1.1) activerecord (= 8.0.1)
activestorage (= 7.2.1.1) activestorage (= 8.0.1)
activesupport (= 7.2.1.1) activesupport (= 8.0.1)
globalid (>= 0.6.0) globalid (>= 0.6.0)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
actionview (7.2.1.1) actionview (8.0.1)
activesupport (= 7.2.1.1) activesupport (= 8.0.1)
builder (~> 3.1) builder (~> 3.1)
erubi (~> 1.11) erubi (~> 1.11)
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6) rails-html-sanitizer (~> 1.6)
activejob (7.2.1.1) activejob (8.0.1)
activesupport (= 7.2.1.1) activesupport (= 8.0.1)
globalid (>= 0.3.6) globalid (>= 0.3.6)
activemodel (7.2.1.1) activemodel (8.0.1)
activesupport (= 7.2.1.1) activesupport (= 8.0.1)
activerecord (7.2.1.1) activerecord (8.0.1)
activemodel (= 7.2.1.1) activemodel (= 8.0.1)
activesupport (= 7.2.1.1) activesupport (= 8.0.1)
timeout (>= 0.4.0) timeout (>= 0.4.0)
activestorage (7.2.1.1) activestorage (8.0.1)
actionpack (= 7.2.1.1) actionpack (= 8.0.1)
activejob (= 7.2.1.1) activejob (= 8.0.1)
activerecord (= 7.2.1.1) activerecord (= 8.0.1)
activesupport (= 7.2.1.1) activesupport (= 8.0.1)
marcel (~> 1.0) marcel (~> 1.0)
activesupport (7.2.1.1) activesupport (8.0.1)
base64 base64
benchmark (>= 0.3)
bigdecimal bigdecimal
concurrent-ruby (~> 1.0, >= 1.3.1) concurrent-ruby (~> 1.0, >= 1.3.1)
connection_pool (>= 2.2.5) connection_pool (>= 2.2.5)
@@ -71,31 +71,32 @@ GEM
minitest (>= 5.1) minitest (>= 5.1)
securerandom (>= 0.3) securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5) tzinfo (~> 2.0, >= 2.0.5)
uri (>= 0.13.1)
addressable (2.8.7) addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0) public_suffix (>= 2.0.2, < 7.0)
annotate (3.2.0) annotate (2.6.5)
activerecord (>= 3.2, < 8.0) activerecord (>= 2.3.0)
rake (>= 10.4, < 14.0) rake (>= 0.8.7)
arabic-letter-connector (0.1.1) arabic-letter-connector (0.1.1)
ast (2.4.2) ast (2.4.2)
aws-eventstream (1.3.0) aws-eventstream (1.3.0)
aws-partitions (1.985.0) aws-partitions (1.1027.0)
aws-sdk-core (3.209.1) aws-sdk-core (3.214.0)
aws-eventstream (~> 1, >= 1.3.0) aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.651.0) aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9) aws-sigv4 (~> 1.9)
jmespath (~> 1, >= 1.6.1) jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.94.0) aws-sdk-kms (1.96.0)
aws-sdk-core (~> 3, >= 3.207.0) aws-sdk-core (~> 3, >= 3.210.0)
aws-sigv4 (~> 1.5) aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.167.0) aws-sdk-s3 (1.176.1)
aws-sdk-core (~> 3, >= 3.207.0) aws-sdk-core (~> 3, >= 3.210.0)
aws-sdk-kms (~> 1) aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5) aws-sigv4 (~> 1.5)
aws-sdk-secretsmanager (1.108.0) aws-sdk-secretsmanager (1.110.0)
aws-sdk-core (~> 3, >= 3.207.0) aws-sdk-core (~> 3, >= 3.210.0)
aws-sigv4 (~> 1.5) aws-sigv4 (~> 1.5)
aws-sigv4 (1.10.0) aws-sigv4 (1.10.1)
aws-eventstream (~> 1, >= 1.0.2) aws-eventstream (~> 1, >= 1.0.2)
azure-storage-blob (2.0.3) azure-storage-blob (2.0.3)
azure-storage-common (~> 2.0) azure-storage-common (~> 2.0)
@@ -107,6 +108,7 @@ GEM
nokogiri (~> 1, >= 1.10.8) nokogiri (~> 1, >= 1.10.8)
base64 (0.2.0) base64 (0.2.0)
bcrypt (3.1.20) bcrypt (3.1.20)
benchmark (0.4.0)
better_html (2.1.1) better_html (2.1.1)
actionview (>= 6.0) actionview (>= 6.0)
activesupport (>= 6.0) activesupport (>= 6.0)
@@ -119,7 +121,7 @@ GEM
bootsnap (1.18.4) bootsnap (1.18.4)
msgpack (~> 1.2) msgpack (~> 1.2)
builder (3.3.0) builder (3.3.0)
bullet (7.2.0) bullet (8.0.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
uniform_notifier (~> 1.11) uniform_notifier (~> 1.11)
camertron-eprun (1.1.1) camertron-eprun (1.1.1)
@@ -145,14 +147,14 @@ GEM
bigdecimal bigdecimal
rexml rexml
crass (1.0.6) crass (1.0.6)
css_parser (1.19.0) css_parser (1.21.0)
addressable addressable
csv (3.3.0) csv (3.3.2)
cuprite (0.15.1) cuprite (0.15.1)
capybara (~> 3.0) capybara (~> 3.0)
ferrum (~> 0.15.0) ferrum (~> 0.15.0)
date (3.3.4) date (3.4.1)
debug (1.9.2) debug (1.10.0)
irb (~> 1.10) irb (~> 1.10)
reline (>= 0.3.8) reline (>= 0.3.8)
declarative (0.0.20) declarative (0.0.20)
@@ -162,32 +164,32 @@ GEM
railties (>= 4.1.0) railties (>= 4.1.0)
responders responders
warden (~> 1.2.3) warden (~> 1.2.3)
devise-two-factor (6.0.0) devise-two-factor (6.1.0)
activesupport (~> 7.0) activesupport (>= 7.0, < 8.1)
devise (~> 4.0) devise (~> 4.0)
railties (~> 7.0) railties (>= 7.0, < 8.1)
rotp (~> 6.0) rotp (~> 6.0)
diff-lcs (1.5.1) diff-lcs (1.5.1)
digest-crc (0.6.5) digest-crc (0.6.5)
rake (>= 12.0.0, < 14.0.0) rake (>= 12.0.0, < 14.0.0)
docile (1.4.1) docile (1.4.1)
dotenv (3.1.4) dotenv (3.1.7)
drb (2.2.1) drb (2.2.1)
email_typo (0.2.3) email_typo (0.2.3)
erb_lint (0.6.0) erb_lint (0.7.0)
activesupport activesupport
better_html (>= 2.0.1) better_html (>= 2.0.1)
parser (>= 2.7.1.4) parser (>= 2.7.1.4)
rainbow rainbow
rubocop (>= 1) rubocop (>= 1)
smart_properties smart_properties
erubi (1.13.0) erubi (1.13.1)
factory_bot (6.5.0) factory_bot (6.5.0)
activesupport (>= 5.0.0) activesupport (>= 5.0.0)
factory_bot_rails (6.4.3) factory_bot_rails (6.4.4)
factory_bot (~> 6.4) factory_bot (~> 6.5)
railties (>= 5.0.0) railties (>= 5.0.0)
faker (3.4.2) faker (3.5.1)
i18n (>= 1.8.11, < 2) i18n (>= 1.8.11, < 2)
faraday (1.10.4) faraday (1.10.4)
faraday-em_http (~> 1.0) faraday-em_http (~> 1.0)
@@ -207,8 +209,8 @@ GEM
faraday-follow_redirects (0.3.0) faraday-follow_redirects (0.3.0)
faraday (>= 1, < 3) faraday (>= 1, < 3)
faraday-httpclient (1.0.1) faraday-httpclient (1.0.1)
faraday-multipart (1.0.4) faraday-multipart (1.1.0)
multipart-post (~> 2) multipart-post (~> 2.0)
faraday-net_http (1.0.2) faraday-net_http (1.0.2)
faraday-net_http_persistent (1.2.0) faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0) faraday-patron (1.0.0)
@@ -235,9 +237,9 @@ GEM
mutex_m mutex_m
representable (~> 3.0) representable (~> 3.0)
retriable (>= 2.0, < 4.a) retriable (>= 2.0, < 4.a)
google-apis-iamcredentials_v1 (0.21.0) google-apis-iamcredentials_v1 (0.22.0)
google-apis-core (>= 0.15.0, < 2.a) google-apis-core (>= 0.15.0, < 2.a)
google-apis-storage_v1 (0.46.0) google-apis-storage_v1 (0.49.0)
google-apis-core (>= 0.15.0, < 2.a) google-apis-core (>= 0.15.0, < 2.a)
google-cloud-core (1.7.1) google-cloud-core (1.7.1)
google-cloud-env (>= 1.0, < 3.a) google-cloud-env (>= 1.0, < 3.a)
@@ -245,7 +247,7 @@ GEM
google-cloud-env (2.2.1) google-cloud-env (2.2.1)
faraday (>= 1.0, < 3.a) faraday (>= 1.0, < 3.a)
google-cloud-errors (1.4.0) google-cloud-errors (1.4.0)
google-cloud-storage (1.52.0) google-cloud-storage (1.54.0)
addressable (~> 2.8) addressable (~> 2.8)
digest-crc (~> 0.4) digest-crc (~> 0.4)
google-apis-core (~> 0.13) google-apis-core (~> 0.13)
@@ -254,15 +256,17 @@ GEM
google-cloud-core (~> 1.6) google-cloud-core (~> 1.6)
googleauth (~> 1.9) googleauth (~> 1.9)
mini_mime (~> 1.0) mini_mime (~> 1.0)
googleauth (1.11.1) google-logging-utils (0.1.0)
googleauth (1.12.2)
faraday (>= 1.0, < 3.a) faraday (>= 1.0, < 3.a)
google-cloud-env (~> 2.1) google-cloud-env (~> 2.2)
google-logging-utils (~> 0.1)
jwt (>= 1.4, < 3.0) jwt (>= 1.4, < 3.0)
multi_json (~> 1.11) multi_json (~> 1.11)
os (>= 0.9, < 2.0) os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a) signet (>= 0.16, < 2.a)
hashdiff (1.1.1) hashdiff (1.1.2)
hexapdf (0.47.0) hexapdf (1.0.3)
cmdparse (~> 3.0, >= 3.0.3) cmdparse (~> 3.0, >= 3.0.3)
geom2d (~> 0.4, >= 0.4.1) geom2d (~> 0.4, >= 0.4.1)
openssl (>= 2.2.1) openssl (>= 2.2.1)
@@ -273,12 +277,12 @@ GEM
image_processing (1.13.0) image_processing (1.13.0)
mini_magick (>= 4.9.5, < 5) mini_magick (>= 4.9.5, < 5)
ruby-vips (>= 2.0.17, < 3) ruby-vips (>= 2.0.17, < 3)
io-console (0.7.2) io-console (0.8.0)
irb (1.14.1) irb (1.14.3)
rdoc (>= 4.0.0) rdoc (>= 4.0.0)
reline (>= 0.4.2) reline (>= 0.4.2)
jmespath (1.6.2) jmespath (1.6.2)
json (2.7.2) json (2.9.1)
jwt (2.9.3) jwt (2.9.3)
base64 base64
language_server-protocol (3.17.0.3) language_server-protocol (3.17.0.3)
@@ -292,13 +296,13 @@ GEM
letter_opener (~> 1.9) letter_opener (~> 1.9)
railties (>= 6.1) railties (>= 6.1)
rexml rexml
logger (1.6.1) logger (1.6.4)
lograge (0.14.0) lograge (0.14.0)
actionpack (>= 4) actionpack (>= 4)
activesupport (>= 4) activesupport (>= 4)
railties (>= 4) railties (>= 4)
request_store (~> 1.0) request_store (~> 1.0)
loofah (2.22.0) loofah (2.23.1)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.12.0) nokogiri (>= 1.12.0)
mail (2.8.1) mail (2.8.1)
@@ -311,16 +315,16 @@ GEM
method_source (1.1.0) method_source (1.1.0)
mini_magick (4.13.2) mini_magick (4.13.2)
mini_mime (1.1.5) mini_mime (1.1.5)
mini_portile2 (2.8.7) mini_portile2 (2.8.8)
minitest (5.25.1) minitest (5.25.4)
msgpack (1.7.3) msgpack (1.7.5)
multi_json (1.15.0) multi_json (1.15.0)
multipart-post (2.4.1) multipart-post (2.4.1)
mutex_m (0.2.0) mutex_m (0.3.0)
mysql2 (0.5.6) mysql2 (0.5.6)
net-http-persistent (4.0.4) net-http-persistent (4.0.5)
connection_pool (~> 2.2) connection_pool (~> 2.2)
net-imap (0.4.17) net-imap (0.5.3)
date date
net-protocol net-protocol
net-pop (0.1.2) net-pop (0.1.2)
@@ -329,28 +333,28 @@ GEM
timeout timeout
net-smtp (0.5.0) net-smtp (0.5.0)
net-protocol net-protocol
nio4r (2.7.3) nio4r (2.7.4)
nokogiri (1.16.7) nokogiri (1.17.2)
mini_portile2 (~> 2.8.2) mini_portile2 (~> 2.8.2)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.16.7-aarch64-linux) nokogiri (1.17.2-aarch64-linux)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.16.7-x86_64-linux) nokogiri (1.17.2-x86_64-linux)
racc (~> 1.4) racc (~> 1.4)
oj (3.16.6) oj (3.16.8)
bigdecimal (>= 3.0) bigdecimal (>= 3.0)
ostruct (>= 0.2) ostruct (>= 0.2)
openssl (3.2.0) openssl (3.3.0)
orm_adapter (0.5.0) orm_adapter (0.5.0)
os (1.1.4) os (1.1.4)
ostruct (0.6.0) ostruct (0.6.1)
package_json (0.1.0) package_json (0.1.0)
pagy (9.1.0) pagy (9.3.3)
parallel (1.26.3) parallel (1.26.3)
parser (3.3.5.0) parser (3.3.6.0)
ast (~> 2.4.1) ast (~> 2.4.1)
racc racc
pg (1.5.8) pg (1.5.9)
premailer (1.27.0) premailer (1.27.0)
addressable addressable
css_parser (>= 1.19.0) css_parser (>= 1.19.0)
@@ -361,15 +365,16 @@ GEM
premailer (~> 1.7, >= 1.7.9) premailer (~> 1.7, >= 1.7.9)
pretender (0.5.0) pretender (0.5.0)
actionpack (>= 6.1) actionpack (>= 6.1)
pry (0.14.2) pry (0.15.0)
coderay (~> 1.1) coderay (~> 1.1)
method_source (~> 1.0) method_source (~> 1.0)
pry-rails (0.3.11) pry-rails (0.3.11)
pry (>= 0.13.0) pry (>= 0.13.0)
psych (5.1.2) psych (5.2.2)
date
stringio stringio
public_suffix (6.0.1) public_suffix (6.0.1)
puma (6.4.3) puma (6.5.0)
nio4r (~> 2.0) nio4r (~> 2.0)
racc (1.8.1) racc (1.8.1)
rack (3.1.8) rack (3.1.8)
@@ -379,40 +384,39 @@ GEM
rack (>= 3.0.0) rack (>= 3.0.0)
rack-test (2.1.0) rack-test (2.1.0)
rack (>= 1.3) rack (>= 1.3)
rackup (2.1.0) rackup (2.2.1)
rack (>= 3) rack (>= 3)
webrick (~> 1.8) rails (8.0.1)
rails (7.2.1.1) actioncable (= 8.0.1)
actioncable (= 7.2.1.1) actionmailbox (= 8.0.1)
actionmailbox (= 7.2.1.1) actionmailer (= 8.0.1)
actionmailer (= 7.2.1.1) actionpack (= 8.0.1)
actionpack (= 7.2.1.1) actiontext (= 8.0.1)
actiontext (= 7.2.1.1) actionview (= 8.0.1)
actionview (= 7.2.1.1) activejob (= 8.0.1)
activejob (= 7.2.1.1) activemodel (= 8.0.1)
activemodel (= 7.2.1.1) activerecord (= 8.0.1)
activerecord (= 7.2.1.1) activestorage (= 8.0.1)
activestorage (= 7.2.1.1) activesupport (= 8.0.1)
activesupport (= 7.2.1.1)
bundler (>= 1.15.0) bundler (>= 1.15.0)
railties (= 7.2.1.1) railties (= 8.0.1)
rails-dom-testing (2.2.0) rails-dom-testing (2.2.0)
activesupport (>= 5.0.0) activesupport (>= 5.0.0)
minitest minitest
nokogiri (>= 1.6) nokogiri (>= 1.6)
rails-html-sanitizer (1.6.0) rails-html-sanitizer (1.6.2)
loofah (~> 2.21) loofah (~> 2.21)
nokogiri (~> 1.14) nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
rails-i18n (7.0.9) rails-i18n (8.0.1)
i18n (>= 0.7, < 2) i18n (>= 0.7, < 2)
railties (>= 6.0.0, < 8) railties (>= 8.0.0, < 9)
rails_autolink (1.1.8) rails_autolink (1.1.8)
actionview (> 3.1) actionview (> 3.1)
activesupport (> 3.1) activesupport (> 3.1)
railties (> 3.1) railties (> 3.1)
railties (7.2.1.1) railties (8.0.1)
actionpack (= 7.2.1.1) actionpack (= 8.0.1)
activesupport (= 7.2.1.1) activesupport (= 8.0.1)
irb (~> 1.13) irb (~> 1.13)
rackup (>= 1.0.0) rackup (>= 1.0.0)
rake (>= 12.2) rake (>= 12.2)
@@ -420,12 +424,12 @@ GEM
zeitwerk (~> 2.6) zeitwerk (~> 2.6)
rainbow (3.1.1) rainbow (3.1.1)
rake (13.2.1) rake (13.2.1)
rdoc (6.7.0) rdoc (6.10.0)
psych (>= 4.0.0) psych (>= 4.0.0)
redis-client (0.22.2) redis-client (0.23.0)
connection_pool connection_pool
regexp_parser (2.9.2) regexp_parser (2.9.3)
reline (0.5.10) reline (0.6.0)
io-console (~> 0.5) io-console (~> 0.5)
representable (3.2.0) representable (3.2.0)
declarative (< 0.1.0) declarative (< 0.1.0)
@@ -437,13 +441,13 @@ GEM
actionpack (>= 5.2) actionpack (>= 5.2)
railties (>= 5.2) railties (>= 5.2)
retriable (3.1.2) retriable (3.1.2)
rexml (3.3.8) rexml (3.4.0)
rotp (6.3.0) rotp (6.3.0)
rqrcode (2.2.0) rqrcode (2.2.0)
chunky_png (~> 1.0) chunky_png (~> 1.0)
rqrcode_core (~> 1.0) rqrcode_core (~> 1.0)
rqrcode_core (1.2.0) rqrcode_core (1.2.0)
rspec-core (3.13.1) rspec-core (3.13.2)
rspec-support (~> 3.13.0) rspec-support (~> 3.13.0)
rspec-expectations (3.13.3) rspec-expectations (3.13.3)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
@@ -451,7 +455,7 @@ GEM
rspec-mocks (3.13.2) rspec-mocks (3.13.2)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0) rspec-support (~> 3.13.0)
rspec-rails (7.0.1) rspec-rails (7.1.0)
actionpack (>= 7.0) actionpack (>= 7.0)
activesupport (>= 7.0) activesupport (>= 7.0)
railties (>= 7.0) railties (>= 7.0)
@@ -459,48 +463,47 @@ GEM
rspec-expectations (~> 3.13) rspec-expectations (~> 3.13)
rspec-mocks (~> 3.13) rspec-mocks (~> 3.13)
rspec-support (~> 3.13) rspec-support (~> 3.13)
rspec-support (3.13.1) rspec-support (3.13.2)
rubocop (1.66.1) rubocop (1.69.2)
json (~> 2.3) json (~> 2.3)
language_server-protocol (>= 3.17.0) language_server-protocol (>= 3.17.0)
parallel (~> 1.10) parallel (~> 1.10)
parser (>= 3.3.0.2) parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0) rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 2.4, < 3.0) regexp_parser (>= 2.9.3, < 3.0)
rubocop-ast (>= 1.32.2, < 2.0) rubocop-ast (>= 1.36.2, < 2.0)
ruby-progressbar (~> 1.7) ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0) unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.32.3) rubocop-ast (1.37.0)
parser (>= 3.3.1.0) parser (>= 3.3.1.0)
rubocop-performance (1.22.1) rubocop-performance (1.23.0)
rubocop (>= 1.48.1, < 2.0) rubocop (>= 1.48.1, < 2.0)
rubocop-ast (>= 1.31.1, < 2.0) rubocop-ast (>= 1.31.1, < 2.0)
rubocop-rails (2.26.2) rubocop-rails (2.27.0)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
rack (>= 1.1) rack (>= 1.1)
rubocop (>= 1.52.0, < 2.0) rubocop (>= 1.52.0, < 2.0)
rubocop-ast (>= 1.31.1, < 2.0) rubocop-ast (>= 1.31.1, < 2.0)
rubocop-rspec (3.1.0) rubocop-rspec (3.3.0)
rubocop (~> 1.61) rubocop (~> 1.61)
ruby-progressbar (1.13.0) ruby-progressbar (1.13.0)
ruby-vips (2.2.2) ruby-vips (2.2.2)
ffi (~> 1.12) ffi (~> 1.12)
logger logger
ruby2_keywords (0.0.5) ruby2_keywords (0.0.5)
rubyXL (3.4.27) rubyXL (3.4.33)
nokogiri (>= 1.10.8) nokogiri (>= 1.10.8)
rubyzip (>= 1.3.0) rubyzip (>= 1.3.0)
rubyzip (2.3.2) rubyzip (2.3.2)
securerandom (0.3.1) securerandom (0.4.1)
semantic_range (3.0.0) semantic_range (3.1.0)
shakapacker (8.0.2) shakapacker (8.0.2)
activesupport (>= 5.2) activesupport (>= 5.2)
package_json package_json
rack-proxy (>= 0.6.1) rack-proxy (>= 0.6.1)
railties (>= 5.2) railties (>= 5.2)
semantic_range (>= 2.3.0) semantic_range (>= 2.3.0)
sidekiq (7.3.2) sidekiq (7.3.7)
concurrent-ruby (< 2)
connection_pool (>= 2.3.0) connection_pool (>= 2.3.0)
logger logger
rack (>= 2.2.4) rack (>= 2.2.4)
@@ -517,17 +520,17 @@ GEM
simplecov-html (0.13.1) simplecov-html (0.13.1)
simplecov_json_formatter (0.1.4) simplecov_json_formatter (0.1.4)
smart_properties (1.17.0) smart_properties (1.17.0)
sqlite3 (2.1.0) sqlite3 (2.4.1)
mini_portile2 (~> 2.8.0) mini_portile2 (~> 2.8.0)
sqlite3 (2.1.0-aarch64-linux-gnu) sqlite3 (2.4.1-aarch64-linux-gnu)
sqlite3 (2.1.0-x86_64-linux-gnu) sqlite3 (2.4.1-x86_64-linux-gnu)
stringio (3.1.1) stringio (3.1.2)
strip_attributes (1.13.0) strip_attributes (1.14.1)
activemodel (>= 3.0, < 8.0) activemodel (>= 3.0, < 9.0)
thor (1.3.2) thor (1.3.2)
timeout (0.4.1) timeout (0.4.3)
trailblazer-option (0.1.2) trailblazer-option (0.1.2)
turbo-rails (2.0.10) turbo-rails (2.0.11)
actionpack (>= 6.0.0) actionpack (>= 6.0.0)
railties (>= 6.0.0) railties (>= 6.0.0)
twitter_cldr (6.12.1) twitter_cldr (6.12.1)
@@ -539,9 +542,12 @@ GEM
tzinfo-data (1.2024.2) tzinfo-data (1.2024.2)
tzinfo (>= 1.0.0) tzinfo (>= 1.0.0)
uber (0.1.0) uber (0.1.0)
unicode-display_width (2.6.0) unicode-display_width (3.1.2)
unicode-emoji (~> 4.0, >= 4.0.4)
unicode-emoji (4.0.4)
uniform_notifier (1.16.0) uniform_notifier (1.16.0)
useragent (0.16.10) uri (1.0.2)
useragent (0.16.11)
warden (1.2.9) warden (1.2.9)
rack (>= 2.0.9) rack (>= 2.0.9)
web-console (4.2.1) web-console (4.2.1)
@@ -553,13 +559,13 @@ GEM
addressable (>= 2.8.0) addressable (>= 2.8.0)
crack (>= 0.3.2) crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0) hashdiff (>= 0.4.0, < 2.0.0)
webrick (1.8.2) webrick (1.9.1)
websocket-driver (0.7.6) websocket-driver (0.7.6)
websocket-extensions (>= 0.1.0) websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5) websocket-extensions (0.1.5)
xpath (3.2.0) xpath (3.2.0)
nokogiri (~> 1.8) nokogiri (~> 1.8)
zeitwerk (2.7.0) zeitwerk (2.7.1)
PLATFORMS PLATFORMS
aarch64-linux aarch64-linux
+10 -10
View File
@@ -1,6 +1,6 @@
<h1 align="center" style="border-bottom: none"> <h1 align="center" style="border-bottom: none">
<div> <div>
<a href="https://www.docuseal.co"> <a href="https://www.docuseal.com">
<img alt="DocuSeal" src="https://github.com/docusealco/docuseal/assets/5418788/c12cd051-81cd-4402-bc3a-92f2cfdc1b06" width="80" /> <img alt="DocuSeal" src="https://github.com/docusealco/docuseal/assets/5418788/c12cd051-81cd-4402-bc3a-92f2cfdc1b06" width="80" />
<br> <br>
</a> </a>
@@ -54,11 +54,11 @@ DocuSeal is an open source platform that provides secure and efficient digital d
- Conditional fields and formulas - Conditional fields and formulas
- Bulk send with CSV, XLSX spreadsheet import - Bulk send with CSV, XLSX spreadsheet import
- SSO / SAML - SSO / SAML
- Template creation with HTML API ([Guide](https://www.docuseal.co/guides/create-pdf-document-fillable-form-with-html-api)) - Template creation with HTML API ([Guide](https://www.docuseal.com/guides/create-pdf-document-fillable-form-with-html-api))
- Template creation with PDF or DOCX and field tags API ([Guide](https://www.docuseal.co/guides/use-embedded-text-field-tags-in-the-pdf-to-create-a-fillable-form)) - Template creation with PDF or DOCX and field tags API ([Guide](https://www.docuseal.com/guides/use-embedded-text-field-tags-in-the-pdf-to-create-a-fillable-form))
- Embedded signing form ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue), [Angular](https://github.com/docusealco/docuseal-angular) or [JavaScript](https://www.docuseal.co/docs/embedded)) - Embedded signing form ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue), [Angular](https://github.com/docusealco/docuseal-angular) or [JavaScript](https://www.docuseal.com/docs/embedded))
- Embedded document form builder ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue), [Angular](https://github.com/docusealco/docuseal-angular) or [JavaScript](https://www.docuseal.co/docs/embedded)) - Embedded document form builder ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue), [Angular](https://github.com/docusealco/docuseal-angular) or [JavaScript](https://www.docuseal.com/docs/embedded))
- [Learn more](https://www.docuseal.co/pricing) - [Learn more](https://www.docuseal.com/pricing)
## Deploy ## Deploy
@@ -93,7 +93,7 @@ sudo HOST=your-domain-name.com docker compose up
At DocuSeal we have expertise and technologies to make documents creation, filling, signing and processing seamlessly integrated with your product. We specialize in working with various industries, including **Banking, Healthcare, Transport, Real Estate, eCommerce, KYC, CRM, and other software products** that require bulk document signing. By leveraging DocuSeal, we can assist in reducing the overall cost of developing and processing electronic documents while ensuring security and compliance with local electronic document laws. At DocuSeal we have expertise and technologies to make documents creation, filling, signing and processing seamlessly integrated with your product. We specialize in working with various industries, including **Banking, Healthcare, Transport, Real Estate, eCommerce, KYC, CRM, and other software products** that require bulk document signing. By leveraging DocuSeal, we can assist in reducing the overall cost of developing and processing electronic documents while ensuring security and compliance with local electronic document laws.
[Book a Meeting](https://www.docuseal.co/contact) [Book a Meeting](https://www.docuseal.com/contact)
## License ## License
@@ -102,6 +102,6 @@ Unless otherwise noted, all files © 2023 DocuSeal LLC.
## Tools ## Tools
- [Signature Maker](https://www.docuseal.co/online-signature) - [Signature Maker](https://www.docuseal.com/online-signature)
- [Sign Document Online](https://www.docuseal.co/sign-documents-online) - [Sign Document Online](https://www.docuseal.com/sign-documents-online)
- [Fill PDF Online](https://www.docuseal.co/fill-pdf) - [Fill PDF Online](https://www.docuseal.com/fill-pdf)
@@ -17,7 +17,8 @@ class AccountConfigsController < ApplicationController
AccountConfig::FLATTEN_RESULT_PDF_KEY, AccountConfig::FLATTEN_RESULT_PDF_KEY,
AccountConfig::WITH_SIGNATURE_ID, AccountConfig::WITH_SIGNATURE_ID,
AccountConfig::COMBINE_PDF_RESULT_KEY, AccountConfig::COMBINE_PDF_RESULT_KEY,
AccountConfig::REQUIRE_SIGNING_REASON_KEY AccountConfig::REQUIRE_SIGNING_REASON_KEY,
AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY
].freeze ].freeze
InvalidKey = Class.new(StandardError) InvalidKey = Class.new(StandardError)
+13 -5
View File
@@ -67,15 +67,20 @@ module Api
submissions = create_submissions(@template, params) submissions = create_submissions(@template, params)
submissions.each do |submission| WebhookUrls.for_account_id(@template.account_id, 'submission.created').each do |webhook_url|
SendSubmissionCreatedWebhookRequestJob.perform_async({ 'submission_id' => submission.id }) submissions.each do |submission|
SendSubmissionCreatedWebhookRequestJob.perform_async('submission_id' => submission.id,
'webhook_url_id' => webhook_url.id)
end
end end
Submissions.send_signature_requests(submissions) Submissions.send_signature_requests(submissions)
submissions.each do |submission| submissions.each do |submission|
submission.submitters.each do |submitter| submission.submitters.each do |submitter|
ProcessSubmitterCompletionJob.perform_async({ 'submitter_id' => submitter.id }) if submitter.completed_at? next unless submitter.completed_at?
ProcessSubmitterCompletionJob.perform_async('submitter_id' => submitter.id, 'send_invitation_email' => false)
end end
end end
@@ -88,12 +93,15 @@ module Api
end end
def destroy def destroy
if params[:permanently] == 'true' if params[:permanently].in?(['true', true])
@submission.destroy! @submission.destroy!
else else
@submission.update!(archived_at: Time.current) @submission.update!(archived_at: Time.current)
SendSubmissionArchivedWebhookRequestJob.perform_async('submission_id' => @submission.id) WebhookUrls.for_account_id(@submission.account_id, 'submission.archived').each do |webhook_url|
SendSubmissionArchivedWebhookRequestJob.perform_async('submission_id' => @submission.id,
'webhook_url_id' => webhook_url.id)
end
end end
render json: @submission.as_json(only: %i[id archived_at]) render json: @submission.as_json(only: %i[id archived_at])
@@ -13,7 +13,10 @@ module Api
SubmissionEvents.create_with_tracking_data(submitter, 'view_form', request) SubmissionEvents.create_with_tracking_data(submitter, 'view_form', request)
SendFormViewedWebhookRequestJob.perform_async({ 'submitter_id' => submitter.id }) WebhookUrls.for_account_id(submitter.account_id, 'form.viewed').each do |webhook_url|
SendFormViewedWebhookRequestJob.perform_async('submitter_id' => submitter.id,
'webhook_url_id' => webhook_url.id)
end
render json: {} render json: {}
end end
+1 -1
View File
@@ -68,7 +68,7 @@ module Api
end end
if @submitter.completed_at? if @submitter.completed_at?
ProcessSubmitterCompletionJob.perform_async({ 'submitter_id' => @submitter.id }) ProcessSubmitterCompletionJob.perform_async('submitter_id' => @submitter.id)
elsif normalized_params[:send_email] || normalized_params[:send_sms] elsif normalized_params[:send_email] || normalized_params[:send_sms]
Submitters.send_signature_requests([@submitter]) Submitters.send_signature_requests([@submitter])
end end
@@ -23,9 +23,14 @@ module Api
cloned_template.source = :api cloned_template.source = :api
cloned_template.save! cloned_template.save!
schema_documents = Templates::CloneAttachments.call(template: cloned_template, original_template: @template) schema_documents = Templates::CloneAttachments.call(template: cloned_template,
original_template: @template,
documents: params[:documents])
SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => cloned_template.id) WebhookUrls.for_account_id(cloned_template.account_id, 'template.created').each do |webhook_url|
SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => cloned_template.id,
'webhook_url_id' => webhook_url.id)
end
render json: Templates::SerializeForApi.call(cloned_template, schema_documents) render json: Templates::SerializeForApi.call(cloned_template, schema_documents)
end end
+8 -5
View File
@@ -65,13 +65,16 @@ module Api
@template.update!(template_params) @template.update!(template_params)
SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => @template.id) WebhookUrls.for_account_id(@template.account_id, 'template.updated').each do |webhook_url|
SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => @template.id,
'webhook_url_id' => webhook_url.id)
end
render json: @template.as_json(only: %i[id updated_at]) render json: @template.as_json(only: %i[id updated_at])
end end
def destroy def destroy
if params[:permanently] == 'true' if params[:permanently].in?(['true', true])
@template.destroy! @template.destroy!
else else
@template.update!(archived_at: Time.current) @template.update!(archived_at: Time.current)
@@ -84,7 +87,7 @@ module Api
def filter_templates(templates, params) def filter_templates(templates, params)
templates = Templates.search(templates, params[:q]) templates = Templates.search(templates, params[:q])
templates = params[:archived] ? templates.archived : templates.active templates = params[:archived].in?(['true', true]) ? templates.archived : templates.active
templates = templates.where(external_id: params[:application_key]) if params[:application_key].present? templates = templates.where(external_id: params[:application_key]) if params[:application_key].present?
templates = templates.where(external_id: params[:external_id]) if params[:external_id].present? templates = templates.where(external_id: params[:external_id]) if params[:external_id].present?
templates = templates.joins(:folder).where(folder: { name: params[:folder] }) if params[:folder].present? templates = templates.joins(:folder).where(folder: { name: params[:folder] }) if params[:folder].present?
@@ -97,12 +100,12 @@ module Api
:name, :name,
:external_id, :external_id,
{ {
submitters: [%i[name uuid is_requester invite_by_uuid linked_to_uuid email]], submitters: [%i[name uuid is_requester invite_by_uuid optional_invite_by_uuid linked_to_uuid email]],
fields: [[:uuid, :submitter_uuid, :name, :type, fields: [[:uuid, :submitter_uuid, :name, :type,
:required, :readonly, :default_value, :required, :readonly, :default_value,
:title, :description, :title, :description,
{ preferences: {}, { preferences: {},
conditions: [%i[field_uuid value action]], conditions: [%i[field_uuid value action operation]],
options: [%i[value uuid]], options: [%i[value uuid]],
validation: %i[message pattern], validation: %i[message pattern],
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]] areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]]
+11
View File
@@ -15,6 +15,7 @@ class ApplicationController < ActionController::Base
helper_method :button_title, helper_method :button_title,
:current_account, :current_account,
:form_link_host,
:svg_icon :svg_icon
impersonates :user, with: ->(uuid) { User.find_by(uuid:) } impersonates :user, with: ->(uuid) { User.find_by(uuid:) }
@@ -104,4 +105,14 @@ class ApplicationController < ActionController::Base
def svg_icon(icon_name, class: '') def svg_icon(icon_name, class: '')
render_to_string(partial: "icons/#{icon_name}", locals: { class: }) render_to_string(partial: "icons/#{icon_name}", locals: { class: })
end end
def form_link_host
Docuseal.default_url_options[:host]
end
def maybe_redirect_com
return if request.domain != 'docuseal.co'
redirect_to request.url.gsub('.co/', '.com/'), allow_other_host: true, status: :moved_permanently
end
end end
@@ -6,7 +6,7 @@ class ConsoleRedirectController < ApplicationController
def index def index
if request.path == '/upgrade' if request.path == '/upgrade'
params[:redir] = Docuseal.multitenant? ? "#{Docuseal::CONSOLE_URL}/plans" : "#{Docuseal::CONSOLE_URL}/on_premise" params[:redir] = Docuseal.multitenant? ? "#{Docuseal::CONSOLE_URL}/plans" : "#{Docuseal::CONSOLE_URL}/on_premises"
end end
params[:redir] = "#{Docuseal::CONSOLE_URL}/manage" if request.path == '/manage' params[:redir] = "#{Docuseal::CONSOLE_URL}/manage" if request.path == '/manage'
+1 -1
View File
@@ -9,7 +9,7 @@ class EmbedScriptsController < ActionController::Metal
<h2>Upgrade to Pro</h2> <h2>Upgrade to Pro</h2>
<p>Unlock embedded components by upgrading to Pro</p> <p>Unlock embedded components by upgrading to Pro</p>
<div style="margin-top: 40px;"> <div style="margin-top: 40px;">
<a href="#{Docuseal::CONSOLE_URL}/on_premise" target="_blank" style="padding: 15px 25px; background-color: #222; color: white; text-decoration: none; border-radius: 5px; font-size: 16px; cursor: pointer;"> <a href="#{Docuseal::CONSOLE_URL}/on_premises" target="_blank" style="padding: 15px 25px; background-color: #222; color: white; text-decoration: none; border-radius: 5px; font-size: 16px; cursor: pointer;">
Learn More Learn More
</a> </a>
</div> </div>
+1 -1
View File
@@ -2,7 +2,7 @@
class ErrorsController < ActionController::Base class ErrorsController < ActionController::Base
ENTERPRISE_FEATURE_MESSAGE = ENTERPRISE_FEATURE_MESSAGE =
'This feature is available in Pro Edition: https://www.docuseal.co/pricing' 'This feature is available in Pro Edition: https://www.docuseal.com/pricing'
ENTERPRISE_PATHS = [ ENTERPRISE_PATHS = [
'/templates/html', '/templates/html',
@@ -11,6 +11,8 @@ class EsignSettingsController < ApplicationController
end end
end end
prepend_before_action :maybe_redirect_com, only: %i[show]
before_action :load_encrypted_config before_action :load_encrypted_config
authorize_resource :encrypted_config, parent: false, only: %i[new create] authorize_resource :encrypted_config, parent: false, only: %i[new create]
authorize_resource :encrypted_config, only: %i[update destroy show] authorize_resource :encrypted_config, only: %i[update destroy show]
@@ -6,7 +6,8 @@ class PersonalizationSettingsController < ApplicationController
AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY,
AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY, AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY,
AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY, AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY,
AccountConfig::FORM_COMPLETED_MESSAGE_KEY AccountConfig::FORM_COMPLETED_MESSAGE_KEY,
*(Docuseal.multitenant? ? [] : [AccountConfig::POLICY_LINKS_KEY])
].freeze ].freeze
InvalidKey = Class.new(StandardError) InvalidKey = Class.new(StandardError)
+5 -1
View File
@@ -7,6 +7,7 @@ class StartFormController < ApplicationController
skip_authorization_check skip_authorization_check
around_action :with_browser_locale, only: %i[show completed] around_action :with_browser_locale, only: %i[show completed]
before_action :maybe_redirect_com, only: %i[show completed]
before_action :load_template before_action :load_template
def show def show
@@ -37,7 +38,10 @@ class StartFormController < ApplicationController
if @submitter.save if @submitter.save
if is_new_record if is_new_record
SendSubmissionCreatedWebhookRequestJob.perform_async({ 'submission_id' => @submitter.submission.id }) WebhookUrls.for_account_id(@submitter.account_id, 'submission.created').each do |webhook_url|
SendSubmissionCreatedWebhookRequestJob.perform_async('submission_id' => @submitter.submission_id,
'webhook_url_id' => webhook_url.id)
end
end end
redirect_to submit_form_path(@submitter.slug) redirect_to submit_form_path(@submitter.slug)
@@ -8,8 +8,18 @@ class SubmissionsArchivedController < ApplicationController
@submissions = @submissions.where.not(archived_at: nil) @submissions = @submissions.where.not(archived_at: nil)
.or(@submissions.where.not(templates: { archived_at: nil })) .or(@submissions.where.not(templates: { archived_at: nil }))
.preload(:created_by_user, template: :author) .preload(:created_by_user, template: :author)
@submissions = Submissions.search(@submissions, params[:q], search_template: true)
@pagy, @submissions = pagy(@submissions.preload(:submitters).order(id: :desc)) @submissions = @submissions.preload(:template_accesses) unless current_user.role.in?(%w[admin superadmin])
@submissions = Submissions.search(@submissions, params[:q], search_template: true)
@submissions = Submissions::Filter.call(@submissions, current_user, params)
@submissions = if params[:completed_at_from].present? || params[:completed_at_to].present?
@submissions.order(Submitter.arel_table[:completed_at].maximum.desc)
else
@submissions.order(id: :desc)
end
@pagy, @submissions = pagy(@submissions.preload(submitters: :start_form_submission_events))
end end
end end
+17 -5
View File
@@ -6,6 +6,8 @@ class SubmissionsController < ApplicationController
load_and_authorize_resource :submission, only: %i[show destroy] load_and_authorize_resource :submission, only: %i[show destroy]
prepend_before_action :maybe_redirect_com, only: %i[show]
def show def show
@submission = Submissions.preload_with_pages(@submission) @submission = Submissions.preload_with_pages(@submission)
@@ -50,9 +52,7 @@ class SubmissionsController < ApplicationController
params: params.merge('send_completed_email' => true)) params: params.merge('send_completed_email' => true))
end end
submissions.each do |submission| enqueue_submission_created_webhooks(@template, submissions)
SendSubmissionCreatedWebhookRequestJob.perform_async({ 'submission_id' => submission.id })
end
Submissions.send_signature_requests(submissions) Submissions.send_signature_requests(submissions)
@@ -61,14 +61,17 @@ class SubmissionsController < ApplicationController
def destroy def destroy
notice = notice =
if params[:permanently].present? if params[:permanently].in?(['true', true])
@submission.destroy! @submission.destroy!
I18n.t('submission_has_been_removed') I18n.t('submission_has_been_removed')
else else
@submission.update!(archived_at: Time.current) @submission.update!(archived_at: Time.current)
SendSubmissionArchivedWebhookRequestJob.perform_async('submission_id' => @submission.id) WebhookUrls.for_account_id(@submission.account_id, 'submission.archived').each do |webhook_url|
SendSubmissionArchivedWebhookRequestJob.perform_async('submission_id' => @submission.id,
'webhook_url_id' => webhook_url.id)
end
I18n.t('submission_has_been_archived') I18n.t('submission_has_been_archived')
end end
@@ -85,6 +88,15 @@ class SubmissionsController < ApplicationController
template.save! template.save!
end end
def enqueue_submission_created_webhooks(template, submissions)
WebhookUrls.for_account_id(template.account_id, 'submission.created').each do |webhook_url|
submissions.each do |submission|
SendSubmissionCreatedWebhookRequestJob.perform_async('submission_id' => submission.id,
'webhook_url_id' => webhook_url.id)
end
end
end
def submissions_params def submissions_params
params.permit(submission: { submitters: [%i[uuid email phone name]] }) params.permit(submission: { submitters: [%i[uuid email phone name]] })
end end
@@ -10,11 +10,17 @@ class SubmissionsDashboardController < ApplicationController
.where(templates: { archived_at: nil }) .where(templates: { archived_at: nil })
.preload(:created_by_user, template: :author) .preload(:created_by_user, template: :author)
@submissions = @submissions.preload(:template_accesses) unless current_user.role.in?(%w[admin superadmin])
@submissions = Submissions.search(@submissions, params[:q], search_template: true) @submissions = Submissions.search(@submissions, params[:q], search_template: true)
@submissions = Submissions::Filter.call(@submissions, current_user, params)
@submissions = @submissions.pending if params[:status] == 'pending' @submissions = if params[:completed_at_from].present? || params[:completed_at_to].present?
@submissions = @submissions.completed if params[:status] == 'completed' @submissions.order(Submitter.arel_table[:completed_at].maximum.desc)
else
@submissions.order(id: :desc)
end
@pagy, @submissions = pagy(@submissions.preload(submitters: :start_form_submission_events).order(id: :desc)) @pagy, @submissions = pagy(@submissions.preload(submitters: :start_form_submission_events))
end end
end end
@@ -53,8 +53,15 @@ class SubmissionsDownloadController < ApplicationController
end end
def build_urls(submitter) def build_urls(submitter)
filename_format = AccountConfig.find_or_initialize_by(account_id: submitter.account_id,
key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value
Submitters.select_attachments_for_download(submitter).map do |attachment| Submitters.select_attachments_for_download(submitter).map do |attachment|
ActiveStorage::Blob.proxy_url(attachment.blob, expires_at: FILES_TTL.from_now.to_i) ActiveStorage::Blob.proxy_url(
attachment.blob,
expires_at: FILES_TTL.from_now.to_i,
filename: Submitters.build_document_filename(submitter, attachment.blob, filename_format)
)
end end
end end
@@ -65,6 +72,13 @@ class SubmissionsDownloadController < ApplicationController
attachment = submitter.submission.combined_document_attachment attachment = submitter.submission.combined_document_attachment
attachment ||= Submissions::GenerateCombinedAttachment.call(submitter) attachment ||= Submissions::GenerateCombinedAttachment.call(submitter)
ActiveStorage::Blob.proxy_url(attachment.blob, expires_at: FILES_TTL.from_now.to_i) filename_format = AccountConfig.find_or_initialize_by(account_id: submitter.account_id,
key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value
ActiveStorage::Blob.proxy_url(
attachment.blob,
expires_at: FILES_TTL.from_now.to_i,
filename: Submitters.build_document_filename(submitter, attachment.blob, filename_format)
)
end end
end end
@@ -0,0 +1,18 @@
# frozen_string_literal: true
class SubmissionsFiltersController < ApplicationController
ALLOWED_NAMES = %w[
author
completed_at
status
created_at
].freeze
skip_authorization_check
def show
return head :not_found unless ALLOWED_NAMES.include?(params[:name])
render params[:name]
end
end
@@ -4,6 +4,8 @@ class SubmissionsPreviewController < ApplicationController
skip_before_action :authenticate_user! skip_before_action :authenticate_user!
skip_authorization_check skip_authorization_check
prepend_before_action :maybe_redirect_com, only: %i[show completed]
TTL = 40.minutes TTL = 40.minutes
def show def show
@@ -18,7 +20,7 @@ class SubmissionsPreviewController < ApplicationController
@submission ||= Submission.find_by!(slug: params[:slug]) @submission ||= Submission.find_by!(slug: params[:slug])
if !@submission.submitters.all?(&:completed_at?) && current_user.blank? if @submission.account.archived_at? || (!@submission.submitters.all?(&:completed_at?) && current_user.blank?)
raise ActionController::RoutingError, I18n.t('not_found') raise ActionController::RoutingError, I18n.t('not_found')
end end
@@ -0,0 +1,11 @@
# frozen_string_literal: true
class SubmissionsUnarchiveController < ApplicationController
load_and_authorize_resource :submission
def create
@submission.update!(archived_at: nil)
redirect_to submission_path(@submission), notice: I18n.t('submission_has_been_unarchived')
end
end
+11 -3
View File
@@ -15,7 +15,9 @@ class SubmitFormController < ApplicationController
submission = @submitter.submission submission = @submitter.submission
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at? return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
return render :archived if submission.template.archived_at? || submission.archived_at? return render :archived if submission.template.archived_at? ||
submission.archived_at? ||
@submitter.account.archived_at?
return render :expired if submission.expired? return render :expired if submission.expired?
return render :declined if @submitter.declined_at? return render :declined if @submitter.declined_at?
return render :awaiting if submission.template.preferences['submitters_order'] == 'preserved' && return render :awaiting if submission.template.preferences['submitters_order'] == 'preserved' &&
@@ -25,8 +27,7 @@ class SubmitFormController < ApplicationController
Submitters::MaybeUpdateDefaultValues.call(@submitter, current_user) Submitters::MaybeUpdateDefaultValues.call(@submitter, current_user)
@attachments_index = ActiveStorage::Attachment.where(record: submission.submitters, name: :attachments) @attachments_index = build_attachments_index(submission)
.preload(:blob).index_by(&:uuid)
@form_configs = Submitters::FormConfigs.call(@submitter, CONFIG_KEYS) @form_configs = Submitters::FormConfigs.call(@submitter, CONFIG_KEYS)
@@ -78,4 +79,11 @@ class SubmitFormController < ApplicationController
end end
def success; end def success; end
private
def build_attachments_index(submission)
ActiveStorage::Attachment.where(record: submission.submitters, name: :attachments)
.preload(:blob).index_by(&:uuid)
end
end end
@@ -21,8 +21,14 @@ class SubmitFormDeclineController < ApplicationController
user = submitter.submission.created_by_user || submitter.template.author user = submitter.submission.created_by_user || submitter.template.author
SubmitterMailer.declined_email(submitter, user).deliver_later! if user.user_configs.find_by(key: UserConfig::RECEIVE_DECLINED_EMAIL)&.value != false
SendFormDeclinedWebhookRequestJob.perform_async('submitter_id' => submitter.id) SubmitterMailer.declined_email(submitter, user).deliver_later!
end
WebhookUrls.for_account_id(submitter.account_id, 'form.declined').each do |webhook_url|
SendFormDeclinedWebhookRequestJob.perform_async('submitter_id' => submitter.id,
'webhook_url_id' => webhook_url.id)
end
redirect_to submit_form_path(submitter.slug) redirect_to submit_form_path(submitter.slug)
end end
@@ -9,13 +9,15 @@ class SubmitFormInviteController < ApplicationController
return head :unprocessable_entity unless can_invite?(submitter) return head :unprocessable_entity unless can_invite?(submitter)
invite_submitters = filter_invite_submitters(submitter) invite_submitters = filter_invite_submitters(submitter, 'invite_by_uuid')
optional_invite_submitters = filter_invite_submitters(submitter, 'optional_invite_by_uuid')
ApplicationRecord.transaction do ApplicationRecord.transaction do
invite_submitters.each do |item| (invite_submitters + optional_invite_submitters).each do |item|
attrs = submitters_attributes.find { |e| e[:uuid] == item['uuid'] } attrs = submitters_attributes.find { |e| e[:uuid] == item['uuid'] }
next unless attrs next unless attrs
next if attrs[:email].blank?
submitter.submission.submitters.create!(**attrs, account_id: submitter.account_id) submitter.submission.submitters.create!(**attrs, account_id: submitter.account_id)
@@ -46,9 +48,9 @@ class SubmitFormInviteController < ApplicationController
!submitter.submission.template.archived_at? !submitter.submission.template.archived_at?
end end
def filter_invite_submitters(submitter) def filter_invite_submitters(submitter, key = 'invite_by_uuid')
(submitter.submission.template_submitters || submitter.submission.template.submitters).select do |s| (submitter.submission.template_submitters || submitter.submission.template.submitters).select do |s|
s['invite_by_uuid'] == submitter.uuid && submitter.submission.submitters.none? { |e| e.uuid == s['uuid'] } s[key] == submitter.uuid && submitter.submission.submitters.none? { |e| e.uuid == s['uuid'] }
end end
end end
@@ -9,7 +9,8 @@ class SubmittersAutocompleteController < ApplicationController
def index def index
submitters = search_submitters(@submitters) submitters = search_submitters(@submitters)
values = submitters.limit(LIMIT).group(SELECT_COLUMNS.join(', ')).pluck(SELECT_COLUMNS.join(', ')) arel_columns = SELECT_COLUMNS.map { |col| Submitter.arel_table[col] }
values = submitters.limit(LIMIT).group(arel_columns).pluck(arel_columns)
attrs = values.map { |row| SELECT_COLUMNS.zip(row).to_h } attrs = values.map { |row| SELECT_COLUMNS.zip(row).to_h }
attrs = attrs.uniq { |e| e[params[:field]] } if params[:field].present? attrs = attrs.uniq { |e| e[params[:field]] } if params[:field].present?
@@ -6,7 +6,7 @@ class SubmittersSendEmailController < ApplicationController
def create def create
if Docuseal.multitenant? && SubmissionEvent.exists?(submitter: @submitter, if Docuseal.multitenant? && SubmissionEvent.exists?(submitter: @submitter,
event_type: 'send_email', event_type: 'send_email',
created_at: 24.hours.ago..Time.current) created_at: 10.hours.ago..Time.current)
Rollbar.warning("Already sent: #{@submitter.id}") if defined?(Rollbar) Rollbar.warning("Already sent: #{@submitter.id}") if defined?(Rollbar)
return redirect_back(fallback_location: submission_path(@submitter.submission), return redirect_back(fallback_location: submission_path(@submitter.submission),
@@ -4,7 +4,8 @@ class TemplateFoldersController < ApplicationController
load_and_authorize_resource :template_folder load_and_authorize_resource :template_folder
def show def show
@templates = @template_folder.templates.active.preload(:author).order(id: :desc) @templates = @template_folder.templates.active.accessible_by(current_ability)
.preload(:author, :template_accesses).order(id: :desc)
@templates = Templates.search(@templates, params[:q]) @templates = Templates.search(@templates, params[:q])
@pagy, @templates = pagy(@templates, limit: 12) @pagy, @templates = pagy(@templates, limit: 12)
@@ -4,7 +4,7 @@ class TemplatesArchivedController < ApplicationController
load_and_authorize_resource :template, parent: false load_and_authorize_resource :template, parent: false
def index def index
@templates = @templates.where.not(archived_at: nil).preload(:author, :folder).order(id: :desc) @templates = @templates.where.not(archived_at: nil).preload(:author, :folder, :template_accesses).order(id: :desc)
@templates = Templates.search(@templates, params[:q]) @templates = Templates.search(@templates, params[:q])
@pagy, @templates = pagy(@templates, limit: 12) @pagy, @templates = pagy(@templates, limit: 12)
@@ -7,8 +7,15 @@ class TemplatesArchivedSubmissionsController < ApplicationController
def index def index
@submissions = @submissions.where.not(archived_at: nil) @submissions = @submissions.where.not(archived_at: nil)
@submissions = Submissions.search(@submissions, params[:q], search_values: true) @submissions = Submissions.search(@submissions, params[:q], search_values: true)
@submissions = Submissions::Filter.call(@submissions, current_user, params)
@pagy, @submissions = pagy(@submissions.preload(:submitters).order(id: :desc)) @submissions = if params[:completed_at_from].present? || params[:completed_at_to].present?
@submissions.order(Submitter.arel_table[:completed_at].maximum.desc)
else
@submissions.order(id: :desc)
end
@pagy, @submissions = pagy(@submissions.preload(submitters: :start_form_submission_events))
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound
redirect_to root_path redirect_to root_path
end end
+31 -9
View File
@@ -9,13 +9,21 @@ class TemplatesController < ApplicationController
submissions = @template.submissions.accessible_by(current_ability) submissions = @template.submissions.accessible_by(current_ability)
submissions = submissions.active if @template.archived_at.blank? submissions = submissions.active if @template.archived_at.blank?
submissions = Submissions.search(submissions, params[:q], search_values: true) submissions = Submissions.search(submissions, params[:q], search_values: true)
submissions = Submissions::Filter.call(submissions, current_user, params.except(:status))
@base_submissions = submissions @base_submissions = submissions
submissions = submissions.pending if params[:status] == 'pending' submissions = Submissions::Filter.filter_by_status(submissions, params)
submissions = submissions.completed if params[:status] == 'completed'
@pagy, @submissions = pagy(submissions.preload(submitters: :start_form_submission_events).order(id: :desc)) submissions = if params[:completed_at_from].present? || params[:completed_at_to].present?
submissions.order(Submitter.arel_table[:completed_at].maximum.desc)
else
submissions.order(id: :desc)
end
submissions = submissions.preload(:template_accesses) unless current_user.role.in?(%w[admin superadmin])
@pagy, @submissions = pagy(submissions.preload(submitters: :start_form_submission_events))
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound
redirect_to root_path redirect_to root_path
end end
@@ -66,7 +74,7 @@ class TemplatesController < ApplicationController
if @template.save if @template.save
Templates::CloneAttachments.call(template: @template, original_template: @base_template) if @base_template Templates::CloneAttachments.call(template: @template, original_template: @base_template) if @base_template
SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => @template.id) enqueue_template_created_webhooks(@template)
maybe_redirect_to_template(@template) maybe_redirect_to_template(@template)
else else
@@ -77,14 +85,14 @@ class TemplatesController < ApplicationController
def update def update
@template.update!(template_params) @template.update!(template_params)
SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => @template.id) enqueue_template_updated_webhooks(@template)
head :ok head :ok
end end
def destroy def destroy
notice = notice =
if params[:permanently].present? if params[:permanently].in?(['true', true])
@template.destroy! @template.destroy!
I18n.t('template_has_been_removed') I18n.t('template_has_been_removed')
@@ -102,13 +110,13 @@ class TemplatesController < ApplicationController
def template_params def template_params
params.require(:template).permit( params.require(:template).permit(
:name, :name,
{ schema: [%i[attachment_uuid name]], { schema: [[:attachment_uuid, :name, { conditions: [%i[field_uuid value action operation]] }]],
submitters: [%i[name uuid is_requester linked_to_uuid invite_by_uuid email]], submitters: [%i[name uuid is_requester linked_to_uuid invite_by_uuid optional_invite_by_uuid email]],
fields: [[:uuid, :submitter_uuid, :name, :type, fields: [[:uuid, :submitter_uuid, :name, :type,
:required, :readonly, :default_value, :required, :readonly, :default_value,
:title, :description, :title, :description,
{ preferences: {}, { preferences: {},
conditions: [%i[field_uuid value action]], conditions: [%i[field_uuid value action operation]],
options: [%i[value uuid]], options: [%i[value uuid]],
validation: %i[message pattern], validation: %i[message pattern],
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]] } areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]] }
@@ -128,6 +136,20 @@ class TemplatesController < ApplicationController
end end
end end
def enqueue_template_created_webhooks(template)
WebhookUrls.for_account_id(template.account_id, 'template.created').each do |webhook_url|
SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => template.id,
'webhook_url_id' => webhook_url.id)
end
end
def enqueue_template_updated_webhooks(template)
WebhookUrls.for_account_id(template.account_id, 'template.updated').each do |webhook_url|
SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => template.id,
'webhook_url_id' => webhook_url.id)
end
end
def load_base_template def load_base_template
return if params[:base_template_id].blank? return if params[:base_template_id].blank?
@@ -9,7 +9,9 @@ class TemplatesDashboardController < ApplicationController
FOLDERS_PER_PAGE = 18 FOLDERS_PER_PAGE = 18
def index def index
@template_folders = filter_template_folders(@template_folders) @template_folders = @template_folders.where(id: @templates.active.select(:folder_id)).order(id: :desc)
@template_folders = TemplateFolders.search(@template_folders, params[:q])
@pagy, @template_folders = pagy( @pagy, @template_folders = pagy(
@template_folders, @template_folders,
@@ -36,16 +38,8 @@ class TemplatesDashboardController < ApplicationController
private private
def filter_template_folders(template_folders)
rel = template_folders.joins(:active_templates)
.order(id: :desc)
.distinct
TemplateFolders.search(rel, params[:q])
end
def filter_templates(templates) def filter_templates(templates)
rel = templates.active.preload(:author).order(id: :desc) rel = templates.active.preload(:author, :template_accesses).order(id: :desc)
if params[:q].blank? if params[:q].blank?
if Docuseal.multitenant? && !current_account.testing? if Docuseal.multitenant? && !current_account.testing?
@@ -22,6 +22,7 @@ class TemplatesPreferencesController < ApplicationController
preferences: %i[bcc_completed request_email_subject request_email_body preferences: %i[bcc_completed request_email_subject request_email_body
documents_copy_email_subject documents_copy_email_body documents_copy_email_subject documents_copy_email_body
documents_copy_email_enabled documents_copy_email_attach_audit documents_copy_email_enabled documents_copy_email_attach_audit
documents_copy_email_attach_documents
completed_notification_email_attach_documents completed_notification_email_attach_documents
completed_redirect_url completed_redirect_url
submitters_order submitters_order
@@ -17,38 +17,47 @@ class TemplatesRecipientsController < ApplicationController
private private
def submitters_params def submitters_params
params.require(:template).permit( permit_params = { submitters: [%i[name uuid is_requester optional_invite_by_uuid
submitters: [%i[name uuid is_requester invite_by_uuid linked_to_uuid email option]] invite_by_uuid linked_to_uuid email option]] }
).fetch(:submitters, {}).values.filter_map do |s|
params.require(:template).permit(permit_params).fetch(:submitters, {}).values.filter_map do |s|
next if s[:uuid].blank? next if s[:uuid].blank?
if s[:is_requester] == '1' && s[:invite_by_uuid].blank? if s[:is_requester] == '1' && s[:invite_by_uuid].blank? && s[:optional_invite_by_uuid].blank?
s[:is_requester] = true s[:is_requester] = true
else else
s.delete(:is_requester) s.delete(:is_requester)
end end
s.delete(:invite_by_uuid) if s[:invite_by_uuid].blank? s.delete(:invite_by_uuid) if s[:invite_by_uuid].blank?
s.delete(:optional_invite_by_uuid) if s[:optional_invite_by_uuid].blank?
option = s.delete(:option) normalize_option_value(s)
if option.present?
case option
when 'is_requester'
s[:is_requester] = true
when 'not_set'
s.delete(:is_requester)
s.delete(:email)
s.delete(:linked_to_uuid)
s.delete(:invite_by_uuid)
when /\Alinked_to_(.*)\z/
s[:linked_to_uuid] = ::Regexp.last_match(-1)
when /\Ainvite_by_(.*)\z/
s[:invite_by_uuid] = ::Regexp.last_match(-1)
end
end
s
end end
end end
def normalize_option_value(attrs)
option = attrs.delete(:option)
if option.present?
case option
when 'is_requester'
attrs[:is_requester] = true
when 'not_set'
attrs.delete(:is_requester)
attrs.delete(:email)
attrs.delete(:linked_to_uuid)
attrs.delete(:invite_by_uuid)
attrs.delete(:optional_invite_by_uuid)
when /\Alinked_to_(.*)\z/
attrs[:linked_to_uuid] = ::Regexp.last_match(-1)
when /\Aoptional_invite_by_(.*)\z/
attrs[:optional_invite_by_uuid] = ::Regexp.last_match(-1)
when /\Ainvite_by_(.*)\z/
attrs[:invite_by_uuid] = ::Regexp.last_match(-1)
end
end
attrs
end
end end
@@ -23,7 +23,7 @@ class TemplatesUploadsController < ApplicationController
@template.update!(schema:) @template.update!(schema:)
SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => @template.id) enqueue_template_created_webhooks(@template)
redirect_to edit_template_path(@template) redirect_to edit_template_path(@template)
rescue Templates::CreateAttachments::PdfEncrypted rescue Templates::CreateAttachments::PdfEncrypted
@@ -55,14 +55,22 @@ class TemplatesUploadsController < ApplicationController
tempfile.write(DownloadUtils.call(params[:url]).body) tempfile.write(DownloadUtils.call(params[:url]).body)
tempfile.rewind tempfile.rewind
filename = URI.decode_www_form_component(params[:filename]) if params[:filename].present?
filename ||= File.basename(URI.decode_www_form_component(params[:url]))
file = ActionDispatch::Http::UploadedFile.new( file = ActionDispatch::Http::UploadedFile.new(
tempfile:, tempfile:,
filename: File.basename( filename:,
URI.decode_www_form_component(params[:filename].presence || params[:url]), '.*'
),
type: Marcel::MimeType.for(tempfile) type: Marcel::MimeType.for(tempfile)
) )
{ files: [file] } { files: [file] }
end end
def enqueue_template_created_webhooks(template)
WebhookUrls.for_account_id(template.account_id, 'template.created').each do |webhook_url|
SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => template.id,
'webhook_url_id' => webhook_url.id)
end
end
end end
@@ -4,9 +4,8 @@ class TestingApiSettingsController < ApplicationController
def index def index
authorize!(:manage, current_user.access_token) authorize!(:manage, current_user.access_token)
@webhook_config = @webhook_url = current_account.webhook_urls.first_or_initialize
current_account.encrypted_configs.find_or_initialize_by(key: EncryptedConfig::WEBHOOK_URL_KEY)
authorize!(:manage, @webhook_config) authorize!(:manage, @webhook_url)
end end
end end
+6 -15
View File
@@ -24,16 +24,14 @@ class UsersController < ApplicationController
def edit; end def edit; end
def create def create
existing_user = User.accessible_by(current_ability).find_by(email: @user.email) if User.accessible_by(current_ability).exists?(email: @user.email)
@user.errors.add(:email, I18n.t('already_exists'))
if existing_user return render turbo_stream: turbo_stream.replace(:modal, template: 'users/new'), status: :unprocessable_entity
existing_user.archived_at = nil
existing_user.assign_attributes(user_params)
existing_user.account = current_account
@user = existing_user
end end
@user.role = User::ADMIN_ROLE unless role_valid?(@user.role)
if @user.save if @user.save
UserMailer.invitation_email(@user).deliver_later! UserMailer.invitation_email(@user).deliver_later!
@@ -81,14 +79,7 @@ class UsersController < ApplicationController
end end
def build_user def build_user
@user = current_account.users.find_by(email: user_params[:email])&.tap do |user| @user = current_account.users.new(user_params)
user.assign_attributes(user_params)
user.archived_at = nil
end
@user ||= current_account.users.new(user_params)
@user
end end
def user_params def user_params
@@ -1,37 +1,22 @@
# frozen_string_literal: true # frozen_string_literal: true
class WebhookPreferencesController < ApplicationController class WebhookPreferencesController < ApplicationController
EVENTS = %w[ load_and_authorize_resource :webhook_url, parent: false
form.viewed
form.started
form.completed
form.declined
template.created
template.updated
submission.created
submission.archived
].freeze
before_action :load_account_config def update
authorize_resource :account_config, parent: false webhook_preferences_params[:events].each do |event, val|
@webhook_url.events.delete(event) if val == '0'
@webhook_url.events.push(event) if val == '1' && @webhook_url.events.exclude?(event)
end
def create @webhook_url.save!
@account_config.value[account_config_params[:event]] = account_config_params[:value] == '1'
@account_config.save!
head :ok head :ok
end end
private private
def load_account_config def webhook_preferences_params
@account_config = params.require(:webhook_url).permit(events: {})
current_account.account_configs.find_or_initialize_by(key: AccountConfig::WEBHOOK_PREFERENCES_KEY)
@account_config.value ||= {}
end
def account_config_params
params.permit(:event, :value)
end end
end end
+9 -16
View File
@@ -1,29 +1,22 @@
# frozen_string_literal: true # frozen_string_literal: true
class WebhookSecretController < ApplicationController class WebhookSecretController < ApplicationController
before_action :load_encrypted_config load_and_authorize_resource :webhook_url, parent: false
authorize_resource :encrypted_config, parent: false
def index; end def show; end
def create def update
@encrypted_config.assign_attributes(value: { @webhook_url.update!(secret: {
encrypted_config_params[:key] => encrypted_config_params[:value] webhook_secret_params[:key] => webhook_secret_params[:value]
}.compact_blank) }.compact_blank)
@encrypted_config.value.present? ? @encrypted_config.save! : @encrypted_config.delete redirect_back(fallback_location: settings_webhook_path(@webhook_url),
notice: I18n.t('webhook_secret_has_been_saved'))
redirect_back(fallback_location: settings_webhooks_path, notice: I18n.t('webhook_secret_has_been_saved'))
end end
private private
def load_encrypted_config def webhook_secret_params
@encrypted_config = params.require(:webhook_url).permit(secret: %i[key value]).fetch(:secret, {})
current_account.encrypted_configs.find_or_initialize_by(key: EncryptedConfig::WEBHOOK_SECRET_KEY)
end
def encrypted_config_params
params.require(:encrypted_config).permit(value: %i[key value]).fetch(:value, {})
end end
end end
+37 -13
View File
@@ -1,36 +1,60 @@
# frozen_string_literal: true # frozen_string_literal: true
class WebhookSettingsController < ApplicationController class WebhookSettingsController < ApplicationController
before_action :load_encrypted_config load_and_authorize_resource :webhook_url, parent: false, only: %i[index show new create update destroy]
authorize_resource :encrypted_config, parent: false load_and_authorize_resource :webhook_url, only: %i[resend], id_param: :webhook_id
def index
@webhook_urls = @webhook_urls.order(id: :desc)
@webhook_url = @webhook_urls.first_or_initialize
render @webhook_urls.size > 1 ? 'index' : 'show'
end
def show; end def show; end
def new; end
def create def create
@encrypted_config.assign_attributes(encrypted_config_params) @webhook_url.save!
@encrypted_config.value.present? ? @encrypted_config.save! : @encrypted_config.delete redirect_to settings_webhooks_path, notice: I18n.t('webhook_url_has_been_saved')
redirect_back(fallback_location: settings_webhooks_path, notice: I18n.t('webhook_url_has_been_saved'))
end end
def update def update
@webhook_url.update!(update_params)
redirect_back(fallback_location: settings_webhook_path(@webhook_url),
notice: I18n.t('webhook_url_has_been_updated'))
end
def destroy
@webhook_url.destroy!
redirect_to settings_webhooks_path, notice: I18n.t('webhook_url_has_been_deleted')
end
def resend
submitter = current_account.submitters.where.not(completed_at: nil).order(:id).last submitter = current_account.submitters.where.not(completed_at: nil).order(:id).last
SendFormCompletedWebhookRequestJob.perform_async({ 'submitter_id' => submitter.id, if submitter.blank? || @webhook_url.blank?
'encrypted_config_id' => @encrypted_config.id }) return redirect_back(fallback_location: settings_webhooks_path,
alert: I18n.t('unable_to_resend_webhook_request'))
end
SendFormCompletedWebhookRequestJob.perform_async('submitter_id' => submitter.id,
'webhook_url_id' => @webhook_url.id)
redirect_back(fallback_location: settings_webhooks_path, notice: I18n.t('webhook_request_has_been_sent')) redirect_back(fallback_location: settings_webhooks_path, notice: I18n.t('webhook_request_has_been_sent'))
end end
private private
def load_encrypted_config def create_params
@encrypted_config = params.require(:webhook_url).permit(:url, events: []).reverse_merge(events: [])
current_account.encrypted_configs.find_or_initialize_by(key: EncryptedConfig::WEBHOOK_URL_KEY)
end end
def encrypted_config_params def update_params
params.require(:encrypted_config).permit(:value) params.require(:webhook_url).permit(:url)
end end
end end
+5
View File
@@ -30,6 +30,8 @@ import ToggleAttribute from './elements/toggle_attribute'
import LinkedInput from './elements/linked_input' import LinkedInput from './elements/linked_input'
import CheckboxGroup from './elements/checkbox_group' import CheckboxGroup from './elements/checkbox_group'
import MaskedInput from './elements/masked_input' import MaskedInput from './elements/masked_input'
import SetDateButton from './elements/set_date_button'
import IndeterminateCheckbox from './elements/indeterminate_checkbox'
import * as TurboInstantClick from './lib/turbo_instant_click' import * as TurboInstantClick from './lib/turbo_instant_click'
@@ -97,6 +99,8 @@ safeRegisterElement('toggle-attribute', ToggleAttribute)
safeRegisterElement('linked-input', LinkedInput) safeRegisterElement('linked-input', LinkedInput)
safeRegisterElement('checkbox-group', CheckboxGroup) safeRegisterElement('checkbox-group', CheckboxGroup)
safeRegisterElement('masked-input', MaskedInput) safeRegisterElement('masked-input', MaskedInput)
safeRegisterElement('set-date-button', SetDateButton)
safeRegisterElement('indeterminate-checkbox', IndeterminateCheckbox)
safeRegisterElement('template-builder', class extends HTMLElement { safeRegisterElement('template-builder', class extends HTMLElement {
connectedCallback () { connectedCallback () {
@@ -111,6 +115,7 @@ safeRegisterElement('template-builder', class extends HTMLElement {
backgroundColor: '#faf7f5', backgroundColor: '#faf7f5',
locale: this.dataset.locale, locale: this.dataset.locale,
withPhone: this.dataset.withPhone === 'true', withPhone: this.dataset.withPhone === 'true',
withVerification: ['true', 'false'].includes(this.dataset.withVerification) ? this.dataset.withVerification === 'true' : null,
withLogo: this.dataset.withLogo !== 'false', withLogo: this.dataset.withLogo !== 'false',
editable: this.dataset.editable !== 'false', editable: this.dataset.editable !== 'false',
authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content, authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content,
+17
View File
@@ -97,6 +97,23 @@ button[disabled] .enabled {
bottom: auto; bottom: auto;
} }
.tooltip-bottom-start:before {
transform: translateX(-31%);
top: var(--tooltip-offset);
left: 100%;
right: auto;
bottom: auto;
}
.tooltip-bottom-start:after {
transform: translateX(-25%);
border-color: transparent transparent var(--tooltip-color) transparent;
top: var(--tooltip-tail-offset);
left: 50%;
right: auto;
bottom: auto;
}
.autocomplete { .autocomplete {
background: white; background: white;
z-index: 1000; z-index: 1000;
+4
View File
@@ -12,6 +12,10 @@ window.customElements.define('draw-signature', class extends HTMLElement {
this.pad = new SignaturePad(this.canvas) this.pad = new SignaturePad(this.canvas)
if (this.dataset.color) {
this.pad.penColor = this.dataset.color
}
this.pad.addEventListener('endStroke', () => { this.pad.addEventListener('endStroke', () => {
this.updateSubmitButtonVisibility() this.updateSubmitButtonVisibility()
}) })
@@ -0,0 +1,35 @@
export default class extends HTMLElement {
connectedCallback () {
if (this.dataset.indeterminate === 'true') {
this.checkbox.indeterminate = true
this.checkbox.readOnly = true
}
this.checkbox.addEventListener('click', () => {
this.checkbox.setAttribute('name', this.dataset.name)
if (this.showIndeterminateEl) {
this.showIndeterminateEl.classList.add('hidden')
}
if (this.checkbox.readOnly) {
this.checkbox.checked = this.checkbox.readOnly = false
} else if (!this.checkbox.checked) {
if (this.showIndeterminateEl) {
this.showIndeterminateEl.classList.remove('hidden')
}
this.checkbox.setAttribute('name', this.dataset.indeterminateName)
this.checkbox.checked = this.checkbox.readOnly = this.checkbox.indeterminate = true
}
})
}
get checkbox () {
return this.querySelector('input[type="checkbox"]')
}
get showIndeterminateEl () {
return document.getElementById(this.dataset.showIndeterminateId)
}
}
@@ -0,0 +1,20 @@
export default class extends HTMLElement {
connectedCallback () {
this.button.addEventListener('click', () => {
this.fromInput.value = this.dataset.fromValue || ''
this.toInput.value = this.dataset.toValue || ''
})
}
get button () {
return this.querySelector('button')
}
get fromInput () {
return document.getElementById(this.dataset.fromId)
}
get toInput () {
return document.getElementById(this.dataset.toId)
}
}
+2
View File
@@ -15,6 +15,8 @@ safeRegisterElement('submission-form', class extends HTMLElement {
this.app = createApp(Form, { this.app = createApp(Form, {
submitter: JSON.parse(this.dataset.submitter), submitter: JSON.parse(this.dataset.submitter),
inviteSubmitters: JSON.parse(this.dataset.inviteSubmitters), inviteSubmitters: JSON.parse(this.dataset.inviteSubmitters),
optionalInviteSubmitters: JSON.parse(this.dataset.optionalInviteSubmitters),
schema: JSON.parse(this.dataset.schema),
canSendEmail: this.dataset.canSendEmail === 'true', canSendEmail: this.dataset.canSendEmail === 'true',
previousSignatureValue: this.dataset.previousSignatureValue, previousSignatureValue: this.dataset.previousSignatureValue,
goToLast: this.dataset.goToLast === 'true', goToLast: this.dataset.goToLast === 'true',
+13 -7
View File
@@ -39,7 +39,6 @@
</template> </template>
</div> </div>
<div <div
v-if="isActive"
ref="scrollToElem" ref="scrollToElem"
class="absolute" class="absolute"
:style="{ top: scrollPadding }" :style="{ top: scrollPadding }"
@@ -75,7 +74,7 @@
ID: {{ signature.uuid }} ID: {{ signature.uuid }}
</div> </div>
<div> <div>
{{ t('reason') }}: {{ values[field.preferences?.reason_field_uuid] || t('digitally_signed_by') }} {{ submitter.name }} <span v-if="values[field.preferences?.reason_field_uuid]">{{ t('reason') }}: </span>{{ values[field.preferences?.reason_field_uuid] || t('digitally_signed_by') }} {{ submitter.name }}
<template v-if="submitter.email"> <template v-if="submitter.email">
&lt;{{ submitter.email }}&gt; &lt;{{ submitter.email }}&gt;
</template> </template>
@@ -169,6 +168,7 @@
<div <div
v-else-if="field.type === 'cells'" v-else-if="field.type === 'cells'"
class="w-full flex items-center" class="w-full flex items-center"
:class="{ 'justify-end': field.preferences?.align === 'right' }"
> >
<div <div
v-for="(char, index) in modelValue" v-for="(char, index) in modelValue"
@@ -210,7 +210,7 @@
</template> </template>
<script> <script>
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconCheck, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp, IconSquareNumber1 } from '@tabler/icons-vue' import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconCheck, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp, IconSquareNumber1, IconId } from '@tabler/icons-vue'
export default { export default {
name: 'FieldArea', name: 'FieldArea',
@@ -312,7 +312,8 @@ export default {
cells: this.t('cells'), cells: this.t('cells'),
stamp: this.t('stamp'), stamp: this.t('stamp'),
payment: this.t('payment'), payment: this.t('payment'),
phone: this.t('phone') phone: this.t('phone'),
verification: this.t('verify_id')
} }
}, },
alignClasses () { alignClasses () {
@@ -341,7 +342,8 @@ export default {
cells: IconColumns3, cells: IconColumns3,
multiple: IconChecks, multiple: IconChecks,
phone: IconPhoneCheck, phone: IconPhoneCheck,
payment: IconCreditCard payment: IconCreditCard,
verification: IconId
} }
}, },
image () { image () {
@@ -405,8 +407,12 @@ export default {
} }
if (this.field.preferences?.font_size) { if (this.field.preferences?.font_size) {
style.fontSize = `clamp(4pt, 1.6vw, ${this.field.preferences.font_size}pt)` style.fontSize = `clamp(4pt, 1.6vw, ${parseInt(this.field.preferences.font_size) * 1.23}pt)`
style.lineHeight = `clamp(6pt, 2.0vw, ${parseInt(this.field.preferences.font_size) + 3}pt)` style.lineHeight = `clamp(6pt, 2.0vw, ${parseInt(this.field.preferences.font_size) * 1.23 + 3}pt)`
}
if (this.field.preferences?.color) {
style.color = this.field.preferences.color
} }
return style return style
+1 -1
View File
@@ -91,7 +91,7 @@
> >
{{ t('powered_by') }} {{ t('powered_by') }}
<a <a
href="https://www.docuseal.co/start" href="https://www.docuseal.com/start"
target="_blank" target="_blank"
class="underline" class="underline"
>DocuSeal</a> - {{ t('open_source_documents_software') }} >DocuSeal</a> - {{ t('open_source_documents_software') }}
@@ -37,6 +37,7 @@
<AppearsOn :field="field" /> <AppearsOn :field="field" />
<div class="text-center"> <div class="text-center">
<input <input
:id="field.uuid"
ref="input" ref="input"
v-model="value" v-model="value"
class="base-input !text-2xl text-center w-full" class="base-input !text-2xl text-center w-full"
@@ -1,5 +1,6 @@
<template> <template>
<div <div
id="dropzone"
class="flex h-32 w-full" class="flex h-32 w-full"
@dragover.prevent @dragover.prevent
@drop.prevent="onDropFiles" @drop.prevent="onDropFiles"
+238 -38
View File
@@ -11,12 +11,13 @@
:with-label="!isAnonymousChecboxes && showFieldNames" :with-label="!isAnonymousChecboxes && showFieldNames"
:current-step="currentStepFields" :current-step="currentStepFields"
:scroll-padding="scrollPadding" :scroll-padding="scrollPadding"
@focus-step="[saveStep(), currentField.type !== 'checkbox' ? isFormVisible = true : '', goToStep($event, false, true)]" @focus-step="[saveStep(), goToStep($event, false, true), currentField.type !== 'checkbox' ? isFormVisible = true : '']"
/> />
<FieldAreas <FieldAreas
:steps="readonlyConditionalFields.map((e) => [e])" :steps="readonlyConditionalFields.map((e) => [e])"
:values="readonlyConditionalFields.reduce((acc, f) => { acc[f.uuid] = f.default_value; return acc }, {})" :values="readonlyConditionalFields.reduce((acc, f) => { acc[f.uuid] = (values[f.uuid] || f.default_value); return acc }, {})"
:submitter="submitter" :submitter="submitter"
:attachments-index="attachmentsIndex"
:submittable="false" :submittable="false"
/> />
<FormulaFieldAreas <FormulaFieldAreas
@@ -24,6 +25,44 @@
:fields="formulaFields" :fields="formulaFields"
:values="values" :values="values"
/> />
<Teleport
v-if="completeButtonToRef"
:to="completeButtonToRef"
>
<span
v-if="(emptyValueRequiredStep && ((stepFields.length - 1) !== currentStep || currentStepFields !== emptyValueRequiredStep)) || isCompleted"
class="tooltip-left"
:class="{ tooltip: !isCompleted }"
:data-tip="t('fill_all_required_fields_to_complete')"
>
<button
class="btn btn-sm btn-neutral text-white px-4 w-full flex justify-center btn-disabled pointer-events-auto"
@click="[isFormVisible = true, !isCompleted && goToStep(stepFields.indexOf(emptyValueRequiredStep), true, false)]"
>
{{ t('complete') }}
</button>
</span>
<button
v-else
id="complete_form_button"
class="btn btn-sm btn-neutral text-white px-4 w-full flex justify-center"
form="steps_form"
type="submit"
name="completed"
value="true"
:disabled="isSubmittingComplete"
>
<span class="flex items-center">
<IconInnerShadowTop
v-if="isSubmittingComplete"
class="mr-1 animate-spin w-5 h-5"
/>
<span>
{{ t('complete') }}
</span>
</span>
</button>
</Teleport>
<button <button
v-if="!isFormVisible" v-if="!isFormVisible"
id="expand_form_button" id="expand_form_button"
@@ -74,6 +113,7 @@
> >
<form <form
v-if="!isCompleted && !isInvite" v-if="!isCompleted && !isInvite"
id="steps_form"
ref="form" ref="form"
:action="submitPath" :action="submitPath"
method="post" method="post"
@@ -409,9 +449,22 @@
@focus="scrollIntoField(currentField)" @focus="scrollIntoField(currentField)"
@submit="!isSubmitting && submitStep()" @submit="!isSubmitting && submitStep()"
/> />
<VerificationStep
v-else-if="currentField.type === 'verification'"
ref="currentStep"
:key="currentField.uuid"
:locale="language?.toLowerCase() || browserLanguage"
:submitter="submitter"
:empty-value-required-step="emptyValueRequiredStep"
:field="currentField"
:submitter-slug="submitterSlug"
:values="values"
@focus="scrollIntoField(currentField)"
@submit="!isSubmitting && submitStep()"
/>
</div> </div>
<div <div
v-if="currentField.type !== 'payment' || submittedValues[currentField.uuid]" v-if="(currentField.type !== 'payment' && currentField.type !== 'verification') || submittedValues[currentField.uuid]"
:class="currentField.type === 'signature' ? 'mt-2' : 'mt-6 md:mt-8'" :class="currentField.type === 'signature' ? 'mt-2' : 'mt-6 md:mt-8'"
> >
<button <button
@@ -445,6 +498,7 @@
<InviteForm <InviteForm
v-else-if="isInvite" v-else-if="isInvite"
:submitters="inviteSubmitters" :submitters="inviteSubmitters"
:optional-submitters="optionalInviteSubmitters"
:submitter-slug="submitterSlug" :submitter-slug="submitterSlug"
:authenticity-token="authenticityToken" :authenticity-token="authenticityToken"
:url="baseUrl + submitPath + '/invite'" :url="baseUrl + submitPath + '/invite'"
@@ -498,6 +552,7 @@ import AttachmentStep from './attachment_step'
import MultiSelectStep from './multi_select_step' import MultiSelectStep from './multi_select_step'
import PhoneStep from './phone_step' import PhoneStep from './phone_step'
import PaymentStep from './payment_step' import PaymentStep from './payment_step'
import VerificationStep from './verification_step'
import TextStep from './text_step' import TextStep from './text_step'
import NumberStep from './number_step' import NumberStep from './number_step'
import DateStep from './date_step' import DateStep from './date_step'
@@ -540,6 +595,7 @@ export default {
IconWritingSign, IconWritingSign,
AttachmentStep, AttachmentStep,
InitialsStep, InitialsStep,
VerificationStep,
InviteForm, InviteForm,
MultiSelectStep, MultiSelectStep,
IconInnerShadowTop, IconInnerShadowTop,
@@ -572,6 +628,11 @@ export default {
required: false, required: false,
default: () => [] default: () => []
}, },
optionalInviteSubmitters: {
type: Array,
required: false,
default: () => []
},
withSignatureId: { withSignatureId: {
type: Boolean, type: Boolean,
required: false, required: false,
@@ -582,6 +643,11 @@ export default {
required: false, required: false,
default: '-80px' default: '-80px'
}, },
orderAsOnPage: {
type: Boolean,
required: false,
default: false
},
requireSigningReason: { requireSigningReason: {
type: Boolean, type: Boolean,
required: false, required: false,
@@ -592,6 +658,16 @@ export default {
required: false, required: false,
default: false default: false
}, },
completeButtonToRef: {
type: Object,
required: false,
default: null
},
schema: {
type: Array,
required: false,
default: () => []
},
attachments: { attachments: {
type: Array, type: Array,
required: false, required: false,
@@ -772,6 +848,7 @@ export default {
phoneVerifiedValues: {}, phoneVerifiedValues: {},
orientation: screen?.orientation?.type, orientation: screen?.orientation?.type,
isSubmitting: false, isSubmitting: false,
isSubmittingComplete: false,
submittedValues: {}, submittedValues: {},
recalculateButtonDisabledKey: '' recalculateButtonDisabledKey: ''
} }
@@ -780,6 +857,28 @@ export default {
isMobile () { isMobile () {
return /android|iphone|ipad/i.test(navigator.userAgent) return /android|iphone|ipad/i.test(navigator.userAgent)
}, },
attachmentConditionsIndex () {
return this.schema.reduce((acc, item) => {
if (item.conditions?.length) {
if (item.conditions.every((c) => this.fieldsUuidIndex[c.field_uuid])) {
acc[item.attachment_uuid] = this.checkFieldConditions(item)
} else {
acc[item.attachment_uuid] = true
}
} else {
acc[item.attachment_uuid] = true
}
return acc
}, {})
},
emptyValueRequiredStep () {
return this.stepFields.find((fields, index) => {
return fields.some((f) => {
return f.required && isEmpty(this.values[f.uuid])
})
})
},
submitButtonText () { submitButtonText () {
if (this.alwaysMinimize) { if (this.alwaysMinimize) {
return this.t('submit') return this.t('submit')
@@ -853,13 +952,61 @@ export default {
return this.currentStepFields[0] return this.currentStepFields[0]
}, },
readonlyConditionalFields () { readonlyConditionalFields () {
return this.fields.filter((f) => f.readonly && f.conditions?.length && this.checkFieldConditions(f)) return this.fields.filter((f) => f.readonly && f.conditions?.length && this.checkFieldConditions(f) && this.checkFieldDocumentsConditions(f))
}, },
stepFields () { stepFields () {
return this.fields.filter((f) => !f.readonly).reduce((acc, f) => { const verificationFields = []
const sortedFields = this.fields.reduce((acc, f) => {
if (f.type === 'verification') {
verificationFields.push(f)
} else if (!f.readonly) {
acc.push(f)
}
return acc
}, [])
if (this.orderAsOnPage) {
const fieldAreasIndex = {}
const attachmentUuids = Object.keys(this.attachmentConditionsIndex)
const sortArea = (aArea, bArea) => {
if (aArea.attachment_uuid === bArea.attachment_uuid) {
if (aArea.page === bArea.page) {
if (Math.abs(aArea.y - bArea.y) < 0.01) {
if (aArea.x === bArea.x) {
return 0
} else {
return aArea.x - bArea.x
}
} else {
return aArea.y - bArea.y
}
} else {
return aArea.page - bArea.page
}
} else {
return attachmentUuids.indexOf(aArea.attachment_uuid) - attachmentUuids.indexOf(bArea.attachment_uuid)
}
}
sortedFields.sort((aField, bField) => {
const aArea = (fieldAreasIndex[aField.uuid] ||= [...(aField.areas || [])].sort(sortArea)[0])
const bArea = (fieldAreasIndex[bField.uuid] ||= [...(bField.areas || [])].sort(sortArea)[0])
return sortArea(aArea, bArea)
})
}
if (verificationFields.length) {
sortedFields.push(verificationFields.pop())
}
return sortedFields.reduce((acc, f) => {
const prevStep = acc[acc.length - 1] const prevStep = acc[acc.length - 1]
if (this.checkFieldConditions(f)) { if (this.checkFieldConditions(f) && this.checkFieldDocumentsConditions(f)) {
if (f.type === 'checkbox' && Array.isArray(prevStep) && prevStep[0].type === 'checkbox' && !f.description) { if (f.type === 'checkbox' && Array.isArray(prevStep) && prevStep[0].type === 'checkbox' && !f.description) {
prevStep.push(f) prevStep.push(f)
} else { } else {
@@ -892,6 +1039,23 @@ export default {
if (isEmpty(value) && this.currentStep > 0) { if (isEmpty(value) && this.currentStep > 0) {
this.currentStep -= 1 this.currentStep -= 1
} }
},
attachmentConditionsIndex: {
deep: true,
immediate: true,
handler (value) {
this.$nextTick(() => {
const root = this.$root.$el.parentNode.getRootNode()
for (const key in value) {
const doc = root.querySelector(`[id="document-${key}"`)
if (doc) {
doc.classList.toggle('hidden', !value[key])
}
}
})
}
} }
}, },
beforeUnmount () { beforeUnmount () {
@@ -973,33 +1137,57 @@ export default {
onOrientationChange (event) { onOrientationChange (event) {
this.orientation = event.target.type this.orientation = event.target.type
}, },
checkFieldDocumentsConditions (field) {
if (field.areas?.length) {
return field.areas.some((area) => {
return this.attachmentConditionsIndex[area.attachment_uuid]
})
} else {
return true
}
},
checkFieldConditions (field) { checkFieldConditions (field) {
if (field.conditions?.length) { if (field.conditions?.length) {
return field.conditions.reduce((acc, c) => { const result = field.conditions.reduce((acc, cond) => {
const field = this.fieldsUuidIndex[c.field_uuid] if (cond.operation === 'or') {
acc.push(acc.pop() || this.checkFieldCondition(cond))
if (['not_empty', 'checked', 'equal', 'contains'].includes(c.action) && field && !this.checkFieldConditions(field)) {
return false
}
if (['empty', 'unchecked'].includes(c.action)) {
return acc && isEmpty(this.values[c.field_uuid])
} else if (['not_empty', 'checked'].includes(c.action)) {
return acc && !isEmpty(this.values[c.field_uuid])
} else if (['equal', 'contains'].includes(c.action) && field) {
const option = field.options.find((o) => o.uuid === c.value)
const values = [this.values[c.field_uuid]].flat()
return acc && values.includes(this.optionValue(option, field.options.indexOf(option)))
} else if (['not_equal', 'does_not_contain'].includes(c.action) && field) {
const option = field.options.find((o) => o.uuid === c.value)
const values = [this.values[c.field_uuid]].flat()
return acc && !values.includes(this.optionValue(option, field.options.indexOf(option)))
} else { } else {
return acc acc.push(this.checkFieldCondition(cond))
} }
}, true)
return acc
}, [])
return !result.includes(false)
} else {
return true
}
},
checkFieldCondition (condition) {
const field = this.fieldsUuidIndex[condition.field_uuid]
if (['not_empty', 'checked', 'equal', 'contains'].includes(condition.action) && field && !this.checkFieldConditions(field)) {
return false
}
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 { } else {
return true return true
} }
@@ -1146,18 +1334,24 @@ export default {
scrollIntoArea (area) { scrollIntoArea (area) {
return this.$refs.areas.scrollIntoArea(area) return this.$refs.areas.scrollIntoArea(area)
}, },
async submitStep () { async submitStep (e) {
this.isSubmitting = true this.isSubmitting = true
const forceComplete = e?.submitter?.getAttribute('name') === 'completed'
if (forceComplete) {
this.isSubmittingComplete = true
}
const submitStep = this.currentStep const submitStep = this.currentStep
const stepPromise = ['signature', 'phone', 'initials', 'payment'].includes(this.currentField.type) const stepPromise = ['signature', 'phone', 'initials', 'payment', 'verification'].includes(this.currentField.type)
? this.$refs.currentStep.submit ? this.$refs.currentStep.submit
: () => Promise.resolve({}) : () => Promise.resolve({})
stepPromise().then(async () => { stepPromise().then(async () => {
const emptyRequiredField = this.stepFields.find((fields, index) => { const emptyRequiredField = this.stepFields.find((fields, index) => {
if (index >= submitStep) { if (forceComplete ? index === submitStep : index >= submitStep) {
return false return false
} }
@@ -1167,9 +1361,9 @@ export default {
}) })
const formData = new FormData(this.$refs.form) const formData = new FormData(this.$refs.form)
const isLastStep = submitStep === this.stepFields.length - 1 const isLastStep = (submitStep === this.stepFields.length - 1) || forceComplete
if (isLastStep && !emptyRequiredField && !this.inviteSubmitters.length) { if (isLastStep && !emptyRequiredField && !this.inviteSubmitters.length && !this.optionalInviteSubmitters.length) {
formData.append('completed', 'true') formData.append('completed', 'true')
} }
@@ -1185,14 +1379,18 @@ export default {
if (response.status === 422 || response.status === 500) { if (response.status === 422 || response.status === 500) {
const data = await response.json() const data = await response.json()
const i18nError = data.error ? this.t(data.error.replace(/\s+/g, '_').toLowerCase()) : '' if (data.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)) return Promise.reject(new Error(data.error))
} }
const nextStep = (isLastStep && emptyRequiredField) || this.stepFields[submitStep + 1] const nextStep = (isLastStep && emptyRequiredField) || (forceComplete ? null : this.stepFields[submitStep + 1])
if (nextStep) { if (nextStep) {
if (this.alwaysMinimize) { if (this.alwaysMinimize) {
@@ -1204,7 +1402,7 @@ export default {
if (emptyRequiredField === nextStep) { if (emptyRequiredField === nextStep) {
this.showFillAllRequiredFields = true this.showFillAllRequiredFields = true
} }
} else if (this.inviteSubmitters.length) { } else if (this.inviteSubmitters.length || this.optionalInviteSubmitters.length) {
this.isInvite = true this.isInvite = true
} else { } else {
this.performComplete(response) this.performComplete(response)
@@ -1213,6 +1411,7 @@ export default {
console.error(error) console.error(error)
}).finally(() => { }).finally(() => {
this.isSubmitting = false this.isSubmitting = false
this.isSubmittingComplete = false
}) })
}).catch(error => { }).catch(error => {
if (error?.message === 'Image too small') { if (error?.message === 'Image too small') {
@@ -1222,6 +1421,7 @@ export default {
} }
}).finally(() => { }).finally(() => {
this.isSubmitting = false this.isSubmitting = false
this.isSubmittingComplete = false
}) })
}, },
minimizeForm () { minimizeForm () {
+103 -26
View File
@@ -1,5 +1,9 @@
const en = { const en = {
complete_all_required_fields_to_proceed_with_identity_verification: 'Complete all required fields to proceed with identity verification.',
verify_id: 'Verify ID',
identity_verification: 'Identity verification',
complete: 'Complete', complete: 'Complete',
fill_all_required_fields_to_complete: 'Fill all required fields to complete',
sign_and_complete: 'Sign and Complete', sign_and_complete: 'Sign and Complete',
text: 'Text', text: 'Text',
by_clicking_you_agree_to_the: 'By clicking "{button}", you agree to the', by_clicking_you_agree_to_the: 'By clicking "{button}", you agree to the',
@@ -66,7 +70,7 @@ const en = {
documents_have_been_signed: 'Documents have been signed!', documents_have_been_signed: 'Documents have been signed!',
create_a_free_account: 'Create a Free Account', create_a_free_account: 'Create a Free Account',
powered_by: 'Powered by', powered_by: 'Powered by',
please_check_the_box_to_continue: 'Please check the box to continue', please_check_the_box_to_continue: 'Please check the box to continue.',
open_source_documents_software: 'open source documents software', open_source_documents_software: 'open source documents software',
verified_phone_number: 'Verify Phone Number', verified_phone_number: 'Verify Phone Number',
use_international_format: 'Use international format: +1xxx', use_international_format: 'Use international format: +1xxx',
@@ -88,11 +92,17 @@ const en = {
pay_with_strip: 'Pay with Stripe', pay_with_strip: 'Pay with Stripe',
reupload: 'Reupload', reupload: 'Reupload',
upload: 'Upload', upload: 'Upload',
files: 'Files' files: 'Files',
signature_is_too_small_please_redraw: 'Signature is too small. Please redraw.',
wait_countdown_seconds: 'Wait {countdown} seconds'
} }
const es = { const es = {
complete_all_required_fields_to_proceed_with_identity_verification: 'Complete todos los campos requeridos para continuar con la verificación de identidad.',
verify_id: 'Verificar ID',
identity_verification: 'Verificación de identidad',
complete: 'Completar', complete: 'Completar',
fill_all_required_fields_to_complete: 'Complete todos los campos requeridos para finalizar',
sign_and_complete: 'Firmar y Completar', sign_and_complete: 'Firmar y Completar',
invite: 'Invitar', invite: 'Invitar',
email: 'Correo electrónico', email: 'Correo electrónico',
@@ -158,7 +168,7 @@ const es = {
documents_have_been_signed: '¡Los documentos han sido firmados!', documents_have_been_signed: '¡Los documentos han sido firmados!',
create_a_free_account: 'Crear una Cuenta Gratuita', create_a_free_account: 'Crear una Cuenta Gratuita',
powered_by: 'Desarrollado por', powered_by: 'Desarrollado por',
please_check_the_box_to_continue: 'Por favor marque la casilla para continuar', please_check_the_box_to_continue: 'Por favor marque la casilla para continuar.',
open_source_documents_software: 'software de documentos de código abierto', open_source_documents_software: 'software de documentos de código abierto',
verified_phone_number: 'Verificar número de teléfono', verified_phone_number: 'Verificar número de teléfono',
use_international_format: 'Usar formato internacional: +1xxx', use_international_format: 'Usar formato internacional: +1xxx',
@@ -180,11 +190,17 @@ const es = {
pay_with_strip: 'Pagar con Stripe', pay_with_strip: 'Pagar con Stripe',
reupload: 'Volver a subir', reupload: 'Volver a subir',
upload: 'Subir', upload: 'Subir',
files: 'Archivos' files: 'Archivos',
signature_is_too_small_please_redraw: 'La firma es demasiado pequeña. Por favor, dibújala de nuevo.',
wait_countdown_seconds: 'Espera {countdown} segundos'
} }
const it = { const it = {
complete_all_required_fields_to_proceed_with_identity_verification: "Compila tutti i campi obbligatori per procedere con la verifica dell'identità.",
verify_id: 'Verifica ID',
identity_verification: "Verifica dell'identità",
complete: 'Completa', complete: 'Completa',
fill_all_required_fields_to_complete: 'Compila tutti i campi obbligatori per completare',
sign_and_complete: 'Firma e Completa', sign_and_complete: 'Firma e Completa',
invite: 'Invita', invite: 'Invita',
email: 'Email', email: 'Email',
@@ -250,7 +266,7 @@ const it = {
documents_have_been_signed: 'I documenti sono stati firmati!', documents_have_been_signed: 'I documenti sono stati firmati!',
create_a_free_account: 'Crea un Account Gratuito', create_a_free_account: 'Crea un Account Gratuito',
powered_by: 'Desarrollado por', powered_by: 'Desarrollado por',
please_check_the_box_to_continue: 'Si prega di spuntare la casella per continuare', please_check_the_box_to_continue: 'Si prega di spuntare la casella per continuare.',
open_source_documents_software: 'software di documenti open source', open_source_documents_software: 'software di documenti open source',
verified_phone_number: 'Verifica numero di telefono', verified_phone_number: 'Verifica numero di telefono',
use_international_format: 'Usa formato internazionale: +1xxx', use_international_format: 'Usa formato internazionale: +1xxx',
@@ -272,11 +288,17 @@ const it = {
pay_with_strip: 'Paga con Stripe', pay_with_strip: 'Paga con Stripe',
reupload: 'Ricarica', reupload: 'Ricarica',
upload: 'Carica', upload: 'Carica',
files: 'File' files: 'File',
signature_is_too_small_please_redraw: 'La firma è troppo piccola. Ridisegnala per favore.',
wait_countdown_seconds: 'Attendi {countdown} secondi'
} }
const de = { const de = {
complete_all_required_fields_to_proceed_with_identity_verification: 'Vervollständigen Sie alle erforderlichen Felder, um mit der Identitätsverifizierung fortzufahren.',
verify_id: 'ID überprüfen',
identity_verification: 'Identitätsüberprüfung',
complete: 'Abschließen', complete: 'Abschließen',
fill_all_required_fields_to_complete: 'Alle erforderlichen Felder ausfüllen, um abzuschließen',
sign_and_complete: 'Signieren und Abschließen', sign_and_complete: 'Signieren und Abschließen',
invite: 'Einladen', invite: 'Einladen',
email: 'E-Mail', email: 'E-Mail',
@@ -342,7 +364,7 @@ const de = {
documents_have_been_signed: 'Dokumente wurden unterschrieben!', documents_have_been_signed: 'Dokumente wurden unterschrieben!',
create_a_free_account: 'Kostenloses Konto erstellen', create_a_free_account: 'Kostenloses Konto erstellen',
powered_by: 'Bereitgestellt von', powered_by: 'Bereitgestellt von',
please_check_the_box_to_continue: 'Bitte setzen Sie das Häkchen, um fortzufahren', please_check_the_box_to_continue: 'Bitte setzen Sie das Häkchen, um fortzufahren.',
open_source_documents_software: 'Open-Source-Dokumentensoftware', open_source_documents_software: 'Open-Source-Dokumentensoftware',
verified_phone_number: 'Telefonnummer überprüfen', verified_phone_number: 'Telefonnummer überprüfen',
use_international_format: 'Internationales Format verwenden: +1xxx', use_international_format: 'Internationales Format verwenden: +1xxx',
@@ -364,11 +386,17 @@ const de = {
pay_with_strip: 'Mit Stripe bezahlen', pay_with_strip: 'Mit Stripe bezahlen',
reupload: 'Erneut hochladen', reupload: 'Erneut hochladen',
upload: 'Hochladen', upload: 'Hochladen',
files: 'Dateien' files: 'Dateien',
signature_is_too_small_please_redraw: 'Die Unterschrift ist zu klein. Bitte erneut zeichnen.',
wait_countdown_seconds: 'Warte {countdown} Sekunden'
} }
const fr = { const fr = {
complete_all_required_fields_to_proceed_with_identity_verification: "Veuillez remplir tous les champs obligatoires pour continuer la vérification de l'identité.",
verif_id: "Vérification de l'ID",
verif_identite: "Vérification de l'identité",
complete: 'Terminer', complete: 'Terminer',
fill_all_required_fields_to_complete: 'Veuillez remplir tous les champs obligatoires pour compléter',
sign_and_complete: 'Signer et Terminer', sign_and_complete: 'Signer et Terminer',
invite: 'Inviter', invite: 'Inviter',
email: 'Courriel', email: 'Courriel',
@@ -434,7 +462,7 @@ const fr = {
documents_have_been_signed: 'Les documents ont été signés!', documents_have_been_signed: 'Les documents ont été signés!',
create_a_free_account: 'Créer un Compte Gratuit', create_a_free_account: 'Créer un Compte Gratuit',
powered_by: 'Propulsé par', powered_by: 'Propulsé par',
please_check_the_box_to_continue: 'Veuillez cocher la case pour continuer', please_check_the_box_to_continue: 'Veuillez cocher la case pour continuer.',
open_source_documents_software: 'logiciel de documents open source', open_source_documents_software: 'logiciel de documents open source',
verified_phone_number: 'Vérifier le numéro de téléphone', verified_phone_number: 'Vérifier le numéro de téléphone',
use_international_format: 'Utiliser le format international : +1xxx', use_international_format: 'Utiliser le format international : +1xxx',
@@ -456,11 +484,17 @@ const fr = {
pay_with_strip: 'Paiement avec Stripe', pay_with_strip: 'Paiement avec Stripe',
reupload: 'Recharger', reupload: 'Recharger',
upload: 'Télécharger', upload: 'Télécharger',
files: 'Fichiers' files: 'Fichiers',
signature_is_too_small_please_redraw: 'La signature est trop petite. Veuillez la redessiner.',
wait_countdown_seconds: 'Attendez {countdown} secondes'
} }
const pl = { const pl = {
complete_all_required_fields_to_proceed_with_identity_verification: 'Uzupełnij wszystkie wymagane pola, aby kontynuować weryfikację tożsamości.',
verify_id: 'Zweryfikuj ID',
identity_verification: 'Weryfikacja tożsamości',
complete: 'Zakończ', complete: 'Zakończ',
fill_all_required_fields_to_complete: 'Uzupełnij wszystkie wymagane pola, aby zakończyć',
sign_and_complete: 'Podpisz i zakończ', sign_and_complete: 'Podpisz i zakończ',
invite: 'Zaproś', invite: 'Zaproś',
email: 'E-mail', email: 'E-mail',
@@ -526,7 +560,7 @@ const pl = {
documents_have_been_signed: 'Dokumenty zostały podpisane!', documents_have_been_signed: 'Dokumenty zostały podpisane!',
create_a_free_account: 'Utwórz darmowe konto', create_a_free_account: 'Utwórz darmowe konto',
powered_by: 'Napędzany przez', powered_by: 'Napędzany przez',
please_check_the_box_to_continue: 'Proszę zaznaczyć pole, aby kontynuować', please_check_the_box_to_continue: 'Proszę zaznaczyć pole, aby kontynuować.',
open_source_documents_software: 'oprogramowanie do dokumentów open source', open_source_documents_software: 'oprogramowanie do dokumentów open source',
verified_phone_number: 'Zweryfikuj numer telefonu', verified_phone_number: 'Zweryfikuj numer telefonu',
use_international_format: 'Użyj międzynarodowego formatu: +1xxx', use_international_format: 'Użyj międzynarodowego formatu: +1xxx',
@@ -548,11 +582,16 @@ const pl = {
pay_with_strip: 'Płatność za pomocą Stripe', pay_with_strip: 'Płatność za pomocą Stripe',
reupload: 'Ponowne przesłanie', reupload: 'Ponowne przesłanie',
upload: 'Przesyłanie', upload: 'Przesyłanie',
files: 'Pliki' files: 'Pliki',
signature_is_too_small_please_redraw: 'Podpis jest zbyt mały. Proszę narysować go ponownie.'
} }
const uk = { const uk = {
complete_all_required_fields_to_proceed_with_identity_verification: "Заповніть всі обов'язкові поля, щоб продовжити перевірку особи.",
verify_id: 'Підтвердження ідентичності',
identity_verification: 'Ідентифікація особи',
complete: 'Завершити', complete: 'Завершити',
fill_all_required_fields_to_complete: "Заповніть всі обов'язкові поля для завершення",
sign_and_complete: 'Підписати і завершити', sign_and_complete: 'Підписати і завершити',
invite: 'Запросити', invite: 'Запросити',
email: 'Електронна пошта', email: 'Електронна пошта',
@@ -618,7 +657,7 @@ const uk = {
documents_have_been_signed: 'Документи були підписані!', documents_have_been_signed: 'Документи були підписані!',
create_a_free_account: 'Створити безкоштовний обліковий запис', create_a_free_account: 'Створити безкоштовний обліковий запис',
powered_by: 'Працює на базі', powered_by: 'Працює на базі',
please_check_the_box_to_continue: 'Будь ласка, позначте прапорець, щоб продовжити', please_check_the_box_to_continue: 'Будь ласка, позначте прапорець, щоб продовжити.',
open_source_documents_software: 'відкритий програмний засіб для документів', open_source_documents_software: 'відкритий програмний засіб для документів',
verified_phone_number: 'Підтвердіть номер телефону', verified_phone_number: 'Підтвердіть номер телефону',
use_international_format: 'Використовуйте міжнародний формат: +1xxx', use_international_format: 'Використовуйте міжнародний формат: +1xxx',
@@ -640,11 +679,17 @@ const uk = {
pay_with_strip: 'Сплатити за допомогою Stripe', pay_with_strip: 'Сплатити за допомогою Stripe',
reupload: 'Перезавантажити', reupload: 'Перезавантажити',
upload: 'Завантажити', upload: 'Завантажити',
files: 'Файли' files: 'Файли',
signature_is_too_small_please_redraw: 'Підпис занадто малий. Будь ласка, перемалюйте його.',
wait_countdown_seconds: 'Зачекайте {countdown} секунд'
} }
const cs = { const cs = {
complete_all_required_fields_to_proceed_with_identity_verification: 'Vyplňte všechna povinná pole, abyste mohli pokračovat v ověření identity.',
verify_id: 'Ověřit ID',
identity_verification: 'Ověření identity',
complete: 'Dokončit', complete: 'Dokončit',
fill_all_required_fields_to_complete: 'Please complete all mandatory fields',
sign_and_complete: 'Podepsat a dokončit', sign_and_complete: 'Podepsat a dokončit',
invite: 'Pozvat', invite: 'Pozvat',
email: 'E-mail', email: 'E-mail',
@@ -710,7 +755,7 @@ const cs = {
documents_have_been_signed: 'Dokumenty byly podepsány!', documents_have_been_signed: 'Dokumenty byly podepsány!',
create_a_free_account: 'Vytvořit bezplatný účet', create_a_free_account: 'Vytvořit bezplatný účet',
powered_by: 'Poháněno', powered_by: 'Poháněno',
please_check_the_box_to_continue: 'Prosím, zaškrtněte políčko pro pokračování', please_check_the_box_to_continue: 'Prosím, zaškrtněte políčko pro pokračování.',
open_source_documents_software: 'open source software pro dokumenty', open_source_documents_software: 'open source software pro dokumenty',
verified_phone_number: 'Ověřte telefonní číslo', verified_phone_number: 'Ověřte telefonní číslo',
use_international_format: 'Použijte mezinárodní formát: +1xxx', use_international_format: 'Použijte mezinárodní formát: +1xxx',
@@ -732,11 +777,17 @@ const cs = {
pay_with_strip: 'Zaplacení přes Stripe', pay_with_strip: 'Zaplacení přes Stripe',
reupload: 'Znovu nahrát', reupload: 'Znovu nahrát',
upload: 'Nahrát', upload: 'Nahrát',
files: 'Soubory' files: 'Soubory',
signature_is_too_small_please_redraw: 'Podpis je příliš malý. Prosím, překreslete ho.',
wait_countdown_seconds: 'Počkejte {countdown} sekund'
} }
const pt = { const pt = {
complete_all_required_fields_to_proceed_with_identity_verification: 'Preencha todos os campos obrigatórios para prosseguir com a verificação de identidade.',
verify_id: 'Verificar ID',
identity_verification: 'Verificação de identidade',
complete: 'Completar', complete: 'Completar',
fill_all_required_fields_to_complete: 'Preencher todos os campos obrigatórios para concluir',
sign_and_complete: 'Assinar e Completar', sign_and_complete: 'Assinar e Completar',
invite: 'Convidar', invite: 'Convidar',
email: 'E-mail', email: 'E-mail',
@@ -802,7 +853,7 @@ const pt = {
documents_have_been_signed: 'Os documentos foram assinados!', documents_have_been_signed: 'Os documentos foram assinados!',
create_a_free_account: 'Criar uma Conta Gratuita', create_a_free_account: 'Criar uma Conta Gratuita',
powered_by: 'Desenvolvido por', powered_by: 'Desenvolvido por',
please_check_the_box_to_continue: 'Por favor, marque a caixa para continuar', please_check_the_box_to_continue: 'Por favor, marque a caixa para continuar.',
open_source_documents_software: 'software de documentos de código aberto', open_source_documents_software: 'software de documentos de código aberto',
verified_phone_number: 'Verificar Número de Telefone', verified_phone_number: 'Verificar Número de Telefone',
use_international_format: 'Use formato internacional: +1xxx', use_international_format: 'Use formato internacional: +1xxx',
@@ -824,11 +875,17 @@ const pt = {
pay_with_strip: 'Pagar com Stripe', pay_with_strip: 'Pagar com Stripe',
reupload: 'Reenviar', reupload: 'Reenviar',
upload: 'Carregar', upload: 'Carregar',
files: 'Arquivos' files: 'Arquivos',
signature_is_too_small_please_redraw: 'A assinatura é muito pequena. Por favor, redesenhe-a.',
wait_countdown_seconds: 'Aguarde {countdown} segundos'
} }
const he = { const he = {
complete_all_required_fields_to_proceed_with_identity_verification: 'מלא את כל השדות הנדרשים כדי להמשיך עם אימות זהות.',
verify_id: 'אמת מזהה',
identity_verification: 'אימות זהות',
complete: 'השלם', complete: 'השלם',
fill_all_required_fields_to_complete: 'נא למלא את כל השדות הנדרשים להשלמה',
sign_and_complete: 'חתום והשלם', sign_and_complete: 'חתום והשלם',
invite: 'הזמן', invite: 'הזמן',
email: 'דוא"ל', email: 'דוא"ל',
@@ -895,7 +952,7 @@ const he = {
documents_have_been_signed: 'המסמכים נחתמו!', documents_have_been_signed: 'המסמכים נחתמו!',
create_a_free_account: 'צור חשבון חינם', create_a_free_account: 'צור חשבון חינם',
powered_by: 'מופעל על ידי', powered_by: 'מופעל על ידי',
please_check_the_box_to_continue: 'אנא סמן את התיבה כדי להמשיך', please_check_the_box_to_continue: 'אנא סמן את התיבה כדי להמשיך.',
open_source_documents_software: 'תוכנה פתוחה למסמכים', open_source_documents_software: 'תוכנה פתוחה למסמכים',
verified_phone_number: 'אימות מספר טלפון', verified_phone_number: 'אימות מספר טלפון',
use_international_format: 'השתמש בפורמט בינלאומי: +1xxx', use_international_format: 'השתמש בפורמט בינלאומי: +1xxx',
@@ -917,11 +974,17 @@ const he = {
pay_with_strip: 'שלם עם סטרייפ', pay_with_strip: 'שלם עם סטרייפ',
reupload: 'העלה שוב', reupload: 'העלה שוב',
upload: 'העלאה', upload: 'העלאה',
files: 'קבצים' files: 'קבצים',
signature_is_too_small_please_redraw: 'החתימה קטנה מדי. אנא צייר מחדש.',
wait_countdown_seconds: 'המתן {countdown} שניות'
} }
const nl = { const nl = {
complete_all_required_fields_to_proceed_with_identity_verification: 'Vul alle verplichte velden in om door te gaan met de identiteitsverificatie.',
verify_id: 'Verifiëren ID',
identity_verification: 'Identiteitsverificatie',
complete: 'Voltooien', complete: 'Voltooien',
fill_all_required_fields_to_complete: 'Vul alle verplichte velden in om te voltooien',
sign_and_complete: 'Ondertekenen en voltooien', sign_and_complete: 'Ondertekenen en voltooien',
invite: 'Uitnodigen', invite: 'Uitnodigen',
email: 'E-mail', email: 'E-mail',
@@ -988,7 +1051,7 @@ const nl = {
documents_have_been_signed: 'De documenten zijn ondertekend!', documents_have_been_signed: 'De documenten zijn ondertekend!',
create_a_free_account: 'Maak een gratis account aan', create_a_free_account: 'Maak een gratis account aan',
powered_by: 'Aangedreven door', powered_by: 'Aangedreven door',
please_check_the_box_to_continue: 'Vink het vakje aan om door te gaan', please_check_the_box_to_continue: 'Vink het vakje aan om door te gaan.',
open_source_documents_software: 'Open source documenten software', open_source_documents_software: 'Open source documenten software',
verified_phone_number: 'Geverifieerd telefoonnummer', verified_phone_number: 'Geverifieerd telefoonnummer',
use_international_format: 'Gebruik internationaal formaat: +1xxx', use_international_format: 'Gebruik internationaal formaat: +1xxx',
@@ -1010,11 +1073,17 @@ const nl = {
pay_with_strip: 'Betalen met Stripe', pay_with_strip: 'Betalen met Stripe',
reupload: 'Opnieuw uploaden', reupload: 'Opnieuw uploaden',
upload: 'Uploaden', upload: 'Uploaden',
files: 'Bestanden' files: 'Bestanden',
signature_is_too_small_please_redraw: 'De handtekening is te klein. Teken deze opnieuw, alstublieft.',
wait_countdown_seconds: 'Wacht {countdown} seconden'
} }
const ar = { const ar = {
complete_all_required_fields_to_proceed_with_identity_verification: 'أكمل جميع الحقول المطلوبة للمتابعة في التحقق من الهوية.',
verify_id: 'تحقق من الهوية',
identity_verification: 'التحقق من الهوية',
complete: 'اكتمال', complete: 'اكتمال',
fill_all_required_fields_to_complete: 'يرجى ملء جميع الحقول المطلوبة لإكمال',
sign_and_complete: 'التوقيع والاكتمال', sign_and_complete: 'التوقيع والاكتمال',
invite: 'دعوة', invite: 'دعوة',
email: 'البريد الإلكتروني', email: 'البريد الإلكتروني',
@@ -1080,7 +1149,7 @@ const ar = {
documents_have_been_signed: 'تم توقيع الوثائق!', documents_have_been_signed: 'تم توقيع الوثائق!',
create_a_free_account: 'إنشاء حساب مجاني', create_a_free_account: 'إنشاء حساب مجاني',
powered_by: 'مدعوم من', powered_by: 'مدعوم من',
please_check_the_box_to_continue: 'الرجاء التحقق من الخانة للمتابعة', please_check_the_box_to_continue: 'الرجاء التحقق من الخانة للمتابعة.',
open_source_documents_software: 'برنامج وثائق مفتوح المصدر', open_source_documents_software: 'برنامج وثائق مفتوح المصدر',
verified_phone_number: 'تحقق من رقم الهاتف', verified_phone_number: 'تحقق من رقم الهاتف',
use_international_format: 'استخدم الشكل الدولي: +1xxx', use_international_format: 'استخدم الشكل الدولي: +1xxx',
@@ -1102,11 +1171,17 @@ const ar = {
pay_with_strip: 'الدفع بواسطة Stripe', pay_with_strip: 'الدفع بواسطة Stripe',
reupload: 'إعادة التحميل', reupload: 'إعادة التحميل',
upload: 'تحميل', upload: 'تحميل',
files: 'الملفات' files: 'الملفات',
signature_is_too_small_please_redraw: 'التوقيع صغير جدًا. يرجى إعادة الرسم.',
wait_countdown_seconds: 'انتظر {countdown} ثانية'
} }
const ko = { const ko = {
complete_all_required_fields_to_proceed_with_identity_verification: '신원 확인을 진행하려면 모든 필수 필드를 작성하십시오.',
verify_id: '아이디 확인',
identity_verification: '신원 확인',
complete: '완료', complete: '완료',
fill_all_required_fields_to_complete: '모든 필수 필드를 작성하여 완료하세요',
sign_and_complete: '서명하고 완료하기', sign_and_complete: '서명하고 완료하기',
invite: '초대하기', invite: '초대하기',
email: '이메일', email: '이메일',
@@ -1171,7 +1246,7 @@ const ko = {
documents_have_been_signed: '문서가 서명되었습니다!', documents_have_been_signed: '문서가 서명되었습니다!',
create_a_free_account: '무료 계정 생성', create_a_free_account: '무료 계정 생성',
powered_by: '구동', powered_by: '구동',
please_check_the_box_to_continue: '계속하려면 확인란을 선택하십시오', please_check_the_box_to_continue: '계속하려면 확인란을 선택하십시오.',
open_source_documents_software: '오픈 소스 문서 소프트웨어', open_source_documents_software: '오픈 소스 문서 소프트웨어',
verified_phone_number: '전화번호 확인됨', verified_phone_number: '전화번호 확인됨',
use_international_format: '국제 포맷 사용: +1xxx', use_international_format: '국제 포맷 사용: +1xxx',
@@ -1193,7 +1268,9 @@ const ko = {
pay_with_strip: '스트라이프로 결제', pay_with_strip: '스트라이프로 결제',
reupload: '다시 업로드', reupload: '다시 업로드',
upload: '업로드', upload: '업로드',
files: '파일' files: '파일',
signature_is_too_small_please_redraw: '서명이 너무 작습니다. 다시 그려주세요.',
wait_countdown_seconds: '{countdown}초 기다리세요'
} }
const i18n = { en, es, it, de, fr, pl, uk, cs, pt, he, nl, ar, ko } const i18n = { en, es, it, de, fr, pl, uk, cs, pt, he, nl, ar, ko }
@@ -226,6 +226,10 @@ export default {
if (this.$refs.canvas) { if (this.$refs.canvas) {
this.pad = new SignaturePad(this.$refs.canvas) this.pad = new SignaturePad(this.$refs.canvas)
if (this.field.preferences?.color) {
this.pad.penColor = this.field.preferences.color
}
this.pad.addEventListener('beginStroke', () => { this.pad.addEventListener('beginStroke', () => {
this.isInitialsStarted = true this.isInitialsStarted = true
@@ -12,7 +12,7 @@
:value="authenticityToken" :value="authenticityToken"
> >
<div <div
v-for="(submitter, index) in submitters" v-for="(submitter, index) in [...submitters, ...optionalSubmitters]"
:key="submitter.uuid" :key="submitter.uuid"
:class="{ 'mt-4': index !== 0 }" :class="{ 'mt-4': index !== 0 }"
> >
@@ -26,7 +26,7 @@
dir="auto" dir="auto"
class="label text-2xl" class="label text-2xl"
> >
{{ t('invite') }} {{ submitter.name }} {{ t('invite') }} {{ submitter.name }} <template v-if="!submitters.includes(submitter)">({{ t('optional') }})</template>
</label> </label>
<input <input
:id="submitter.uuid" :id="submitter.uuid"
@@ -34,7 +34,7 @@
class="base-input !text-2xl w-full" class="base-input !text-2xl w-full"
:placeholder="t('email')" :placeholder="t('email')"
type="email" type="email"
required :required="submitters.includes(submitter)"
autofocus="true" autofocus="true"
name="submission[submitters][][email]" name="submission[submitters][][email]"
> >
@@ -53,7 +53,7 @@
class="mr-1 animate-spin" class="mr-1 animate-spin"
/> />
<span> <span>
{{ t('submit') }} {{ t('complete') }}
</span><span </span><span
v-if="isSubmitting" v-if="isSubmitting"
class="w-6 flex justify-start mr-1" class="w-6 flex justify-start mr-1"
@@ -78,6 +78,11 @@ export default {
type: Array, type: Array,
required: true required: true
}, },
optionalSubmitters: {
type: Array,
required: false,
default: () => []
},
url: { url: {
type: String, type: String,
required: true required: true
@@ -1,7 +1,6 @@
<template> <template>
<label <label
v-if="!modelValue && !sessionId" v-if="!modelValue && !sessionId"
:for="field.uuid"
class="label text-2xl mb-2" class="label text-2xl mb-2"
> >
<MarkdownContent <MarkdownContent
File diff suppressed because it is too large. Load diff
+51 -10
View File
@@ -52,7 +52,14 @@
> >
{{ t('change_phone_number') }} {{ t('change_phone_number') }}
</a> </a>
<span
v-if="resendCodeCountdown > 0"
class="link"
>
{{ t('wait_countdown_seconds').replace('{countdown}', resendCodeCountdown) }}
</span>
<a <a
v-else
href="#" href="#"
class="link" class="link"
@click.prevent="resendCode" @click.prevent="resendCode"
@@ -65,11 +72,15 @@
v-show="!isCodeSent" v-show="!isCodeSent"
class="flex w-full rounded-full outline-neutral-content outline-2 outline-offset-2 focus-within:outline" class="flex w-full rounded-full outline-neutral-content outline-2 outline-offset-2 focus-within:outline"
> >
<div class="relative inline-block"> <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"> <div class="btn bg-base-200 border border-neutral-300 text-2xl whitespace-nowrap font-normal rounded-l-full">
{{ selectedCountry.flag }} +{{ selectedCountry.dial }} {{ selectedCountry.flag }} +{{ selectedCountry.dial }}
</div> </div>
<select <select
id="country_code_select"
class="absolute top-0 bottom-0 right-0 left-0 opacity-0 w-full h-full cursor-pointer" class="absolute top-0 bottom-0 right-0 left-0 opacity-0 w-full h-full cursor-pointer"
:disabled="!!defaultValue" :disabled="!!defaultValue"
@change="onCountrySelect(countries.find((country) => country.flag === $event.target.value))" @change="onCountrySelect(countries.find((country) => country.flag === $event.target.value))"
@@ -169,6 +180,8 @@ export default {
data () { data () {
return { return {
isCodeSent: false, isCodeSent: false,
codeSentAt: null,
resendCodeCountdown: 0,
isResendLoading: false, isResendLoading: false,
phoneValue: this.modelValue || this.defaultValue || '', phoneValue: this.modelValue || this.defaultValue || '',
selectedCountry: {} selectedCountry: {}
@@ -176,8 +189,8 @@ export default {
}, },
computed: { computed: {
countries () { countries () {
return phoneData.map(([name, dial, flag, tz]) => { return phoneData.map(([iso, name, dial, flag, tz]) => {
return { name, dial, flag, tz } return { iso, name, dial, flag, tz }
}) })
}, },
countriesDialIndex () { countriesDialIndex () {
@@ -216,6 +229,11 @@ export default {
this.selectedCountry = this.countries.find((country) => country.tz.includes(tz)) || this.countries[0] this.selectedCountry = this.countries.find((country) => country.tz.includes(tz)) || this.countries[0]
} }
}, },
beforeUnmount () {
if (this.interval) {
clearInterval(this.interval)
}
},
methods: { methods: {
emitSubmit: throttle(function (e) { emitSubmit: throttle(function (e) {
this.$emit('submit') this.$emit('submit')
@@ -242,13 +260,28 @@ export default {
} }
}, },
resendCode () { resendCode () {
this.isResendLoading = true if (this.codeSentAt && Date.now() - this.codeSentAt < 15000) {
this.startResendCodeCountdown()
} else {
this.isResendLoading = true
this.sendVerificationCode().finally(() => { this.sendVerificationCode().then(() => {
alert(this.t('verification_code_has_been_resent')) alert(this.t('verification_code_has_been_resent'))
}).finally(() => {
this.isResendLoading = false
})
}
},
startResendCodeCountdown () {
this.resendCodeCountdown = 15 - parseInt((Date.now() - this.codeSentAt) / 1000)
this.isResendLoading = false this.interval = setInterval(() => {
}) this.resendCodeCountdown--
if (this.resendCodeCountdown <= 0) {
clearInterval(this.interval)
}
}, 1000)
}, },
sendVerificationCode () { sendVerificationCode () {
return fetch(this.baseUrl + '/api/send_phone_verification_code', { return fetch(this.baseUrl + '/api/send_phone_verification_code', {
@@ -260,12 +293,20 @@ export default {
}), }),
headers: { 'Content-Type': 'application/json' } headers: { 'Content-Type': 'application/json' }
}).then(async (resp) => { }).then(async (resp) => {
if (resp.status === 422) { if ([422, 429].includes(resp.status)) {
const data = await resp.json() const data = await resp.json()
alert(this.t('number_phone_is_invalid').replace('{number}', this.fullInternationalPhoneValue)) if (resp.status === 422) {
alert(this.t('number_phone_is_invalid').replace('{number}', this.fullInternationalPhoneValue))
} else if (resp.status === 429) {
alert(data.error)
}
return Promise.reject(new Error(data.error)) return Promise.reject(new Error(data.error))
} else if (resp.ok) {
this.codeSentAt = Date.now()
return resp
} }
}) })
}, },
@@ -20,7 +20,7 @@
</label> </label>
<div class="space-x-2 flex"> <div class="space-x-2 flex">
<span <span
v-if="isTextSignature && field.preferences?.format !== 'typed'" v-if="isTextSignature && format !== 'typed' && format !== 'upload'"
class="tooltip" class="tooltip"
:data-tip="t('draw_signature')" :data-tip="t('draw_signature')"
> >
@@ -37,7 +37,7 @@
</a> </a>
</span> </span>
<span <span
v-else-if="withTypedSignature && field.preferences?.format !== 'typed' && field.preferences?.format !== 'drawn'" v-else-if="withTypedSignature && format !== 'typed' && format !== 'drawn' && format !== 'upload'"
class="tooltip ml-2" class="tooltip ml-2"
:data-tip="t('type_text')" :data-tip="t('type_text')"
> >
@@ -54,7 +54,7 @@
</a> </a>
</span> </span>
<span <span
v-if="field.preferences?.format !== 'typed' && field.preferences?.format !== 'drawn'" v-if="format !== 'typed' && format !== 'drawn' && format !== 'upload' && format !== 'drawn_or_typed'"
class="tooltip" class="tooltip"
:data-tip="t('take_photo')" :data-tip="t('take_photo')"
> >
@@ -81,10 +81,10 @@
@click.prevent="remove" @click.prevent="remove"
> >
<IconReload :width="16" /> <IconReload :width="16" />
{{ t('redraw') }} {{ t(format === 'upload' ? 'reupload' : 'redraw') }}
</a> </a>
<span <span
v-if="withQrButton && !modelValue && !computedPreviousValue && field.preferences?.format !== 'typed'" v-if="withQrButton && !modelValue && !computedPreviousValue && format !== 'typed' && format !== 'upload'"
class=" tooltip" class=" tooltip"
:data-tip="t('drawn_signature_on_a_touchscreen_device')" :data-tip="t('drawn_signature_on_a_touchscreen_device')"
> >
@@ -131,7 +131,18 @@
:src="attachmentsIndex[modelValue || computedPreviousValue].url" :src="attachmentsIndex[modelValue || computedPreviousValue].url"
class="mx-auto bg-white border border-base-300 rounded max-h-44" class="mx-auto bg-white border border-base-300 rounded max-h-44"
> >
<div class="relative"> <FileDropzone
v-if="format === 'upload' && !modelValue"
:message="`${t('upload')} ${field.name || t('signature')}`"
:submitter-slug="submitterSlug"
:dry-run="dryRun"
:accept="'image/*'"
@upload="[$emit('attached', $event[0]), $emit('update:model-value', $event[0].uuid)]"
/>
<div
v-else
class="relative"
>
<div <div
v-if="!modelValue && !computedPreviousValue && !isShowQr && !isTextSignature && isSignatureStarted" v-if="!modelValue && !computedPreviousValue && !isShowQr && !isTextSignature && isSignatureStarted"
class="absolute top-0.5 right-0.5" class="absolute top-0.5 right-0.5"
@@ -145,6 +156,10 @@
{{ t('clear') }} {{ t('clear') }}
</a> </a>
</div> </div>
<div
v-if="isTextSignature"
class="absolute top-0 right-0 left-0 bottom-0"
/>
<canvas <canvas
v-show="!modelValue && !computedPreviousValue" v-show="!modelValue && !computedPreviousValue"
ref="canvas" ref="canvas"
@@ -255,7 +270,7 @@
class="text-base-content/60 text-xs text-center w-full mt-1" class="text-base-content/60 text-xs text-center w-full mt-1"
> >
{{ t('by_clicking_you_agree_to_the').replace('{button}', buttonText.charAt(0).toUpperCase() + buttonText.slice(1)) }} <a {{ t('by_clicking_you_agree_to_the').replace('{button}', buttonText.charAt(0).toUpperCase() + buttonText.slice(1)) }} <a
href="https://www.docuseal.co/esign-disclosure" href="https://www.docuseal.com/esign-disclosure"
target="_blank" target="_blank"
> >
<span class="inline md:hidden"> <span class="inline md:hidden">
@@ -278,6 +293,7 @@ import { IconReload, IconCamera, IconSignature, IconTextSize, IconArrowsDiagonal
import { cropCanvasAndExportToPNG } from './crop_canvas' import { cropCanvasAndExportToPNG } from './crop_canvas'
import SignaturePad from 'signature_pad' import SignaturePad from 'signature_pad'
import AppearsOn from './appears_on' import AppearsOn from './appears_on'
import FileDropzone from './dropzone'
import MarkdownContent from './markdown_content' import MarkdownContent from './markdown_content'
import { v4 } from 'uuid' import { v4 } from 'uuid'
@@ -290,6 +306,7 @@ export default {
components: { components: {
AppearsOn, AppearsOn,
IconReload, IconReload,
FileDropzone,
IconCamera, IconCamera,
IconQrcode, IconQrcode,
MarkdownContent, MarkdownContent,
@@ -384,6 +401,9 @@ export default {
submitterSlug () { submitterSlug () {
return this.submitter.slug return this.submitter.slug
}, },
format () {
return this.field.preferences?.format
},
defaultReasons () { defaultReasons () {
return { return {
[this.t('approved_by')]: this.t('approved'), [this.t('approved_by')]: this.t('approved'),
@@ -419,6 +439,10 @@ export default {
if (this.$refs.canvas) { if (this.$refs.canvas) {
this.pad = new SignaturePad(this.$refs.canvas) this.pad = new SignaturePad(this.$refs.canvas)
if (this.field.preferences?.color) {
this.pad.penColor = this.field.preferences.color
}
this.pad.addEventListener('endStroke', () => { this.pad.addEventListener('endStroke', () => {
this.isSignatureStarted = true this.isSignatureStarted = true
+3 -1
View File
@@ -114,7 +114,9 @@ export default {
const area = this.field.areas?.[0] const area = this.field.areas?.[0]
if (area) { if (area) {
return parseInt(area.w / area.cell_w) + 1 const num = area.w / area.cell_w
return (num % 1) > 0.2 ? parseInt(num) + 1 : parseInt(num)
} else { } else {
return null return null
} }
@@ -0,0 +1,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>
+3 -2
View File
@@ -175,6 +175,7 @@
<div <div
ref="textContainer" ref="textContainer"
class="flex items-center px-0.5" class="flex items-center px-0.5"
:style="{ color: field.preferences?.color }"
:class="{ 'w-full h-full': ['cells', 'checkbox'].includes(field.type) }" :class="{ 'w-full h-full': ['cells', 'checkbox'].includes(field.type) }"
> >
<IconCheck <IconCheck
@@ -236,7 +237,7 @@
/> />
<span <span
v-if="field?.type && editable" v-if="field?.type && editable"
class="h-4 w-4 md:h-2.5 md:w-2.5 -right-1 rounded-full -bottom-1 border-gray-400 bg-white shadow-md border absolute cursor-nwse-resize" class="h-4 w-4 lg:h-2.5 lg:w-2.5 -right-1 rounded-full -bottom-1 border-gray-400 bg-white shadow-md border absolute cursor-nwse-resize"
@mousedown.stop="startResize" @mousedown.stop="startResize"
@touchstart="startTouchResize" @touchstart="startTouchResize"
/> />
@@ -256,7 +257,7 @@
:to="modalContainerEl" :to="modalContainerEl"
> >
<ConditionsModal <ConditionsModal
:field="field" :item="field"
:build-default-name="buildDefaultName" :build-default-name="buildDefaultName"
@close="isShowConditionsModal = false" @close="isShowConditionsModal = false"
/> />
+71 -11
View File
@@ -1,10 +1,11 @@
<template> <template>
<div <div
style="max-width: 1600px" style="max-width: 1600px"
class="mx-auto pl-3 md:pl-4 h-full" class="mx-auto pl-3 h-full"
:class="isMobile ? 'pl-4' : 'md:pl-4'"
> >
<div <div
v-if="pendingFieldAttachmentUuids.length" v-if="pendingFieldAttachmentUuids.length && editable"
class="top-1.5 sticky h-0 z-20 max-w-2xl mx-auto" class="top-1.5 sticky h-0 z-20 max-w-2xl mx-auto"
> >
<div class="alert border-base-content/30 py-2 px-2.5"> <div class="alert border-base-content/30 py-2 px-2.5">
@@ -163,7 +164,7 @@
<div <div
id="main_container" id="main_container"
class="flex" 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 <div
v-if="withDocumentsList" v-if="withDocumentsList"
@@ -219,7 +220,8 @@
</div> </div>
<div <div
id="pages_container" id="pages_container"
class="w-full overflow-y-hidden md:overflow-y-auto overflow-x-hidden mt-0.5 pt-0.5" class="w-full overflow-y-hidden overflow-x-hidden mt-0.5 pt-0.5"
:class="isMobile ? 'overflow-y-auto' : 'md:overflow-y-auto'"
> >
<div <div
ref="documents" ref="documents"
@@ -320,7 +322,7 @@
</div> </div>
</div> </div>
<div <div
v-if="withFieldsList" v-if="withFieldsList && !isMobile"
id="fields_list_container" id="fields_list_container"
class="relative w-80 flex-none mt-1 pr-4 pl-0.5 hidden md:block" class="relative w-80 flex-none mt-1 pr-4 pl-0.5 hidden md:block"
:class="drawField ? 'overflow-hidden' : 'overflow-y-auto overflow-x-hidden'" :class="drawField ? 'overflow-hidden' : 'overflow-y-auto overflow-x-hidden'"
@@ -362,6 +364,7 @@
:default-submitters="defaultSubmitters" :default-submitters="defaultSubmitters"
:draw-field-type="drawFieldType" :draw-field-type="drawFieldType"
:default-fields="[...defaultRequiredFields, ...defaultFields]" :default-fields="[...defaultRequiredFields, ...defaultFields]"
:template="template"
:default-required-fields="defaultRequiredFields" :default-required-fields="defaultRequiredFields"
:field-types="fieldTypes" :field-types="fieldTypes"
:with-sticky-submitters="withStickySubmitters" :with-sticky-submitters="withStickySubmitters"
@@ -378,14 +381,14 @@
</div> </div>
</div> </div>
</div> </div>
<div class="sticky bottom-0"> <div class="sticky bottom-0 z-10">
<MobileDrawField <MobileDrawField
v-if="drawField && isBreakpointLg" v-if="drawField && (isBreakpointLg || isMobile)"
:draw-field="drawField" :draw-field="drawField"
:fields="template.fields" :fields="template.fields"
:submitters="template.submitters" :submitters="template.submitters"
:selected-submitter="selectedSubmitter" :selected-submitter="selectedSubmitter"
class="md:hidden" :class="{ 'md:hidden': !isMobile }"
:editable="editable" :editable="editable"
@cancel="[drawField = null, drawOption = null]" @cancel="[drawField = null, drawOption = null]"
@change-submitter="[selectedSubmitter = $event, drawField.submitter_uuid = $event.uuid]" @change-submitter="[selectedSubmitter = $event, drawField.submitter_uuid = $event.uuid]"
@@ -396,6 +399,7 @@
:default-fields="[...defaultRequiredFields, ...defaultFields]" :default-fields="[...defaultRequiredFields, ...defaultFields]"
:default-required-fields="defaultRequiredFields" :default-required-fields="defaultRequiredFields"
:field-types="fieldTypes" :field-types="fieldTypes"
:class="{ 'md:hidden': !isMobile }"
:selected-submitter="selectedSubmitter" :selected-submitter="selectedSubmitter"
@select="startFieldDraw($event)" @select="startFieldDraw($event)"
/> />
@@ -415,6 +419,7 @@ import Contenteditable from './contenteditable'
import DocumentPreview from './preview' import DocumentPreview from './preview'
import DocumentControls from './controls' import DocumentControls from './controls'
import MobileFields from './mobile_fields' import MobileFields from './mobile_fields'
import FieldSubmitter from './field_submitter'
import { IconPlus, IconUsersPlus, IconDeviceFloppy, IconChevronDown, IconEye, IconWritingSign, IconInnerShadowTop, IconInfoCircle, IconAdjustments } from '@tabler/icons-vue' import { IconPlus, IconUsersPlus, IconDeviceFloppy, IconChevronDown, IconEye, IconWritingSign, IconInnerShadowTop, IconInfoCircle, IconAdjustments } from '@tabler/icons-vue'
import { v4 } from 'uuid' import { v4 } from 'uuid'
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
@@ -454,6 +459,7 @@ export default {
fieldTypes: this.fieldTypes, fieldTypes: this.fieldTypes,
backgroundColor: this.backgroundColor, backgroundColor: this.backgroundColor,
withPhone: this.withPhone, withPhone: this.withPhone,
withVerification: this.withVerification,
withPayment: this.withPayment, withPayment: this.withPayment,
isPaymentConnected: this.isPaymentConnected, isPaymentConnected: this.isPaymentConnected,
withFormula: this.withFormula, withFormula: this.withFormula,
@@ -558,6 +564,11 @@ export default {
required: false, required: false,
default: () => [] default: () => []
}, },
defineSubmitters: {
type: Array,
required: false,
default: () => []
},
acceptFileTypes: { acceptFileTypes: {
type: String, type: String,
required: false, required: false,
@@ -629,6 +640,11 @@ export default {
required: false, required: false,
default: false default: false
}, },
withVerification: {
type: Boolean,
required: false,
default: null
},
withPayment: { withPayment: {
type: Boolean, type: Boolean,
required: false, required: false,
@@ -677,11 +693,15 @@ export default {
} }
}, },
computed: { computed: {
submitterDefaultNames: FieldSubmitter.computed.names,
selectedAreaRef: () => ref(), selectedAreaRef: () => ref(),
fieldsDragFieldRef: () => ref(), fieldsDragFieldRef: () => ref(),
language () { language () {
return this.locale.split('-')[0].toLowerCase() return this.locale.split('-')[0].toLowerCase()
}, },
isMobile () {
return /android|iphone|ipad/i.test(navigator.userAgent)
},
defaultDateFormat () { defaultDateFormat () {
const isUsBrowser = Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US') const isUsBrowser = Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US')
const isUsTimezone = new Intl.DateTimeFormat('en-US', { timeZoneName: 'short' }).format(new Date()).match(/\s(?:CST|CDT|PST|PDT|EST|EDT)$/) const isUsTimezone = new Intl.DateTimeFormat('en-US', { timeZoneName: 'short' }).format(new Date()).match(/\s(?:CST|CDT|PST|PDT|EST|EDT)$/)
@@ -749,6 +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] this.selectedSubmitter = this.template.submitters[0]
}, },
mounted () { mounted () {
@@ -998,6 +1030,22 @@ export default {
if (!field.areas.length) { if (!field.areas.length) {
this.template.fields.splice(this.template.fields.indexOf(field), 1) this.template.fields.splice(this.template.fields.indexOf(field), 1)
this.template.fields.forEach((f) => {
(f.conditions || []).forEach((c) => {
if (c.field_uuid === field.uuid) {
f.conditions.splice(f.conditions.indexOf(c), 1)
}
})
})
this.template.schema.forEach((item) => {
(item.conditions || []).forEach((c) => {
if (c.field_uuid === field.uuid) {
item.conditions.splice(item.conditions.indexOf(c), 1)
}
})
})
} }
this.save() this.save()
@@ -1056,7 +1104,7 @@ export default {
} else if (type === 'image') { } else if (type === 'image') {
area.w = pageMask.clientWidth / 5 / pageMask.clientWidth area.w = pageMask.clientWidth / 5 / pageMask.clientWidth
area.h = (pageMask.clientWidth / 5 / pageMask.clientWidth) * (pageMask.clientWidth / pageMask.clientHeight) area.h = (pageMask.clientWidth / 5 / pageMask.clientWidth) * (pageMask.clientWidth / pageMask.clientHeight)
} else if (type === 'signature' || type === 'stamp') { } else if (type === 'signature' || type === 'stamp' || type === 'verification') {
area.w = pageMask.clientWidth / 5 / pageMask.clientWidth area.w = pageMask.clientWidth / 5 / pageMask.clientWidth
area.h = (pageMask.clientWidth / 5 / pageMask.clientWidth) * (pageMask.clientWidth / pageMask.clientHeight) / 2 area.h = (pageMask.clientWidth / 5 / pageMask.clientWidth) * (pageMask.clientWidth / pageMask.clientHeight) / 2
} else if (type === 'initials') { } else if (type === 'initials') {
@@ -1220,7 +1268,7 @@ export default {
w: area.maskW / 5 / area.maskW, w: area.maskW / 5 / area.maskW,
h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH) h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH)
} }
} else if (field.type === 'signature' || field.type === 'stamp') { } else if (field.type === 'signature' || field.type === 'stamp' || field.type === 'verification') {
baseArea = { baseArea = {
w: area.maskW / 5 / area.maskW, w: area.maskW / 5 / area.maskW,
h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH) / 2 h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH) / 2
@@ -1256,6 +1304,16 @@ export default {
fieldArea.h = lastArea.h fieldArea.h = lastArea.h
} }
if (field.width) {
fieldArea.w = field.width / area.maskW
delete field.width
}
if (field.height) {
fieldArea.h = field.height / area.maskH
delete field.height
}
field.areas.push(fieldArea) field.areas.push(fieldArea)
this.selectedAreaRef.value = fieldArea this.selectedAreaRef.value = fieldArea
@@ -1323,7 +1381,9 @@ export default {
} }
this.$nextTick(() => { this.$nextTick(() => {
this.$refs.previews.scrollTop = this.$refs.previews.scrollHeight if (this.$refs.previews) {
this.$refs.previews.scrollTop = this.$refs.previews.scrollHeight
}
this.scrollIntoDocument(data.schema[0]) this.scrollIntoDocument(data.schema[0])
}) })
@@ -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="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl">
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium"> <div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
<span> <span>
{{ t('condition') }} - {{ field.name || buildDefaultName(field, template.fields) }} {{ t('condition') }} - {{ item.name || buildDefaultName(item, template.fields) }}
</span> </span>
<a <a
href="#" href="#"
@@ -23,21 +23,32 @@
class="bg-base-300 rounded-xl py-2 px-3 text-center" class="bg-base-300 rounded-xl py-2 px-3 text-center"
> >
<a <a
href="https://www.docuseal.co/pricing" href="https://www.docuseal.com/pricing"
target="_blank" target="_blank"
class="link" class="link"
>{{ t('available_in_pro') }}</a> >{{ t('available_in_pro') }}</a>
</div> </div>
<form @submit.prevent="validateSaveAndClose"> <form @submit.prevent="validateSaveAndClose">
<div class="my-4 space-y-5"> <div class="my-4">
<div <div
v-for="(condition, cindex) in conditions" v-for="(condition, cindex) in conditions"
:key="cindex" :key="cindex"
class="space-y-4 relative" class="space-y-4 relative"
> >
<div
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 <div
v-if="conditions.length > 1" 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"> <span class="text-sm">
{{ t('condition') }} {{ cindex + 1 }} {{ t('condition') }} {{ cindex + 1 }}
@@ -127,12 +138,12 @@
</button> </button>
</form> </form>
<div <div
v-if="field.conditions?.[0]?.field_uuid" v-if="item.conditions?.[0]?.field_uuid"
class="text-center w-full mt-4" class="text-center w-full mt-4"
> >
<button <button
class="link" class="link"
@click="[conditions = [], delete field.conditions, validateSaveAndClose()]" @click="[conditions = [], delete item.conditions, validateSaveAndClose()]"
> >
{{ t('remove_condition') }} {{ t('remove_condition') }}
</button> </button>
@@ -147,7 +158,7 @@ export default {
name: 'ConditionModal', name: 'ConditionModal',
inject: ['t', 'save', 'template', 'withConditions'], inject: ['t', 'save', 'template', 'withConditions'],
props: { props: {
field: { item: {
type: Object, type: Object,
required: true required: true
}, },
@@ -159,22 +170,26 @@ export default {
emits: ['close'], emits: ['close'],
data () { data () {
return { return {
conditions: this.field.conditions?.[0] ? JSON.parse(JSON.stringify(this.field.conditions)) : [{}] conditions: this.item.conditions?.[0] ? JSON.parse(JSON.stringify(this.item.conditions)) : [{}]
} }
}, },
computed: { computed: {
fields () { fields () {
return this.template.fields.reduce((acc, f) => { if (this.item.submitter_uuid) {
if (f !== this.field && f.submitter_uuid === this.field.submitter_uuid) { return this.template.fields.reduce((acc, f) => {
acc.push(f) if (f !== this.item && f.submitter_uuid === this.item.submitter_uuid) {
} acc.push(f)
}
return acc return acc
}, []) }, [])
} else {
return this.template.fields
}
} }
}, },
created () { created () {
this.field.conditions ||= [] this.item.conditions ||= []
}, },
methods: { methods: {
conditionField (condition) { conditionField (condition) {
@@ -208,9 +223,9 @@ export default {
} }
if (this.conditions.find((f) => f.field_uuid)) { if (this.conditions.find((f) => f.field_uuid)) {
this.field.conditions = this.conditions this.item.conditions = this.conditions
} else { } else {
delete this.field.conditions delete this.item.conditions
} }
this.save() this.save()
+2 -2
View File
@@ -236,7 +236,7 @@
:to="modalContainerEl" :to="modalContainerEl"
> >
<ConditionsModal <ConditionsModal
:field="field" :item="field"
:build-default-name="buildDefaultName" :build-default-name="buildDefaultName"
@close="isShowConditionsModal = false" @close="isShowConditionsModal = false"
/> />
@@ -365,7 +365,7 @@ export default {
} else { } else {
const typeIndex = fields.filter((f) => f.type === field.type).indexOf(field) const typeIndex = fields.filter((f) => f.type === field.type).indexOf(field)
if (this.field.type === 'heading') { if (field.type === 'heading') {
return `${this.fieldNames[field.type]} ${typeIndex + 1}` return `${this.fieldNames[field.type]} ${typeIndex + 1}`
} else { } else {
return `${this.fieldLabels[field.type]} ${typeIndex + 1}` return `${this.fieldLabels[field.type]} ${typeIndex + 1}`
@@ -27,7 +27,7 @@
</label> </label>
</div> </div>
<div <div
v-if="['number'].includes(field.type)" v-if="['number', 'cells'].includes(field.type)"
class="py-1.5 px-1 relative" class="py-1.5 px-1 relative"
@click.stop @click.stop
> >
@@ -36,7 +36,7 @@
@change="[field.preferences ||= {}, field.preferences.align = $event.target.value, save()]" @change="[field.preferences ||= {}, field.preferences.align = $event.target.value, save()]"
> >
<option <option
v-for="value in ['left', 'right', 'center']" v-for="value in ['left', 'right', field.type === 'cells' ? null : 'center'].filter(Boolean)"
:key="value" :key="value"
:selected="field.preferences?.align ? value === field.preferences.align : value === 'left'" :selected="field.preferences?.align ? value === field.preferences.align : value === 'left'"
:value="value" :value="value"
@@ -212,16 +212,12 @@
{{ t('any') }} {{ t('any') }}
</option> </option>
<option <option
value="drawn" v-for="type in ['drawn', 'typed', 'drawn_or_typed', 'upload']"
:selected="field.preferences?.format === 'drawn'" :key="type"
:value="type"
:selected="field.preferences?.format === type"
> >
{{ t('drawn') }} {{ t(type) }}
</option>
<option
value="typed"
:selected="field.preferences?.format === 'typed'"
>
{{ t('typed') }}
</option> </option>
</select> </select>
<label <label
@@ -240,7 +236,7 @@
<input <input
v-model="field.required" v-model="field.required"
type="checkbox" type="checkbox"
:disabled="!editable || defaultField" :disabled="!editable || (defaultField && [true, false].includes(defaultField.required))"
class="toggle toggle-xs" class="toggle toggle-xs"
@update:model-value="save" @update:model-value="save"
> >
@@ -79,7 +79,7 @@
:stroke-width="1.6" :stroke-width="1.6"
/> />
<span class="py-1"> <span class="py-1">
{{ t('add') }} {{ names[submitters.length] }} {{ t('add') }} {{ names[lastPartyIndex] }}
</span> </span>
</a> </a>
</li> </li>
@@ -202,7 +202,7 @@
:stroke-width="1.6" :stroke-width="1.6"
/> />
<span class="py-1"> <span class="py-1">
{{ t('add') }} {{ names[submitters.length] }} {{ t('add') }} {{ names[lastPartyIndex] }}
</span> </span>
</a> </a>
</li> </li>
@@ -321,6 +321,15 @@ export default {
this.t('twentieth_party') 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 () { selectedSubmitter () {
return this.submitters.find((e) => e.uuid === this.modelValue) return this.submitters.find((e) => e.uuid === this.modelValue)
} }
@@ -353,7 +362,7 @@ export default {
}, },
addSubmitter () { addSubmitter () {
const newSubmitter = { const newSubmitter = {
name: this.names[this.submitters.length], name: this.names[this.lastPartyIndex],
uuid: v4() uuid: v4()
} }
@@ -30,7 +30,7 @@
v-for="(icon, type) in fieldIconsSorted" v-for="(icon, type) in fieldIconsSorted"
:key="type" :key="type"
> >
<li v-if="(fieldTypes.length === 0 || fieldTypes.includes(type)) && (withPhone || type != 'phone') && (withPayment || type != 'payment')"> <li v-if="(fieldTypes.length === 0 || fieldTypes.includes(type)) && (withPhone || type != 'phone') && (withPayment || type != 'payment') && (withVerification || type != 'verification')">
<a <a
href="#" href="#"
class="text-sm py-1 px-2" class="text-sm py-1 px-2"
@@ -51,11 +51,11 @@
</template> </template>
<script> <script>
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp, IconSquareNumber1, IconHeading } from '@tabler/icons-vue' import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp, IconSquareNumber1, IconHeading, IconId } from '@tabler/icons-vue'
export default { export default {
name: 'FiledTypeDropdown', name: 'FiledTypeDropdown',
inject: ['withPhone', 'withPayment', 't', 'fieldTypes'], inject: ['withPhone', 'withPayment', 'withVerification', 't', 'fieldTypes'],
props: { props: {
modelValue: { modelValue: {
type: String, type: String,
@@ -111,7 +111,8 @@ export default {
cells: this.t('cells'), cells: this.t('cells'),
stamp: this.t('stamp'), stamp: this.t('stamp'),
payment: this.t('payment'), payment: this.t('payment'),
phone: this.t('phone') phone: this.t('phone'),
verification: this.t('verify_id')
} }
}, },
fieldLabels () { fieldLabels () {
@@ -130,7 +131,8 @@ export default {
cells: this.t('cells_field'), cells: this.t('cells_field'),
stamp: this.t('stamp_field'), stamp: this.t('stamp_field'),
payment: this.t('payment_field'), payment: this.t('payment_field'),
phone: this.t('phone_field') phone: this.t('phone_field'),
verification: this.t('verify_id')
} }
}, },
fieldIcons () { fieldIcons () {
@@ -150,7 +152,8 @@ export default {
cells: IconColumns3, cells: IconColumns3,
stamp: IconRubberStamp, stamp: IconRubberStamp,
payment: IconCreditCard, payment: IconCreditCard,
phone: IconPhoneCheck phone: IconPhoneCheck,
verification: IconId
} }
}, },
fieldIconsSorted () { fieldIconsSorted () {
+42 -4
View File
@@ -111,14 +111,14 @@
:key="type" :key="type"
> >
<button <button
v-if="(fieldTypes.length === 0 || fieldTypes.includes(type)) && (withPhone || type != 'phone') && (withPayment || type != 'payment')" v-if="(fieldTypes.length === 0 || fieldTypes.includes(type)) && (withPhone || type != 'phone') && (withPayment || type != 'payment') && (withVerification || type != 'verification')"
draggable="true" draggable="true"
class="field-type-button group flex items-center justify-center border border-dashed w-full rounded relative" class="field-type-button group flex items-center justify-center border border-dashed w-full rounded relative"
:style="{ backgroundColor }" :style="{ backgroundColor }"
:class="drawFieldType === type ? 'border-base-content/40' : 'border-base-300 hover:border-base-content/20'" :class="drawFieldType === type ? 'border-base-content/40' : 'border-base-300 hover:border-base-content/20'"
@dragstart="onDragstart({ type: type })" @dragstart="onDragstart({ type: type })"
@dragend="$emit('drag-end')" @dragend="$emit('drag-end')"
@click="['file', 'payment'].includes(type) ? $emit('add-field', type) : $emit('set-draw-type', type)" @click="['file', 'payment', 'verification'].includes(type) ? $emit('add-field', type) : $emit('set-draw-type', type)"
> >
<div <div
class="flex items-console transition-all cursor-grab h-full absolute left-0" class="flex items-console transition-all cursor-grab h-full absolute left-0"
@@ -140,7 +140,33 @@
:data-tip="t('unlock_sms_verified_phone_number_field_with_paid_plan_use_text_field_for_phone_numbers_without_verification')" :data-tip="t('unlock_sms_verified_phone_number_field_with_paid_plan_use_text_field_for_phone_numbers_without_verification')"
> >
<a <a
href="https://www.docuseal.co/pricing" href="https://www.docuseal.com/pricing"
target="_blank"
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" target="_blank"
class="opacity-50 flex items-center justify-center border border-dashed border-base-300 w-full rounded relative" class="opacity-50 flex items-center justify-center border border-dashed border-base-300 w-full rounded relative"
:style="{ backgroundColor }" :style="{ backgroundColor }"
@@ -197,12 +223,16 @@ export default {
IconDrag, IconDrag,
IconLock IconLock
}, },
inject: ['save', 'backgroundColor', 'withPhone', 'withPayment', 't', 'fieldsDragFieldRef'], inject: ['save', 'backgroundColor', 'withPhone', 'withVerification', 'withPayment', 't', 'fieldsDragFieldRef'],
props: { props: {
fields: { fields: {
type: Array, type: Array,
required: true required: true
}, },
template: {
type: Object,
required: true
},
withHelp: { withHelp: {
type: Boolean, type: Boolean,
required: false, 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() this.save()
} }
} }
@@ -23,7 +23,7 @@
class="bg-base-300 rounded-xl py-2 px-3 text-center" class="bg-base-300 rounded-xl py-2 px-3 text-center"
> >
<a <a
href="https://www.docuseal.co/pricing" href="https://www.docuseal.com/pricing"
target="_blank" target="_blank"
class="link" class="link"
>{{ t('available_in_pro') }}</a> >{{ t('available_in_pro') }}</a>
+42 -12
View File
@@ -1,4 +1,6 @@
const en = { 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', editable: 'Editable',
recurrent: 'Recurrent', recurrent: 'Recurrent',
one_off: 'One-off', one_off: 'One-off',
@@ -46,6 +48,8 @@ const en = {
cancel: 'Cancel', cancel: 'Cancel',
any: 'Any', any: 'Any',
drawn: 'Drawn', drawn: 'Drawn',
drawn_or_typed: 'Drawn or Typed',
upload: 'Upload',
formula: 'Formula', formula: 'Formula',
typed: 'Typed', typed: 'Typed',
draw_field_on_the_document: 'Draw a field on the document', draw_field_on_the_document: 'Draw a field on the document',
@@ -126,7 +130,6 @@ const en = {
unlock_sms_verified_phone_number_field_with_paid_plan_use_text_field_for_phone_numbers_without_verification: 'Unlock SMS-verified phone number field with paid plan. Use text field for phone numbers without verification.', 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', available_only_in_pro: 'Available only in Pro',
failed_to_download_files: 'Failed to download files', failed_to_download_files: 'Failed to download files',
signature_is_too_small_please_redraw: 'Signature is too small - please redraw.',
please_add_fields_for_the_submitter_name_or_remove_the_submitter_name_if_not_needed: 'Please add fields for the {submitter_name}. Or, remove the {submitter_name} if not needed.', 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', draw_field: 'Draw {field} Field',
replace: 'Replace', replace: 'Replace',
@@ -149,10 +152,14 @@ const en = {
preferences: 'Preferences', preferences: 'Preferences',
available_in_pro: 'Available in Pro', available_in_pro: 'Available in Pro',
some_fields_are_missing_in_the_formula: 'Some fields are missing in the formula.', some_fields_are_missing_in_the_formula: 'Some fields are missing in the formula.',
learn_more: 'Learn more' learn_more: 'Learn more',
and: 'and',
or: 'or'
} }
const es = { 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', recurrent: 'Recurrente',
one_off: 'Único', one_off: 'Único',
editable: 'Editable', editable: 'Editable',
@@ -277,7 +284,6 @@ const es = {
unlock_sms_verified_phone_number_field_with_paid_plan_use_text_field_for_phone_numbers_without_verification: 'Desbloquea el campo de número de teléfono verificado por SMS con un plan pago. Usa el campo de texto para números de teléfono sin verificación.', 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', available_only_in_pro: 'Disponible solo en Pro',
failed_to_download_files: 'Error al descargar los archivos', failed_to_download_files: 'Error al descargar los archivos',
signature_is_too_small_please_redraw: 'La firma es demasiado pequeña. Por favor, dibújala de nuevo.',
please_add_fields_for_the_submitter_name_or_remove_the_submitter_name_if_not_needed: 'Por favor, añade campos para {submitter_name} o elimina {submitter_name} si no es necesario.', 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}', draw_field: 'Dibujar campo {field}',
replace: 'Reemplazar', replace: 'Reemplazar',
@@ -285,6 +291,8 @@ const es = {
add_document: 'Subir', add_document: 'Subir',
any: 'Cualquier', any: 'Cualquier',
drawn: 'Dibujado', drawn: 'Dibujado',
drawn_or_typed: 'Dibujado o Escrito',
upload: 'Subir',
typed: 'Escrito', typed: 'Escrito',
none: 'Ninguno', none: 'Ninguno',
ssn: 'SSN', ssn: 'SSN',
@@ -303,10 +311,14 @@ const es = {
preferences: 'Preferencias', preferences: 'Preferencias',
available_in_pro: 'Disponible en Pro', available_in_pro: 'Disponible en Pro',
some_fields_are_missing_in_the_formula: 'Faltan algunos campos en la fórmula.', some_fields_are_missing_in_the_formula: 'Faltan algunos campos en la fórmula.',
learn_more: 'Aprende más' learn_more: 'Aprende más',
and: 'y',
or: 'o'
} }
const it = { 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', ricorrente: 'Ricorrente',
una_volta: 'Una volta', una_volta: 'Una volta',
editable: 'Modificabile', editable: 'Modificabile',
@@ -354,6 +366,8 @@ const it = {
cancel: 'Annulla', cancel: 'Annulla',
any: 'Qualsiasi', any: 'Qualsiasi',
drawn: 'Disegnato', drawn: 'Disegnato',
drawn_or_typed: 'Disegnato o Digitato',
upload: 'Caricare',
formula: 'Formula', formula: 'Formula',
typed: 'Digitato', typed: 'Digitato',
draw_field_on_the_document: 'Disegnare un campo sul documento', draw_field_on_the_document: 'Disegnare un campo sul documento',
@@ -434,7 +448,6 @@ const it = {
unlock_sms_verified_phone_number_field_with_paid_plan_use_text_field_for_phone_numbers_without_verification: 'Sblocca il campo numero di telefono verificato tramite SMS con un piano a pagamento. Usa il campo di testo per numeri di telefono senza verifica.', 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', available_only_in_pro: 'Disponibile solo in Pro',
failed_to_download_files: 'Impossibile scaricare i file', failed_to_download_files: 'Impossibile scaricare i file',
signature_is_too_small_please_redraw: 'La firma è troppo piccola. Ridisegnala per favore.',
please_add_fields_for_the_submitter_name_or_remove_the_submitter_name_if_not_needed: 'Aggiungi campi per {submitter_name} o rimuovi {submitter_name} se non necessario.', 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}', draw_field: 'Disegna il campo {field}',
replace: 'Sostituisci', replace: 'Sostituisci',
@@ -457,10 +470,14 @@ const it = {
preferences: 'Preferenze', preferences: 'Preferenze',
available_in_pro: 'Disponibile in Pro', available_in_pro: 'Disponibile in Pro',
some_fields_are_missing_in_the_formula: 'Alcuni campi mancano nella formula.', some_fields_are_missing_in_the_formula: 'Alcuni campi mancano nella formula.',
learn_more: 'Scopri di più' learn_more: 'Scopri di più',
and: 'e',
or: 'o'
} }
const pt = { 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', recurrent: 'Recurrente',
one_off: 'Único', one_off: 'Único',
editable: 'Editável', editable: 'Editável',
@@ -585,7 +602,6 @@ const pt = {
unlock_sms_verified_phone_number_field_with_paid_plan_use_text_field_for_phone_numbers_without_verification: 'Desbloqueie o campo de número de telefone verificado por SMS com um plano pago. Use o campo de texto para números de telefone sem verificação.', 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', available_only_in_pro: 'Disponível apenas no Pro',
failed_to_download_files: 'Falha ao baixar arquivos', failed_to_download_files: 'Falha ao baixar arquivos',
signature_is_too_small_please_redraw: 'A assinatura é muito pequena. Por favor, redesenhe.',
please_add_fields_for_the_submitter_name_or_remove_the_submitter_name_if_not_needed: 'Adicione campos para {submitter_name} ou remova {submitter_name} se não for necessário.', 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}', draw_field: 'Desenhar campo {field}',
replace: 'Substituir', replace: 'Substituir',
@@ -593,6 +609,8 @@ const pt = {
add_document: 'Enviar', add_document: 'Enviar',
any: 'Qualquer', any: 'Qualquer',
drawn: 'Desenhado', drawn: 'Desenhado',
drawn_or_typed: 'Desenhado ou Digitado',
upload: 'Carregar',
typed: 'Digitado', typed: 'Digitado',
none: 'Nenhum', none: 'Nenhum',
ssn: 'SSN', ssn: 'SSN',
@@ -611,10 +629,14 @@ const pt = {
preferences: 'Preferências', preferences: 'Preferências',
available_in_pro: 'Disponível no Pro', available_in_pro: 'Disponível no Pro',
some_fields_are_missing_in_the_formula: 'Faltam alguns campos na fórmula.', some_fields_are_missing_in_the_formula: 'Faltam alguns campos na fórmula.',
learn_more: 'Saiba mais' learn_more: 'Saiba mais',
and: 'e',
or: 'ou'
} }
const fr = { 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', recurrent: 'Récurrent',
one_off: 'Ponctuel', one_off: 'Ponctuel',
editable: 'Éditable', editable: 'Éditable',
@@ -739,7 +761,6 @@ const fr = {
unlock_sms_verified_phone_number_field_with_paid_plan_use_text_field_for_phone_numbers_without_verification: 'Débloquez le champ de numéro de téléphone vérifié par SMS avec un plan payant. Utilisez un champ texte pour les numéros de téléphone sans vérification.', 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', available_only_in_pro: 'Disponible uniquement en Pro',
failed_to_download_files: 'Échec du téléchargement des fichiers', failed_to_download_files: 'Échec du téléchargement des fichiers',
signature_is_too_small_please_redraw: 'La signature est trop petite. Veuillez la redessiner.',
please_add_fields_for_the_submitter_name_or_remove_the_submitter_name_if_not_needed: 'Veuillez ajouter des champs pour {submitter_name} ou retirer {submitter_name} si ce n\'est pas nécessaire.', 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}', draw_field: 'Dessiner le champ {field}',
replace: 'Remplacer', replace: 'Remplacer',
@@ -747,6 +768,8 @@ const fr = {
add_document: 'Télécharger', add_document: 'Télécharger',
any: 'Tout', any: 'Tout',
drawn: 'Dessiné', drawn: 'Dessiné',
drawn_or_typed: 'Dessiné ou Tapé',
upload: 'Téléverser',
typed: 'Tapé', typed: 'Tapé',
none: 'Aucun', none: 'Aucun',
ssn: 'SSN', ssn: 'SSN',
@@ -765,10 +788,14 @@ const fr = {
preferences: 'Préférences', preferences: 'Préférences',
available_in_pro: 'Disponible en version Pro', available_in_pro: 'Disponible en version Pro',
some_fields_are_missing_in_the_formula: 'Certains champs manquent dans la formule.', some_fields_are_missing_in_the_formula: 'Certains champs manquent dans la formule.',
learn_more: 'En savoir plus' learn_more: 'En savoir plus',
and: 'et',
or: 'ou'
} }
const de = { 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', wiederkehrend: 'Wiederkehrend',
einmalig: 'Einmalig', einmalig: 'Einmalig',
editable: 'Bearbeitbar', editable: 'Bearbeitbar',
@@ -893,7 +920,6 @@ const de = {
unlock_sms_verified_phone_number_field_with_paid_plan_use_text_field_for_phone_numbers_without_verification: 'Schalte das SMS-verifizierte Telefonnummernfeld mit einem kostenpflichtigen Plan frei. Verwende das Textfeld für Telefonnummern ohne Verifizierung.', 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', available_only_in_pro: 'Nur in Pro verfügbar',
failed_to_download_files: 'Fehler beim Herunterladen der Dateien', failed_to_download_files: 'Fehler beim Herunterladen der Dateien',
signature_is_too_small_please_redraw: 'Die Unterschrift ist zu klein. Bitte erneut zeichnen.',
please_add_fields_for_the_submitter_name_or_remove_the_submitter_name_if_not_needed: 'Bitte füge Felder für {submitter_name} hinzu oder entferne {submitter_name}, falls nicht erforderlich.', 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', draw_field: 'Feld {field} zeichnen',
replace: 'Ersetzen', replace: 'Ersetzen',
@@ -901,6 +927,8 @@ const de = {
add_document: 'Hochladen', add_document: 'Hochladen',
any: 'Jede', any: 'Jede',
drawn: 'Gezeichnet', drawn: 'Gezeichnet',
drawn_or_typed: 'Gezeichnet oder getippt',
upload: 'Upload',
typed: 'Getippt', typed: 'Getippt',
none: 'Keine', none: 'Keine',
ssn: 'SSN', ssn: 'SSN',
@@ -919,7 +947,9 @@ const de = {
preferences: 'Einstellungen', preferences: 'Einstellungen',
available_in_pro: 'In Pro verfügbar', available_in_pro: 'In Pro verfügbar',
some_fields_are_missing_in_the_formula: 'Einige Felder fehlen in der Formel.', some_fields_are_missing_in_the_formula: 'Einige Felder fehlen in der Formel.',
learn_more: 'Erfahren Sie mehr' learn_more: 'Erfahren Sie mehr',
and: 'und',
or: 'oder'
} }
export { en, es, it, pt, fr, de } export { en, es, it, pt, fr, de }
@@ -77,7 +77,7 @@
<div class="w-full relative"> <div class="w-full relative">
<select <select
class="base-select !select-sm !h-10" class="base-select !select-sm !h-10"
:class="{ '!text-gray-300': !mapping.column_index }" :class="{ '!text-gray-300': !mapping.column_index && mapping.column_index != 0 }"
required required
@change="mapping.column_index = parseInt($event.target.value)" @change="mapping.column_index = parseInt($event.target.value)"
> >
@@ -279,13 +279,17 @@ export default {
this.mappings.forEach((mapping) => { this.mappings.forEach((mapping) => {
if (mapping.field_name && mapping.column_index != null) { 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())) { if (['name', 'email', 'phone', 'external_id'].includes(mapping.field_name.toLowerCase())) {
submittersIndex[mapping.submitter_uuid][mapping.field_name.toLowerCase()] = row[mapping.column_index] 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') { if (fieldType && fieldType !== 'phone') {
submittersIndex[mapping.submitter_uuid].fields.push({ submittersIndex[mapping.submitter_uuid].fields.push({
@@ -296,7 +300,7 @@ export default {
}) })
if (Object.keys(submittersIndex).length !== 0) { if (Object.keys(submittersIndex).length !== 0) {
submissions.push({ submitters: Object.values(submittersIndex) }) submissions.push({ submitters: this.submitters.map((s) => submittersIndex[s.uuid]).filter(Boolean) })
} }
}) })
@@ -342,7 +346,7 @@ export default {
return this.$el.closest('form') return this.$el.closest('form')
}, },
fieldTypes () { fieldTypes () {
return ['text', 'cells', 'date', 'number', 'radio', 'select', 'checkbox'] return ['text', 'cells', 'date', 'number', 'radio', 'select', 'checkbox', 'image']
}, },
defaultFields () { defaultFields () {
return [ return [
@@ -1,6 +1,6 @@
<template> <template>
<div class="absolute text-center w-full bottom-0 pr-3 mb-4"> <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"> <div class="flex items-center space-x-2">
<component <component
:is="fieldIcons[drawField.type]" :is="fieldIcons[drawField.type]"
@@ -1,6 +1,6 @@
<template> <template>
<span <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 <label
class="btn btn-neutral text-white btn-circle btn-lg group" class="btn btn-neutral text-white btn-circle btn-lg group"
@@ -12,9 +12,10 @@
height="28" height="28"
/> />
<IconX <IconX
class="hidden group-focus:inline" class="hidden group-focus:inline p-3"
width="28" width="64"
height="28" height="50"
@click="closeDropdown"
/> />
</label> </label>
<ul <ul
@@ -55,7 +56,7 @@
v-for="(icon, type) in fieldIconsSorted" v-for="(icon, type) in fieldIconsSorted"
:key="type" :key="type"
> >
<li v-if="(fieldTypes.length === 0 || fieldTypes.includes(type)) && (withPhone || type != 'phone') && (withPayment || type != 'payment')"> <li v-if="(fieldTypes.length === 0 || fieldTypes.includes(type)) && (withPhone || type != 'phone') && (withPayment || type != 'payment') && (withVerification || type != 'verification')">
<a <a
href="#" href="#"
class="text-sm py-1 px-2" class="text-sm py-1 px-2"
@@ -85,7 +86,7 @@ export default {
IconPlus, IconPlus,
IconX IconX
}, },
inject: ['withPhone', 'withPayment', 'backgroundColor', 't'], inject: ['withPhone', 'withPayment', 'withVerification', 'backgroundColor', 't'],
props: { props: {
modelValue: { modelValue: {
type: String, type: String,
@@ -178,7 +178,7 @@
<a <a
v-if="!isConnected" v-if="!isConnected"
class="block link text-center mt-1" 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" target="_blank"
data-turbo="false" data-turbo="false"
>{{ t('learn_more') }}</a> >{{ t('learn_more') }}</a>
+46 -2
View File
@@ -9,14 +9,30 @@
loading="lazy" loading="lazy"
> >
<div <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)" @click="$emit('scroll-to', item)"
> >
<div <div
v-if="editable" v-if="editable"
class="flex justify-between w-full" 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=""> <div class="">
<ReplaceButton <ReplaceButton
v-if="withReplaceButton" v-if="withReplaceButton"
@@ -73,19 +89,36 @@
/> />
</div> </div>
</div> </div>
<Teleport
v-if="isShowConditionsModal"
:to="modalContainerEl"
>
<ConditionsModal
:item="item"
:build-default-name="buildDefaultName"
@close="isShowConditionsModal = false"
/>
</Teleport>
</template> </template>
<script> <script>
import Contenteditable from './contenteditable' import Contenteditable from './contenteditable'
import Upload from './upload' import Upload from './upload'
import { IconRouteAltLeft } from '@tabler/icons-vue'
import ConditionsModal from './conditions_modal'
import ReplaceButton from './replace' import ReplaceButton from './replace'
import Field from './field'
import FieldType from './field_type'
export default { export default {
name: 'DocumentPreview', name: 'DocumentPreview',
components: { components: {
Contenteditable, Contenteditable,
IconRouteAltLeft,
ConditionsModal,
ReplaceButton ReplaceButton
}, },
inject: ['t'],
props: { props: {
item: { item: {
type: Object, type: Object,
@@ -121,13 +154,24 @@ export default {
} }
}, },
emits: ['scroll-to', 'change', 'remove', 'up', 'down', 'replace'], emits: ['scroll-to', 'change', 'remove', 'up', 'down', 'replace'],
data () {
return {
isShowConditionsModal: false
}
},
computed: { computed: {
fieldNames: FieldType.computed.fieldNames,
fieldLabels: FieldType.computed.fieldLabels,
previewImage () { previewImage () {
return [...this.document.preview_images].sort((a, b) => parseInt(a.filename) - parseInt(b.filename))[0] 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: { methods: {
upload: Upload.methods.upload, upload: Upload.methods.upload,
buildDefaultName: Field.methods.buildDefaultName,
onUpdateName (value) { onUpdateName (value) {
this.item.name = value this.item.name = value
+9 -26
View File
@@ -10,10 +10,6 @@ class ProcessSubmitterCompletionJob
is_all_completed = !submitter.submission.submitters.exists?(completed_at: nil) is_all_completed = !submitter.submission.submitters.exists?(completed_at: nil)
if !is_all_completed && submitter.submission.submitters_order_preserved?
enqueue_next_submitter_request_notification(submitter)
end
Submissions::EnsureResultGenerated.call(submitter) Submissions::EnsureResultGenerated.call(submitter)
if is_all_completed && submitter.completed_at == submitter.submission.submitters.maximum(:completed_at) if is_all_completed && submitter.completed_at == submitter.submission.submitters.maximum(:completed_at)
@@ -28,6 +24,10 @@ class ProcessSubmitterCompletionJob
create_completed_documents!(submitter) create_completed_documents!(submitter)
if !is_all_completed && submitter.submission.submitters_order_preserved? && params['send_invitation_email'] != false
enqueue_next_submitter_request_notification(submitter)
end
enqueue_completed_webhooks(submitter, is_all_completed:) enqueue_completed_webhooks(submitter, is_all_completed:)
end end
@@ -63,32 +63,15 @@ class ProcessSubmitterCompletionJob
end end
def enqueue_completed_webhooks(submitter, is_all_completed: false) def enqueue_completed_webhooks(submitter, is_all_completed: false)
webhook_config = Accounts.load_webhook_config(submitter.account) WebhookUrls.for_account_id(submitter.account_id, %w[form.completed submission.completed]).each do |webhook|
if webhook_config
SendFormCompletedWebhookRequestJob.perform_async({ 'submitter_id' => submitter.id,
'encrypted_config_id' => webhook_config.id })
end
webhook_urls = submitter.account.webhook_urls
webhook_urls = webhook_urls.where(
Arel::Table.new(:webhook_urls)[:events].matches('%"form.completed"%')
).or(
webhook_urls.where(
Arel::Table.new(:webhook_urls)[:events].matches('%"submission.completed"%')
)
)
webhook_urls.each do |webhook|
if webhook.events.include?('form.completed') if webhook.events.include?('form.completed')
SendFormCompletedWebhookRequestJob.perform_async({ 'submitter_id' => submitter.id, SendFormCompletedWebhookRequestJob.perform_async('submitter_id' => submitter.id,
'webhook_url_id' => webhook.id }) 'webhook_url_id' => webhook.id)
end end
if webhook.events.include?('submission.completed') && is_all_completed if webhook.events.include?('submission.completed') && is_all_completed
SendSubmissionCompletedWebhookRequestJob.perform_async({ 'submission_id' => submitter.submission_id, SendSubmissionCompletedWebhookRequestJob.perform_async('submission_id' => submitter.submission_id,
'webhook_url_id' => webhook.id }) 'webhook_url_id' => webhook.id)
end end
end end
end end
@@ -5,33 +5,32 @@ class SendFormCompletedWebhookRequestJob
sidekiq_options queue: :webhooks sidekiq_options queue: :webhooks
USER_AGENT = 'DocuSeal.co Webhook' USER_AGENT = 'DocuSeal.com Webhook'
MAX_ATTEMPTS = 10 MAX_ATTEMPTS = 10
def perform(params = {}) def perform(params = {})
submitter = Submitter.find(params['submitter_id']) submitter = Submitter.find(params['submitter_id'])
webhook_url = WebhookUrl.find(params['webhook_url_id'])
attempt = params['attempt'].to_i attempt = params['attempt'].to_i
url, secret = load_url_and_secret(submitter, params) return if webhook_url.url.blank? || webhook_url.events.exclude?('form.completed')
return if url.blank?
Submissions::EnsureResultGenerated.call(submitter) Submissions::EnsureResultGenerated.call(submitter)
ActiveStorage::Current.url_options = Docuseal.default_url_options ActiveStorage::Current.url_options = Docuseal.default_url_options
resp = begin resp = begin
Faraday.post(url, Faraday.post(webhook_url.url,
{ {
event_type: 'form.completed', event_type: 'form.completed',
timestamp: Time.current, timestamp: Time.current,
data: Submitters::SerializeForWebhook.call(submitter) data: Submitters::SerializeForWebhook.call(submitter)
}.to_json, }.to_json,
**secret.to_h,
'Content-Type' => 'application/json', 'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT) 'User-Agent' => USER_AGENT,
**webhook_url.secret.to_h)
rescue Faraday::Error rescue Faraday::Error
nil nil
end end
@@ -45,27 +44,4 @@ class SendFormCompletedWebhookRequestJob
}) })
end end
end end
def load_url_and_secret(submitter, params)
if params['encrypted_config_id']
config = EncryptedConfig.find(params['encrypted_config_id'])
url = config.value
return if url.blank?
preferences = Accounts.load_webhook_preferences(submitter.submission.account)
return if preferences['form.completed'] == false
secret = EncryptedConfig.find_or_initialize_by(account_id: config.account_id,
key: EncryptedConfig::WEBHOOK_SECRET_KEY)&.value.to_h
[url, secret]
elsif params['webhook_url_id']
webhook_url = submitter.account.webhook_urls.find(params['webhook_url_id'])
webhook_url.url if webhook_url.events.include?('form.completed')
end
end
end end
@@ -5,34 +5,28 @@ class SendFormDeclinedWebhookRequestJob
sidekiq_options queue: :webhooks sidekiq_options queue: :webhooks
USER_AGENT = 'DocuSeal.co Webhook' USER_AGENT = 'DocuSeal.com Webhook'
MAX_ATTEMPTS = 10 MAX_ATTEMPTS = 10
def perform(params = {}) def perform(params = {})
submitter = Submitter.find(params['submitter_id']) submitter = Submitter.find(params['submitter_id'])
webhook_url = WebhookUrl.find(params['webhook_url_id'])
attempt = params['attempt'].to_i attempt = params['attempt'].to_i
config = Accounts.load_webhook_config(submitter.submission.account)
url = config&.value.presence
return if url.blank? return if webhook_url.url.blank? || webhook_url.events.exclude?('form.declined')
preferences = Accounts.load_webhook_preferences(submitter.submission.account)
return if preferences['form.declined'] == false
ActiveStorage::Current.url_options = Docuseal.default_url_options ActiveStorage::Current.url_options = Docuseal.default_url_options
resp = begin resp = begin
Faraday.post(url, Faraday.post(webhook_url.url,
{ {
event_type: 'form.declined', event_type: 'form.declined',
timestamp: Time.current, timestamp: Time.current,
data: Submitters::SerializeForWebhook.call(submitter) data: Submitters::SerializeForWebhook.call(submitter)
}.to_json, }.to_json,
**EncryptedConfig.find_or_initialize_by(account_id: config.account_id, **webhook_url.secret.to_h,
key: EncryptedConfig::WEBHOOK_SECRET_KEY)&.value.to_h,
'Content-Type' => 'application/json', 'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT) 'User-Agent' => USER_AGENT)
rescue Faraday::Error rescue Faraday::Error
@@ -43,6 +37,7 @@ class SendFormDeclinedWebhookRequestJob
(!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan)) (!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
SendFormDeclinedWebhookRequestJob.perform_in((2**attempt).minutes, { SendFormDeclinedWebhookRequestJob.perform_in((2**attempt).minutes, {
'submitter_id' => submitter.id, 'submitter_id' => submitter.id,
'webhook_url_id' => webhook_url.id,
'attempt' => attempt + 1, 'attempt' => attempt + 1,
'last_status' => resp&.status.to_i 'last_status' => resp&.status.to_i
}) })
@@ -5,34 +5,28 @@ class SendFormStartedWebhookRequestJob
sidekiq_options queue: :webhooks sidekiq_options queue: :webhooks
USER_AGENT = 'DocuSeal.co Webhook' USER_AGENT = 'DocuSeal.com Webhook'
MAX_ATTEMPTS = 10 MAX_ATTEMPTS = 10
def perform(params = {}) def perform(params = {})
submitter = Submitter.find(params['submitter_id']) submitter = Submitter.find(params['submitter_id'])
webhook_url = WebhookUrl.find(params['webhook_url_id'])
attempt = params['attempt'].to_i attempt = params['attempt'].to_i
config = Accounts.load_webhook_config(submitter.submission.account)
url = config&.value.presence
return if url.blank? return if webhook_url.url.blank? || webhook_url.events.exclude?('form.started')
preferences = Accounts.load_webhook_preferences(submitter.submission.account)
return if preferences['form.started'] == false
ActiveStorage::Current.url_options = Docuseal.default_url_options ActiveStorage::Current.url_options = Docuseal.default_url_options
resp = begin resp = begin
Faraday.post(url, Faraday.post(webhook_url.url,
{ {
event_type: 'form.started', event_type: 'form.started',
timestamp: Time.current, timestamp: Time.current,
data: Submitters::SerializeForWebhook.call(submitter) data: Submitters::SerializeForWebhook.call(submitter)
}.to_json, }.to_json,
**EncryptedConfig.find_or_initialize_by(account_id: config.account_id, **webhook_url.secret.to_h,
key: EncryptedConfig::WEBHOOK_SECRET_KEY)&.value.to_h,
'Content-Type' => 'application/json', 'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT) 'User-Agent' => USER_AGENT)
rescue Faraday::Error rescue Faraday::Error
@@ -43,6 +37,7 @@ class SendFormStartedWebhookRequestJob
(!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan)) (!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
SendFormStartedWebhookRequestJob.perform_in((2**attempt).minutes, { SendFormStartedWebhookRequestJob.perform_in((2**attempt).minutes, {
'submitter_id' => submitter.id, 'submitter_id' => submitter.id,
'webhook_url_id' => webhook_url.id,
'attempt' => attempt + 1, 'attempt' => attempt + 1,
'last_status' => resp&.status.to_i 'last_status' => resp&.status.to_i
}) })
@@ -5,34 +5,28 @@ class SendFormViewedWebhookRequestJob
sidekiq_options queue: :webhooks sidekiq_options queue: :webhooks
USER_AGENT = 'DocuSeal.co Webhook' USER_AGENT = 'DocuSeal.com Webhook'
MAX_ATTEMPTS = 10 MAX_ATTEMPTS = 10
def perform(params = {}) def perform(params = {})
submitter = Submitter.find(params['submitter_id']) submitter = Submitter.find(params['submitter_id'])
webhook_url = WebhookUrl.find(params['webhook_url_id'])
attempt = params['attempt'].to_i attempt = params['attempt'].to_i
config = Accounts.load_webhook_config(submitter.submission.account)
url = config&.value.presence
return if url.blank? return if webhook_url.url.blank? || webhook_url.events.exclude?('form.viewed')
preferences = Accounts.load_webhook_preferences(submitter.submission.account)
return if preferences['form.viewed'] == false
ActiveStorage::Current.url_options = Docuseal.default_url_options ActiveStorage::Current.url_options = Docuseal.default_url_options
resp = begin resp = begin
Faraday.post(url, Faraday.post(webhook_url.url,
{ {
event_type: 'form.viewed', event_type: 'form.viewed',
timestamp: Time.current, timestamp: Time.current,
data: Submitters::SerializeForWebhook.call(submitter) data: Submitters::SerializeForWebhook.call(submitter)
}.to_json, }.to_json,
**EncryptedConfig.find_or_initialize_by(account_id: config.account_id, **webhook_url.secret.to_h,
key: EncryptedConfig::WEBHOOK_SECRET_KEY)&.value.to_h,
'Content-Type' => 'application/json', 'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT) 'User-Agent' => USER_AGENT)
rescue Faraday::Error rescue Faraday::Error
@@ -43,6 +37,7 @@ class SendFormViewedWebhookRequestJob
(!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan)) (!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
SendFormViewedWebhookRequestJob.perform_in((2**attempt).minutes, { SendFormViewedWebhookRequestJob.perform_in((2**attempt).minutes, {
'submitter_id' => submitter.id, 'submitter_id' => submitter.id,
'webhook_url_id' => webhook_url.id,
'attempt' => attempt + 1, 'attempt' => attempt + 1,
'last_status' => resp&.status.to_i 'last_status' => resp&.status.to_i
}) })
@@ -5,33 +5,26 @@ class SendSubmissionArchivedWebhookRequestJob
sidekiq_options queue: :webhooks sidekiq_options queue: :webhooks
USER_AGENT = 'DocuSeal.co Webhook' USER_AGENT = 'DocuSeal.com Webhook'
MAX_ATTEMPTS = 10 MAX_ATTEMPTS = 10
def perform(params = {}) def perform(params = {})
submission = Submission.find(params['submission_id']) submission = Submission.find(params['submission_id'])
webhook_url = WebhookUrl.find(params['webhook_url_id'])
attempt = params['attempt'].to_i attempt = params['attempt'].to_i
config = Accounts.load_webhook_config(submission.account) return if webhook_url.url.blank? || webhook_url.events.exclude?('submission.archived')
url = config&.value.presence
return if url.blank?
preferences = Accounts.load_webhook_preferences(submission.account)
return if preferences['submission.archived'].blank?
resp = begin resp = begin
Faraday.post(url, Faraday.post(webhook_url.url,
{ {
event_type: 'submission.archived', event_type: 'submission.archived',
timestamp: Time.current, timestamp: Time.current,
data: submission.as_json(only: %i[id archived_at]) data: submission.as_json(only: %i[id archived_at])
}.to_json, }.to_json,
**EncryptedConfig.find_or_initialize_by(account_id: config.account_id, **webhook_url.secret.to_h,
key: EncryptedConfig::WEBHOOK_SECRET_KEY)&.value.to_h,
'Content-Type' => 'application/json', 'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT) 'User-Agent' => USER_AGENT)
rescue Faraday::Error rescue Faraday::Error
@@ -42,6 +35,7 @@ class SendSubmissionArchivedWebhookRequestJob
(!Docuseal.multitenant? || submission.account.account_configs.exists?(key: :plan)) (!Docuseal.multitenant? || submission.account.account_configs.exists?(key: :plan))
SendSubmissionArchivedWebhookRequestJob.perform_in((2**attempt).minutes, { SendSubmissionArchivedWebhookRequestJob.perform_in((2**attempt).minutes, {
'submission_id' => submission.id, 'submission_id' => submission.id,
'webhook_url_id' => webhook_url.id,
'attempt' => attempt + 1, 'attempt' => attempt + 1,
'last_status' => resp&.status.to_i 'last_status' => resp&.status.to_i
}) })
@@ -5,32 +5,28 @@ class SendSubmissionCompletedWebhookRequestJob
sidekiq_options queue: :webhooks sidekiq_options queue: :webhooks
USER_AGENT = 'DocuSeal.co Webhook' USER_AGENT = 'DocuSeal.com Webhook'
MAX_ATTEMPTS = 10 MAX_ATTEMPTS = 10
def perform(params = {}) def perform(params = {})
submission = Submission.find(params['submission_id']) submission = Submission.find(params['submission_id'])
webhook_url = WebhookUrl.find(params['webhook_url_id'])
attempt = params['attempt'].to_i attempt = params['attempt'].to_i
webhook_url = submission.account.webhook_urls.find(params['webhook_url_id']) return if webhook_url.url.blank? || webhook_url.events.exclude?('submission.completed')
url = webhook_url.url if webhook_url.events.include?('submission.completed')
return if url.blank?
resp = begin resp = begin
Faraday.post(url, Faraday.post(webhook_url.url,
{ {
event_type: 'submission.completed', event_type: 'submission.completed',
timestamp: Time.current, timestamp: Time.current,
data: Submissions::SerializeForApi.call(submission) data: Submissions::SerializeForApi.call(submission)
}.to_json, }.to_json,
**EncryptedConfig.find_or_initialize_by(account_id: submission.account_id,
key: EncryptedConfig::WEBHOOK_SECRET_KEY)&.value.to_h,
'Content-Type' => 'application/json', 'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT) 'User-Agent' => USER_AGENT,
**webhook_url.secret.to_h)
rescue Faraday::Error rescue Faraday::Error
nil nil
end end
@@ -5,33 +5,26 @@ class SendSubmissionCreatedWebhookRequestJob
sidekiq_options queue: :webhooks sidekiq_options queue: :webhooks
USER_AGENT = 'DocuSeal.co Webhook' USER_AGENT = 'DocuSeal.com Webhook'
MAX_ATTEMPTS = 10 MAX_ATTEMPTS = 10
def perform(params = {}) def perform(params = {})
submission = Submission.find(params['submission_id']) submission = Submission.find(params['submission_id'])
webhook_url = WebhookUrl.find(params['webhook_url_id'])
attempt = params['attempt'].to_i attempt = params['attempt'].to_i
config = Accounts.load_webhook_config(submission.account) return if webhook_url.url.blank? || webhook_url.events.exclude?('submission.created')
url = config&.value.presence
return if url.blank?
preferences = Accounts.load_webhook_preferences(submission.account)
return if preferences['submission.created'].blank?
resp = begin resp = begin
Faraday.post(url, Faraday.post(webhook_url.url,
{ {
event_type: 'submission.created', event_type: 'submission.created',
timestamp: Time.current, timestamp: Time.current,
data: Submissions::SerializeForApi.call(submission) data: Submissions::SerializeForApi.call(submission)
}.to_json, }.to_json,
**EncryptedConfig.find_or_initialize_by(account_id: config.account_id, **webhook_url.secret.to_h,
key: EncryptedConfig::WEBHOOK_SECRET_KEY)&.value.to_h,
'Content-Type' => 'application/json', 'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT) 'User-Agent' => USER_AGENT)
rescue Faraday::Error rescue Faraday::Error
@@ -42,6 +35,7 @@ class SendSubmissionCreatedWebhookRequestJob
(!Docuseal.multitenant? || submission.account.account_configs.exists?(key: :plan)) (!Docuseal.multitenant? || submission.account.account_configs.exists?(key: :plan))
SendSubmissionCreatedWebhookRequestJob.perform_in((2**attempt).minutes, { SendSubmissionCreatedWebhookRequestJob.perform_in((2**attempt).minutes, {
'submission_id' => submission.id, 'submission_id' => submission.id,
'webhook_url_id' => webhook_url.id,
'attempt' => attempt + 1, 'attempt' => attempt + 1,
'last_status' => resp&.status.to_i 'last_status' => resp&.status.to_i
}) })
@@ -5,33 +5,26 @@ class SendTemplateCreatedWebhookRequestJob
sidekiq_options queue: :webhooks sidekiq_options queue: :webhooks
USER_AGENT = 'DocuSeal.co Webhook' USER_AGENT = 'DocuSeal.com Webhook'
MAX_ATTEMPTS = 10 MAX_ATTEMPTS = 10
def perform(params = {}) def perform(params = {})
template = Template.find(params['template_id']) template = Template.find(params['template_id'])
webhook_url = WebhookUrl.find(params['webhook_url_id'])
attempt = params['attempt'].to_i attempt = params['attempt'].to_i
config = Accounts.load_webhook_config(template.account) return if webhook_url.url.blank? || webhook_url.events.exclude?('template.created')
url = config&.value.presence
return if url.blank?
preferences = Accounts.load_webhook_preferences(template.account)
return if preferences['template.created'].blank?
resp = begin resp = begin
Faraday.post(url, Faraday.post(webhook_url.url,
{ {
event_type: 'template.created', event_type: 'template.created',
timestamp: Time.current, timestamp: Time.current,
data: Templates::SerializeForApi.call(template) data: Templates::SerializeForApi.call(template)
}.to_json, }.to_json,
**EncryptedConfig.find_or_initialize_by(account_id: config.account_id, **webhook_url.secret.to_h,
key: EncryptedConfig::WEBHOOK_SECRET_KEY)&.value.to_h,
'Content-Type' => 'application/json', 'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT) 'User-Agent' => USER_AGENT)
rescue Faraday::Error rescue Faraday::Error
@@ -42,6 +35,7 @@ class SendTemplateCreatedWebhookRequestJob
(!Docuseal.multitenant? || template.account.account_configs.exists?(key: :plan)) (!Docuseal.multitenant? || template.account.account_configs.exists?(key: :plan))
SendTemplateCreatedWebhookRequestJob.perform_in((2**attempt).minutes, { SendTemplateCreatedWebhookRequestJob.perform_in((2**attempt).minutes, {
'template_id' => template.id, 'template_id' => template.id,
'webhook_url_id' => webhook_url.id,
'attempt' => attempt + 1, 'attempt' => attempt + 1,
'last_status' => resp&.status.to_i 'last_status' => resp&.status.to_i
}) })
@@ -5,33 +5,26 @@ class SendTemplateUpdatedWebhookRequestJob
sidekiq_options queue: :webhooks sidekiq_options queue: :webhooks
USER_AGENT = 'DocuSeal.co Webhook' USER_AGENT = 'DocuSeal.com Webhook'
MAX_ATTEMPTS = 10 MAX_ATTEMPTS = 10
def perform(params = {}) def perform(params = {})
template = Template.find(params['template_id']) template = Template.find(params['template_id'])
webhook_url = WebhookUrl.find(params['webhook_url_id'])
attempt = params['attempt'].to_i attempt = params['attempt'].to_i
config = Accounts.load_webhook_config(template.account) return if webhook_url.url.blank? || webhook_url.events.exclude?('template.updated')
url = config&.value.presence
return if url.blank?
preferences = Accounts.load_webhook_preferences(template.account)
return if preferences['template.updated'].blank?
resp = begin resp = begin
Faraday.post(url, Faraday.post(webhook_url.url,
{ {
event_type: 'template.updated', event_type: 'template.updated',
timestamp: Time.current, timestamp: Time.current,
data: Templates::SerializeForApi.call(template) data: Templates::SerializeForApi.call(template)
}.to_json, }.to_json,
**EncryptedConfig.find_or_initialize_by(account_id: config.account_id, **webhook_url.secret.to_h,
key: EncryptedConfig::WEBHOOK_SECRET_KEY)&.value.to_h,
'Content-Type' => 'application/json', 'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT) 'User-Agent' => USER_AGENT)
rescue Faraday::Error rescue Faraday::Error
@@ -42,6 +35,7 @@ class SendTemplateUpdatedWebhookRequestJob
(!Docuseal.multitenant? || template.account.account_configs.exists?(key: :plan)) (!Docuseal.multitenant? || template.account.account_configs.exists?(key: :plan))
SendTemplateUpdatedWebhookRequestJob.perform_in((2**attempt).minutes, { SendTemplateUpdatedWebhookRequestJob.perform_in((2**attempt).minutes, {
'template_id' => template.id, 'template_id' => template.id,
'webhook_url_id' => webhook_url.id,
'attempt' => attempt + 1, 'attempt' => attempt + 1,
'last_status' => resp&.status.to_i 'last_status' => resp&.status.to_i
}) })
+1 -1
View File
@@ -16,7 +16,7 @@ class ApplicationMailer < ActionMailer::Base
after_action :set_message_uuid after_action :set_message_uuid
def default_url_options def default_url_options
Docuseal.default_url_options Docuseal.default_url_options.merge(host: ENV.fetch('EMAIL_HOST', Docuseal.default_url_options[:host]))
end end
def set_message_metadata def set_message_metadata
+24 -10
View File
@@ -106,8 +106,11 @@ class SubmitterMailer < ApplicationMailer
@email_config = AccountConfigs.find_for_account(@current_account, AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY) @email_config = AccountConfigs.find_for_account(@current_account, AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY)
@documents = add_completed_email_attachments!( add_completed_email_attachments!(
submitter, with_audit_log: @submitter.template.preferences['documents_copy_email_attach_audit'] != false && submitter,
with_documents: @submitter.template.preferences['documents_copy_email_attach_documents'] != false &&
(@email_config.nil? || @email_config.value['attach_documents'] != false),
with_audit_log: @submitter.template.preferences['documents_copy_email_attach_audit'] != false &&
(@email_config.nil? || @email_config.value['attach_audit_log'] != false) (@email_config.nil? || @email_config.value['attach_audit_log'] != false)
) )
@@ -138,9 +141,11 @@ class SubmitterMailer < ApplicationMailer
private private
def build_submitter_reply_to(submitter) def build_submitter_reply_to(submitter)
reply_to = reply_to = submitter.preferences['reply_to'].presence
submitter.preferences['reply_to'].presence ||
(submitter.submission.created_by_user || submitter.template.author)&.friendly_name&.sub(/\+\w+@/, '@') if reply_to.blank? && (submitter.submission.created_by_user || submitter.template.author)&.email != submitter.email
reply_to = (submitter.submission.created_by_user || submitter.template.author)&.friendly_name&.sub(/\+\w+@/, '@')
end
return nil if reply_to.to_s.match?(NO_REPLY_REGEXP) return nil if reply_to.to_s.match?(NO_REPLY_REGEXP)
@@ -150,6 +155,9 @@ class SubmitterMailer < ApplicationMailer
def add_completed_email_attachments!(submitter, with_audit_log: true, with_documents: true) def add_completed_email_attachments!(submitter, with_audit_log: true, with_documents: true)
documents = with_documents ? Submitters.select_attachments_for_download(submitter) : [] documents = with_documents ? Submitters.select_attachments_for_download(submitter) : []
filename_format = AccountConfig.find_or_initialize_by(account_id: submitter.account_id,
key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value
total_size = 0 total_size = 0
audit_trail_data = nil audit_trail_data = nil
@@ -159,9 +167,14 @@ class SubmitterMailer < ApplicationMailer
total_size = audit_trail_data.size total_size = audit_trail_data.size
end end
total_size = add_attachments_with_size_limit(documents, total_size) total_size = add_attachments_with_size_limit(submitter, documents, total_size, filename_format)
attachments[submitter.submission.audit_trail.filename.to_s.tr('"', "'")] = audit_trail_data if audit_trail_data if audit_trail_data
audit_trail_filename =
Submitters.build_document_filename(submitter, submitter.submission.audit_trail.blob, filename_format)
attachments[audit_trail_filename.tr('"', "'")] = audit_trail_data
end
if with_documents if with_documents
file_fields = submitter.submission.template_fields.select { |e| e['type'].in?(%w[file payment]) } file_fields = submitter.submission.template_fields.select { |e| e['type'].in?(%w[file payment]) }
@@ -170,7 +183,7 @@ class SubmitterMailer < ApplicationMailer
storage_attachments = storage_attachments =
submitter.attachments.where(uuid: submitter.values.values_at(*file_fields.pluck('uuid')).flatten) submitter.attachments.where(uuid: submitter.values.values_at(*file_fields.pluck('uuid')).flatten)
add_attachments_with_size_limit(storage_attachments, total_size) add_attachments_with_size_limit(submitter, storage_attachments, total_size)
end end
end end
@@ -181,7 +194,7 @@ class SubmitterMailer < ApplicationMailer
user.role == 'integration' ? user.friendly_name.sub(/\+\w+@/, '@') : user.friendly_name user.role == 'integration' ? user.friendly_name.sub(/\+\w+@/, '@') : user.friendly_name
end end
def add_attachments_with_size_limit(storage_attachments, current_size) def add_attachments_with_size_limit(submitter, storage_attachments, current_size, filename_format = nil)
total_size = current_size total_size = current_size
storage_attachments.each do |attachment| storage_attachments.each do |attachment|
@@ -189,7 +202,8 @@ class SubmitterMailer < ApplicationMailer
break if total_size >= MAX_ATTACHMENTS_SIZE break if total_size >= MAX_ATTACHMENTS_SIZE
attachments[attachment.filename.to_s.tr('"', "'")] = attachment.download filename = Submitters.build_document_filename(submitter, attachment.blob, filename_format)
attachments[filename.to_s.tr('"', "'")] = attachment.download
end end
total_size total_size
+2 -1
View File
@@ -35,7 +35,6 @@ class AccountConfig < ApplicationRecord
FORM_WITH_CONFETTI_KEY = 'form_with_confetti' FORM_WITH_CONFETTI_KEY = 'form_with_confetti'
FORM_PREFILL_SIGNATURE_KEY = 'form_prefill_signature' FORM_PREFILL_SIGNATURE_KEY = 'form_prefill_signature'
ESIGNING_PREFERENCE_KEY = 'esigning_preference' ESIGNING_PREFERENCE_KEY = 'esigning_preference'
WEBHOOK_PREFERENCES_KEY = 'webhook_preferences'
DOWNLOAD_LINKS_AUTH_KEY = 'download_links_auth' DOWNLOAD_LINKS_AUTH_KEY = 'download_links_auth'
FORCE_SSO_AUTH_KEY = 'force_sso_auth' FORCE_SSO_AUTH_KEY = 'force_sso_auth'
FLATTEN_RESULT_PDF_KEY = 'flatten_result_pdf' FLATTEN_RESULT_PDF_KEY = 'flatten_result_pdf'
@@ -44,6 +43,8 @@ class AccountConfig < ApplicationRecord
REQUIRE_SIGNING_REASON_KEY = 'require_signing_reason' REQUIRE_SIGNING_REASON_KEY = 'require_signing_reason'
REUSE_SIGNATURE_KEY = 'reuse_signature' REUSE_SIGNATURE_KEY = 'reuse_signature'
COMBINE_PDF_RESULT_KEY = 'combine_pdf_result_key' COMBINE_PDF_RESULT_KEY = 'combine_pdf_result_key'
DOCUMENT_FILENAME_FORMAT_KEY = 'document_filename_format'
POLICY_LINKS_KEY = 'policy_links'
DEFAULT_VALUES = { DEFAULT_VALUES = {
SUBMITTER_INVITATION_EMAIL_KEY => lambda { SUBMITTER_INVITATION_EMAIL_KEY => lambda {
+1 -3
View File
@@ -26,9 +26,7 @@ class EncryptedConfig < ApplicationRecord
EMAIL_SMTP_KEY = 'action_mailer_smtp', EMAIL_SMTP_KEY = 'action_mailer_smtp',
ESIGN_CERTS_KEY = 'esign_certs', ESIGN_CERTS_KEY = 'esign_certs',
TIMESTAMP_SERVER_URL_KEY = 'timestamp_server_url', TIMESTAMP_SERVER_URL_KEY = 'timestamp_server_url',
APP_URL_KEY = 'app_url', APP_URL_KEY = 'app_url'
WEBHOOK_URL_KEY = 'webhook_url',
WEBHOOK_SECRET_KEY = 'webhook_secret'
].freeze ].freeze
belongs_to :account belongs_to :account
+9 -2
View File
@@ -57,13 +57,20 @@ class Submission < ApplicationRecord
has_many_attached :preview_documents has_many_attached :preview_documents
has_many :template_accesses, primary_key: :template_id, foreign_key: :template_id, dependent: nil, inverse_of: false
has_many :template_schema_documents, has_many :template_schema_documents,
->(e) { where(uuid: (e.template_schema.presence || e.template.schema).pluck('attachment_uuid')) }, ->(e) { where(uuid: (e.template_schema.presence || e.template.schema).pluck('attachment_uuid')) },
through: :template, source: :documents_attachments through: :template, source: :documents_attachments
scope :active, -> { where(archived_at: nil) } scope :active, -> { where(archived_at: nil) }
scope :pending, -> { joins(:submitters).where(submitters: { completed_at: nil }).distinct } scope :pending, -> { joins(:submitters).where(submitters: { completed_at: nil }).group(:id) }
scope :completed, -> { where.not(id: pending.select(:submission_id)) } scope :completed, lambda {
where.not(Submitter.where(Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id])
.and(Submitter.arel_table[:completed_at].eq(nil))).select(1).arel.exists)
}
scope :declined, -> { joins(:submitters).where.not(submitters: { declined_at: nil }).group(:id) }
scope :expired, -> { where(expire_at: ..Time.current) }
enum :source, { enum :source, {
invite: 'invite', invite: 'invite',
+2
View File
@@ -46,6 +46,8 @@ class SubmissionEvent < ApplicationRecord
click_sms: 'click_sms', click_sms: 'click_sms',
phone_verified: 'phone_verified', phone_verified: 'phone_verified',
start_form: 'start_form', start_form: 'start_form',
start_verification: 'start_verification',
complete_verification: 'complete_verification',
view_form: 'view_form', view_form: 'view_form',
invite_party: 'invite_party', invite_party: 'invite_party',
complete_form: 'complete_form', complete_form: 'complete_form',
+2
View File
@@ -53,6 +53,8 @@ class Submitter < ApplicationRecord
has_many_attached :documents has_many_attached :documents
has_many_attached :attachments has_many_attached :attachments
has_many_attached :preview_documents
has_many :template_accesses, through: :template
has_many :document_generation_events, dependent: :destroy has_many :document_generation_events, dependent: :destroy
has_many :submission_events, dependent: :destroy has_many :submission_events, dependent: :destroy
+1
View File
@@ -62,6 +62,7 @@ class Template < ApplicationRecord
has_many :submissions, dependent: :destroy has_many :submissions, dependent: :destroy
has_many :template_sharings, dependent: :destroy has_many :template_sharings, dependent: :destroy
has_many :template_accesses, dependent: :destroy
scope :active, -> { where(archived_at: nil) } scope :active, -> { where(archived_at: nil) }
scope :archived, -> { where.not(archived_at: nil) } scope :archived, -> { where.not(archived_at: nil) }
+26
View File
@@ -0,0 +1,26 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: template_accesses
#
# id :bigint not null, primary key
# created_at :datetime not null
# updated_at :datetime not null
# template_id :bigint not null
# user_id :bigint not null
#
# Indexes
#
# index_template_accesses_on_template_id_and_user_id (template_id,user_id) UNIQUE
#
# Foreign Keys
#
# fk_rails_... (template_id => templates.id)
#
class TemplateAccess < ApplicationRecord
ADMIN_USER_ID = -1
belongs_to :template
belongs_to :user, optional: true
end
+1
View File
@@ -57,6 +57,7 @@ class User < ApplicationRecord
belongs_to :account belongs_to :account
has_one :access_token, dependent: :destroy has_one :access_token, dependent: :destroy
has_many :access_tokens, dependent: :destroy
has_many :templates, dependent: :destroy, foreign_key: :author_id, inverse_of: :author has_many :templates, dependent: :destroy, foreign_key: :author_id, inverse_of: :author
has_many :template_folders, dependent: :destroy, foreign_key: :author_id, inverse_of: :author has_many :template_folders, dependent: :destroy, foreign_key: :author_id, inverse_of: :author
has_many :user_configs, dependent: :destroy has_many :user_configs, dependent: :destroy
+1
View File
@@ -24,6 +24,7 @@ class UserConfig < ApplicationRecord
SIGNATURE_KEY = 'signature' SIGNATURE_KEY = 'signature'
INITIALS_KEY = 'initials' INITIALS_KEY = 'initials'
RECEIVE_COMPLETED_EMAIL = 'receive_completed_email' RECEIVE_COMPLETED_EMAIL = 'receive_completed_email'
RECEIVE_DECLINED_EMAIL = 'receive_declined_email'
belongs_to :user belongs_to :user
+16 -2
View File
@@ -6,6 +6,7 @@
# #
# id :bigint not null, primary key # id :bigint not null, primary key
# events :text not null # events :text not null
# secret :text not null
# sha1 :string not null # sha1 :string not null
# url :text not null # url :text not null
# created_at :datetime not null # created_at :datetime not null
@@ -22,15 +23,28 @@
# fk_rails_... (account_id => accounts.id) # fk_rails_... (account_id => accounts.id)
# #
class WebhookUrl < ApplicationRecord class WebhookUrl < ApplicationRecord
EVENTS = %w[
form.viewed
form.started
form.completed
form.declined
template.created
template.updated
submission.created
submission.archived
].freeze
belongs_to :account belongs_to :account
attribute :events, :string, default: -> { [] } attribute :events, :string, default: -> { %w[form.viewed form.started form.completed form.declined] }
attribute :secret, :string, default: -> { {} }
serialize :events, coder: JSON serialize :events, coder: JSON
serialize :secret, coder: JSON
before_validation :set_sha1 before_validation :set_sha1
encrypts :url encrypts :url, :secret
def set_sha1 def set_sha1
self.sha1 = Digest::SHA1.hexdigest(url) self.sha1 = Digest::SHA1.hexdigest(url)
+1
View File
@@ -1,3 +1,4 @@
<%= content_for(:canonical_url, new_user_session_url) %>
<div class="max-w-lg mx-auto px-2"> <div class="max-w-lg mx-auto px-2">
<%= render 'devise/shared/select_server' if Docuseal.multitenant? %> <%= render 'devise/shared/select_server' if Docuseal.multitenant? %>
<h1 class="text-4xl font-bold text-center mt-8"><%= t('sign_in') %></h1> <h1 class="text-4xl font-bold text-center mt-8"><%= t('sign_in') %></h1>
Loaded 100 of 233 files, more files were not shown because too many files have changed in this diff. Show more