Compare commits

..
106 Commits
Author SHA1 Message Date
Alex Turchyn 50acff2e81 Merge from docusealco/wip 2024-11-04 16:13:29 +02:00
Pete Matsyburka cb6d24dfca add canonical urls 2024-11-04 11:37:58 +02:00
Pete Matsyburka ab4a22d8ba fix test links 2024-11-03 13:43:28 +02:00
Pete Matsyburka bc40a15de1 use com 2024-11-03 10:19:59 +02:00
Pete Matsyburka 2542f26d96 adjust webhooks 2024-11-03 09:49:30 +02:00
Alex Turchyn 84edb6dbda use webhook urls instead of encrypted configs 2024-11-02 22:12:41 +02:00
Pete Matsyburka 88776e7316 Merge from docusealco/wip 2024-11-02 14:48:21 +02:00
Pete Matsyburka f669045362 redirect com 2024-11-02 09:48:47 +02:00
Pete Matsyburka 68d3efbdba use com 2024-11-02 09:02:50 +02:00
Pete Matsyburka 22d4833496 allow to unarchive submissions 2024-11-01 21:37:02 +02:00
Pete Matsyburka ff14caa9f7 fix image 2024-11-01 19:01:51 +02:00
Pete Matsyburka 05647135bc add status to export 2024-10-31 17:22:14 +02:00
Pete Matsyburka ae76c2a4a8 disable draw when type 2024-10-31 16:43:43 +02:00
Pete Matsyburka fb2b3d44fe fix bulk 2024-10-31 00:51:21 +02:00
Pete Matsyburka 3868557efd add define submitters 2024-10-30 23:07:10 +02:00
dependabot[bot] 9bef28c8ba Bump rexml from 3.3.8 to 3.3.9 in the bundler group across 1 directory (#50) 2024-10-30 12:12:36 +02:00
Pete Matsyburka f25fd3b03d do not edit if declined 2024-10-29 17:11:44 +02:00
Pete Matsyburka d1d7571ee4 disable decline email 2024-10-29 16:44:07 +02:00
Pete Matsyburka 6a298a068c fix updated templates 2024-10-29 12:11:21 +02:00
Pete Matsyburka d4aef79b68 use tsa fallback url 2024-10-29 08:29:26 +02:00
Alex Turchyn 05d578d88f Merge from docusealco/wip 2024-10-28 14:44:22 +02:00
Pete Matsyburka 8773b30b10 adjust auto migration 2024-10-27 14:26:52 +02:00
Pete Matsyburka 1b636c5878 fix completed submitters 2024-10-26 19:46:22 +03:00
Pete Matsyburka 46e948ab99 download combined completed 2024-10-26 19:30:23 +03:00
Pete Matsyburka 889103aa5e use com 2024-10-26 09:54:41 +03:00
Pete Matsyburka 558a14a5f2 fix email validate 2024-10-25 22:17:23 +03:00
Pete Matsyburka 72bbdb38bf mask key 2024-10-25 16:17:57 +03:00
Pete Matsyburka 3975a03cb4 adjust from email 2024-10-24 23:32:36 +03:00
Pete Matsyburka e398327fb7 refactor 2024-10-24 20:27:29 +03:00
Oleksandr Turchyn 9266694d65 add integration users page 2024-10-24 20:09:57 +03:00
Pete Matsyburka 350c4b1c2a do not submit invalid field value 2024-10-24 18:45:08 +03:00
Pete Matsyburka c3a933d87d fix fields detection 2024-10-24 17:56:02 +03:00
Pete Matsyburka 970e53a5a3 remove log 2024-10-24 12:30:14 +03:00
Pete Matsyburka 520bac2ec7 add decline account preferences toggle 2024-10-24 12:15:32 +03:00
Pete Matsyburka e1eddd31c4 fix field condition 2024-10-24 12:08:10 +03:00
Oleksandr Turchyn 3c616a824c show error for cross-environment API key usage 2024-10-24 12:07:58 +03:00
Pete Matsyburka 90d86b58c2 adjust migration 2024-10-23 22:57:33 +03:00
Pete Matsyburka 4332b4ebb8 fix specs 2024-10-23 12:01:55 +03:00
Oleksandr Turchyn e2f930f6f7 add completed submitters and documents 2024-10-23 12:01:43 +03:00
Pete Matsyburka c303159c63 adjust email interceptor 2024-10-23 10:46:53 +03:00
Pete Matsyburka c1ce2bec00 fix update name 2024-10-23 10:45:30 +03:00
Oleksandr Turchyn 3d2046f227 add validation for submitters count 2024-10-22 22:27:11 +03:00
Pete Matsyburka babaf1aa48 adjust error message 2024-10-22 18:18:14 +03:00
Pete Matsyburka 3f03d343ca Revert "dry run email validation"
This reverts commit 910490528d.
2024-10-22 17:41:25 +03:00
Pete Matsyburka 110cdb5123 fix specs 2024-10-22 15:56:59 +03:00
Pete Matsyburka 440d1114be fix email validation 2024-10-22 09:58:07 +03:00
Pete Matsyburka 9032517d4e smtp settings 2024-10-21 23:40:56 +03:00
Pete Matsyburka 7c71fefc47 optimize query 2024-10-21 14:34:11 +03:00
Pete Matsyburka e8bd8ff76f fix cdn url 2024-10-21 12:33:36 +03:00
Pete Matsyburka fed420cae1 Merge pull request #39 from docusealco/wip 2024-10-21 12:15:46 +03:00
Pete Matsyburka 910490528d dry run email validation 2024-10-20 15:44:24 +03:00
Alex Turchyn ab810be250 validate email format 2024-10-20 15:42:20 +03:00
Alex Turchyn 178ee47093 add more specs 2024-10-20 15:26:14 +03:00
Alex Turchyn 6f90147a11 validate the uniqueness of submitter roles 2024-10-20 15:26:04 +03:00
Pete Matsyburka 3ebaacbb7a locale date format 2024-10-20 09:48:39 +03:00
Pete Matsyburka 64b39b53a2 fix com from 2024-10-19 10:09:53 +03:00
Pete Matsyburka 2948910c25 fix com selector 2024-10-19 09:41:13 +03:00
Pete Matsyburka 1d20c79cf4 com host 2024-10-18 23:15:55 +03:00
Pete Matsyburka 48f3b13f3e adjust smtp from 2024-10-18 23:07:40 +03:00
Pete Matsyburka 1fc1e9af16 fix font size 2024-10-18 17:48:31 +03:00
Pete Matsyburka ce70016603 update rails 2024-10-16 23:58:37 +03:00
Pete Matsyburka e0e79e343d update email 2024-10-16 12:16:20 +03:00
Pete Matsyburka 6568f81ee9 adjust pdf generation 2024-10-15 20:58:08 +03:00
Oleksandr Turchyn 386f130d7a allow to paste date in date field 2024-10-15 19:31:25 +03:00
Pete Matsyburka 3ee41975fd add documents preview endpoint 2024-10-15 19:31:24 +03:00
Pete Matsyburka d9f314898b with style false 2024-10-14 12:28:26 +03:00
Pete Matsyburka 4fc7412cac fix ios 18 heif images 2024-10-14 12:28:26 +03:00
Pete Matsyburka 9adbfc52c2 fix signing date format 2024-10-14 12:28:26 +03:00
Pete Matsyburka 1ad8d9196a optimize phone step 2024-10-14 12:28:26 +03:00
Oleksandr Turchyn e9172392b5 add the phone code selector 2024-10-14 12:28:26 +03:00
Pete Matsyburka d6d94de328 fix submit form step 2024-10-14 12:28:26 +03:00
Alex Turchyn 77261f457c fix selector colors 2024-10-14 12:28:26 +03:00
Pete Matsyburka c258915031 fix z index 2024-10-14 12:28:26 +03:00
Pete Matsyburka c1db8bdd3b rename job 2024-10-14 12:28:26 +03:00
Pete Matsyburka c700484ce8 add reuse signature config 2024-10-14 12:28:26 +03:00
Pete Matsyburka b62bd200ed fix layout 2024-10-14 12:28:26 +03:00
Pete Matsyburka a164fe8dd8 fix sign 2024-10-14 12:28:26 +03:00
Pete Matsyburka bb40e872b3 fix custom message 2024-10-14 12:28:26 +03:00
Pete Matsyburka bab80e9488 fix image preview format 2024-10-14 12:28:26 +03:00
Pete Matsyburka d090d79ebf process images async 2024-10-14 12:28:26 +03:00
Pete Matsyburka 2299ba4e29 fix clear button 2024-10-14 12:28:26 +03:00
Pete Matsyburka d6474f0e6e adjust submitter edit 2024-10-14 12:28:26 +03:00
Oleksandr Turchyn e0a297cf81 add ability to edit submitter details 2024-10-14 12:28:26 +03:00
Pete Matsyburka 40f5fa4217 add recurrent payment 2024-10-14 12:28:26 +03:00
Oleksandr Turchyn 94e552d170 improve translations 2024-10-07 15:30:39 +03:00
Pete Matsyburka cf1b0a4c16 add noverify smtp option 2024-10-07 15:30:39 +03:00
Pete Matsyburka 9aa0a8ecbf skip autocorrect tld 2024-10-07 15:30:39 +03:00
Pete Matsyburka fd4bef1706 fix i18n 2024-10-07 15:30:39 +03:00
Pete Matsyburka d32b0e6d20 add completed id 2024-10-07 15:30:39 +03:00
Pete Matsyburka 4a846be214 fix js 2024-10-07 15:30:39 +03:00
Pete Matsyburka 3dd3877ea9 update deps 2024-10-07 15:30:39 +03:00
Pete Matsyburka b4dba0d809 fix smtp without password 2024-10-07 15:30:39 +03:00
Pete Matsyburka dea80a82d8 fix pagination 2024-10-07 15:30:39 +03:00
Pete Matsyburka eefa994724 rails 7.2 2024-10-07 15:30:39 +03:00
Pete Matsyburka 35cce24539 adjust audit values 2024-10-07 15:30:39 +03:00
Pete Matsyburka e6c8203959 fix variables 2024-10-07 15:30:39 +03:00
Oleksandr Turchyn 9479771fc8 fix i18n 2024-10-07 15:30:39 +03:00
Pete Matsyburka 711427521c fix file url performance 2024-10-07 15:30:39 +03:00
Alex Turchyn 5dae742361 add I18n invitation email 2024-10-07 15:30:39 +03:00
Pete Matsyburka d8ccd4beee i18n signature 2024-10-07 15:30:39 +03:00
Pete Matsyburka c532a0115d remove audit log file links 2024-10-07 15:30:39 +03:00
Alex Turchyn c9c3364764 add matcher 2024-10-07 15:30:39 +03:00
Alex Turchyn 1ff23d7c40 add I18n Audit log 2024-10-07 15:30:39 +03:00
Pete Matsyburka 95e9b34033 fix sign field name 2024-10-07 15:30:39 +03:00
Pete Matsyburka 6b42dcf91b adjust combined pdf sign reason 2024-10-07 15:30:39 +03:00
Pete Matsyburka 28962c949b configure audit log values 2024-10-07 15:30:39 +03:00
177 changed files with 5357 additions and 1011 deletions

No files matched your search

+1 -1
View File
@@ -66,7 +66,7 @@ RSpec/ExampleLength:
Max: 40 Max: 40
RSpec/MultipleMemoizedHelpers: RSpec/MultipleMemoizedHelpers:
Max: 6 Max: 9
Metrics/BlockNesting: Metrics/BlockNesting:
Max: 4 Max: 4
+2 -2
View File
@@ -41,7 +41,7 @@ ENV OPENSSL_CONF=/app/openssl_legacy.cnf
WORKDIR /app WORKDIR /app
RUN apk add --no-cache sqlite-dev libpq-dev mariadb-dev vips-dev vips-poppler poppler-utils redis vips-heif gcompat ttf-freefont && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf RUN echo '@edge https://dl-cdn.alpinelinux.org/alpine/edge/community' >> /etc/apk/repositories && apk add --no-cache sqlite-dev libpq-dev mariadb-dev vips-dev vips-poppler poppler-utils redis libheif@edge vips-heif gcompat ttf-freefont && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf
RUN echo $'.include = /etc/ssl/openssl.cnf\n\ RUN echo $'.include = /etc/ssl/openssl.cnf\n\
\n\ \n\
@@ -57,7 +57,7 @@ activate = 1' >> /app/openssl_legacy.cnf
COPY ./Gemfile ./Gemfile.lock ./ COPY ./Gemfile ./Gemfile.lock ./
RUN apk add --no-cache build-base && bundle install && apk del build-base && rm -rf ~/.bundle /usr/local/bundle/cache && ruby -e "puts Dir['/usr/local/bundle/**/{spec,rdoc,resources/shared,resources/collation,resources/locales}']" | xargs rm -rf RUN apk add --no-cache build-base && bundle install && apk del --no-cache build-base && rm -rf ~/.bundle /usr/local/bundle/cache && ruby -e "puts Dir['/usr/local/bundle/**/{spec,rdoc,resources/shared,resources/collation,resources/locales}']" | xargs rm -rf
COPY ./bin ./bin COPY ./bin ./bin
COPY ./app ./app COPY ./app ./app
+1 -2
View File
@@ -23,7 +23,6 @@ gem 'image_processing'
gem 'jwt' gem 'jwt'
gem 'lograge' gem 'lograge'
gem 'mysql2', require: false gem 'mysql2', require: false
gem 'net-smtp', '0.4.0'
gem 'oj' gem 'oj'
gem 'pagy' gem 'pagy'
gem 'pg', require: false gem 'pg', require: false
@@ -40,7 +39,7 @@ gem 'ruby-vips'
gem 'rubyXL' gem 'rubyXL'
gem 'shakapacker' gem 'shakapacker'
gem 'sidekiq' gem 'sidekiq'
gem 'sqlite3', require: false, force_ruby_platform: true gem 'sqlite3', require: false
gem 'strip_attributes' gem 'strip_attributes'
gem 'turbo-rails' gem 'turbo-rails'
gem 'twitter_cldr', require: false gem 'twitter_cldr', require: false
+170 -164
View File
@@ -1,105 +1,101 @@
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
actioncable (7.1.3.4) actioncable (7.2.1.1)
actionpack (= 7.1.3.4) actionpack (= 7.2.1.1)
activesupport (= 7.1.3.4) activesupport (= 7.2.1.1)
nio4r (~> 2.0) nio4r (~> 2.0)
websocket-driver (>= 0.6.1) websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6) zeitwerk (~> 2.6)
actionmailbox (7.1.3.4) actionmailbox (7.2.1.1)
actionpack (= 7.1.3.4) actionpack (= 7.2.1.1)
activejob (= 7.1.3.4) activejob (= 7.2.1.1)
activerecord (= 7.1.3.4) activerecord (= 7.2.1.1)
activestorage (= 7.1.3.4) activestorage (= 7.2.1.1)
activesupport (= 7.1.3.4) activesupport (= 7.2.1.1)
mail (>= 2.7.1) mail (>= 2.8.0)
net-imap actionmailer (7.2.1.1)
net-pop actionpack (= 7.2.1.1)
net-smtp actionview (= 7.2.1.1)
actionmailer (7.1.3.4) activejob (= 7.2.1.1)
actionpack (= 7.1.3.4) activesupport (= 7.2.1.1)
actionview (= 7.1.3.4) mail (>= 2.8.0)
activejob (= 7.1.3.4)
activesupport (= 7.1.3.4)
mail (~> 2.5, >= 2.5.4)
net-imap
net-pop
net-smtp
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
actionpack (7.1.3.4) actionpack (7.2.1.1)
actionview (= 7.1.3.4) actionview (= 7.2.1.1)
activesupport (= 7.1.3.4) activesupport (= 7.2.1.1)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
racc 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)
actiontext (7.1.3.4) useragent (~> 0.16)
actionpack (= 7.1.3.4) actiontext (7.2.1.1)
activerecord (= 7.1.3.4) actionpack (= 7.2.1.1)
activestorage (= 7.1.3.4) activerecord (= 7.2.1.1)
activesupport (= 7.1.3.4) activestorage (= 7.2.1.1)
activesupport (= 7.2.1.1)
globalid (>= 0.6.0) globalid (>= 0.6.0)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
actionview (7.1.3.4) actionview (7.2.1.1)
activesupport (= 7.1.3.4) activesupport (= 7.2.1.1)
builder (~> 3.1) builder (~> 3.1)
erubi (~> 1.11) erubi (~> 1.11)
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6) rails-html-sanitizer (~> 1.6)
activejob (7.1.3.4) activejob (7.2.1.1)
activesupport (= 7.1.3.4) activesupport (= 7.2.1.1)
globalid (>= 0.3.6) globalid (>= 0.3.6)
activemodel (7.1.3.4) activemodel (7.2.1.1)
activesupport (= 7.1.3.4) activesupport (= 7.2.1.1)
activerecord (7.1.3.4) activerecord (7.2.1.1)
activemodel (= 7.1.3.4) activemodel (= 7.2.1.1)
activesupport (= 7.1.3.4) activesupport (= 7.2.1.1)
timeout (>= 0.4.0) timeout (>= 0.4.0)
activestorage (7.1.3.4) activestorage (7.2.1.1)
actionpack (= 7.1.3.4) actionpack (= 7.2.1.1)
activejob (= 7.1.3.4) activejob (= 7.2.1.1)
activerecord (= 7.1.3.4) activerecord (= 7.2.1.1)
activesupport (= 7.1.3.4) activesupport (= 7.2.1.1)
marcel (~> 1.0) marcel (~> 1.0)
activesupport (7.1.3.4) activesupport (7.2.1.1)
base64 base64
bigdecimal bigdecimal
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.3.1)
connection_pool (>= 2.2.5) connection_pool (>= 2.2.5)
drb drb
i18n (>= 1.6, < 2) i18n (>= 1.6, < 2)
logger (>= 1.4.2)
minitest (>= 5.1) minitest (>= 5.1)
mutex_m securerandom (>= 0.3)
tzinfo (~> 2.0) tzinfo (~> 2.0, >= 2.0.5)
addressable (2.8.6) addressable (2.8.7)
public_suffix (>= 2.0.2, < 6.0) public_suffix (>= 2.0.2, < 7.0)
annotate (3.2.0) annotate (3.2.0)
activerecord (>= 3.2, < 8.0) activerecord (>= 3.2, < 8.0)
rake (>= 10.4, < 14.0) rake (>= 10.4, < 14.0)
arabic-letter-connector (0.1.1) arabic-letter-connector (0.1.1)
ast (2.4.2) ast (2.4.2)
aws-eventstream (1.3.0) aws-eventstream (1.3.0)
aws-partitions (1.913.0) aws-partitions (1.985.0)
aws-sdk-core (3.191.6) aws-sdk-core (3.209.1)
aws-eventstream (~> 1, >= 1.3.0) aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.651.0) aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.8) aws-sigv4 (~> 1.9)
jmespath (~> 1, >= 1.6.1) jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.79.0) aws-sdk-kms (1.94.0)
aws-sdk-core (~> 3, >= 3.191.0) aws-sdk-core (~> 3, >= 3.207.0)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.146.1) aws-sdk-s3 (1.167.0)
aws-sdk-core (~> 3, >= 3.191.0) aws-sdk-core (~> 3, >= 3.207.0)
aws-sdk-kms (~> 1) aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.8) aws-sigv4 (~> 1.5)
aws-sdk-secretsmanager (1.91.0) aws-sdk-secretsmanager (1.108.0)
aws-sdk-core (~> 3, >= 3.191.0) aws-sdk-core (~> 3, >= 3.207.0)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.5)
aws-sigv4 (1.8.0) aws-sigv4 (1.10.0)
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)
@@ -120,14 +116,14 @@ GEM
smart_properties smart_properties
bigdecimal (3.1.8) bigdecimal (3.1.8)
bindex (0.8.1) bindex (0.8.1)
bootsnap (1.18.3) bootsnap (1.18.4)
msgpack (~> 1.2) msgpack (~> 1.2)
builder (3.3.0) builder (3.3.0)
bullet (7.1.6) bullet (7.2.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
uniform_notifier (~> 1.11) uniform_notifier (~> 1.11)
camertron-eprun (1.1.1) camertron-eprun (1.1.1)
cancancan (3.5.0) cancancan (3.6.1)
capybara (3.40.0) capybara (3.40.0)
addressable addressable
matrix matrix
@@ -137,7 +133,8 @@ GEM
rack-test (>= 0.6.3) rack-test (>= 0.6.3)
regexp_parser (>= 1.5, < 3.0) regexp_parser (>= 1.5, < 3.0)
xpath (~> 3.2) xpath (~> 3.2)
childprocess (5.0.0) childprocess (5.1.0)
logger (~> 1.5)
chunky_png (1.4.0) chunky_png (1.4.0)
cldr-plurals-runtime-rb (1.1.0) cldr-plurals-runtime-rb (1.1.0)
cmdparse (3.0.7) cmdparse (3.0.7)
@@ -148,12 +145,12 @@ GEM
bigdecimal bigdecimal
rexml rexml
crass (1.0.6) crass (1.0.6)
css_parser (1.17.1) css_parser (1.19.0)
addressable addressable
csv (3.3.0) csv (3.3.0)
cuprite (0.15) cuprite (0.15.1)
capybara (~> 3.0) capybara (~> 3.0)
ferrum (~> 0.14.0) ferrum (~> 0.15.0)
date (3.3.4) date (3.3.4)
debug (1.9.2) debug (1.9.2)
irb (~> 1.10) irb (~> 1.10)
@@ -173,26 +170,26 @@ GEM
diff-lcs (1.5.1) diff-lcs (1.5.1)
digest-crc (0.6.5) digest-crc (0.6.5)
rake (>= 12.0.0, < 14.0.0) rake (>= 12.0.0, < 14.0.0)
docile (1.4.0) docile (1.4.1)
dotenv (3.1.0) dotenv (3.1.4)
drb (2.2.1) drb (2.2.1)
email_typo (0.2.3) email_typo (0.2.3)
erb_lint (0.5.0) erb_lint (0.6.0)
activesupport activesupport
better_html (>= 2.0.1) better_html (>= 2.0.1)
parser (>= 2.7.1.4) parser (>= 2.7.1.4)
rainbow rainbow
rubocop rubocop (>= 1)
smart_properties smart_properties
erubi (1.13.0) erubi (1.13.0)
factory_bot (6.4.6) factory_bot (6.5.0)
activesupport (>= 5.0.0) activesupport (>= 5.0.0)
factory_bot_rails (6.4.3) factory_bot_rails (6.4.3)
factory_bot (~> 6.4) factory_bot (~> 6.4)
railties (>= 5.0.0) railties (>= 5.0.0)
faker (3.3.1) faker (3.4.2)
i18n (>= 1.8.11, < 2) i18n (>= 1.8.11, < 2)
faraday (1.10.3) faraday (1.10.4)
faraday-em_http (~> 1.0) faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0) faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1) faraday-excon (~> 1.1)
@@ -212,58 +209,60 @@ GEM
faraday-httpclient (1.0.1) faraday-httpclient (1.0.1)
faraday-multipart (1.0.4) faraday-multipart (1.0.4)
multipart-post (~> 2) multipart-post (~> 2)
faraday-net_http (1.0.1) faraday-net_http (1.0.2)
faraday-net_http_persistent (1.2.0) faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0) faraday-patron (1.0.0)
faraday-rack (1.0.0) faraday-rack (1.0.0)
faraday-retry (1.0.3) faraday-retry (1.0.3)
faraday_middleware (1.2.0) faraday_middleware (1.2.1)
faraday (~> 1.0) faraday (~> 1.0)
ferrum (0.14) ferrum (0.15)
addressable (~> 2.5) addressable (~> 2.5)
concurrent-ruby (~> 1.1) concurrent-ruby (~> 1.1)
webrick (~> 1.7) webrick (~> 1.7)
websocket-driver (>= 0.6, < 0.8) websocket-driver (~> 0.7)
ffi (1.16.3) ffi (1.17.0)
ffi (1.17.0-aarch64-linux-gnu)
ffi (1.17.0-x86_64-linux-gnu)
geom2d (0.4.1) geom2d (0.4.1)
globalid (1.2.1) globalid (1.2.1)
activesupport (>= 6.1) activesupport (>= 6.1)
google-apis-core (0.14.1) google-apis-core (0.15.1)
addressable (~> 2.5, >= 2.5.1) addressable (~> 2.5, >= 2.5.1)
googleauth (~> 1.9) googleauth (~> 1.9)
httpclient (>= 2.8.1, < 3.a) httpclient (>= 2.8.3, < 3.a)
mini_mime (~> 1.0) mini_mime (~> 1.0)
mutex_m
representable (~> 3.0) representable (~> 3.0)
retriable (>= 2.0, < 4.a) retriable (>= 2.0, < 4.a)
rexml google-apis-iamcredentials_v1 (0.21.0)
google-apis-iamcredentials_v1 (0.20.0) google-apis-core (>= 0.15.0, < 2.a)
google-apis-core (>= 0.14.0, < 2.a) google-apis-storage_v1 (0.46.0)
google-apis-storage_v1 (0.37.0) google-apis-core (>= 0.15.0, < 2.a)
google-apis-core (>= 0.14.0, < 2.a) google-cloud-core (1.7.1)
google-cloud-core (1.7.0)
google-cloud-env (>= 1.0, < 3.a) google-cloud-env (>= 1.0, < 3.a)
google-cloud-errors (~> 1.0) google-cloud-errors (~> 1.0)
google-cloud-env (2.1.1) google-cloud-env (2.2.1)
faraday (>= 1.0, < 3.a) faraday (>= 1.0, < 3.a)
google-cloud-errors (1.4.0) google-cloud-errors (1.4.0)
google-cloud-storage (1.49.0) google-cloud-storage (1.52.0)
addressable (~> 2.8) addressable (~> 2.8)
digest-crc (~> 0.4) digest-crc (~> 0.4)
google-apis-core (~> 0.13) google-apis-core (~> 0.13)
google-apis-iamcredentials_v1 (~> 0.18) google-apis-iamcredentials_v1 (~> 0.18)
google-apis-storage_v1 (~> 0.33) google-apis-storage_v1 (~> 0.38)
google-cloud-core (~> 1.6) google-cloud-core (~> 1.6)
googleauth (~> 1.9) googleauth (~> 1.9)
mini_mime (~> 1.0) mini_mime (~> 1.0)
googleauth (1.11.0) googleauth (1.11.1)
faraday (>= 1.0, < 3.a) faraday (>= 1.0, < 3.a)
google-cloud-env (~> 2.1) google-cloud-env (~> 2.1)
jwt (>= 1.4, < 3.0) jwt (>= 1.4, < 3.0)
multi_json (~> 1.11) multi_json (~> 1.11)
os (>= 0.9, < 2.0) os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a) signet (>= 0.16, < 2.a)
hashdiff (1.1.0) hashdiff (1.1.1)
hexapdf (0.42.0) hexapdf (0.47.0)
cmdparse (~> 3.0, >= 3.0.3) cmdparse (~> 3.0, >= 3.0.3)
geom2d (~> 0.4, >= 0.4.1) geom2d (~> 0.4, >= 0.4.1)
openssl (>= 2.2.1) openssl (>= 2.2.1)
@@ -271,28 +270,29 @@ GEM
httpclient (2.8.3) httpclient (2.8.3)
i18n (1.14.6) i18n (1.14.6)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
image_processing (1.12.2) image_processing (1.13.0)
mini_magick (>= 4.9.5, < 5) mini_magick (>= 4.9.5, < 5)
ruby-vips (>= 2.0.17, < 3) ruby-vips (>= 2.0.17, < 3)
io-console (0.7.2) io-console (0.7.2)
irb (1.14.0) irb (1.14.1)
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.7.2)
jwt (2.8.1) jwt (2.9.3)
base64 base64
language_server-protocol (3.17.0.3) language_server-protocol (3.17.0.3)
launchy (3.0.0) launchy (3.0.1)
addressable (~> 2.8) addressable (~> 2.8)
childprocess (~> 5.0) childprocess (~> 5.0)
letter_opener (1.10.0) letter_opener (1.10.0)
launchy (>= 2.2, < 4) launchy (>= 2.2, < 4)
letter_opener_web (2.0.0) letter_opener_web (3.0.0)
actionmailer (>= 5.2) actionmailer (>= 6.1)
letter_opener (~> 1.7) letter_opener (~> 1.9)
railties (>= 5.2) railties (>= 6.1)
rexml rexml
logger (1.6.1)
lograge (0.14.0) lograge (0.14.0)
actionpack (>= 4) actionpack (>= 4)
activesupport (>= 4) activesupport (>= 4)
@@ -309,25 +309,25 @@ GEM
marcel (1.0.4) marcel (1.0.4)
matrix (0.4.2) matrix (0.4.2)
method_source (1.1.0) method_source (1.1.0)
mini_magick (4.12.0) mini_magick (4.13.2)
mini_mime (1.1.5) mini_mime (1.1.5)
mini_portile2 (2.8.7) mini_portile2 (2.8.7)
minitest (5.25.1) minitest (5.25.1)
msgpack (1.7.2) msgpack (1.7.3)
multi_json (1.15.0) multi_json (1.15.0)
multipart-post (2.4.0) multipart-post (2.4.1)
mutex_m (0.2.0) mutex_m (0.2.0)
mysql2 (0.5.6) mysql2 (0.5.6)
net-http-persistent (4.0.2) net-http-persistent (4.0.4)
connection_pool (~> 2.2) connection_pool (~> 2.2)
net-imap (0.4.14) net-imap (0.4.17)
date date
net-protocol net-protocol
net-pop (0.1.2) net-pop (0.1.2)
net-protocol net-protocol
net-protocol (0.2.2) net-protocol (0.2.2)
timeout timeout
net-smtp (0.4.0) net-smtp (0.5.0)
net-protocol net-protocol
nio4r (2.7.3) nio4r (2.7.3)
nokogiri (1.16.7) nokogiri (1.16.7)
@@ -337,21 +337,23 @@ GEM
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.16.7-x86_64-linux) nokogiri (1.16.7-x86_64-linux)
racc (~> 1.4) racc (~> 1.4)
oj (3.16.3) oj (3.16.6)
bigdecimal (>= 3.0) bigdecimal (>= 3.0)
ostruct (>= 0.2)
openssl (3.2.0) openssl (3.2.0)
orm_adapter (0.5.0) orm_adapter (0.5.0)
os (1.1.4) os (1.1.4)
ostruct (0.6.0)
package_json (0.1.0) package_json (0.1.0)
pagy (8.1.2) pagy (9.1.0)
parallel (1.26.3) parallel (1.26.3)
parser (3.3.5.0) parser (3.3.5.0)
ast (~> 2.4.1) ast (~> 2.4.1)
racc racc
pg (1.5.6) pg (1.5.8)
premailer (1.23.0) premailer (1.27.0)
addressable addressable
css_parser (>= 1.12.0) css_parser (>= 1.19.0)
htmlentities (>= 4.0.0) htmlentities (>= 4.0.0)
premailer-rails (1.12.0) premailer-rails (1.12.0)
actionmailer (>= 3) actionmailer (>= 3)
@@ -362,15 +364,15 @@ GEM
pry (0.14.2) pry (0.14.2)
coderay (~> 1.1) coderay (~> 1.1)
method_source (~> 1.0) method_source (~> 1.0)
pry-rails (0.3.9) pry-rails (0.3.11)
pry (>= 0.10.4) pry (>= 0.13.0)
psych (5.1.2) psych (5.1.2)
stringio stringio
public_suffix (5.0.5) public_suffix (6.0.1)
puma (6.4.3) puma (6.4.3)
nio4r (~> 2.0) nio4r (~> 2.0)
racc (1.8.1) racc (1.8.1)
rack (3.1.7) rack (3.1.8)
rack-proxy (0.7.7) rack-proxy (0.7.7)
rack rack
rack-session (2.0.0) rack-session (2.0.0)
@@ -380,20 +382,20 @@ GEM
rackup (2.1.0) rackup (2.1.0)
rack (>= 3) rack (>= 3)
webrick (~> 1.8) webrick (~> 1.8)
rails (7.1.3.4) rails (7.2.1.1)
actioncable (= 7.1.3.4) actioncable (= 7.2.1.1)
actionmailbox (= 7.1.3.4) actionmailbox (= 7.2.1.1)
actionmailer (= 7.1.3.4) actionmailer (= 7.2.1.1)
actionpack (= 7.1.3.4) actionpack (= 7.2.1.1)
actiontext (= 7.1.3.4) actiontext (= 7.2.1.1)
actionview (= 7.1.3.4) actionview (= 7.2.1.1)
activejob (= 7.1.3.4) activejob (= 7.2.1.1)
activemodel (= 7.1.3.4) activemodel (= 7.2.1.1)
activerecord (= 7.1.3.4) activerecord (= 7.2.1.1)
activestorage (= 7.1.3.4) activestorage (= 7.2.1.1)
activesupport (= 7.1.3.4) activesupport (= 7.2.1.1)
bundler (>= 1.15.0) bundler (>= 1.15.0)
railties (= 7.1.3.4) railties (= 7.2.1.1)
rails-dom-testing (2.2.0) rails-dom-testing (2.2.0)
activesupport (>= 5.0.0) activesupport (>= 5.0.0)
minitest minitest
@@ -408,10 +410,10 @@ GEM
actionview (> 3.1) actionview (> 3.1)
activesupport (> 3.1) activesupport (> 3.1)
railties (> 3.1) railties (> 3.1)
railties (7.1.3.4) railties (7.2.1.1)
actionpack (= 7.1.3.4) actionpack (= 7.2.1.1)
activesupport (= 7.1.3.4) activesupport (= 7.2.1.1)
irb irb (~> 1.13)
rackup (>= 1.0.0) rackup (>= 1.0.0)
rake (>= 12.2) rake (>= 12.2)
thor (~> 1.0, >= 1.2.2) thor (~> 1.0, >= 1.2.2)
@@ -429,30 +431,30 @@ GEM
declarative (< 0.1.0) declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0) trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0) uber (< 0.2.0)
request_store (1.6.0) request_store (1.7.0)
rack (>= 1.4) rack (>= 1.4)
responders (3.1.1) responders (3.1.1)
actionpack (>= 5.2) actionpack (>= 5.2)
railties (>= 5.2) railties (>= 5.2)
retriable (3.1.2) retriable (3.1.2)
rexml (3.3.8) rexml (3.3.9)
rotp (6.3.0) rotp (6.3.0)
rqrcode (2.2.0) rqrcode (2.2.0)
chunky_png (~> 1.0) chunky_png (~> 1.0)
rqrcode_core (~> 1.0) rqrcode_core (~> 1.0)
rqrcode_core (1.2.0) rqrcode_core (1.2.0)
rspec-core (3.13.0) rspec-core (3.13.1)
rspec-support (~> 3.13.0) rspec-support (~> 3.13.0)
rspec-expectations (3.13.0) rspec-expectations (3.13.3)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0) rspec-support (~> 3.13.0)
rspec-mocks (3.13.0) rspec-mocks (3.13.2)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0) rspec-support (~> 3.13.0)
rspec-rails (6.1.2) rspec-rails (7.0.1)
actionpack (>= 6.1) actionpack (>= 7.0)
activesupport (>= 6.1) activesupport (>= 7.0)
railties (>= 6.1) railties (>= 7.0)
rspec-core (~> 3.13) rspec-core (~> 3.13)
rspec-expectations (~> 3.13) rspec-expectations (~> 3.13)
rspec-mocks (~> 3.13) rspec-mocks (~> 3.13)
@@ -470,36 +472,39 @@ GEM
unicode-display_width (>= 2.4.0, < 3.0) unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.32.3) rubocop-ast (1.32.3)
parser (>= 3.3.1.0) parser (>= 3.3.1.0)
rubocop-performance (1.21.0) rubocop-performance (1.22.1)
rubocop (>= 1.48.1, < 2.0) rubocop (>= 1.48.1, < 2.0)
rubocop-ast (>= 1.31.1, < 2.0) rubocop-ast (>= 1.31.1, < 2.0)
rubocop-rails (2.24.1) rubocop-rails (2.26.2)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
rack (>= 1.1) rack (>= 1.1)
rubocop (>= 1.33.0, < 2.0) rubocop (>= 1.52.0, < 2.0)
rubocop-ast (>= 1.31.1, < 2.0) rubocop-ast (>= 1.31.1, < 2.0)
rubocop-rspec (3.0.3) rubocop-rspec (3.1.0)
rubocop (~> 1.61) rubocop (~> 1.61)
ruby-progressbar (1.13.0) ruby-progressbar (1.13.0)
ruby-vips (2.2.1) ruby-vips (2.2.2)
ffi (~> 1.12) ffi (~> 1.12)
logger
ruby2_keywords (0.0.5) ruby2_keywords (0.0.5)
rubyXL (3.4.27) rubyXL (3.4.27)
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)
semantic_range (3.0.0) semantic_range (3.0.0)
shakapacker (8.0.0) shakapacker (8.0.2)
activesupport (>= 5.2) activesupport (>= 5.2)
package_json package_json
rack-proxy (>= 0.6.1) rack-proxy (>= 0.6.1)
railties (>= 5.2) railties (>= 5.2)
semantic_range (>= 2.3.0) semantic_range (>= 2.3.0)
sidekiq (7.2.4) sidekiq (7.3.2)
concurrent-ruby (< 2) concurrent-ruby (< 2)
connection_pool (>= 2.3.0) connection_pool (>= 2.3.0)
logger
rack (>= 2.2.4) rack (>= 2.2.4)
redis-client (>= 0.19.0) redis-client (>= 0.22.2)
signet (0.19.0) signet (0.19.0)
addressable (~> 2.8) addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a) faraday (>= 0.17.5, < 3.a)
@@ -509,32 +514,34 @@ GEM
docile (~> 1.1) docile (~> 1.1)
simplecov-html (~> 0.11) simplecov-html (~> 0.11)
simplecov_json_formatter (~> 0.1) simplecov_json_formatter (~> 0.1)
simplecov-html (0.12.3) simplecov-html (0.13.1)
simplecov_json_formatter (0.1.4) simplecov_json_formatter (0.1.4)
smart_properties (1.17.0) smart_properties (1.17.0)
sqlite3 (1.7.3) sqlite3 (2.1.0)
mini_portile2 (~> 2.8.0) mini_portile2 (~> 2.8.0)
sqlite3 (2.1.0-aarch64-linux-gnu)
sqlite3 (2.1.0-x86_64-linux-gnu)
stringio (3.1.1) stringio (3.1.1)
strip_attributes (1.13.0) strip_attributes (1.13.0)
activemodel (>= 3.0, < 8.0) activemodel (>= 3.0, < 8.0)
thor (1.3.2) thor (1.3.2)
timeout (0.4.1) timeout (0.4.1)
trailblazer-option (0.1.2) trailblazer-option (0.1.2)
turbo-rails (2.0.5) turbo-rails (2.0.10)
actionpack (>= 6.0.0) actionpack (>= 6.0.0)
activejob (>= 6.0.0)
railties (>= 6.0.0) railties (>= 6.0.0)
twitter_cldr (6.12.0) twitter_cldr (6.12.1)
camertron-eprun camertron-eprun
cldr-plurals-runtime-rb (~> 1.1) cldr-plurals-runtime-rb (~> 1.1)
tzinfo tzinfo
tzinfo (2.0.6) tzinfo (2.0.6)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
tzinfo-data (1.2024.1) tzinfo-data (1.2024.2)
tzinfo (>= 1.0.0) tzinfo (>= 1.0.0)
uber (0.1.0) uber (0.1.0)
unicode-display_width (2.6.0) unicode-display_width (2.6.0)
uniform_notifier (1.16.0) uniform_notifier (1.16.0)
useragent (0.16.10)
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)
@@ -542,17 +549,17 @@ GEM
activemodel (>= 6.0.0) activemodel (>= 6.0.0)
bindex (>= 0.4.0) bindex (>= 0.4.0)
railties (>= 6.0.0) railties (>= 6.0.0)
webmock (3.23.0) webmock (3.24.0)
addressable (>= 2.8.0) addressable (>= 2.8.0)
crack (>= 0.3.2) crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0) hashdiff (>= 0.4.0, < 2.0.0)
webrick (1.8.1) webrick (1.8.2)
websocket-driver (0.7.6) websocket-driver (0.7.6)
websocket-extensions (>= 0.1.0) websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5) websocket-extensions (0.1.5)
xpath (3.2.0) xpath (3.2.0)
nokogiri (~> 1.8) nokogiri (~> 1.8)
zeitwerk (2.6.18) zeitwerk (2.7.0)
PLATFORMS PLATFORMS
aarch64-linux aarch64-linux
@@ -589,7 +596,6 @@ DEPENDENCIES
letter_opener_web letter_opener_web
lograge lograge
mysql2 mysql2
net-smtp (= 0.4.0)
oj oj
pagy pagy
pg pg
+13 -13
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>
@@ -25,12 +25,12 @@
DocuSeal is an open source platform that provides secure and efficient digital document signing and processing. Create PDF forms to have them filled and signed online on any device with an easy-to-use, mobile-optimized web tool. DocuSeal is an open source platform that provides secure and efficient digital document signing and processing. Create PDF forms to have them filled and signed online on any device with an easy-to-use, mobile-optimized web tool.
</p> </p>
<h2 align="center"> <h2 align="center">
<a href="https://demo.docuseal.co">✨ Live Demo</a> <a href="https://demo.docuseal.tech">✨ Live Demo</a>
<span>|</span> <span>|</span>
<a href="https://docuseal.co/sign_up">☁️ Try in Cloud</a> <a href="https://docuseal.com/sign_up">☁️ Try in Cloud</a>
</h2> </h2>
[![Demo](https://github.com/docusealco/docuseal/assets/5418788/d8703ea3-361a-423f-8bfe-eff1bd9dbe14)](https://demo.docuseal.co) [![Demo](https://github.com/docusealco/docuseal/assets/5418788/d8703ea3-361a-423f-8bfe-eff1bd9dbe14)](https://demo.docuseal.tech)
## Features ## Features
- PDF form fields builder (WYSIWYG) - PDF form fields builder (WYSIWYG)
@@ -54,11 +54,11 @@ DocuSeal is an open source platform that provides secure and efficient digital d
- Conditional fields and formulas - Conditional fields and formulas
- Bulk send with CSV, XLSX spreadsheet import - Bulk send with CSV, XLSX spreadsheet import
- SSO / SAML - SSO / SAML
- Template creation with HTML API ([Guide](https://www.docuseal.co/guides/create-pdf-document-fillable-form-with-html-api)) - Template creation with HTML API ([Guide](https://www.docuseal.com/guides/create-pdf-document-fillable-form-with-html-api))
- Template creation with PDF or DOCX and field tags API ([Guide](https://www.docuseal.co/guides/use-embedded-text-field-tags-in-the-pdf-to-create-a-fillable-form)) - Template creation with PDF or DOCX and field tags API ([Guide](https://www.docuseal.com/guides/use-embedded-text-field-tags-in-the-pdf-to-create-a-fillable-form))
- Embedded signing form ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue), [Angular](https://github.com/docusealco/docuseal-angular) or [JavaScript](https://www.docuseal.co/docs/embedded)) - Embedded signing form ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue), [Angular](https://github.com/docusealco/docuseal-angular) or [JavaScript](https://www.docuseal.com/docs/embedded))
- Embedded document form builder ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue), [Angular](https://github.com/docusealco/docuseal-angular) or [JavaScript](https://www.docuseal.co/docs/embedded)) - Embedded document form builder ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue), [Angular](https://github.com/docusealco/docuseal-angular) or [JavaScript](https://www.docuseal.com/docs/embedded))
- [Learn more](https://www.docuseal.co/pricing) - [Learn more](https://www.docuseal.com/pricing)
## Deploy ## Deploy
@@ -93,7 +93,7 @@ sudo HOST=your-domain-name.com docker compose up
At DocuSeal we have expertise and technologies to make documents creation, filling, signing and processing seamlessly integrated with your product. We specialize in working with various industries, including **Banking, Healthcare, Transport, Real Estate, eCommerce, KYC, CRM, and other software products** that require bulk document signing. By leveraging DocuSeal, we can assist in reducing the overall cost of developing and processing electronic documents while ensuring security and compliance with local electronic document laws. At DocuSeal we have expertise and technologies to make documents creation, filling, signing and processing seamlessly integrated with your product. We specialize in working with various industries, including **Banking, Healthcare, Transport, Real Estate, eCommerce, KYC, CRM, and other software products** that require bulk document signing. By leveraging DocuSeal, we can assist in reducing the overall cost of developing and processing electronic documents while ensuring security and compliance with local electronic document laws.
[Book a Meeting](https://www.docuseal.co/contact) [Book a Meeting](https://www.docuseal.com/contact)
## License ## License
@@ -102,6 +102,6 @@ Unless otherwise noted, all files © 2023 DocuSeal LLC.
## Tools ## Tools
- [Signature Maker](https://www.docuseal.co/online-signature) - [Signature Maker](https://www.docuseal.com/online-signature)
- [Sign Document Online](https://www.docuseal.co/sign-documents-online) - [Sign Document Online](https://www.docuseal.com/sign-documents-online)
- [Fill PDF Online](https://www.docuseal.co/fill-pdf) - [Fill PDF Online](https://www.docuseal.com/fill-pdf)
+1 -1
View File
@@ -1,6 +1,6 @@
# Reporting a Vulnerability # Reporting a Vulnerability
If you come across any security concern or vulnarability, please report the information via email to security@docuseal.co instead of opening a GitHub issue. We will promptly respond and will collaborate with you to validate the issue, and resolve it ASAP. If you come across any security concern or vulnarability, please report the information via email to security@docuseal.com instead of opening a GitHub issue. We will promptly respond and will collaborate with you to validate the issue, and resolve it ASAP.
**We have a bug bounty program to reward security researchers.** **We have a bug bounty program to reward security researchers.**
@@ -8,6 +8,7 @@ class AccountConfigsController < ApplicationController
AccountConfig::ALLOW_TYPED_SIGNATURE, AccountConfig::ALLOW_TYPED_SIGNATURE,
AccountConfig::FORCE_MFA, AccountConfig::FORCE_MFA,
AccountConfig::ALLOW_TO_RESUBMIT, AccountConfig::ALLOW_TO_RESUBMIT,
AccountConfig::ALLOW_TO_DECLINE_KEY,
AccountConfig::FORM_PREFILL_SIGNATURE_KEY, AccountConfig::FORM_PREFILL_SIGNATURE_KEY,
AccountConfig::ESIGNING_PREFERENCE_KEY, AccountConfig::ESIGNING_PREFERENCE_KEY,
AccountConfig::FORM_WITH_CONFETTI_KEY, AccountConfig::FORM_WITH_CONFETTI_KEY,
@@ -38,12 +38,12 @@ module Api
private private
def authorization_check!(blob) def authorization_check!(blob)
is_authorized = attachment = blob.attachments.take
blob.attachments.all? do |a|
a.name.in?(%w[logo preview_images]) || is_authorized = attachment.name.in?(%w[logo preview_images]) ||
(current_user && a.record.account.id == current_user.account_id) || (current_user && attachment.record.account.id == current_user.account_id) ||
!a.record.account.account_configs.find_or_initialize_by(key: AccountConfig::DOWNLOAD_LINKS_AUTH_KEY).value !attachment.record.account.account_configs
end .find_or_initialize_by(key: AccountConfig::DOWNLOAD_LINKS_AUTH_KEY).value
return if is_authorized return if is_authorized
+29 -2
View File
@@ -25,9 +25,9 @@ module Api
render json: { error: 'Too many requests' }, status: :too_many_requests render json: { error: 'Too many requests' }, status: :too_many_requests
end end
if Rails.env.production? unless Rails.env.development?
rescue_from CanCan::AccessDenied do |e| rescue_from CanCan::AccessDenied do |e|
render json: { error: e.message }, status: :forbidden render json: { error: access_denied_error_message(e) }, status: :forbidden
end end
rescue_from JSON::ParserError do |e| rescue_from JSON::ParserError do |e|
@@ -39,6 +39,33 @@ module Api
private private
def access_denied_error_message(error)
return 'Not authorized' if request.headers['X-Auth-Token'].blank?
return 'Not authorized' unless error.subject.is_a?(ActiveRecord::Base)
return 'Not authorized' unless error.subject.respond_to?(:account_id)
linked_account_record_exists =
if current_user.account.testing?
current_user.account.linked_account_accounts.where(account_type: 'testing')
.exists?(account_id: error.subject.account_id)
else
current_user.account.testing_accounts.exists?(id: error.subject.account_id)
end
return 'Not authorized' unless linked_account_record_exists
object_name = error.subject.model_name.human
id = error.subject.id
if current_user.account.testing?
"#{object_name} #{id} not found using testing API key; Use production API key to " \
"access production #{object_name.downcase.pluralize}."
else
"#{object_name} #{id} not found using production API key; Use testing API key to " \
"access testing #{object_name.downcase.pluralize}."
end
end
def paginate(relation, field: :id) def paginate(relation, field: :id)
result = relation.order(field => :desc) result = relation.order(field => :desc)
.limit([params.fetch(:limit, DEFAULT_LIMIT).to_i, MAX_LIMIT].min) .limit([params.fetch(:limit, DEFAULT_LIMIT).to_i, MAX_LIMIT].min)
@@ -0,0 +1,45 @@
# frozen_string_literal: true
module Api
class SubmissionDocumentsController < ApiBaseController
load_and_authorize_resource :submission
def index
documents =
if @submission.submitters.all?(&:completed_at?)
last_submitter = @submission.submitters.max_by(&:completed_at)
if last_submitter.documents_attachments.blank?
last_submitter.documents_attachments = Submissions::EnsureResultGenerated.call(submitter)
end
last_submitter.documents_attachments
else
values_hash = Submissions::GeneratePreviewAttachments.build_values_hash(@submission)
if @submission.preview_documents.present? &&
@submission.preview_documents.all? { |s| s.metadata['values_hash'] == values_hash }
@submission.preview_documents
else
ApplicationRecord.no_touching do
@submission.preview_documents.each(&:destroy)
end
Submissions::GeneratePreviewAttachments.call(@submission, values_hash:)
end
end
ActiveRecord::Associations::Preloader.new(
records: documents,
associations: [:blob]
).call
render json: {
id: @submission.id,
documents: documents.map do |attachment|
{ name: attachment.filename.base, url: ActiveStorage::Blob.proxy_url(attachment.blob) }
end
}
end
end
end
+13 -6
View File
@@ -67,20 +67,24 @@ module Api
submissions = create_submissions(@template, params) submissions = create_submissions(@template, params)
submissions.each do |submission| WebhookUrls.for_account_id(@template.account_id, 'submission.created').each do |webhook_url|
SendSubmissionCreatedWebhookRequestJob.perform_async({ 'submission_id' => submission.id }) submissions.each do |submission|
SendSubmissionCreatedWebhookRequestJob.perform_async('submission_id' => submission.id,
'webhook_url_id' => webhook_url.id)
end
end end
Submissions.send_signature_requests(submissions) Submissions.send_signature_requests(submissions)
submissions.each do |submission| submissions.each do |submission|
if submission.submitters.all?(&:completed_at?) && submission.submitters.last submission.submitters.each do |submitter|
ProcessSubmitterCompletionJob.perform_async({ 'submitter_id' => submission.submitters.last.id }) ProcessSubmitterCompletionJob.perform_async('submitter_id' => submitter.id) if submitter.completed_at?
end end
end end
render json: build_create_json(submissions) render json: build_create_json(submissions)
rescue Submitters::NormalizeValues::BaseError, DownloadUtils::UnableToDownload => e rescue Submitters::NormalizeValues::BaseError, Submissions::CreateFromSubmitters::BaseError,
DownloadUtils::UnableToDownload => e
Rollbar.warning(e) if defined?(Rollbar) Rollbar.warning(e) if defined?(Rollbar)
render json: { error: e.message }, status: :unprocessable_entity render json: { error: e.message }, status: :unprocessable_entity
@@ -92,7 +96,10 @@ module Api
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
+2 -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
@@ -120,6 +120,7 @@ module Api
end&.dig('uuid') end&.dig('uuid')
submitter.email = Submissions.normalize_email(attrs[:email]) if attrs.key?(:email) submitter.email = Submissions.normalize_email(attrs[:email]) if attrs.key?(:email)
submitter.name = attrs[:name] if attrs.key?(:name)
if attrs.key?(:phone) if attrs.key?(:phone)
submitter.phone = attrs[:phone].to_s.gsub(/[^0-9+]/, '') submitter.phone = attrs[:phone].to_s.gsub(/[^0-9+]/, '')
@@ -25,7 +25,10 @@ module Api
schema_documents = Templates::CloneAttachments.call(template: cloned_template, original_template: @template) schema_documents = Templates::CloneAttachments.call(template: cloned_template, original_template: @template)
SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => cloned_template.id) WebhookUrls.for_account_id(cloned_template.account_id, 'template.created').each do |webhook_url|
SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => cloned_template.id,
'webhook_url_id' => webhook_url.id)
end
render json: Templates::SerializeForApi.call(cloned_template, schema_documents) render json: Templates::SerializeForApi.call(cloned_template, schema_documents)
end end
+4 -1
View File
@@ -65,7 +65,10 @@ 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
+1 -4
View File
@@ -20,10 +20,7 @@ module Api
pdf = HexaPDF::Document.new(io: StringIO.new(file)) pdf = HexaPDF::Document.new(io: StringIO.new(file))
trusted_certs = Accounts.load_trusted_certs(current_account) trusted_certs = Accounts.load_trusted_certs(current_account)
is_checksum_found = CompletedDocument.exists?(sha256: Base64.urlsafe_encode64(Digest::SHA256.digest(file)))
is_checksum_found = ActiveStorage::Attachment.joins(:blob)
.where(name: 'documents', record_type: 'Submitter')
.exists?(blob: { checksum: Digest::MD5.base64digest(file) })
render json: { render json: {
checksum_status: is_checksum_found ? 'verified' : 'not_found', checksum_status: is_checksum_found ? 'verified' : 'not_found',
+11 -1
View File
@@ -38,6 +38,10 @@ class ApplicationController < ActionController::Base
end end
def default_url_options def default_url_options
if request.domain == 'docuseal.com'
return { host: 'docuseal.com', protocol: ENV['FORCE_SSL'].present? ? 'https' : 'http' }
end
Docuseal.default_url_options Docuseal.default_url_options
end end
@@ -62,7 +66,7 @@ class ApplicationController < ActionController::Base
end end
def with_browser_locale(&) def with_browser_locale(&)
return yield if I18n.locale != :'en-US' return yield if I18n.locale != :'en-US' && I18n.locale != :en
locale = params[:lang].presence locale = params[:lang].presence
locale ||= request.env['HTTP_ACCEPT_LANGUAGE'].to_s[BROWSER_LOCALE_REGEXP].to_s locale ||= request.env['HTTP_ACCEPT_LANGUAGE'].to_s[BROWSER_LOCALE_REGEXP].to_s
@@ -100,4 +104,10 @@ 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 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
@@ -9,7 +9,7 @@ class EmailSmtpSettingsController < ApplicationController
def create def create
if @encrypted_config.update(email_configs) if @encrypted_config.update(email_configs)
SettingsMailer.smtp_successful_setup(@encrypted_config.value['from_email']).deliver_now! SettingsMailer.smtp_successful_setup(@encrypted_config.value['from_email'] || current_user.email).deliver_now!
redirect_to settings_email_index_path, notice: I18n.t('changes_have_been_saved') redirect_to settings_email_index_path, notice: I18n.t('changes_have_been_saved')
else else
+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',
+1 -1
View File
@@ -87,7 +87,7 @@ class EsignSettingsController < ApplicationController
@encrypted_config.save! @encrypted_config.save!
redirect_to settings_esign_path, notice: I18m.t('certificate_has_been_removed') redirect_to settings_esign_path, notice: I18n.t('certificate_has_been_removed')
end end
private private
@@ -12,7 +12,8 @@ class PreviewDocumentPageController < ActionController::API
return head :not_found unless attachment return head :not_found unless attachment
preview_image = attachment.preview_images.joins(:blob).find_by(blob: { filename: "#{params[:id]}#{FORMAT}" }) preview_image = attachment.preview_images.joins(:blob)
.find_by(blob: { filename: ["#{params[:id]}.png", "#{params[:id]}.jpg"] })
return redirect_to preview_image.url, allow_other_host: true if preview_image return redirect_to preview_image.url, allow_other_host: true if preview_image
+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)
+21 -4
View File
@@ -9,6 +9,13 @@ class SubmissionsController < ApplicationController
def show def show
@submission = Submissions.preload_with_pages(@submission) @submission = Submissions.preload_with_pages(@submission)
unless @submission.submitters.all?(&:completed_at?)
ActiveRecord::Associations::Preloader.new(
records: [@submission],
associations: [submitters: :start_form_submission_events]
).call
end
render :show, layout: 'plain' render :show, layout: 'plain'
end end
@@ -43,9 +50,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,7 +66,10 @@ class SubmissionsController < ApplicationController
else else
@submission.update!(archived_at: Time.current) @submission.update!(archived_at: Time.current)
SendSubmissionArchivedWebhookRequestJob.perform_async('submission_id' => @submission.id) WebhookUrls.for_account_id(@submission.account_id, 'submission.archived').each do |webhook_url|
SendSubmissionArchivedWebhookRequestJob.perform_async('submission_id' => @submission.id,
'webhook_url_id' => webhook_url.id)
end
I18n.t('submission_has_been_archived') I18n.t('submission_has_been_archived')
end end
@@ -78,6 +86,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
@@ -15,6 +15,6 @@ class SubmissionsDashboardController < ApplicationController
@submissions = @submissions.pending if params[:status] == 'pending' @submissions = @submissions.pending if params[:status] == 'pending'
@submissions = @submissions.completed if params[:status] == 'completed' @submissions = @submissions.completed if params[:status] == 'completed'
@pagy, @submissions = pagy(@submissions.preload(:submitters).order(id: :desc)) @pagy, @submissions = pagy(@submissions.preload(submitters: :start_form_submission_events).order(id: :desc))
end end
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
before_action :maybe_redirect_com, only: %i[show completed]
TTL = 40.minutes TTL = 40.minutes
def show def show
@@ -0,0 +1,11 @@
# frozen_string_literal: true
class SubmissionsUnarchiveController < ApplicationController
load_and_authorize_resource :submission
def create
@submission.update!(archived_at: nil)
redirect_to submission_path(@submission), notice: I18n.t('submission_has_been_unarchived')
end
end
@@ -7,6 +7,8 @@ class SubmitFormController < ApplicationController
skip_before_action :authenticate_user! skip_before_action :authenticate_user!
skip_authorization_check skip_authorization_check
before_action :maybe_redirect_com, only: %i[show completed]
CONFIG_KEYS = [].freeze CONFIG_KEYS = [].freeze
def show def show
@@ -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
+80
View File
@@ -0,0 +1,80 @@
# frozen_string_literal: true
class SubmittersController < ApplicationController
load_and_authorize_resource :submitter, only: %i[edit update]
def edit
@submitter_email_message =
if @submitter.preferences['email_message_uuid'].present?
@submitter.account.email_messages.find_by(uuid: @submitter.preferences['email_message_uuid'])
end
end
def update
submission = @submitter.submission
if @submitter.submission_events.exists?(event_type: 'start_form') || submission.archived_at? || submission.expired?
return redirect_back fallback_location: submission_path(submission), alert: I18n.t('submitter_cannot_be_updated')
end
if submitter_params.values.all?(&:blank?)
return redirect_back fallback_location: submission_path(submission),
alert: I18n.t('at_least_one_field_must_be_filled')
end
if params[:is_custom_message] != '1'
params.delete(:subject)
params.delete(:body)
end
submitter_preferences = Submitters.normalize_preferences(@submitter.account, current_user, params)
if submitter_preferences.key?('email_message_uuid')
@submitter.preferences['email_message_uuid'] = submitter_preferences['email_message_uuid']
end
assign_submitter_attrs(@submitter, submitter_params)
if @submitter.save
maybe_resend_email_sms(@submitter, params)
redirect_back fallback_location: submission_path(submission), notice: I18n.t('changes_have_been_saved')
else
redirect_back fallback_location: submission_path(submission), alert: I18n.t('unable_to_save')
end
end
private
def maybe_resend_email_sms(submitter, params)
if params[:send_email] == '1' && submitter.email.present?
is_sent_recently = Docuseal.multitenant? &&
EmailEvent.exists?(email: submitter.email,
tag: 'submitter_invitation',
emailable: submitter,
event_type: 'send',
created_at: 4.hours.ago..Time.current)
SendSubmitterInvitationEmailJob.perform_async('submitter_id' => submitter.id) unless is_sent_recently
end
return if submitter.phone.blank?
return unless params[:send_sms] == '1'
SendSubmitterInvitationSmsJob.perform_async('submitter_id' => submitter.id)
end
def assign_submitter_attrs(submitter, attrs)
submitter.phone = attrs[:phone].to_s.gsub(/[^0-9+]/, '') if attrs.key?(:phone)
submitter.email = Submissions.normalize_email(attrs[:email]) if attrs.key?(:email)
submitter.name = attrs[:name] if attrs.key?(:name)
submitter
end
def submitter_params
params.require(:submitter).permit(:email, :name, :phone).transform_values(&:strip)
end
end
@@ -7,7 +7,7 @@ class TemplateFoldersController < ApplicationController
@templates = @template_folder.templates.active.preload(:author).order(id: :desc) @templates = @template_folder.templates.active.preload(:author).order(id: :desc)
@templates = Templates.search(@templates, params[:q]) @templates = Templates.search(@templates, params[:q])
@pagy, @templates = pagy(@templates, items: 12) @pagy, @templates = pagy(@templates, limit: 12)
end end
def edit; end def edit; end
@@ -7,6 +7,6 @@ class TemplatesArchivedController < ApplicationController
@templates = @templates.where.not(archived_at: nil).preload(:author, :folder).order(id: :desc) @templates = @templates.where.not(archived_at: nil).preload(:author, :folder).order(id: :desc)
@templates = Templates.search(@templates, params[:q]) @templates = Templates.search(@templates, params[:q])
@pagy, @templates = pagy(@templates, items: 12) @pagy, @templates = pagy(@templates, limit: 12)
end end
end end
+17 -3
View File
@@ -15,7 +15,7 @@ class TemplatesController < ApplicationController
submissions = submissions.pending if params[:status] == 'pending' submissions = submissions.pending if params[:status] == 'pending'
submissions = submissions.completed if params[:status] == 'completed' submissions = submissions.completed if params[:status] == 'completed'
@pagy, @submissions = pagy(submissions.preload(:submitters).order(id: :desc)) @pagy, @submissions = pagy(submissions.preload(submitters: :start_form_submission_events).order(id: :desc))
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound
redirect_to root_path redirect_to root_path
end end
@@ -66,7 +66,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,7 +77,7 @@ 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
@@ -128,6 +128,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?
@@ -23,14 +23,14 @@ class TemplatesDashboardController < ApplicationController
@template_folders = @template_folders.reject { |e| e.name == TemplateFolder::DEFAULT_NAME } @template_folders = @template_folders.reject { |e| e.name == TemplateFolder::DEFAULT_NAME }
@templates = filter_templates(@templates) @templates = filter_templates(@templates)
items = limit =
if @template_folders.size < 4 if @template_folders.size < 4
TEMPLATES_PER_PAGE TEMPLATES_PER_PAGE
else else
(@template_folders.size < 7 ? 9 : 6) (@template_folders.size < 7 ? 9 : 6)
end end
@pagy, @templates = pagy(@templates, items:) @pagy, @templates = pagy(@templates, limit:)
end end
end end
@@ -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
@@ -65,4 +65,11 @@ class TemplatesUploadsController < ApplicationController
{ 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
+4 -2
View File
@@ -9,9 +9,11 @@ class UsersController < ApplicationController
def index def index
@users = @users =
if params[:status] == 'archived' if params[:status] == 'archived'
@users.archived @users.archived.where.not(role: 'integration')
elsif params[:status] == 'integration'
@users.active.where(role: 'integration')
else else
@users.active @users.active.where.not(role: 'integration')
end end
@pagy, @users = pagy(@users.where(account: current_account).order(id: :desc)) @pagy, @users = pagy(@users.where(account: current_account).order(id: :desc))
@@ -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
+7 -15
View File
@@ -1,29 +1,21 @@
# 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_webhooks_path, 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
+10 -11
View File
@@ -1,15 +1,15 @@
# frozen_string_literal: true # frozen_string_literal: true
class WebhookSettingsController < ApplicationController class WebhookSettingsController < ApplicationController
before_action :load_encrypted_config before_action :load_webhook_url
authorize_resource :encrypted_config, parent: false authorize_resource :webhook_url, parent: false
def show; end def show; end
def create def create
@encrypted_config.assign_attributes(encrypted_config_params) @webhook_url.assign_attributes(webhook_params)
@encrypted_config.value.present? ? @encrypted_config.save! : @encrypted_config.delete @webhook_url.url.present? ? @webhook_url.save! : @webhook_url.delete
redirect_back(fallback_location: 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
@@ -17,20 +17,19 @@ class WebhookSettingsController < ApplicationController
def update def update
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, SendFormCompletedWebhookRequestJob.perform_async('submitter_id' => submitter.id,
'encrypted_config_id' => @encrypted_config.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 load_webhook_url
@encrypted_config = @webhook_url = current_account.webhook_urls.first_or_initialize
current_account.encrypted_configs.find_or_initialize_by(key: EncryptedConfig::WEBHOOK_URL_KEY)
end end
def encrypted_config_params def webhook_params
params.require(:encrypted_config).permit(:value) params.require(:webhook_url).permit(:url)
end end
end end
+2
View File
@@ -29,6 +29,7 @@ import SearchInput from './elements/search_input'
import ToggleAttribute from './elements/toggle_attribute' import ToggleAttribute from './elements/toggle_attribute'
import LinkedInput from './elements/linked_input' import LinkedInput from './elements/linked_input'
import CheckboxGroup from './elements/checkbox_group' import CheckboxGroup from './elements/checkbox_group'
import MaskedInput from './elements/masked_input'
import * as TurboInstantClick from './lib/turbo_instant_click' import * as TurboInstantClick from './lib/turbo_instant_click'
@@ -95,6 +96,7 @@ safeRegisterElement('search-input', SearchInput)
safeRegisterElement('toggle-attribute', ToggleAttribute) safeRegisterElement('toggle-attribute', ToggleAttribute)
safeRegisterElement('linked-input', LinkedInput) safeRegisterElement('linked-input', LinkedInput)
safeRegisterElement('checkbox-group', CheckboxGroup) safeRegisterElement('checkbox-group', CheckboxGroup)
safeRegisterElement('masked-input', MaskedInput)
safeRegisterElement('template-builder', class extends HTMLElement { safeRegisterElement('template-builder', class extends HTMLElement {
connectedCallback () { connectedCallback () {
-4
View File
@@ -130,7 +130,3 @@ button[disabled] .enabled {
outline-offset: 3px; outline-offset: 3px;
outline-color: hsl(var(--bc) / 0.2); outline-color: hsl(var(--bc) / 0.2);
} }
select:required:invalid {
color: gray !important;
}
+18
View File
@@ -0,0 +1,18 @@
export default class extends HTMLElement {
connectedCallback () {
const maskedToken = this.input.value
this.input.addEventListener('focus', () => {
this.input.value = this.dataset.token
this.input.select()
})
this.input.addEventListener('focusout', () => {
this.input.value = maskedToken
})
}
get input () {
return this.querySelector('input')
}
}
+1
View File
@@ -28,6 +28,7 @@ safeRegisterElement('submission-form', class extends HTMLElement {
requireSigningReason: this.dataset.requireSigningReason === 'true', requireSigningReason: this.dataset.requireSigningReason === 'true',
withConfetti: this.dataset.withConfetti !== 'false', withConfetti: this.dataset.withConfetti !== 'false',
withDisclosure: this.dataset.withDisclosure === 'true', withDisclosure: this.dataset.withDisclosure === 'true',
reuseSignature: this.dataset.reuseSignature !== 'false',
withTypedSignature: this.dataset.withTypedSignature !== 'false', withTypedSignature: this.dataset.withTypedSignature !== 'false',
authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content, authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content,
rememberSignature: this.dataset.rememberSignature === 'true', rememberSignature: this.dataset.rememberSignature === 'true',
-4
View File
@@ -31,10 +31,6 @@ button[disabled] .enabled {
display: none; display: none;
} }
select:required:invalid {
@apply text-gray-300;
}
.input-bordered { .input-bordered {
@apply border-base-content/20; @apply border-base-content/20;
} }
+2 -1
View File
@@ -405,7 +405,8 @@ export default {
} }
if (this.field.preferences?.font_size) { if (this.field.preferences?.font_size) {
style.fontSize = this.field.preferences.font_size + 'pt' style.fontSize = `clamp(4pt, 1.6vw, ${this.field.preferences.font_size}pt)`
style.lineHeight = `clamp(6pt, 2.0vw, ${parseInt(this.field.preferences.font_size) + 3}pt)`
} }
return style return style
+3 -2
View File
@@ -1,5 +1,6 @@
<template> <template>
<div <div
id="form_completed"
class="mx-auto max-w-md flex flex-col" class="mx-auto max-w-md flex flex-col"
dir="auto" dir="auto"
> >
@@ -75,7 +76,7 @@
</a> </a>
<a <a
v-if="isDemo" v-if="isDemo"
href="https://docuseal.co/sign_up" href="https://docuseal.com/sign_up"
class="white-button flex items-center space-x-1 w-full" class="white-button flex items-center space-x-1 w-full"
> >
<IconLogin /> <IconLogin />
@@ -90,7 +91,7 @@
> >
{{ t('powered_by') }} {{ t('powered_by') }}
<a <a
href="https://www.docuseal.co/start" href="https://www.docuseal.com/start"
target="_blank" target="_blank"
class="underline" class="underline"
>DocuSeal</a> - {{ t('open_source_documents_software') }} >DocuSeal</a> - {{ t('open_source_documents_software') }}
@@ -45,6 +45,7 @@
:name="`values[${field.uuid}]`" :name="`values[${field.uuid}]`"
@keydown.enter="onEnter" @keydown.enter="onEnter"
@focus="$emit('focus')" @focus="$emit('focus')"
@paste="onPaste"
> >
</div> </div>
</div> </div>
@@ -98,6 +99,25 @@ export default {
this.$emit('submit') this.$emit('submit')
} }
}, },
onPaste (e) {
e.preventDefault()
let pasteData = e.clipboardData.getData('text').trim()
if (pasteData.match(/^\d{2}\.\d{2}\.\d{4}$/)) {
pasteData = pasteData.split('.').reverse().join('-')
}
const parsedDate = new Date(pasteData)
if (!isNaN(parsedDate)) {
const inputEl = this.$refs.input
inputEl.valueAsDate = new Date(parsedDate.getTime() - parsedDate.getTimezoneOffset() * 60000)
inputEl.dispatchEvent(new Event('input', { bubbles: true }))
}
},
setCurrentDate () { setCurrentDate () {
const inputEl = this.$refs.input const inputEl = this.$refs.input
+16 -7
View File
@@ -115,7 +115,7 @@
v-model="values[currentField.uuid]" v-model="values[currentField.uuid]"
:show-field-names="showFieldNames" :show-field-names="showFieldNames"
:field="currentField" :field="currentField"
@submit="submitStep" @submit="!isSubmitting && submitStep()"
@focus="scrollIntoField(currentField)" @focus="scrollIntoField(currentField)"
/> />
<div v-else-if="currentField.type === 'select'"> <div v-else-if="currentField.type === 'select'">
@@ -152,6 +152,7 @@
dir="auto" dir="auto"
:required="currentField.required" :required="currentField.required"
class="select base-input !text-2xl w-full text-center font-normal" class="select base-input !text-2xl w-full text-center font-normal"
:class="{ 'text-gray-300': !values[currentField.uuid] }"
:name="`values[${currentField.uuid}]`" :name="`values[${currentField.uuid}]`"
@change="values[currentField.uuid] = $event.target.value" @change="values[currentField.uuid] = $event.target.value"
@focus="scrollIntoField(currentField)" @focus="scrollIntoField(currentField)"
@@ -159,6 +160,7 @@
<option <option
value="" value=""
:selected="!values[currentField.uuid]" :selected="!values[currentField.uuid]"
class="text-gray-300"
> >
{{ t('select_your_option') }} {{ t('select_your_option') }}
</option> </option>
@@ -167,6 +169,7 @@
:key="option.uuid" :key="option.uuid"
:selected="values[currentField.uuid] == option.value" :selected="values[currentField.uuid] == option.value"
:value="option.value" :value="option.value"
class="text-base-content"
> >
{{ option.value }} {{ option.value }}
</option> </option>
@@ -392,7 +395,7 @@
:default-value="submitter.phone" :default-value="submitter.phone"
:submitter-slug="submitterSlug" :submitter-slug="submitterSlug"
@focus="scrollIntoField(currentField)" @focus="scrollIntoField(currentField)"
@submit="submitStep" @submit="!isSubmitting && submitStep()"
/> />
<PaymentStep <PaymentStep
v-else-if="currentField.type === 'payment'" v-else-if="currentField.type === 'payment'"
@@ -404,7 +407,7 @@
:values="values" :values="values"
@attached="attachments.push($event)" @attached="attachments.push($event)"
@focus="scrollIntoField(currentField)" @focus="scrollIntoField(currentField)"
@submit="submitStep" @submit="!isSubmitting && submitStep()"
/> />
</div> </div>
<div <div
@@ -897,7 +900,7 @@ export default {
mounted () { mounted () {
this.submittedValues = JSON.parse(JSON.stringify(this.values)) this.submittedValues = JSON.parse(JSON.stringify(this.values))
screen?.orientation.addEventListener('change', this.onOrientationChange) screen?.orientation?.addEventListener('change', this.onOrientationChange)
this.fields.forEach((field) => { this.fields.forEach((field) => {
if (field.default_value && !field.readonly) { if (field.default_value && !field.readonly) {
@@ -1104,6 +1107,10 @@ export default {
const currentFieldUuids = this.currentStepFields.map((f) => f.uuid) const currentFieldUuids = this.currentStepFields.map((f) => f.uuid)
const currentFieldType = this.currentField.type const currentFieldType = this.currentField.type
if (!formData && !this.$refs.form.checkValidity()) {
return
}
if (this.dryRun) { if (this.dryRun) {
currentFieldUuids.forEach((fieldUuid) => { currentFieldUuids.forEach((fieldUuid) => {
this.submittedValues[fieldUuid] = this.values[fieldUuid] this.submittedValues[fieldUuid] = this.values[fieldUuid]
@@ -1142,13 +1149,15 @@ export default {
async submitStep () { async submitStep () {
this.isSubmitting = true this.isSubmitting = true
const submitStep = this.currentStep
const stepPromise = ['signature', 'phone', 'initials', 'payment'].includes(this.currentField.type) const stepPromise = ['signature', 'phone', 'initials', 'payment'].includes(this.currentField.type)
? this.$refs.currentStep.submit ? this.$refs.currentStep.submit
: () => Promise.resolve({}) : () => Promise.resolve({})
stepPromise().then(async () => { stepPromise().then(async () => {
const emptyRequiredField = this.stepFields.find((fields, index) => { const emptyRequiredField = this.stepFields.find((fields, index) => {
if (index >= this.currentStep) { if (index >= submitStep) {
return false return false
} }
@@ -1158,7 +1167,7 @@ export default {
}) })
const formData = new FormData(this.$refs.form) const formData = new FormData(this.$refs.form)
const isLastStep = this.currentStep === this.stepFields.length - 1 const isLastStep = submitStep === this.stepFields.length - 1
if (isLastStep && !emptyRequiredField && !this.inviteSubmitters.length) { if (isLastStep && !emptyRequiredField && !this.inviteSubmitters.length) {
formData.append('completed', 'true') formData.append('completed', 'true')
@@ -1183,7 +1192,7 @@ export default {
return Promise.reject(new Error(data.error)) return Promise.reject(new Error(data.error))
} }
const nextStep = (isLastStep && emptyRequiredField) || this.stepFields[this.currentStep + 1] const nextStep = (isLastStep && emptyRequiredField) || this.stepFields[submitStep + 1]
if (nextStep) { if (nextStep) {
if (this.alwaysMinimize) { if (this.alwaysMinimize) {
+1 -1
View File
@@ -216,7 +216,7 @@ const it = {
pay: 'Pagamento', pay: 'Pagamento',
file: 'File', file: 'File',
select: 'Seleziona', select: 'Seleziona',
checkbox: 'Casella di controllo', checkbox: 'Checkbox',
multiple: 'Multiplo', multiple: 'Multiplo',
radio: 'Radio', radio: 'Radio',
cells: 'Celle', cells: 'Celle',
File diff suppressed because it is too large. Load diff
+106 -21
View File
@@ -61,30 +61,55 @@
</a> </a>
</div> </div>
</div> </div>
<input <div
v-show="!isCodeSent" v-show="!isCodeSent"
:id="field.uuid" class="flex w-full rounded-full outline-neutral-content outline-2 outline-offset-2 focus-within:outline"
ref="phone"
:value="modelValue || defaultValue"
:readonly="!!defaultValue"
class="base-input !text-2xl w-full"
autocomplete="tel"
pattern="^\+[0-9\s\-]+$"
type="tel"
inputmode="tel"
:required="field.required"
placeholder="+1 234 567-8900"
:name="`values[${field.uuid}]`"
@invalid="$event.target.value ? $event.target.setCustomValidity(`${t('use_international_format')}...`) : ''"
@input="[$event.target.setCustomValidity(''), $emit('update:model-value', $event.target.value)]"
@focus="$emit('focus')"
> >
<div class="relative inline-block">
<div class="btn bg-base-200 border border-neutral-300 text-2xl whitespace-nowrap font-normal rounded-l-full">
{{ selectedCountry.flag }} +{{ selectedCountry.dial }}
</div>
<select
class="absolute top-0 bottom-0 right-0 left-0 opacity-0 w-full h-full cursor-pointer"
:disabled="!!defaultValue"
@change="onCountrySelect(countries.find((country) => country.flag === $event.target.value))"
>
<option
v-for="(country, index) in countries"
:key="index"
:value="country.flag"
>
{{ country.flag }} {{ country.name }}
</option>
</select>
</div>
<input
:name="`values[${field.uuid}]`"
:value="fullInternationalPhoneValue"
hidden
>
<input
:id="field.uuid"
ref="phone"
:value="phoneValue"
:readonly="!!defaultValue"
class="base-input !text-2xl !rounded-l-none !border-l-0 !outline-none w-full"
autocomplete="tel"
type="tel"
inputmode="tel"
:required="field.required"
placeholder="234 567-8900"
@input="onPhoneInput"
@focus="$emit('focus')"
>
</div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import MarkdownContent from './markdown_content' import MarkdownContent from './markdown_content'
import phoneData from './phone_data'
function throttle (func, delay) { function throttle (func, delay) {
let lastCallTime = 0 let lastCallTime = 0
@@ -144,13 +169,73 @@ export default {
data () { data () {
return { return {
isCodeSent: false, isCodeSent: false,
isResendLoading: false isResendLoading: false,
phoneValue: this.modelValue || this.defaultValue || '',
selectedCountry: {}
}
},
computed: {
countries () {
return phoneData.map(([name, dial, flag, tz]) => {
return { name, dial, flag, tz }
})
},
countriesDialIndex () {
return this.countries.reduce((acc, item) => {
acc[item.dial] ||= item
return acc
}, {})
},
dialCodesRegexp () {
const dialCodes = this.countries.map((country) => country.dial).sort((a, b) => b.length - a.length)
return new RegExp(`^\\+(${dialCodes.join('|')})`)
},
detectedPhoneValueDialCode () {
return (this.phoneValue || '').replace(/[^\d+]/g, '').match(this.dialCodesRegexp)?.[1]
},
fullInternationalPhoneValue () {
if (this.detectedPhoneValueDialCode) {
return this.phoneValue
} else if (this.phoneValue) {
return ['+', this.selectedCountry.dial, this.phoneValue].filter(Boolean).join('')
} else {
return ''
}
}
},
mounted () {
const browserTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone
if (this.detectedPhoneValueDialCode) {
this.selectedCountry = this.countriesDialIndex[this.detectedPhoneValueDialCode]
} else if (browserTimeZone) {
const tz = browserTimeZone.split('/')[1]
this.selectedCountry = this.countries.find((country) => country.tz.includes(tz)) || this.countries[0]
} }
}, },
methods: { methods: {
emitSubmit: throttle(function (e) { emitSubmit: throttle(function (e) {
this.$emit('submit') this.$emit('submit')
}, 1000), }, 1000),
onCountrySelect (country) {
if (this.selectedCountry.flag !== country.flag) {
this.phoneValue = this.phoneValue.replace(`+${this.selectedCountry.dial}`, `+${country.dial}`)
}
this.selectedCountry = country
this.$refs.phone.focus()
},
onPhoneInput (e) {
this.phoneValue = e.target.value
if (this.detectedPhoneValueDialCode) {
this.selectedCountry = this.countriesDialIndex[this.detectedPhoneValueDialCode]
}
},
onInputCode (e) { onInputCode (e) {
if (e.target.value.length === 6) { if (e.target.value.length === 6) {
this.emitSubmit() this.emitSubmit()
@@ -171,14 +256,14 @@ export default {
body: JSON.stringify({ body: JSON.stringify({
submitter_slug: this.submitterSlug, submitter_slug: this.submitterSlug,
locale: this.locale, locale: this.locale,
phone: this.$refs.phone.value phone: this.fullInternationalPhoneValue
}), }),
headers: { 'Content-Type': 'application/json' } headers: { 'Content-Type': 'application/json' }
}).then(async (resp) => { }).then(async (resp) => {
if (resp.status === 422) { if (resp.status === 422) {
const data = await resp.json() const data = await resp.json()
alert(this.t('number_phone_is_invalid').replace('{number}', this.$refs.phone.value)) alert(this.t('number_phone_is_invalid').replace('{number}', this.fullInternationalPhoneValue))
return Promise.reject(new Error(data.error)) return Promise.reject(new Error(data.error))
} }
@@ -189,13 +274,13 @@ export default {
return Promise.resolve({}) return Promise.resolve({})
} }
if (!this.$refs.phone.value.toString().startsWith('+')) { if (!this.fullInternationalPhoneValue.toString().startsWith('+')) {
alert(this.t('use_international_format')) alert(this.t('use_international_format'))
return Promise.reject(new Error('phone invalid')) return Promise.reject(new Error('phone invalid'))
} else if (!this.isCodeSent) { } else if (!this.isCodeSent) {
return this.sendVerificationCode().then(() => { return this.sendVerificationCode().then(() => {
this.$emit('update:model-value', this.$refs.phone.value) this.$emit('update:model-value', this.fullInternationalPhoneValue)
this.isCodeSent = true this.isCodeSent = true
@@ -145,6 +145,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"
@@ -197,6 +201,7 @@
<select <select
v-if="requireSigningReason && !isOtherReason" v-if="requireSigningReason && !isOtherReason"
class="select base-input !text-2xl w-full mt-6 text-center" class="select base-input !text-2xl w-full mt-6 text-center"
:class="{ 'text-gray-300': !reason }"
required required
:name="`values[${field.preferences.reason_field_uuid}]`" :name="`values[${field.preferences.reason_field_uuid}]`"
@change="$event.target.value === 'other' ? [reason = '', isOtherReason = true] : $emit('update:reason', $event.target.value)" @change="$event.target.value === 'other' ? [reason = '', isOtherReason = true] : $emit('update:reason', $event.target.value)"
@@ -205,6 +210,7 @@
value="" value=""
disabled disabled
:selected="!reason" :selected="!reason"
class="text-gray-300"
> >
{{ t('select_a_reason') }} {{ t('select_a_reason') }}
</option> </option>
@@ -213,10 +219,14 @@
:key="option" :key="option"
:value="option" :value="option"
:selected="reason === option" :selected="reason === option"
class="text-base-content"
> >
{{ label }} {{ label }}
</option> </option>
<option value="other"> <option
value="other"
class="text-base-content"
>
{{ t('other') }} {{ t('other') }}
</option> </option>
</select> </select>
@@ -249,7 +259,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">
+1
View File
@@ -360,6 +360,7 @@ export default {
}, },
computed: { computed: {
fieldNames: FieldType.computed.fieldNames, fieldNames: FieldType.computed.fieldNames,
fieldLabels: FieldType.computed.fieldLabels,
fieldIcons: FieldType.computed.fieldIcons, fieldIcons: FieldType.computed.fieldIcons,
isDefaultValuePresent () { isDefaultValuePresent () {
if (this.field?.type === 'radio' && this.field?.areas?.length > 1) { if (this.field?.type === 'radio' && this.field?.areas?.length > 1) {
+22 -2
View File
@@ -332,7 +332,7 @@
> >
<div class="bg-base-200 rounded-lg p-5 text-center space-y-4"> <div class="bg-base-200 rounded-lg p-5 text-center space-y-4">
<p> <p>
{{ t('draw_field_on_the_document').replace('{field}', drawField?.name || '') }} {{ t('draw_field_on_the_document') }}
</p> </p>
<div> <div>
<button <button
@@ -415,6 +415,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'
@@ -558,6 +559,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,
@@ -677,6 +683,7 @@ export default {
} }
}, },
computed: { computed: {
submitterDefaultNames: FieldSubmitter.computed.names,
selectedAreaRef: () => ref(), selectedAreaRef: () => ref(),
fieldsDragFieldRef: () => ref(), fieldsDragFieldRef: () => ref(),
language () { language () {
@@ -693,6 +700,7 @@ export default {
'de-DE': 'DD.MM.YYYY', 'de-DE': 'DD.MM.YYYY',
'fr-FR': 'DD/MM/YYYY', 'fr-FR': 'DD/MM/YYYY',
'it-IT': 'DD/MM/YYYY', 'it-IT': 'DD/MM/YYYY',
'en-GB': 'DD/MM/YYYY',
'es-ES': 'DD/MM/YYYY' 'es-ES': 'DD/MM/YYYY'
} }
}, },
@@ -748,6 +756,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 () {
@@ -1355,7 +1375,7 @@ export default {
this.save() this.save()
}, },
onDocumentRemove (item) { onDocumentRemove (item) {
if (window.confirm(this.t('are_you_sure'))) { if (window.confirm(this.t('are_you_sure_'))) {
this.template.schema.splice(this.template.schema.indexOf(item), 1) this.template.schema.splice(this.template.schema.indexOf(item), 1)
} }
@@ -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>
@@ -50,6 +50,7 @@
</div> </div>
<select <select
class="select select-bordered select-sm w-full bg-white h-11 pl-4 text-base font-normal" class="select select-bordered select-sm w-full bg-white h-11 pl-4 text-base font-normal"
:class="{ 'text-gray-300': !condition.field_uuid }"
required required
@change="[ @change="[
condition.field_uuid = $event.target.value, condition.field_uuid = $event.target.value,
@@ -68,6 +69,7 @@
v-for="f in fields" v-for="f in fields"
:key="f.uuid" :key="f.uuid"
:value="f.uuid" :value="f.uuid"
class="text-base-content"
:selected="condition.field_uuid === f.uuid" :selected="condition.field_uuid === f.uuid"
> >
{{ f.name || buildDefaultName(f, template.fields) }} {{ f.name || buildDefaultName(f, template.fields) }}
@@ -88,8 +90,9 @@
</option> </option>
</select> </select>
<select <select
v-if="conditionField(condition)?.options?.length" v-if="['radio', 'select', 'multiple'].includes(conditionField(condition)?.type) && conditionField(condition)?.options"
class="select select-bordered select-sm w-full bg-white h-11 pl-4 text-base font-normal" class="select select-bordered select-sm w-full bg-white h-11 pl-4 text-base font-normal"
:class="{ 'text-gray-300': !condition.value }"
required required
@change="condition.value = $event.target.value" @change="condition.value = $event.target.value"
> >
@@ -105,6 +108,7 @@
:key="option.uuid" :key="option.uuid"
:value="option.uuid" :value="option.uuid"
:selected="condition.value === option.uuid" :selected="condition.value === option.uuid"
class="text-base-content"
> >
{{ option.value || `${t('option')} ${index + 1}` }} {{ option.value || `${t('option')} ${index + 1}` }}
</option> </option>
+2 -2
View File
@@ -312,6 +312,7 @@ export default {
}, },
computed: { computed: {
fieldNames: FieldType.computed.fieldNames, fieldNames: FieldType.computed.fieldNames,
fieldLabels: FieldType.computed.fieldLabels,
dropdownBgColor () { dropdownBgColor () {
return ['', null, 'transparent'].includes(this.backgroundColor) ? 'white' : this.backgroundColor return ['', null, 'transparent'].includes(this.backgroundColor) ? 'white' : this.backgroundColor
}, },
@@ -367,8 +368,7 @@ export default {
if (this.field.type === 'heading') { if (this.field.type === 'heading') {
return `${this.fieldNames[field.type]} ${typeIndex + 1}` return `${this.fieldNames[field.type]} ${typeIndex + 1}`
} else { } else {
const suffix = { multiple: this.t('select'), radio: this.t('group') }[field.type] || this.t('field') return `${this.fieldLabels[field.type]} ${typeIndex + 1}`
return `${this.fieldNames[field.type]} ${suffix} ${typeIndex + 1}`
} }
} }
}, },
@@ -330,7 +330,7 @@ export default {
this.$emit('update:model-value', submitter.uuid) this.$emit('update:model-value', submitter.uuid)
}, },
remove (submitter) { remove (submitter) {
if (window.confirm(this.t('are_you_sure'))) { if (window.confirm(this.t('are_you_sure_'))) {
this.$emit('remove', submitter) this.$emit('remove', submitter)
} }
}, },
@@ -114,6 +114,25 @@ export default {
phone: this.t('phone') phone: this.t('phone')
} }
}, },
fieldLabels () {
return {
text: this.t('text_field'),
signature: this.t('signature_field'),
initials: this.t('initials_field'),
date: this.t('date_field'),
number: this.t('number_field'),
image: this.t('image_field'),
file: this.t('file_field'),
select: this.t('select_field'),
checkbox: this.t('checkbox_field'),
multiple: this.t('multiple_field'),
radio: this.t('radio_field'),
cells: this.t('cells_field'),
stamp: this.t('stamp_field'),
payment: this.t('payment_field'),
phone: this.t('phone_field')
}
},
fieldIcons () { fieldIcons () {
return { return {
heading: IconHeading, heading: IconHeading,
+1 -1
View File
@@ -140,7 +140,7 @@
:data-tip="t('unlock_sms_verified_phone_number_field_with_paid_plan_use_text_field_for_phone_numbers_without_verification')" :data-tip="t('unlock_sms_verified_phone_number_field_with_paid_plan_use_text_field_for_phone_numbers_without_verification')"
> >
<a <a
href="https://www.docuseal.co/pricing" href="https://www.docuseal.com/pricing"
target="_blank" target="_blank"
class="opacity-50 flex items-center justify-center border border-dashed border-base-300 w-full rounded relative" class="opacity-50 flex items-center justify-center border border-dashed border-base-300 w-full rounded relative"
:style="{ backgroundColor }" :style="{ backgroundColor }"
@@ -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>
+115 -25
View File
@@ -1,5 +1,7 @@
const en = { const en = {
editable: 'Editable', editable: 'Editable',
recurrent: 'Recurrent',
one_off: 'One-off',
search_field: 'Search field', search_field: 'Search field',
field_not_found: 'Field not found', field_not_found: 'Field not found',
clear: 'Clear', clear: 'Clear',
@@ -29,7 +31,7 @@ const en = {
select_value_: 'Select value...', select_value_: 'Select value...',
remove_condition: 'Remove condition', remove_condition: 'Remove condition',
add_condition: 'Add condition', add_condition: 'Add condition',
are_you_sure: 'Are you sure?', are_you_sure_: 'Are you sure?',
sign_yourself: 'Sign Yourself', sign_yourself: 'Sign Yourself',
set_signing_date: 'Set signing date', set_signing_date: 'Set signing date',
signing_date: 'Signing Date', signing_date: 'Signing Date',
@@ -46,7 +48,7 @@ const en = {
drawn: 'Drawn', drawn: 'Drawn',
formula: 'Formula', formula: 'Formula',
typed: 'Typed', typed: 'Typed',
draw_field_on_the_document: 'Draw {field} field on the document', draw_field_on_the_document: 'Draw a field on the document',
click_to_upload: 'Click to upload', click_to_upload: 'Click to upload',
or_drag_and_drop_files: 'or drag and drop files', or_drag_and_drop_files: 'or drag and drop files',
uploading: 'Uploading', uploading: 'Uploading',
@@ -101,8 +103,21 @@ const en = {
stamp: 'Stamp', stamp: 'Stamp',
payment: 'Payment', payment: 'Payment',
phone: 'Phone', phone: 'Phone',
field: 'Field', text_field: 'Text Field',
group: 'Group', signature_field: 'Signature Field',
initials_field: 'Initials Field',
date_field: 'Date Field',
number_field: 'Number Field',
image_field: 'Image Field',
file_field: 'File Field',
select_field: 'Select Field',
checkbox_field: 'Checkbox Field',
multiple_field: 'Multiple Select Field',
radio_field: 'Radio Group Field',
cells_field: 'Cells Field',
stamp_field: 'Stamp Field',
payment_field: 'Payment Field',
phone_field: 'Phone Field',
draw_a_text_field_on_the_page_with_a_mouse: 'Draw a text field on the page with a mouse', draw_a_text_field_on_the_page_with_a_mouse: 'Draw a text field on the page with a mouse',
drag_and_drop_any_other_field_type_on_the_page: 'Drag & drop any other field type on the page', drag_and_drop_any_other_field_type_on_the_page: 'Drag & drop any other field type on the page',
click_on_the_field_type_above_to_start_drawing_it: 'Click on the field type above to start drawing it', click_on_the_field_type_above_to_start_drawing_it: 'Click on the field type above to start drawing it',
@@ -138,6 +153,8 @@ const en = {
} }
const es = { const es = {
recurrent: 'Recurrente',
one_off: 'Único',
editable: 'Editable', editable: 'Editable',
search_field: 'Campo de búsqueda', search_field: 'Campo de búsqueda',
field_not_found: 'Campo no encontrado', field_not_found: 'Campo no encontrado',
@@ -176,7 +193,7 @@ const es = {
up: 'Arriba', up: 'Arriba',
down: 'Abajo', down: 'Abajo',
set_signing_date: 'Establecer fecha de firma', set_signing_date: 'Establecer fecha de firma',
are_you_sure: '¿Estás seguro?', are_you_sure_: '¿Estás seguro?',
sign_yourself: 'Firma tú mismo', sign_yourself: 'Firma tú mismo',
checked: 'Seleccionado', checked: 'Seleccionado',
send: 'Enviar', send: 'Enviar',
@@ -184,7 +201,7 @@ const es = {
save: 'Guardar', save: 'Guardar',
cancel: 'Cancelar', cancel: 'Cancelar',
or_add_field_without_drawing: 'O añadir campo sin dibujar', or_add_field_without_drawing: 'O añadir campo sin dibujar',
draw_field_on_the_document: 'Dibujar campo {field} en el documento', draw_field_on_the_document: 'Dibujar un campo en el documento',
click_to_upload: 'Haz clic para cargar', click_to_upload: 'Haz clic para cargar',
or_drag_and_drop_files: 'o arrastra y suelta archivos', or_drag_and_drop_files: 'o arrastra y suelta archivos',
uploading: 'Subiendo', uploading: 'Subiendo',
@@ -232,13 +249,26 @@ const es = {
select: 'Seleccionar', select: 'Seleccionar',
checkbox: 'Casilla', checkbox: 'Casilla',
multiple: 'Múltiple', multiple: 'Múltiple',
field: 'Campo',
group: 'Grupo',
radio: 'Radio', radio: 'Radio',
cells: 'Celdas', cells: 'Celdas',
stamp: 'Sello', stamp: 'Sello',
payment: 'Pago', payment: 'Pago',
phone: 'Teléfono', phone: 'Teléfono',
text_field: 'Campo de Texto',
signature_field: 'Campo de Firma',
initials_field: 'Campo de Iniciales',
date_field: 'Campo de Fecha',
number_field: 'Campo de Número',
image_field: 'Campo de Imagen',
file_field: 'Campo de Archivo',
select_field: 'Campo de Selección',
checkbox_field: 'Campo de Casilla',
multiple_field: 'Campo Múltiple',
radio_field: 'Campo de Grupo Radio',
cells_field: 'Campo de Celdas',
stamp_field: 'Campo de Sello',
payment_field: 'Campo de Pago',
phone_field: 'Campo de Teléfono',
draw_a_text_field_on_the_page_with_a_mouse: 'Dibujar un campo de texto en la página con el mouse', draw_a_text_field_on_the_page_with_a_mouse: 'Dibujar un campo de texto en la página con el mouse',
drag_and_drop_any_other_field_type_on_the_page: 'Arrastra y suelta cualquier otro tipo de campo en la página', drag_and_drop_any_other_field_type_on_the_page: 'Arrastra y suelta cualquier otro tipo de campo en la página',
click_on_the_field_type_above_to_start_drawing_it: 'Haz clic en el tipo de campo de arriba para comenzar a dibujarlo', click_on_the_field_type_above_to_start_drawing_it: 'Haz clic en el tipo de campo de arriba para comenzar a dibujarlo',
@@ -277,6 +307,8 @@ const es = {
} }
const it = { const it = {
ricorrente: 'Ricorrente',
una_volta: 'Una volta',
editable: 'Modificabile', editable: 'Modificabile',
search_field: 'Campo di ricerca', search_field: 'Campo di ricerca',
field_not_found: 'Campo non trovato', field_not_found: 'Campo non trovato',
@@ -307,7 +339,7 @@ const it = {
select_value_: 'Seleziona valore...', select_value_: 'Seleziona valore...',
remove_condition: 'Rimuovi condizione', remove_condition: 'Rimuovi condizione',
add_condition: 'Aggiungi condizione', add_condition: 'Aggiungi condizione',
are_you_sure: 'Sei sicuro?', are_you_sure_: 'Sei sicuro?',
sign_yourself: 'Firma te stesso', sign_yourself: 'Firma te stesso',
set_signing_date: 'Imposta data di firma', set_signing_date: 'Imposta data di firma',
signing_date: 'Data di firma', signing_date: 'Data di firma',
@@ -324,7 +356,7 @@ const it = {
drawn: 'Disegnato', drawn: 'Disegnato',
formula: 'Formula', formula: 'Formula',
typed: 'Digitato', typed: 'Digitato',
draw_field_on_the_document: 'Disegna il campo {field} sul documento', draw_field_on_the_document: 'Disegnare un campo sul documento',
click_to_upload: 'Clicca per caricare', click_to_upload: 'Clicca per caricare',
or_drag_and_drop_files: 'o trascina e rilascia i file', or_drag_and_drop_files: 'o trascina e rilascia i file',
uploading: 'Caricamento in corso', uploading: 'Caricamento in corso',
@@ -372,15 +404,28 @@ const it = {
image: 'Immagine', image: 'Immagine',
file: 'File', file: 'File',
select: 'Seleziona', select: 'Seleziona',
checkbox: 'Casella di controllo', checkbox: 'Checkbox',
multiple: 'Multiplo', multiple: 'Multiplo',
radio: 'Radio', radio: 'Radio',
cells: 'Celle', cells: 'Celle',
stamp: 'Timbro', stamp: 'Timbro',
payment: 'Pagamento', payment: 'Pagamento',
phone: 'Telefono', phone: 'Telefono',
field: 'Campo', text_field: 'Campo di Testo',
group: 'Gruppo', signature_field: 'Campo di Firma',
initials_field: 'Campo di Iniziali',
date_field: 'Campo Data',
number_field: 'Campo Numero',
image_field: 'Campo Immagine',
file_field: 'Campo File',
select_field: 'Campo di Selezione',
checkbox_field: 'Campo di Checkbox',
multiple_field: 'Campo Selezione Multipla',
radio_field: 'Campo di Gruppo Radio',
cells_field: 'Campo Celle',
stamp_field: 'Campo Timbro',
payment_field: 'Campo Pagamento',
phone_field: 'Campo Telefono',
draw_a_text_field_on_the_page_with_a_mouse: 'Disegna un campo di testo sulla pagina con il mouse', draw_a_text_field_on_the_page_with_a_mouse: 'Disegna un campo di testo sulla pagina con il mouse',
drag_and_drop_any_other_field_type_on_the_page: 'Trascina e rilascia qualsiasi altro tipo di campo sulla pagina', drag_and_drop_any_other_field_type_on_the_page: 'Trascina e rilascia qualsiasi altro tipo di campo sulla pagina',
click_on_the_field_type_above_to_start_drawing_it: 'Clicca sul tipo di campo sopra per iniziare a disegnarlo', click_on_the_field_type_above_to_start_drawing_it: 'Clicca sul tipo di campo sopra per iniziare a disegnarlo',
@@ -416,6 +461,8 @@ const it = {
} }
const pt = { const pt = {
recurrent: 'Recurrente',
one_off: 'Único',
editable: 'Editável', editable: 'Editável',
search_field: 'Campo de busca', search_field: 'Campo de busca',
field_not_found: 'Campo não encontrado', field_not_found: 'Campo não encontrado',
@@ -454,7 +501,7 @@ const pt = {
up: 'Para cima', up: 'Para cima',
down: 'Para baixo', down: 'Para baixo',
set_signing_date: 'Definir data de assinatura', set_signing_date: 'Definir data de assinatura',
are_you_sure: 'Tem certeza?', are_you_sure_: 'Tem certeza?',
sign_yourself: 'Assine você mesmo', sign_yourself: 'Assine você mesmo',
checked: 'Marcado', checked: 'Marcado',
send: 'Enviar', send: 'Enviar',
@@ -462,7 +509,7 @@ const pt = {
save: 'Salvar', save: 'Salvar',
cancel: 'Cancelar', cancel: 'Cancelar',
or_add_field_without_drawing: 'Ou adicione campo sem desenhar', or_add_field_without_drawing: 'Ou adicione campo sem desenhar',
draw_field_on_the_document: 'Desenhar campo {field} no documento', draw_field_on_the_document: 'Desenhar um campo no documento',
click_to_upload: 'Clique para enviar', click_to_upload: 'Clique para enviar',
or_drag_and_drop_files: 'ou arraste e solte arquivos', or_drag_and_drop_files: 'ou arraste e solte arquivos',
uploading: 'Carregando', uploading: 'Carregando',
@@ -509,14 +556,27 @@ const pt = {
file: 'Arquivo', file: 'Arquivo',
select: 'Selecionar', select: 'Selecionar',
checkbox: 'Caixa', checkbox: 'Caixa',
field: 'Campo',
group: 'Grupo',
multiple: 'Múltiplo', multiple: 'Múltiplo',
radio: 'Rádio', radio: 'Rádio',
cells: 'Células', cells: 'Células',
stamp: 'Carimbo', stamp: 'Carimbo',
payment: 'Pagamento', payment: 'Pagamento',
phone: 'Telefone', phone: 'Telefone',
text_field: 'Campo de Texto',
signature_field: 'Campo de Assinatura',
initials_field: 'Campo de Rúbrica',
date_field: 'Campo de Data',
number_field: 'Campo de Número',
image_field: 'Campo de Imagem',
file_field: 'Campo de Arquivo',
select_field: 'Campo de Seleção',
checkbox_field: 'Campo de Caixa',
multiple_field: 'Campo de Opção Múltipla',
radio_field: 'Campo de Grupo Rádio',
cells_field: 'Campo de Células',
stamp_field: 'Campo de Carimbo',
payment_field: 'Campo de Pagamento',
phone_field: 'Campo de Telefone',
draw_a_text_field_on_the_page_with_a_mouse: 'Desenhar um campo de texto na página com o mouse', draw_a_text_field_on_the_page_with_a_mouse: 'Desenhar um campo de texto na página com o mouse',
drag_and_drop_any_other_field_type_on_the_page: 'Arraste e solte qualquer outro tipo de campo na página', drag_and_drop_any_other_field_type_on_the_page: 'Arraste e solte qualquer outro tipo de campo na página',
click_on_the_field_type_above_to_start_drawing_it: 'Clique no tipo de campo acima para começar a desenhá-lo', click_on_the_field_type_above_to_start_drawing_it: 'Clique no tipo de campo acima para começar a desenhá-lo',
@@ -555,6 +615,8 @@ const pt = {
} }
const fr = { const fr = {
recurrent: 'Récurrent',
one_off: 'Ponctuel',
editable: 'Éditable', editable: 'Éditable',
search_field: 'Champ de recherche', search_field: 'Champ de recherche',
field_not_found: 'Champ non trouvé', field_not_found: 'Champ non trouvé',
@@ -593,14 +655,14 @@ const fr = {
up: 'Haut', up: 'Haut',
down: 'Bas', down: 'Bas',
set_signing_date: 'Définir la date de signature', set_signing_date: 'Définir la date de signature',
are_you_sure: 'Êtes-vous sûr ?', are_you_sure_: 'Êtes-vous sûr?',
sign_yourself: 'Signez-vous', sign_yourself: 'Signez-vous',
checked: 'Coché', checked: 'Coché',
send: 'Envoyer', send: 'Envoyer',
remove: 'Supprimer', remove: 'Supprimer',
save: 'Enregistrer', save: 'Enregistrer',
cancel: 'Annuler', cancel: 'Annuler',
draw_field_on_the_document: 'Dessinez le champ {field} sur le document', draw_field_on_the_document: 'Dessiner un champ sur le document',
click_to_upload: 'Cliquez pour télécharger', click_to_upload: 'Cliquez pour télécharger',
or_drag_and_drop_files: 'ou faites glisser-déposer des fichiers', or_drag_and_drop_files: 'ou faites glisser-déposer des fichiers',
uploading: 'Téléchargement en cours', uploading: 'Téléchargement en cours',
@@ -654,8 +716,21 @@ const fr = {
stamp: 'Tampon', stamp: 'Tampon',
payment: 'Paiement', payment: 'Paiement',
phone: 'Téléphone', phone: 'Téléphone',
field: 'Champ', text_field: 'Champ de Texte',
group: 'Groupe', signature_field: 'Champ de Signature',
initials_field: "Champ d'Initiales",
date_field: 'Champ de Date',
number_field: 'Champ de Numéro',
image_field: "Champ d'Image",
file_field: 'Champ de Fichier',
select_field: 'Champ de Choix',
checkbox_field: 'Champ de Coche',
multiple_field: 'Champ de Choix Multiple',
radio_field: 'Champ de Groupe Radio',
cells_field: 'Champ de Cellules',
stamp_field: 'Champ de Tampon',
payment_field: 'Champ de Paiement',
phone_field: 'Champ de Téléphone',
draw_a_text_field_on_the_page_with_a_mouse: 'Dessinez un champ texte sur la page avec une souris', draw_a_text_field_on_the_page_with_a_mouse: 'Dessinez un champ texte sur la page avec une souris',
drag_and_drop_any_other_field_type_on_the_page: 'Glissez et déposez tout autre type de champ sur la page', drag_and_drop_any_other_field_type_on_the_page: 'Glissez et déposez tout autre type de champ sur la page',
click_on_the_field_type_above_to_start_drawing_it: 'Cliquez sur le type de champ ci-dessus pour commencer à le dessiner', click_on_the_field_type_above_to_start_drawing_it: 'Cliquez sur le type de champ ci-dessus pour commencer à le dessiner',
@@ -694,6 +769,8 @@ const fr = {
} }
const de = { const de = {
wiederkehrend: 'Wiederkehrend',
einmalig: 'Einmalig',
editable: 'Bearbeitbar', editable: 'Bearbeitbar',
search_field: 'Suchfeld', search_field: 'Suchfeld',
field_not_found: 'Feld nicht gefunden', field_not_found: 'Feld nicht gefunden',
@@ -732,14 +809,14 @@ const de = {
up: 'Nach oben', up: 'Nach oben',
down: 'Nach unten', down: 'Nach unten',
set_signing_date: 'Unterschriftdatum festlegen', set_signing_date: 'Unterschriftdatum festlegen',
are_you_sure: 'Bist du sicher?', are_you_sure_: 'Bist du sicher?',
sign_yourself: 'Unterschreiben Sie selbst', sign_yourself: 'Unterschreiben Sie selbst',
checked: 'Ausgewählt', checked: 'Ausgewählt',
send: 'Senden', send: 'Senden',
remove: 'Entfernen', remove: 'Entfernen',
save: 'Speichern', save: 'Speichern',
cancel: 'Abbrechen', cancel: 'Abbrechen',
draw_field_on_the_document: 'Feld {field} auf dem Dokument zeichnen', draw_field_on_the_document: 'Ein Feld auf dem Dokument zeichnen',
click_to_upload: 'Klicken Sie zum Hochladen', click_to_upload: 'Klicken Sie zum Hochladen',
or_drag_and_drop_files: 'oder Dateien hierher ziehen', or_drag_and_drop_files: 'oder Dateien hierher ziehen',
uploading: 'Hochladen', uploading: 'Hochladen',
@@ -793,8 +870,21 @@ const de = {
stamp: 'Stempel', stamp: 'Stempel',
payment: 'Zahlung', payment: 'Zahlung',
phone: 'Telefon', phone: 'Telefon',
field: 'Feld', text_field: 'Textfeld',
group: 'Gruppe', signature_field: 'Unterschriftsfeld',
initials_field: 'Initialenfeld',
date_field: 'Datumsfeld',
number_field: 'Zahlenfeld',
image_field: 'Bildfeld',
file_field: 'Dateifeld',
select_field: 'Auswahlfeld',
checkbox_field: 'Checkbox-Feld',
multiple_field: 'Mehrfach-Auswahlfeld',
radio_field: 'Radio-Gruppenfeld',
cells_field: 'Zellenfeld',
stamp_field: 'Stempelfeld',
payment_field: 'Zahlungsfeld',
phone_field: 'Telefonfeld',
draw_a_text_field_on_the_page_with_a_mouse: 'Textfeld auf der Seite mit der Maus zeichnen', draw_a_text_field_on_the_page_with_a_mouse: 'Textfeld auf der Seite mit der Maus zeichnen',
drag_and_drop_any_other_field_type_on_the_page: 'Ziehe und lasse einen anderen Feldtyp auf die Seite fallen', drag_and_drop_any_other_field_type_on_the_page: 'Ziehe und lasse einen anderen Feldtyp auf die Seite fallen',
click_on_the_field_type_above_to_start_drawing_it: 'Klicke auf den Feldtyp oben, um mit dem Zeichnen zu beginnen', click_on_the_field_type_above_to_start_drawing_it: 'Klicke auf den Feldtyp oben, um mit dem Zeichnen zu beginnen',
@@ -50,6 +50,7 @@
<div class="flex"> <div class="flex">
<select <select
class="base-select !select-sm !h-10" class="base-select !select-sm !h-10"
:class="{ '!text-gray-300': !mapping.field_name }"
required required
@change="mapping.field_name = $event.target.value" @change="mapping.field_name = $event.target.value"
> >
@@ -65,6 +66,7 @@
:key="index" :key="index"
:value="field.name" :value="field.name"
:selected="mapping.field_name === field.name" :selected="mapping.field_name === field.name"
class="text-base-content"
> >
{{ field.name }} {{ field.name }}
</option> </option>
@@ -75,6 +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 && mapping.column_index != 0 }"
required required
@change="mapping.column_index = parseInt($event.target.value)" @change="mapping.column_index = parseInt($event.target.value)"
> >
@@ -93,6 +96,7 @@
v-if="column" v-if="column"
:value="index" :value="index"
:selected="index === mapping.column_index" :selected="index === mapping.column_index"
class="text-base-content"
> >
{{ column }} {{ column }}
</option> </option>
@@ -281,7 +285,7 @@ export default {
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({
@@ -21,6 +21,7 @@
@click="closeDropdown" @click="closeDropdown"
> >
<div <div
v-if="!('price_id' in field.preferences)"
class="py-1.5 px-1 relative" class="py-1.5 px-1 relative"
@click.stop @click.stop
> >
@@ -58,6 +59,13 @@
class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0" class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0"
@blur="save" @blur="save"
> >
<input
v-else-if="'price_id' in field.preferences"
v-model="field.preferences.price_id"
placeholder="Price ID: price_XXXXX"
class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0"
@blur="save"
>
<input <input
v-else v-else
v-model="field.preferences.price" v-model="field.preferences.price"
@@ -74,6 +82,23 @@
> >
{{ t('price') }} {{ t('price') }}
</label> </label>
<div class="flex items-center justify-center">
<a
href="#"
class="hover:underline"
style="font-size: 11px"
:class="{'underline': !('price_id' in field.preferences)}"
@click="delete field.preferences.price_id"
>{{ t('one_off') }}</a>
<span class="h-2.5 border-l border-base-content mx-1" />
<a
href="#"
class="hover:underline"
style="font-size: 11px"
:class="{'underline': ('price_id' in field.preferences)}"
@click="field.preferences.price_id ??= ''"
>{{ t('recurrent') }}</a>
</div>
</div> </div>
<div <div
v-if="!isConnected || isOauthSuccess" v-if="!isConnected || isOauthSuccess"
@@ -153,12 +178,15 @@
<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>
</div> </div>
<li class="mb-1"> <li
v-if="!('price_id' in field.preferences)"
class="mb-1"
>
<label <label
class="label-text cursor-pointer text-center w-full flex items-center" class="label-text cursor-pointer text-center w-full flex items-center"
@click="$emit('click-formula')" @click="$emit('click-formula')"
+17
View File
@@ -0,0 +1,17 @@
# frozen_string_literal: true
class GeneratePreviewImagesJob
include Sidekiq::Job
sidekiq_options queue: :images
def perform(params = {})
attachment = ActiveStorage::Attachment.find(params['attachment_id'])
max_page = [attachment.metadata['pdf']['number_of_pages'].to_i - 1,
Templates::ProcessDocument::MAX_NUMBER_OF_PAGES_PROCESSED].min
Templates::ProcessDocument.generate_document_preview_images(attachment, attachment.download, (1..max_page),
concurrency: 1)
end
end
+36 -18
View File
@@ -6,6 +6,8 @@ class ProcessSubmitterCompletionJob
def perform(params = {}) def perform(params = {})
submitter = Submitter.find(params['submitter_id']) submitter = Submitter.find(params['submitter_id'])
create_completed_submitter!(submitter)
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? if !is_all_completed && submitter.submission.submitters_order_preserved?
@@ -24,36 +26,52 @@ class ProcessSubmitterCompletionJob
enqueue_completed_emails(submitter) enqueue_completed_emails(submitter)
end end
create_completed_documents!(submitter)
enqueue_completed_webhooks(submitter, is_all_completed:) enqueue_completed_webhooks(submitter, is_all_completed:)
end end
def enqueue_completed_webhooks(submitter, is_all_completed: false) def create_completed_submitter!(submitter)
webhook_config = Accounts.load_webhook_config(submitter.account) completed_submitter = CompletedSubmitter.find_or_initialize_by(submitter_id: submitter.id)
if webhook_config return completed_submitter if completed_submitter.persisted?
SendFormCompletedWebhookRequestJob.perform_async({ 'submitter_id' => submitter.id,
'encrypted_config_id' => webhook_config.id })
end
webhook_urls = submitter.account.webhook_urls submission = submitter.submission
webhook_urls = webhook_urls.where( completed_submitter.assign_attributes(
Arel::Table.new(:webhook_urls)[:events].matches('%"form.completed"%') submission_id: submitter.submission_id,
).or( account_id: submission.account_id,
webhook_urls.where( template_id: submission.template_id,
Arel::Table.new(:webhook_urls)[:events].matches('%"submission.completed"%') source: submission.source,
) sms_count: submitter.submission_events.where(event_type: %w[send_sms send_2fa_sms]).count,
completed_at: submitter.completed_at
) )
webhook_urls.each do |webhook| completed_submitter.save!
completed_submitter
rescue ActiveRecord::RecordNotUnique
retry
end
def create_completed_documents!(submitter)
submitter.documents.filter_map do |attachment|
next if attachment.metadata['sha256'].blank?
CompletedDocument.find_or_create_by!(sha256: attachment.metadata['sha256'], submitter_id: submitter.id)
end
end
def enqueue_completed_webhooks(submitter, is_all_completed: false)
WebhookUrls.for_account_id(submitter.account_id, %w[form.completed submission.completed]).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,31 +5,30 @@ 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, **webhook_url.secret.to_h,
'Content-Type' => 'application/json', 'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT) 'User-Agent' => USER_AGENT)
rescue Faraday::Error rescue Faraday::Error
@@ -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,30 +5,26 @@ 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, **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
@@ -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
@@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class ApplicationMailer < ActionMailer::Base class ApplicationMailer < ActionMailer::Base
default from: 'DocuSeal <info@docuseal.co>' default from: 'DocuSeal <info@docuseal.com>'
layout 'mailer' layout 'mailer'
register_interceptor ActionMailerConfigsInterceptor register_interceptor ActionMailerConfigsInterceptor
+1 -1
View File
@@ -2,6 +2,6 @@
class SettingsMailer < ApplicationMailer class SettingsMailer < ApplicationMailer
def smtp_successful_setup(email) def smtp_successful_setup(email)
mail(to: email, subject: 'SMTP has been configured') mail(to: email, from: email, subject: 'SMTP has been configured')
end end
end end
+4 -2
View File
@@ -8,7 +8,9 @@ class UserMailer < ApplicationMailer
assign_message_metadata('user_invitation', @user) assign_message_metadata('user_invitation', @user)
mail(to: @user.friendly_name, I18n.with_locale(@current_account.locale) do
subject: "You are invited to #{Docuseal.product_name}") mail(to: @user.friendly_name,
subject: I18n.t('you_are_invited_to_product_name', product_name: Docuseal.product_name))
end
end end
end end
+3 -1
View File
@@ -28,18 +28,20 @@ class AccountConfig < ApplicationRecord
FORCE_MFA = 'force_mfa' FORCE_MFA = 'force_mfa'
ALLOW_TYPED_SIGNATURE = 'allow_typed_signature' ALLOW_TYPED_SIGNATURE = 'allow_typed_signature'
ALLOW_TO_RESUBMIT = 'allow_to_resubmit' ALLOW_TO_RESUBMIT = 'allow_to_resubmit'
ALLOW_TO_DECLINE_KEY = 'allow_to_decline'
SUBMITTER_REMINDERS = 'submitter_reminders' SUBMITTER_REMINDERS = 'submitter_reminders'
FORM_COMPLETED_BUTTON_KEY = 'form_completed_button' FORM_COMPLETED_BUTTON_KEY = 'form_completed_button'
FORM_COMPLETED_MESSAGE_KEY = 'form_completed_message' FORM_COMPLETED_MESSAGE_KEY = 'form_completed_message'
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'
WITH_SIGNATURE_ID = 'with_signature_id' WITH_SIGNATURE_ID = 'with_signature_id'
WITH_AUDIT_VALUES_KEY = 'with_audit_values'
REQUIRE_SIGNING_REASON_KEY = 'require_signing_reason' REQUIRE_SIGNING_REASON_KEY = 'require_signing_reason'
REUSE_SIGNATURE_KEY = 'reuse_signature'
COMBINE_PDF_RESULT_KEY = 'combine_pdf_result_key' COMBINE_PDF_RESULT_KEY = 'combine_pdf_result_key'
DEFAULT_VALUES = { DEFAULT_VALUES = {
+22
View File
@@ -0,0 +1,22 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: completed_documents
#
# id :bigint not null, primary key
# sha256 :string not null
# created_at :datetime not null
# updated_at :datetime not null
# submitter_id :bigint not null
#
# Indexes
#
# index_completed_documents_on_sha256 (sha256)
# index_completed_documents_on_submitter_id (submitter_id)
#
class CompletedDocument < ApplicationRecord
belongs_to :submitter, optional: true
has_one :completed_submitter, primary_key: :submitter_id, inverse_of: :completed_documents, dependent: :destroy
end
+33
View File
@@ -0,0 +1,33 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: completed_submitters
#
# id :bigint not null, primary key
# completed_at :datetime not null
# sms_count :integer not null
# source :string not null
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint not null
# submission_id :bigint not null
# submitter_id :bigint not null
# template_id :bigint not null
#
# Indexes
#
# index_completed_submitters_on_account_id (account_id)
# index_completed_submitters_on_submitter_id (submitter_id) UNIQUE
#
class CompletedSubmitter < ApplicationRecord
belongs_to :submitter
belongs_to :submission
belongs_to :account
belongs_to :template
has_many :completed_documents, dependent: :destroy,
primary_key: :submitter_id,
foreign_key: :submitter_id,
inverse_of: :submitter
end
+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
+2
View File
@@ -55,6 +55,8 @@ class Submission < ApplicationRecord
has_one_attached :audit_trail has_one_attached :audit_trail
has_one_attached :combined_document has_one_attached :combined_document
has_many_attached :preview_documents
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
+2
View File
@@ -56,6 +56,8 @@ class Submitter < ApplicationRecord
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
has_many :start_form_submission_events, -> { where(event_type: :start_form) },
class_name: 'SubmissionEvent', dependent: :destroy, inverse_of: :submitter
scope :completed, -> { where.not(completed_at: nil) } scope :completed, -> { where.not(completed_at: nil) }
+3
View File
@@ -49,6 +49,9 @@ class User < ApplicationRecord
EMAIL_REGEXP = /[^@;,<>\s]+@[^@;,<>\s]+/ EMAIL_REGEXP = /[^@;,<>\s]+@[^@;,<>\s]+/
FULL_EMAIL_REGEXP =
/\A[a-z0-9][\.']?(?:(?:[a-z0-9_-]+[\.\+'])*[a-z0-9_-]+)*@(?:[a-z0-9]+[\.-])*[a-z0-9]+\.[a-z]{2,}\z/i
has_one_attached :signature has_one_attached :signature
has_one_attached :initials has_one_attached :initials
+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)
+12
View File
@@ -103,6 +103,18 @@
</div> </div>
<% end %> <% end %>
<% end %> <% end %>
<% account_config = AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::ALLOW_TO_DECLINE_KEY) %>
<% if can?(:manage, account_config) %>
<%= form_for account_config, url: account_configs_path, method: :post do |f| %>
<%= f.hidden_field :key %>
<div class="flex items-center justify-between py-2.5">
<span>
<%= t('allow_to_decline_documents') %>
</span>
<%= f.check_box :value, class: 'toggle', checked: account_config.value != false, onchange: 'this.form.requestSubmit()' %>
</div>
<% end %>
<% end %>
<% account_config = AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::FORM_PREFILL_SIGNATURE_KEY) %> <% account_config = AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::FORM_PREFILL_SIGNATURE_KEY) %>
<% if can?(:manage, account_config) %> <% if can?(:manage, account_config) %>
<%= form_for account_config, url: account_configs_path, method: :post do |f| %> <%= form_for account_config, url: account_configs_path, method: :post do |f| %>
+5 -2
View File
@@ -10,8 +10,11 @@
<label for="api_key" class="text-sm font-semibold">X-Auth-Token</label> <label for="api_key" class="text-sm font-semibold">X-Auth-Token</label>
<div class="flex flex-col md:flex-row gap-4"> <div class="flex flex-col md:flex-row gap-4">
<div class="flex w-full space-x-4"> <div class="flex w-full space-x-4">
<input id="api_key" type="text" value="<%= current_user.access_token.token %>" class="input font-mono input-bordered w-full" autocomplete="off" readonly> <% token = current_user.access_token.token %>
<%= render 'shared/clipboard_copy', icon: 'copy', text: current_user.access_token.token, class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: t('copy'), copied_title: t('copied') %> <masked-input class="block w-full" data-token="<%= token %>">
<input id="api_key" type="text" value="<%= token.sub(token[5..], '*' * token[5..].size) %>" class="input font-mono input-bordered w-full" autocomplete="off" readonly>
</masked-input>
<%= render 'shared/clipboard_copy', icon: 'copy', text: token, class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: t('copy'), copied_title: t('copied') %>
</div> </div>
<%= button_to button_title(title: t('rotate'), disabled_with: t('rotate'), icon: svg_icon('reload', class: 'w-6 h-6')), settings_api_index_path, class: 'white-button w-full', data: { turbo_confirm: t('remove_existing_api_token_and_generated_a_new_one_are_you_sure_') } %> <%= button_to button_title(title: t('rotate'), disabled_with: t('rotate'), icon: svg_icon('reload', class: 'w-6 h-6')), settings_api_index_path, class: 'white-button w-full', data: { turbo_confirm: t('remove_existing_api_token_and_generated_a_new_one_are_you_sure_') } %>
</div> </div>
+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>
@@ -1,10 +1,10 @@
<div class="text-center"> <div class="text-center">
<div class="join"> <div class="join">
<a href="https://docuseal.co<%= request.fullpath.gsub('docuseal.eu', 'docuseal.co') %>" class="btn bg-base-200 join-item w-32 <%= 'bg-base-300' if request.host == 'docuseal.co' %>"> <a href="https://docuseal.com<%= request.fullpath.gsub('docuseal.eu', 'docuseal.com') %>" class="btn bg-base-200 join-item w-32 <%= 'bg-base-300' if request.host == 'docuseal.com' %>">
<%= svg_icon 'world', class: 'w-5 h-5' %> <%= svg_icon 'world', class: 'w-5 h-5' %>
Global Global
</a> </a>
<a href="https://docuseal.eu<%= request.fullpath.gsub('docuseal.co', 'docuseal.eu') %>" class="btn bg-base-200 join-item w-32 <%= 'bg-base-300' if request.host == 'docuseal.eu' %>"> <a href="https://docuseal.eu<%= request.fullpath.gsub('docuseal.com', 'docuseal.eu') %>" class="btn bg-base-200 join-item w-32 <%= 'bg-base-300' if request.host == 'docuseal.eu' %>">
<%= svg_icon 'eu_flag', class: 'w-5 h-5' %> <%= svg_icon 'eu_flag', class: 'w-5 h-5' %>
Europe Europe
</a> </a>
+2 -2
View File
@@ -38,7 +38,7 @@
<div class="form-control"> <div class="form-control">
<%= ff.label :security_label, 'SMTP Security', class: 'label' %> <%= ff.label :security_label, 'SMTP Security', class: 'label' %>
<div class="flex items-center space-x-6"> <div class="flex items-center space-x-6">
<% [%w[Auto none], %w[SSL ssl], %w[TLS tls]].each do |(label, val)| %> <% [%w[Auto none], %w[SSL ssl], %w[TLS tls], %w[Noverify noverify]].each do |(label, val)| %>
<%= ff.label :security, value: val, for: "#{val}_radio", class: 'label' do %> <%= ff.label :security, value: val, for: "#{val}_radio", class: 'label' do %>
<%= ff.radio_button :security, val, checked: (value['security'].blank? && val == 'none') || value['security'] == val, id: "#{val}_radio", class: 'base-radio mr-2' %> <%= ff.radio_button :security, val, checked: (value['security'].blank? && val == 'none') || value['security'] == val, id: "#{val}_radio", class: 'base-radio mr-2' %>
<%= label %> <%= label %>
@@ -48,7 +48,7 @@
</div> </div>
<div class="form-control"> <div class="form-control">
<%= ff.label :from_email, t('send_from_email'), class: 'label' %> <%= ff.label :from_email, t('send_from_email'), class: 'label' %>
<%= ff.email_field :from_email, value: value['from_email'], required: true, class: 'base-input' %> <%= ff.email_field :from_email, value: value['from_email'], required: !Docuseal.multitenant?, class: 'base-input' %>
</div> </div>
<% end %> <% end %>
<div class="form-control pt-2"> <div class="form-control pt-2">
+3
View File
@@ -11,6 +11,9 @@
<% else %> <% else %>
<%= javascript_pack_tag 'application', defer: true %> <%= javascript_pack_tag 'application', defer: true %>
<% end %> <% end %>
<% if canonical_url = content_for(:canonical_url) %>
<link href="<%= canonical_url %>" rel="canonical">
<% end %>
<%= stylesheet_pack_tag 'application', media: 'all' %> <%= stylesheet_pack_tag 'application', media: 'all' %>
<%= render 'shared/posthog' if ENV['POSTHOG_TOKEN'] %> <%= render 'shared/posthog' if ENV['POSTHOG_TOKEN'] %>
<%= render 'shared/plausible' if !signed_in? && ENV['PLAUSIBLE_DOMAIN'] %> <%= render 'shared/plausible' if !signed_in? && ENV['PLAUSIBLE_DOMAIN'] %>
+1 -1
View File
@@ -27,7 +27,7 @@
</div> </div>
<h3 class="mb-4 text-2xl font-semibold">Easy to Start</h3> <h3 class="mb-4 text-2xl font-semibold">Easy to Start</h3>
<p class="text-base text-gray-500"> <p class="text-base text-gray-500">
Run on your own host using Docker container, or deploy on your favorite managed PaaS with a single <a href="https://www.docuseal.co/install" class="link link-neutral font-bold">click</a>. Run on your own host using Docker container, or deploy on your favorite managed PaaS with a single <a href="https://www.docuseal.com/install" class="link link-neutral font-bold">click</a>.
</p> </p>
</div> </div>
</div> </div>
+1 -1
View File
@@ -2,5 +2,5 @@
--- ---
</p> </p>
<p> <p>
Sent using <a href="<%= Docuseal::PRODUCT_URL %>/start"><%= Docuseal.product_name %></a> free document signing. <%= t('sent_using_product_name_free_document_signing_html', product_url: "#{Docuseal::PRODUCT_URL}/start", product_name: Docuseal.product_name) %>
</p> </p>
+1 -1
View File
@@ -1,3 +1,3 @@
<a href="<%= Docuseal::GITHUB_URL %>" target="_blank" class="inline"> <a href="<%= Docuseal::GITHUB_URL %>" target="_blank" class="inline">
<img alt="GitHub Repo stars" src="https://www.docuseal.co/github-badge.svg" style="height: 22px"> <img alt="GitHub Repo stars" src="https://www.docuseal.com/github-badge.svg" style="height: 22px">
</a> </a>
+1 -1
View File
@@ -10,7 +10,7 @@
<% if signed_in? %> <% if signed_in? %>
<div class="space-x-4 flex items-center"> <div class="space-x-4 flex items-center">
<% if Docuseal.demo? %> <% if Docuseal.demo? %>
<a href="https://docuseal.co/sign_up" class="btn btn-neutral btn-sm btn-outline inline-flex items-center justify-center" style="height: 37px"> <a href="https://docuseal.com/sign_up" class="btn btn-neutral btn-sm btn-outline inline-flex items-center justify-center" style="height: 37px">
<%= t('sign_up') %> <%= t('sign_up') %>
</a> </a>
<span class="hidden sm:inline"> <span class="hidden sm:inline">
+1 -1
View File
@@ -1,6 +1,6 @@
<div class="text-center px-2"> <div class="text-center px-2">
<% if local_assigns[:with_counter] %> <% if local_assigns[:with_counter] %>
<% count = Submitter.where.not(completed_at: nil).distinct.count(:submission_id) %> <% count = CompletedSubmitter.distinct.count(:submission_id) %>
<% if count > 1 %> <% if count > 1 %>
<%= t('count_documents_signed_with_html', count:) %> <%= t('count_documents_signed_with_html', count:) %>
<% else %> <% else %>
+3 -3
View File
@@ -56,7 +56,7 @@
<%= link_to 'API', settings_api_index_path, class: 'text-base hover:bg-base-300' %> <%= link_to 'API', settings_api_index_path, class: 'text-base hover:bg-base-300' %>
</li> </li>
<% end %> <% end %>
<% if can?(:read, EncryptedConfig.new(key: EncryptedConfig::WEBHOOK_URL_KEY, account: current_account)) %> <% if can?(:read, WebhookUrl) %>
<li> <li>
<%= link_to 'Webhooks', settings_webhooks_path, class: 'text-base hover:bg-base-300' %> <%= link_to 'Webhooks', settings_webhooks_path, class: 'text-base hover:bg-base-300' %>
</li> </li>
@@ -72,13 +72,13 @@
<% end %> <% end %>
<% if !Docuseal.demo? && can?(:manage, EncryptedConfig) && (current_user != true_user || !current_account.testing?) %> <% if !Docuseal.demo? && can?(:manage, EncryptedConfig) && (current_user != true_user || !current_account.testing?) %>
<li> <li>
<%= link_to Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/api") : "#{Docuseal::CONSOLE_URL}/on_premise", class: 'text-base hover:bg-base-300', data: { prefetch: false } do %> <%= link_to Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}#{'/test' if current_account.testing?}/api") : "#{Docuseal::CONSOLE_URL}/on_premise", class: 'text-base hover:bg-base-300', data: { prefetch: false } do %>
<% if Docuseal.multitenant? %> API <% else %> <%= t('console') %> <% end %> <% if Docuseal.multitenant? %> API <% else %> <%= t('console') %> <% end %>
<% end %> <% end %>
</li> </li>
<% if Docuseal.multitenant? %> <% if Docuseal.multitenant? %>
<li> <li>
<%= link_to console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/embedding/form"), class: 'text-base hover:bg-base-300', data: { prefetch: false } do %> <%= link_to console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}#{'/test' if current_account.testing?}/embedding/form"), class: 'text-base hover:bg-base-300', data: { prefetch: false } do %>
<%= t('embedding') %> <%= t('embedding') %>
<% end %> <% end %>
</li> </li>
@@ -31,7 +31,7 @@
</submitters-autocomplete> </submitters-autocomplete>
<submitters-autocomplete data-field="phone"> <submitters-autocomplete data-field="phone">
<linked-input data-target-id="<%= "detailed_phone_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>"> <linked-input data-target-id="<%= "detailed_phone_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
<input type="tel" pattern="^\+[0-9\s\-]+$" oninvalid="this.value ? this.setCustomValidity('Use internatioanl format: +1xxx...') : ''" oninput="this.setCustomValidity('')" name="submission[1][submitters][][phone]" autocomplete="off" class="base-input !h-10 mt-1.5 w-full" placeholder="<%= "#{t('phone')} (#{t('optional')})" %>" id="detailed_phone_<%= item['uuid'] %>"> <input type="tel" pattern="^\+[0-9\s\-]+$" oninvalid="this.value ? this.setCustomValidity('<%= t('use_international_format_1xxx_') %>') : ''" oninput="this.setCustomValidity('')" name="submission[1][submitters][][phone]" autocomplete="off" class="base-input !h-10 mt-1.5 w-full" placeholder="<%= "#{t('phone')} (#{t('optional')})" %>" id="detailed_phone_<%= item['uuid'] %>">
</linked-input> </linked-input>
</submitters-autocomplete> </submitters-autocomplete>
</div> </div>
+1 -1
View File
@@ -21,7 +21,7 @@
<input type="hidden" name="submission[1][submitters][][uuid]" value="<%= item['uuid'] %>"> <input type="hidden" name="submission[1][submitters][][uuid]" value="<%= item['uuid'] %>">
<submitters-autocomplete data-field="phone"> <submitters-autocomplete data-field="phone">
<linked-input data-target-id="<%= "phone_phone_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>"> <linked-input data-target-id="<%= "phone_phone_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
<%= tag.input type: 'tel', pattern: '^\+[0-9\s\-]+$', oninvalid: "this.value ? this.setCustomValidity('Use internatioanl format: +1xxx...') : ''", oninput: "this.setCustomValidity('')", name: 'submission[1][submitters][][phone]', autocomplete: 'off', class: 'base-input !h-10 w-full', placeholder: t('phone'), required: index.zero?, id: "phone_phone_#{item['uuid']}" %> <%= tag.input type: 'tel', pattern: '^\+[0-9\s\-]+$', oninvalid: "this.value ? this.setCustomValidity('#{t('use_international_format_1xxx_')}') : ''", oninput: "this.setCustomValidity('')", name: 'submission[1][submitters][][phone]', autocomplete: 'off', class: 'base-input !h-10 w-full', placeholder: t('phone'), required: index.zero?, id: "phone_phone_#{item['uuid']}" %>
</linked-input> </linked-input>
</submitters-autocomplete> </submitters-autocomplete>
<% if submitters.size > 1 %> <% if submitters.size > 1 %>
Loaded 100 of 177 files, more files were not shown because too many files have changed in this diff. Show more