Compare commits

..
41 Commits
Author SHA1 Message Date
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
70 changed files with 2684 additions and 481 deletions

No files matched your search

+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
+167 -161
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)
actionpack (= 7.1.3.4) actionpack (= 7.2.1)
activesupport (= 7.1.3.4) activesupport (= 7.2.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)
actionpack (= 7.1.3.4) actionpack (= 7.2.1)
activejob (= 7.1.3.4) activejob (= 7.2.1)
activerecord (= 7.1.3.4) activerecord (= 7.2.1)
activestorage (= 7.1.3.4) activestorage (= 7.2.1)
activesupport (= 7.1.3.4) activesupport (= 7.2.1)
mail (>= 2.7.1) mail (>= 2.8.0)
net-imap actionmailer (7.2.1)
net-pop actionpack (= 7.2.1)
net-smtp actionview (= 7.2.1)
actionmailer (7.1.3.4) activejob (= 7.2.1)
actionpack (= 7.1.3.4) activesupport (= 7.2.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)
actionview (= 7.1.3.4) actionview (= 7.2.1)
activesupport (= 7.1.3.4) activesupport (= 7.2.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)
activerecord (= 7.1.3.4) actionpack (= 7.2.1)
activestorage (= 7.1.3.4) activerecord (= 7.2.1)
activesupport (= 7.1.3.4) activestorage (= 7.2.1)
activesupport (= 7.2.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)
activesupport (= 7.1.3.4) activesupport (= 7.2.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)
activesupport (= 7.1.3.4) activesupport (= 7.2.1)
globalid (>= 0.3.6) globalid (>= 0.3.6)
activemodel (7.1.3.4) activemodel (7.2.1)
activesupport (= 7.1.3.4) activesupport (= 7.2.1)
activerecord (7.1.3.4) activerecord (7.2.1)
activemodel (= 7.1.3.4) activemodel (= 7.2.1)
activesupport (= 7.1.3.4) activesupport (= 7.2.1)
timeout (>= 0.4.0) timeout (>= 0.4.0)
activestorage (7.1.3.4) activestorage (7.2.1)
actionpack (= 7.1.3.4) actionpack (= 7.2.1)
activejob (= 7.1.3.4) activejob (= 7.2.1)
activerecord (= 7.1.3.4) activerecord (= 7.2.1)
activesupport (= 7.1.3.4) activesupport (= 7.2.1)
marcel (~> 1.0) marcel (~> 1.0)
activesupport (7.1.3.4) activesupport (7.2.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.16)
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,11 +364,11 @@ 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)
@@ -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)
actioncable (= 7.1.3.4) actioncable (= 7.2.1)
actionmailbox (= 7.1.3.4) actionmailbox (= 7.2.1)
actionmailer (= 7.1.3.4) actionmailer (= 7.2.1)
actionpack (= 7.1.3.4) actionpack (= 7.2.1)
actiontext (= 7.1.3.4) actiontext (= 7.2.1)
actionview (= 7.1.3.4) actionview (= 7.2.1)
activejob (= 7.1.3.4) activejob (= 7.2.1)
activemodel (= 7.1.3.4) activemodel (= 7.2.1)
activerecord (= 7.1.3.4) activerecord (= 7.2.1)
activestorage (= 7.1.3.4) activestorage (= 7.2.1)
activesupport (= 7.1.3.4) activesupport (= 7.2.1)
bundler (>= 1.15.0) bundler (>= 1.15.0)
railties (= 7.1.3.4) railties (= 7.2.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)
actionpack (= 7.1.3.4) actionpack (= 7.2.1)
activesupport (= 7.1.3.4) activesupport (= 7.2.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,7 +431,7 @@ 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)
@@ -441,18 +443,18 @@ GEM
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,11 +549,11 @@ 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)
@@ -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
@@ -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
+1 -1
View File
@@ -62,7 +62,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
+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
@@ -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
@@ -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
+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
+1 -1
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
@@ -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
-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;
}
+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;
} }
@@ -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"
> >
+12 -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) {
@@ -1142,13 +1145,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 +1163,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 +1188,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
@@ -197,6 +197,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 +206,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 +215,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>
+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) {
+2 -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
@@ -1355,7 +1355,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)
} }
@@ -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) }}
@@ -90,6 +92,7 @@
<select <select
v-if="conditionField(condition)?.options?.length" v-if="conditionField(condition)?.options?.length"
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,
+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 }"
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>
@@ -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"
@@ -158,7 +183,10 @@
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
+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
+2
View File
@@ -39,7 +39,9 @@ class AccountConfig < ApplicationRecord
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 = {
+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) }
+1 -1
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 %>
+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>
@@ -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 %>
+12 -10
View File
@@ -2,12 +2,12 @@
<% can_send_emails = Accounts.can_send_emails?(current_account) %> <% can_send_emails = Accounts.can_send_emails?(current_account) %>
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<%= f.label :send_email, for: uuid = SecureRandom.uuid, class: 'flex items-center cursor-pointer' do %> <%= f.label :send_email, for: uuid = SecureRandom.uuid, class: 'flex items-center cursor-pointer' do %>
<%= f.check_box :send_email, id: uuid, class: 'base-checkbox', disabled: !can_send_emails, checked: can_send_emails %> <%= f.check_box :send_email, id: uuid, class: 'base-checkbox', disabled: !can_send_emails, checked: can_send_emails && !local_assigns.key?(:resend_email) %>
<span class="label"><%= t('send_emails') %></span> <span class="label"><%= local_assigns[:resend_email] ? t('re_send_email') : t('send_email') %></span>
<% end %> <% end %>
<div> <div>
<% if can_send_emails %> <% if can_send_emails %>
<%= render 'email_stats' %> <%= render 'submissions/email_stats' %>
<%= content_for(:edit_button) || capture do %> <%= content_for(:edit_button) || capture do %>
<label> <label>
<%= f.check_box :is_custom_message, onchange: "[this.form.querySelector('#message_field').classList.toggle('hidden', !event.currentTarget.checked)]", checked: false, class: 'hidden peer' %> <%= f.check_box :is_custom_message, onchange: "[this.form.querySelector('#message_field').classList.toggle('hidden', !event.currentTarget.checked)]", checked: false, class: 'hidden peer' %>
@@ -39,7 +39,7 @@
<div class="form-control space-y-2"> <div class="form-control space-y-2">
<div class="form-control"> <div class="form-control">
<%= f.label :subject, t('subject'), class: 'label' %> <%= f.label :subject, t('subject'), class: 'label' %>
<%= f.text_field :subject, value: @template.preferences['request_email_subject'].presence || config.value['subject'], required: true, class: '!text-sm base-input w-full', dir: 'auto' %> <%= f.text_field :subject, value: local_assigns[:submitter_email_message]&.subject.presence || template.preferences['request_email_subject'].presence || config.value['subject'], required: true, class: '!text-sm base-input w-full', dir: 'auto' %>
</div> </div>
<div class="form-control"> <div class="form-control">
<div class="flex items-center"> <div class="flex items-center">
@@ -49,14 +49,16 @@
</span> </span>
</div> </div>
<autoresize-textarea> <autoresize-textarea>
<%= f.text_area :body, value: @template.preferences['request_email_body'].presence || config.value['body'], required: true, class: 'base-textarea w-full', rows: 10, dir: 'auto' %> <%= f.text_area :body, value: local_assigns[:submitter_email_message]&.body.presence || template.preferences['request_email_body'].presence || config.value['body'], required: true, class: 'base-textarea w-full', rows: 10, dir: 'auto' %>
</autoresize-textarea> </autoresize-textarea>
<label for="<%= uuid = SecureRandom.uuid %>" class="flex items-center cursor-pointer"> <% unless local_assigns.fetch(:disable_save_as_default_template_option, false) %>
<%= check_box_tag :save_message, id: uuid, class: 'base-checkbox', checked: false %> <label for="<%= uuid = SecureRandom.uuid %>" class="flex items-center cursor-pointer">
<span class="label"><%= t('save_as_default_template_message') %></span> <%= check_box_tag :save_message, id: uuid, class: 'base-checkbox', checked: false %>
</label> <span class="label"><%= t('save_as_default_template_message') %></span>
</label>
<% end %>
</div> </div>
<%= render 'message_fields' %> <%= render 'submissions/message_fields' %>
</div> </div>
</div> </div>
</div> </div>
+16 -7
View File
@@ -107,12 +107,21 @@
<% (@submission.template_submitters || @submission.template.submitters).each_with_index do |item, index| %> <% (@submission.template_submitters || @submission.template.submitters).each_with_index do |item, index| %>
<% submitter = @submission.submitters.find { |e| e.uuid == item['uuid'] } %> <% submitter = @submission.submitters.find { |e| e.uuid == item['uuid'] } %>
<div class="sticky -top-1 bg-base-100 pt-1 -mt-1"> <div class="sticky -top-1 bg-base-100 pt-1 -mt-1">
<div class="border border-base-300 rounded-md px-2 py-1 mb-1"> <div class="group border border-base-300 rounded-md px-2 py-1 mb-1">
<div class="flex items-center space-x-1"> <div class="flex items-center justify-between">
<span class="mx-1 w-3 h-3 rounded-full <%= colors[index] %>"></span> <div class="flex items-center space-x-1">
<span class="text-lg" dir="auto"> <span class="mx-1 w-3 h-3 shrink-0 rounded-full <%= colors[index] %>"></span>
<%= (@submission.template_submitters || @submission.template.submitters).find { |e| e['uuid'] == submitter&.uuid }&.dig('name') || "#{(index + 1).ordinalize} Submitter" %> <span class="text-lg" dir="auto">
</span> <%= (@submission.template_submitters || @submission.template.submitters).find { |e| e['uuid'] == submitter&.uuid }&.dig('name') || "#{(index + 1).ordinalize} Submitter" %>
</span>
</div>
<% if signed_in? && can?(:update, submitter) && !submitter.completed_at? && !submitter.declined_at? && !@submission.archived_at? && !@submission.expired? && !submitter.start_form_submission_events.any? %>
<span class="tooltip tooltip-left" data-tip="<%= t('edit') %>">
<%= link_to edit_submitter_path(submitter), class: 'shrink-0 inline md:hidden md:group-hover:inline', data: { turbo_frame: 'modal' } do %>
<%= svg_icon('pencil', class: 'w-5 h-5') %>
<% end %>
</span>
<% end %>
</div> </div>
<% if submitter&.name.present? %> <% if submitter&.name.present? %>
<div class="flex items-center space-x-1 mt-1"> <div class="flex items-center space-x-1 mt-1">
@@ -182,7 +191,7 @@
<% next if value.blank? %> <% next if value.blank? %>
<div class="pt-2.5 border-b border-base-300"> <div class="pt-2.5 border-b border-base-300">
<div class="text-xs font-medium uppercase mb-0.5" dir="auto"> <div class="text-xs font-medium uppercase mb-0.5" dir="auto">
<%= field['name'].presence || "#{field['type'].titleize} Field #{submitter_field_counters[field['type']]}" %> <%= field['name'].presence || "#{t("#{field['type']}_field")} #{submitter_field_counters[field['type']]}" %>
</div> </div>
<div dir="auto"> <div dir="auto">
<% if field['type'].in?(%w[signature initials]) %> <% if field['type'].in?(%w[signature initials]) %>
@@ -1,4 +1,4 @@
<% data_attachments = attachments_index.values.select { |e| e.record_id == submitter.id }.to_json(only: %i[uuid created_at], methods: %i[url filename content_type]) %> <% data_attachments = attachments_index.values.select { |e| e.record_id == submitter.id }.to_json(only: %i[uuid created_at], methods: %i[url filename content_type]) %>
<% data_fields = (submitter.submission.template_fields || submitter.submission.template.fields).select { |f| f['submitter_uuid'] == submitter.uuid }.to_json %> <% data_fields = (submitter.submission.template_fields || submitter.submission.template.fields).select { |f| f['submitter_uuid'] == submitter.uuid }.to_json %>
<% invite_submitters = (submitter.submission.template_submitters || submitter.submission.template.submitters).select { |s| s['invite_by_uuid'] == submitter.uuid && submitter.submission.submitters.none? { |e| e.uuid == s['uuid'] } }.to_json %> <% invite_submitters = (submitter.submission.template_submitters || submitter.submission.template.submitters).select { |s| s['invite_by_uuid'] == submitter.uuid && submitter.submission.submitters.none? { |e| e.uuid == s['uuid'] } }.to_json %>
<submission-form data-is-demo="<%= Docuseal.demo? %>" data-require-signing-reason="<%= configs[:require_signing_reason] %>" data-with-signature-id="<%= configs[:with_signature_id] %>" data-with-confetti="<%= configs[:with_confetti] %>" data-completed-redirect-url="<%= submitter.preferences['completed_redirect_url'].presence || submitter.submission.template.preferences['completed_redirect_url'] %>" data-completed-message="<%= (configs[:completed_message]&.compact_blank.presence || submitter.submission.template.preferences['completed_message'] || {}).to_json %>" data-completed-button="<%= configs[:completed_button].to_json %>" data-go-to-last="<%= submitter.preferences.key?('go_to_last') ? submitter.preferences['go_to_last'] : submitter.opened_at? %>" data-submitter="<%= submitter.to_json(only: %i[uuid slug name phone email]) %>" data-can-send-email="<%= Accounts.can_send_emails?(submitter.submission.account) %>" data-invite-submitters="<%= invite_submitters %>" data-attachments="<%= data_attachments %>" data-fields="<%= data_fields %>" data-values="<%= submitter.values.to_json %>" data-with-typed-signature="<%= configs[:with_typed_signature] %>" data-previous-signature-value="<%= local_assigns[:signature_attachment]&.uuid %>" data-remember-signature="<%= configs[:prefill_signature] %>" data-dry-run="<%= local_assigns[:dry_run] %>" data-expand="<%= local_assigns[:expand] %>" data-scroll-padding="<%= local_assigns[:scroll_padding] %>" data-language="<%= I18n.locale.to_s.split('-').first %>"></submission-form> <submission-form data-is-demo="<%= Docuseal.demo? %>" data-reuse-signature="<%= configs[:reuse_signature] %>" data-require-signing-reason="<%= configs[:require_signing_reason] %>" data-with-signature-id="<%= configs[:with_signature_id] %>" data-with-confetti="<%= configs[:with_confetti] %>" data-completed-redirect-url="<%= submitter.preferences['completed_redirect_url'].presence || submitter.submission.template.preferences['completed_redirect_url'] %>" data-completed-message="<%= (configs[:completed_message]&.compact_blank.presence || submitter.submission.template.preferences['completed_message'] || {}).to_json %>" data-completed-button="<%= configs[:completed_button].to_json %>" data-go-to-last="<%= submitter.preferences.key?('go_to_last') ? submitter.preferences['go_to_last'] : submitter.opened_at? %>" data-submitter="<%= submitter.to_json(only: %i[uuid slug name phone email]) %>" data-can-send-email="<%= Accounts.can_send_emails?(submitter.submission.account) %>" data-invite-submitters="<%= invite_submitters %>" data-attachments="<%= data_attachments %>" data-fields="<%= data_fields %>" data-values="<%= submitter.values.to_json %>" data-with-typed-signature="<%= configs[:with_typed_signature] %>" data-previous-signature-value="<%= local_assigns[:signature_attachment]&.uuid %>" data-remember-signature="<%= configs[:prefill_signature] %>" data-dry-run="<%= local_assigns[:dry_run] %>" data-expand="<%= local_assigns[:expand] %>" data-scroll-padding="<%= local_assigns[:scroll_padding] %>" data-language="<%= I18n.locale.to_s.split('-').first %>"></submission-form>
+5 -4
View File
@@ -21,12 +21,13 @@
<% end %> <% end %>
</div> </div>
<download-button data-src="<%= submit_form_download_index_path(@submitter.slug) %>" class="btn btn-neutral text-white btn-sm !px-4"> <download-button data-src="<%= submit_form_download_index_path(@submitter.slug) %>" class="btn btn-neutral text-white btn-sm !px-4">
<span class="flex items-center justify-center space-x-2" data-target="download-button.defaultButton"> <span class="flex items-center justify-center" data-target="download-button.defaultButton">
<span><%= t('download') %></span> <%= svg_icon('download', class: 'w-6 h-6 inline md:hidden') %>
<span class="hidden md:inline"><%= t('download') %></span>
</span> </span>
<span class="flex items-center justify-center space-x-2 hidden" data-target="download-button.loadingButton"> <span class="flex items-center justify-center space-x-2 hidden" data-target="download-button.loadingButton">
<%= svg_icon('loader', class: 'w-6 h-6 animate-spin') %> <%= svg_icon('loader', class: 'w-6 h-6 animate-spin') %>
<span><%= t('downloading') %></span> <span class="hidden md:inline"><%= t('downloading') %></span>
</span> </span>
</download-button> </download-button>
</div> </div>
@@ -59,7 +60,7 @@
</div> </div>
<% end %> <% end %>
<% end %> <% end %>
<%= render 'shared/attribution', link_path: '/start', account: @submitter.account %> <%= render 'shared/attribution', link_path: '/start', account: @submitter.account, with_style: false %>
</div> </div>
</div> </div>
</div> </div>
@@ -23,7 +23,7 @@
</div> </div>
<div class="w-full bg-white rounded-2xl border relative" style="height: 300px"> <div class="w-full bg-white rounded-2xl border relative" style="height: 300px">
<canvas class="w-full"></canvas> <canvas class="w-full"></canvas>
<button aria-label="<%= t('clear') %>" class="btn btn-ghost btn-sm font-medium top-0 right-0 absolute mt-1 mr-1"> <button aria-label="Clear" class="btn btn-ghost btn-sm font-medium top-0 right-0 absolute mt-1 mr-1">
<%= svg_icon('reload', class: 'w-5 h-5') %> <%= svg_icon('reload', class: 'w-5 h-5') %>
<span class="inline"><%= t('clear') %></span> <span class="inline"><%= t('clear') %></span>
</button> </button>
+29
View File
@@ -0,0 +1,29 @@
<%= render 'shared/turbo_modal_large', title: t('edit_recipient') do %>
<div class="px-5 mb-5 mt-4">
<%= form_for '', url: submitter_path(@submitter), method: :patch, html: { autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %>
<div class="mb-2">
<submitter-item class="form-control">
<submitters-autocomplete data-field="name">
<%= text_field_tag 'submitter[name]', @submitter.name, autocomplete: 'off', class: 'base-input !h-10 w-full', placeholder: "#{t('name')} (#{t('optional')})", dir: 'auto' %>
</submitters-autocomplete>
<div class="grid md:grid-cols-2 gap-1.5 mt-1.5">
<submitters-autocomplete data-field="email">
<%= email_field_tag 'submitter[email]', @submitter.email, autocomplete: 'off', class: 'base-input !h-10 mt-1.5 w-full', placeholder: "#{t('email')} (#{t('optional')})" %>
</submitters-autocomplete>
<submitters-autocomplete data-field="phone">
<%= telephone_field_tag 'submitter[phone]', @submitter.phone, autocomplete: 'off', pattern: '^\+[0-9\s\-]+$', class: 'base-input !h-10 mt-1.5 w-full', placeholder: "#{t('phone')} (#{t('optional')})", oninvalid: "this.value ? this.setCustomValidity('#{t('use_international_format_1xxx_')}') : ''", oninput: "this.setCustomValidity('')" %>
</submitters-autocomplete>
</div>
</submitter-item>
</div>
<div>
<%= render 'submissions/send_email', f:, template: @submitter.template, submitter: @submitter, resend_email: @submitter.sent_at?, submitter_email_message: @submitter_email_message, disable_save_as_default_template_option: true %>
<%= render 'submissions/send_sms', f:, resend_sms: @submitter.sent_at? %>
</div>
<div class="form-control mt-4">
<%= f.button button_title(title: t('update_recipient'), disabled_with: t('updating')), class: 'base-button' %>
</div>
<% end %>
</div>
<%= content_for(:modal_extra) %>
<% end %>
+86 -71
View File
@@ -26,7 +26,7 @@
</div> </div>
</a> </a>
<% end %> <% end %>
<a href="<%= submission_path(submission) %>" class="w-full flex flex-col md:flex-row space-y-4 md:space-y-0 md:justify-between px-5 md:px-6 pb-5 md:items-center pt-5"> <div class="w-full flex flex-col md:flex-row space-y-4 md:space-y-0 md:justify-between px-5 md:px-6 pb-5 md:items-center pt-5 relative cursor-pointer">
<% submitters = (submission.template_submitters || submission.template.submitters).filter_map { |item| submission.submitters.find { |e| e.uuid == item['uuid'] } } %> <% submitters = (submission.template_submitters || submission.template.submitters).filter_map { |item| submission.submitters.find { |e| e.uuid == item['uuid'] } } %>
<% is_submission_completed = submitters.all?(&:completed_at?) && submitters.size.positive? %> <% is_submission_completed = submitters.all?(&:completed_at?) && submitters.size.positive? %>
<% if submitters.size == 1 %> <% if submitters.size == 1 %>
@@ -41,22 +41,31 @@
</span> </span>
</div> </div>
<% else %> <% else %>
<div class="tooltip flex" data-tip="<%= l(submitter.status_event_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>"> <a href="<%= submission_path(submission) %>" class="flex z-[1]">
<span class="badge <%= status_badges[submitter.status] %> md:w-32 badge-lg bg-opacity-50 uppercase text-sm font-semibold"> <span class="badge <%= status_badges[submitter.status] %> md:w-32 badge-lg bg-opacity-50 uppercase text-sm font-semibold tooltip" data-tip="<%= l(submitter.status_event_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>">
<%= t(submitter.status) %> <%= t(submitter.status) %>
</span> </span>
</div> </a>
<% end %> <% end %>
<span class="text-lg break-all flex items-center"> <span class="flex items-center z-[1]">
<%= submitter.name || submitter.email || submitter.phone %> <a href="<%= submission_path(submission) %>" class="text-lg break-all peer">
<%= submitter.name || submitter.email || submitter.phone %>
</a>
<% if can?(:update, submitter) && !submitter.start_form_submission_events.any? && !submission.archived_at? && !submission.expired? %>
<span class="pl-0.5 tooltip tooltip-top md:opacity-0 md:hover:opacity-100 md:peer-hover:opacity-100" data-tip="<%= t('edit') %>">
<%= link_to edit_submitter_path(submitter), class: 'shrink-0', data: { turbo_frame: 'modal' } do %>
<%= svg_icon('pencil', class: 'w-5 h-5') %>
<% end %>
</span>
<% end %>
</span> </span>
</span> </span>
</div> </div>
</div> </div>
<div class="flex space-x-2 items-center"> <div class="flex space-x-2 items-center z-[1]">
<% if submitter.completed_at? %> <% if submitter.completed_at? %>
<form onsubmit="event.preventDefault()" class="flex-1 md:flex-none"> <div class="flex-1 md:flex-none">
<button onclick="event.stopPropagation()" class="w-full md:w-fit"> <div class="w-full md:w-fit">
<download-button data-src="<%= submitter_download_index_path(submitter.slug) %>" class="btn btn-sm btn-neutral text-white w-full md:w-36"> <download-button data-src="<%= submitter_download_index_path(submitter.slug) %>" class="btn btn-sm btn-neutral text-white w-full md:w-36">
<span class="flex items-center justify-center space-x-1 <%= 'md:space-x-2' if t('download').length < 11 %>" data-target="download-button.defaultButton"> <span class="flex items-center justify-center space-x-1 <%= 'md:space-x-2' if t('download').length < 11 %>" data-target="download-button.defaultButton">
<%= svg_icon('download', class: 'w-5 h-5 stroke-2') %> <%= svg_icon('download', class: 'w-5 h-5 stroke-2') %>
@@ -67,37 +76,39 @@
<span class="inline"><%= t('download')[..-2] %>...</span> <span class="inline"><%= t('download')[..-2] %>...</span>
</span> </span>
</download-button> </download-button>
</button> </div>
</form> </div>
<% elsif !submission.archived_at? && !template.archived_at? && !submission.expired? && !submitter.declined_at? %> <% elsif !submission.archived_at? && !template.archived_at? && !submission.expired? && !submitter.declined_at? %>
<% if current_user.email == submitter.email %> <% if current_user.email == submitter.email %>
<form data-turbo="false" method="get" action="<%= submit_form_url(slug: submitter.slug) %>" class="flex-1 md:flex-none"> <div class="flex-1 md:flex-none md:w-36 flex">
<button onclick="event.stopPropagation()" class="btn btn-sm btn-neutral btn-outline bg-white w-full md:w-36 flex"> <a href="<%= submit_form_url(slug: submitter.slug) %>" data-turbo="false" target="_blank" class="btn btn-sm btn-neutral btn-outline bg-white w-full md:w-36 flex z-[1]">
<span class="flex items-center justify-center space-x-1 md:space-x-2"> <span class="flex items-center justify-center space-x-1 md:space-x-2">
<% if t('sign_now').length < 12 %> <% if t('sign_now').length < 12 %>
<%= svg_icon('writing_sign', class: 'w-4 h-4 stroke-2') %> <%= svg_icon('writing_sign', class: 'w-4 h-4 stroke-2') %>
<% end %> <% end %>
<span class="inline shrink-0"><%= t('sign_now') %></span> <span class="inline shrink-0"><%= t('sign_now') %></span>
</span> </span>
</button> </a>
</form> </div>
<% else %> <% else %>
<div class="flex-1 md:flex-none"> <div class="flex-1 md:flex-none">
<%= render 'shared/clipboard_copy', text: submit_form_url(slug: submitter.slug), class: 'btn btn-sm btn-neutral text-white md:w-36 flex', icon_class: 'w-6 h-6 text-white', copy_title: t('copy_link').length < 10 ? t('copy_link') : t('copy'), copy_title_md: t('copy'), copied_title_md: t('copied') %> <%= render 'shared/clipboard_copy', text: submit_form_url(slug: submitter.slug), class: 'btn btn-sm btn-neutral text-white md:w-36 flex z-[1]', icon_class: 'w-6 h-6 text-white', copy_title: t('copy_link').length < 10 ? t('copy_link') : t('copy'), copy_title_md: t('copy'), copied_title_md: t('copied') %>
</div> </div>
<% end %> <% end %>
<% end %> <% end %>
<div class="flex-1 md:flex-none"> <div class="flex-1 md:flex-none z-[1]">
<span class="btn btn-outline btn-sm w-full md:w-24"><%= t('view') %></span> <a href="<%= submission_path(submission) %>" class="btn btn-outline btn-sm w-full md:w-24">
<%= t('view') %>
</a>
</div> </div>
<% if !submission.archived_at? && !template.archived_at? && can?(:destroy, submission) %> <% if !submission.archived_at? && !template.archived_at? && can?(:destroy, submission) %>
<span data-tip="<%= t('archive') %>" class="sm:tooltip tooltip-top"> <span data-tip="<%= t('archive') %>" class="sm:tooltip tooltip-top">
<%= button_to button_title(title: nil, disabled_with: t(:archive).first(4), icon: svg_icon('archive', class: 'w-6 h-6')), submission_path(submission), class: 'btn btn-outline btn-sm w-full md:w-fit', form: { class: 'flex' }, title: t('archive'), method: :delete, onclick: 'event.stopPropagation()' %> <%= button_to button_title(title: nil, disabled_with: t(:archive).first(4), icon: svg_icon('archive', class: 'w-6 h-6')), submission_path(submission), class: 'btn btn-outline btn-sm w-full md:w-fit', form: { class: 'flex' }, title: t('archive'), method: :delete %>
</span> </span>
<% end %> <% end %>
<% if local_assigns[:archived] && can?(:destroy, submission) %> <% if local_assigns[:archived] && can?(:destroy, submission) %>
<span data-tip="<%= t('remove') %>" class="sm:tooltip tooltip-top"> <span data-tip="<%= t('remove') %>" class="sm:tooltip tooltip-top">
<%= button_to button_title(title: nil, disabled_with: t(:remove).first(3), icon: svg_icon('trash', class: 'w-6 h-6')), submission_path(submission, permanently: true), class: 'btn btn-outline btn-sm w-full md:w-fit', form: { class: 'flex' }, title: t('remove'), method: :delete, data: { turbo_confirm: t('submission_deletion_is_irreversible_and_will_permanently_remove_all_associated_signed_documents_with_it_are_you_sure_') }, onclick: 'event.stopPropagation()' %> <%= button_to button_title(title: nil, disabled_with: t(:remove).first(3), icon: svg_icon('trash', class: 'w-6 h-6')), submission_path(submission, permanently: true), class: 'btn btn-outline btn-sm w-full md:w-fit', form: { class: 'flex' }, title: t('remove'), method: :delete, data: { turbo_confirm: t('submission_deletion_is_irreversible_and_will_permanently_remove_all_associated_signed_documents_with_it_are_you_sure_') } %>
</span> </span>
<% end %> <% end %>
</div> </div>
@@ -106,11 +117,11 @@
<div class="flex flex-col md:flex-row md:items-center gap-3"> <div class="flex flex-col md:flex-row md:items-center gap-3">
<% if is_submission_completed %> <% if is_submission_completed %>
<% latest_submitter = submitters.select(&:completed_at?).max_by(&:completed_at) %> <% latest_submitter = submitters.select(&:completed_at?).max_by(&:completed_at) %>
<div class="tooltip flex" data-tip="<%= l(latest_submitter.status_event_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>"> <a href="<%= submission_path(submission) %>" class="z-[1] tooltip flex" data-tip="<%= l(latest_submitter.status_event_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>">
<span class="badge <%= status_badges[latest_submitter.status] %> md:w-32 bg-opacity-50 badge-lg uppercase text-sm font-semibold"> <span class="badge <%= status_badges[latest_submitter.status] %> md:w-32 bg-opacity-50 badge-lg uppercase text-sm font-semibold">
<%= t(latest_submitter.status) %> <%= t(latest_submitter.status) %>
</span> </span>
</div> </a>
<% elsif submission.expired? %> <% elsif submission.expired? %>
<div class="tooltip flex" data-tip="<%= l(submission.expire_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>"> <div class="tooltip flex" data-tip="<%= l(submission.expire_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>">
<span class="badge badge-error md:w-32 bg-opacity-50 badge-lg uppercase text-sm font-semibold"> <span class="badge badge-error md:w-32 bg-opacity-50 badge-lg uppercase text-sm font-semibold">
@@ -123,46 +134,49 @@
<div class="relative flex justify-between items-start md:items-center space-x-3"> <div class="relative flex justify-between items-start md:items-center space-x-3">
<span class="flex flex-col md:flex-row md:items-center gap-2"> <span class="flex flex-col md:flex-row md:items-center gap-2">
<% if !is_submission_completed && !submission.expired? %> <% if !is_submission_completed && !submission.expired? %>
<div class="tooltip flex" data-tip="<%= l(submitter.status_event_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>"> <a href="<%= submission_path(submission) %>" class="z-[1] tooltip flex" data-tip="<%= l(submitter.status_event_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>">
<span class="badge md:w-24 <%= status_badges[submitter.status] %> bg-opacity-50 uppercase text-xs font-semibold"> <span class="badge md:w-24 <%= status_badges[submitter.status] %> bg-opacity-50 uppercase text-xs font-semibold">
<%= t(submitter.status) %> <%= t(submitter.status) %>
</span> </span>
</div> </a>
<% end %> <% end %>
<span class="text-lg break-all"> <span class="flex items-center z-[1]">
<%= submitter.name || submitter.email || submitter.phone %> <a href="<%= submission_path(submission) %>" class="text-lg break-all peer">
<%= submitter.name || submitter.email || submitter.phone %>
</a>
<% if can?(:update, submitter) && !submitter.start_form_submission_events.any? && !submission.archived_at? && !submission.expired? %>
<span class="pl-0.5 tooltip tooltip-top md:opacity-0 md:hover:opacity-100 md:peer-hover:opacity-100" data-tip="<%= t('edit') %>">
<%= link_to edit_submitter_path(submitter), class: 'shrink-0', data: { turbo_frame: 'modal' } do %>
<%= svg_icon('pencil', class: 'w-5 h-5') %>
<% end %>
</span>
<% end %>
</span> </span>
</span> </span>
<% if submitter.completed_at? && !is_submission_completed %> <% if submitter.completed_at? && !is_submission_completed %>
<form onsubmit="event.preventDefault()"> <download-button data-src="<%= submitter_download_index_path(submitter.slug) %>" class="absolute md:relative top-0 right-0 btn btn-xs btn-neutral text-white md:w-36 z-[1]">
<button onclick="event.stopPropagation()"> <span class="flex items-center justify-center space-x-1 md:space-x-2" data-target="download-button.defaultButton">
<download-button data-src="<%= submitter_download_index_path(submitter.slug) %>" class="absolute md:relative top-0 right-0 btn btn-xs btn-neutral text-white md:w-36"> <%= svg_icon('download', class: 'w-4 h-4 stroke-2') %>
<span class="flex items-center justify-center space-x-1 md:space-x-2" data-target="download-button.defaultButton"> <span class="inline"><%= t('download') %></span>
<%= svg_icon('download', class: 'w-4 h-4 stroke-2') %> </span>
<span class="inline"><%= t('download') %></span> <span class="flex items-center justify-center space-x-1 md:space-x-2 hidden" data-target="download-button.loadingButton">
</span> <%= svg_icon('loader', class: 'w-4 h-4 animate-spin') %>
<span class="flex items-center justify-center space-x-1 md:space-x-2 hidden" data-target="download-button.loadingButton"> <span class="inline"><%= t('download')[..-2] %>...</span>
<%= svg_icon('loader', class: 'w-4 h-4 animate-spin') %> </span>
<span class="inline"><%= t('download')[..-2] %>...</span> </download-button>
</span>
</download-button>
</button>
</form>
<% elsif !template.archived_at? && !submission.archived_at? && !is_submission_completed && !submission.expired? && !submitter.declined_at? %> <% elsif !template.archived_at? && !submission.archived_at? && !is_submission_completed && !submission.expired? && !submitter.declined_at? %>
<div class="relative flex items-center space-x-3"> <div class="relative flex items-center space-x-3">
<% if current_user.email == submitter.email %> <% if current_user.email == submitter.email %>
<form data-turbo="false" method="get" action="<%= submit_form_url(slug: submitter.slug) %>"> <a href="<%= submit_form_url(slug: submitter.slug) %>" data-turbo="false" target="_blank" class="absolute md:relative top-0 right-0 btn btn-xs btn-outline btn-neutral bg-white w-28 md:w-36 z-[1]">
<button onclick="event.stopPropagation()" class="absolute md:relative top-0 right-0 btn btn-xs btn-outline btn-neutral bg-white w-28 md:w-36"> <span class="flex items-center justify-center space-x-1 md:space-x-2">
<span class="flex items-center justify-center space-x-1 md:space-x-2"> <% if t('sign_now').length < 12 %>
<% if t('sign_now').length < 12 %> <%= svg_icon('writing_sign', class: 'w-4 h-4 stroke-2') %>
<%= svg_icon('writing_sign', class: 'w-4 h-4 stroke-2') %> <% end %>
<% end %> <span class="inline shrink-0"><%= t('sign_now') %></span>
<span class="inline shrink-0"><%= t('sign_now') %></span> </span>
</span> </a>
</button>
</form>
<% else %> <% else %>
<%= render 'shared/clipboard_copy', text: submit_form_url(slug: submitter.slug), class: 'absolute md:relative top-0 right-0 btn btn-xs text-xs btn-neutral text-white w-28 md:w-36 flex', icon_class: 'w-4 h-4 text-white', copy_title: t('copy_link'), copy_title_md: t('copy_link').length < 10 ? t('copy_link') : t('copy'), copied_title_md: t('copied') %> <%= render 'shared/clipboard_copy', text: submit_form_url(slug: submitter.slug), class: 'absolute md:relative top-0 right-0 btn btn-xs text-xs btn-neutral text-white w-28 md:w-36 flex z-[1]', icon_class: 'w-4 h-4 text-white', copy_title: t('copy_link'), copy_title_md: t('copy_link').length < 10 ? t('copy_link') : t('copy'), copied_title_md: t('copied') %>
<% end %> <% end %>
</div> </div>
<% end %> <% end %>
@@ -171,40 +185,41 @@
</div> </div>
</div> </div>
</div> </div>
<div class="flex space-x-1 md:space-x-2 items-center"> <div class="flex space-x-1 md:space-x-2 items-center z-[1]">
<% if is_submission_completed %> <% if is_submission_completed %>
<% latest_submitter = submitters.select(&:completed_at?).max_by(&:completed_at) %> <% latest_submitter = submitters.select(&:completed_at?).max_by(&:completed_at) %>
<div class="flex-1 md:flex-none"> <div class="flex-1 md:flex-none">
<form onsubmit="event.preventDefault()" class="w-full md:w-fit"> <div class="w-full md:w-fit">
<button onclick="event.stopPropagation()" class="w-full"> <download-button data-src="<%= submitter_download_index_path(latest_submitter.slug) %>" class="btn btn-sm btn-neutral text-white w-full md:w-36 z-[1]">
<download-button data-src="<%= submitter_download_index_path(latest_submitter.slug) %>" class="btn btn-sm btn-neutral text-white w-full md:w-36"> <span class="flex items-center justify-center space-x-1 md:space-x-2" data-target="download-button.defaultButton">
<span class="flex items-center justify-center space-x-1 md:space-x-2" data-target="download-button.defaultButton"> <%= svg_icon('download', class: 'w-5 h-5 stroke-2') %>
<%= svg_icon('download', class: 'w-5 h-5 stroke-2') %> <span class="inline"><%= t('download') %></span>
<span class="inline"><%= t('download') %></span> </span>
</span> <span class="flex items-center justify-center space-x-1 md:space-x-2 hidden" data-target="download-button.loadingButton">
<span class="flex items-center justify-center space-x-1 md:space-x-2 hidden" data-target="download-button.loadingButton"> <%= svg_icon('loader', class: 'w-5 h-5 animate-spin') %>
<%= svg_icon('loader', class: 'w-5 h-5 animate-spin') %> <span class="inline"><%= t('download')[..-2] %>...</span>
<span class="inline"><%= t('download')[..-2] %>...</span> </span>
</span> </download-button>
</download-button> </div>
</button>
</form>
</div> </div>
<% end %> <% end %>
<div class="flex-1 md:flex-none"> <div class="flex-1 md:flex-none z-[1]">
<span class="btn btn-outline btn-sm w-full md:w-24"><%= t('view') %></span> <a href="<%= submission_path(submission) %>" class="btn btn-outline btn-sm w-full md:w-24">
<%= t('view') %>
</a>
</div> </div>
<% if !submission.archived_at? && !template.archived_at? %> <% if !submission.archived_at? && !template.archived_at? %>
<span data-tip="<%= t('archive') %>" class="sm:tooltip tooltip-top"> <span data-tip="<%= t('archive') %>" class="sm:tooltip tooltip-top">
<%= button_to button_title(title: nil, disabled_with: t(:archive).first(4), icon: svg_icon('archive', class: 'w-6 h-6')), submission_path(submission), class: 'btn btn-outline btn-sm w-full md:w-fit', form: { class: 'flex' }, title: t('archive'), method: :delete, onclick: 'event.stopPropagation()' %> <%= button_to button_title(title: nil, disabled_with: t(:archive).first(4), icon: svg_icon('archive', class: 'w-6 h-6')), submission_path(submission), class: 'btn btn-outline btn-sm w-full md:w-fit', form: { class: 'flex' }, title: t('archive'), method: :delete %>
</span> </span>
<% end %> <% end %>
<% if local_assigns[:archived] && can?(:destroy, submission) %> <% if local_assigns[:archived] && can?(:destroy, submission) %>
<span data-tip="<%= t('remove') %>" class="sm:tooltip tooltip-top"> <span data-tip="<%= t('remove') %>" class="sm:tooltip tooltip-top">
<%= button_to button_title(title: nil, disabled_with: t(:remove).first(3), icon: svg_icon('trash', class: 'w-6 h-6')), submission_path(submission, permanently: true), class: 'btn btn-outline btn-sm w-full md:w-fit', form: { class: 'flex' }, title: t('remove'), method: :delete, data: { turbo_confirm: t('submission_deletion_is_irreversible_and_will_permanently_remove_all_associated_signed_documents_with_it_are_you_sure_') }, onclick: 'event.stopPropagation()' %> <%= button_to button_title(title: nil, disabled_with: t(:remove).first(3), icon: svg_icon('trash', class: 'w-6 h-6')), submission_path(submission, permanently: true), class: 'btn btn-outline btn-sm w-full md:w-fit', form: { class: 'flex' }, title: t('remove'), method: :delete, data: { turbo_confirm: t('submission_deletion_is_irreversible_and_will_permanently_remove_all_associated_signed_documents_with_it_are_you_sure_') } %>
</span> </span>
<% end %> <% end %>
</div> </div>
<% end %> <% end %>
</a> <a href="<%= submission_path(submission) %>" class="top-0 bottom-0 left-0 right-0 absolute"></a>
</div>
</div> </div>
@@ -1,7 +1,7 @@
<p>Hello <%= @user.first_name %>,</p> <p><%= @user.first_name.present? ? t('hello_name', name: @user.first_name) : t('hi_there') %>,</p>
<p>You have been invited to <%= @user.account.name %> <%= Docuseal.product_name %>. Please sign up using the link below:</p> <p><%= t('you_have_been_invited_to_account_name_product_name_please_sign_up_using_the_link_below_', account_name: @user.account.name, product_name: Docuseal.product_name) %></p>
<p><%= link_to 'Sign up', invitation_url(reset_password_token: @token) %></p> <p><%= link_to t('sign_up'), invitation_url(reset_password_token: @token) %></p>
<p>Please contact us by replying to this email if you didn't request this.</p> <p><%= t('please_contact_us_by_replying_to_this_email_if_you_didn_t_request_this') %></p>
<p> <p>
Thanks,<br><%= @current_account.name %> <%= t('thanks') %>,<br><%= @current_account.name %>
</p> </p>
+1 -1
View File
@@ -17,7 +17,7 @@ Bundler.require(*Rails.groups)
module DocuSeal module DocuSeal
class Application < Rails::Application class Application < Rails::Application
config.load_defaults 7.1 config.load_defaults 7.2
config.autoload_lib(ignore: %w[assets tasks puma]) config.autoload_lib(ignore: %w[assets tasks puma])
+1 -1
View File
@@ -87,7 +87,7 @@ Rails.application.configure do
domain: ENV.fetch('SMTP_DOMAIN', nil), domain: ENV.fetch('SMTP_DOMAIN', nil),
user_name: ENV.fetch('SMTP_USERNAME', nil), user_name: ENV.fetch('SMTP_USERNAME', nil),
password: ENV.fetch('SMTP_PASSWORD', nil), password: ENV.fetch('SMTP_PASSWORD', nil),
authentication: ENV.fetch('SMTP_AUTHENTICATION', 'plain'), authentication: ENV.fetch('SMTP_PASSWORD', nil).present? ? ENV.fetch('SMTP_AUTHENTICATION', 'plain') : nil,
enable_starttls_auto: ENV['SMTP_ENABLE_STARTTLS_AUTO'] != 'false' enable_starttls_auto: ENV['SMTP_ENABLE_STARTTLS_AUTO'] != 'false'
}.compact }.compact
end end
+13
View File
@@ -1,6 +1,19 @@
# frozen_string_literal: true # frozen_string_literal: true
module HexaPDF module HexaPDF
module DigitalSignature
class Signatures
private
def generate_field_name
index = (@document.acro_form.each_field
.map { |field| field.full_field_name.to_s.scan(/\ASignature(\d+)/).first&.first.to_i }
.max || 0) + 1
"Signature#{index}"
end
end
end
module Encryption module Encryption
class SecurityHandler class SecurityHandler
def encrypt_string(str, obj) def encrypt_string(str, obj)
+1 -1
View File
@@ -1,6 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
Pagy::DEFAULT[:items] = 10 Pagy::DEFAULT[:limit] = 10
Pagy::DEFAULT.freeze Pagy::DEFAULT.freeze
ActiveSupport.on_load(:action_view) do ActiveSupport.on_load(:action_view) do
+333 -3
View File
@@ -22,6 +22,10 @@ en: &en
thanks: Thanks thanks: Thanks
first_party: 'First Party' first_party: 'First Party'
docuseal_trusted_signature: DocuSeal Trusted Signature docuseal_trusted_signature: DocuSeal Trusted Signature
hello_name: Hello %{name}
you_are_invited_to_product_name: You are invited to %{product_name}
you_have_been_invited_to_account_name_product_name_please_sign_up_using_the_link_below_: 'You have been invited to %{account_name} %{product_name}. Please sign up using the link below:'
sent_using_product_name_free_document_signing_html: 'Sent using <a href="%{product_url}">%{product_name}</a> free document signing.'
sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature: Sign documents with trusted certificate provided by DocuSeal. Your documents and data are never shared with DocuSeal. PDF checksum is provided to generate a trusted signature. sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature: Sign documents with trusted certificate provided by DocuSeal. Your documents and data are never shared with DocuSeal. PDF checksum is provided to generate a trusted signature.
you_have_been_invited_to_submit_the_name_form: 'You have been invited to submit the "%{name}" form.' you_have_been_invited_to_submit_the_name_form: 'You have been invited to submit the "%{name}" form.'
you_have_been_invited_to_sign_the_name: 'You have been invited to sign the "%{name}".' you_have_been_invited_to_sign_the_name: 'You have been invited to sign the "%{name}".'
@@ -577,6 +581,57 @@ en: &en
opened: Opened opened: Opened
sent: Sent sent: Sent
awaiting: Awaiting awaiting: Awaiting
document_id: Document ID
envelope_id: Envelope ID
event_log: Event Log
verify: Verify
testing_log_not_for_production_use: Testing Log - Not for Production Use
original_sha256: Original SHA256
result_sha256: Result SHA256
generated_at: Generated at
email_verification: Email verification
phone_verification: Phone verification
session_id: Session ID
option: Option
text_field: Text Field
signature_field: Signature Field
initials_field: Initials Field
date_field: Date Field
number_field: Number Field
image_field: Image Field
file_field: File Field
select_field: Select Field
checkbox_field: Checkbox Field
multiple_field: Multiple Select Field
radio_field: Radio Group Field
cells_field: Cells Field
stamp_field: Stamp Field
payment_field: Payment Field
phone_field: Phone Field
paid_price: 'Paid %{price}'
verified: Verified
unverified: Unverified
document: Document
completed_at: Completed At
edit_recipient: Edit Recipient
update_recipient: Update Recipient
use_international_format_1xxx_: 'Use internatioanl format: +1xxx...'
submitter_cannot_be_updated: Submitter cannot be updated.
at_least_one_field_must_be_filled: At least one field must be filled.
submission_event_names:
send_email_to_html: '<b>Email sent</b> to %{submitter_name}'
send_reminder_email_to_html: '<b>Reminder email sent</b> to %{submitter_name}'
send_sms_to_html: '<b>SMS sent</b> to %{submitter_name}'
send_2fa_sms_to_html: '<b>Verification SMS sent</b> to %{submitter_name}'
open_email_by_html: '<b>Email opened</b> by %{submitter_name}'
click_email_by_html: '<b>Email link clicked</b> by %{submitter_name}'
click_sms_by_html: '<b>SMS link clicked</b> by %{submitter_name}'
phone_verified_by_html: '<b>Phone verified</b> by %{submitter_name}'
start_form_by_html: '<b>Submission started</b> by %{submitter_name}'
view_form_by_html: '<b>Form viewed</b> by %{submitter_name}'
invite_party_by_html: '<b>Invited</b> %{invited_submitter_name} by %{submitter_name}'
complete_form_by_html: '<b>Submission completed</b> by %{submitter_name}'
api_complete_form_by_html: '<b>Submission completed via API</b> by %{submitter_name}'
import_list: import_list:
select_worksheet: Select Worksheet select_worksheet: Select Worksheet
open: Open open: Open
@@ -603,6 +658,10 @@ es: &es
enforce_recipients_order: 'Hacer cumplir el orden de los destinatarios' enforce_recipients_order: 'Hacer cumplir el orden de los destinatarios'
first_party: 'Primera Parte' first_party: 'Primera Parte'
docuseal_trusted_signature: Firma de confianza DocuSeal docuseal_trusted_signature: Firma de confianza DocuSeal
hello_name: Hola %{name}
you_are_invited_to_product_name: Estás invitado a %{product_name}
you_have_been_invited_to_account_name_product_name_please_sign_up_using_the_link_below_: 'Has sido invitado a %{account_name} %{product_name}. Por favor, regístrate usando el enlace a continuación:'
sent_using_product_name_free_document_signing_html: 'Enviado usando la firma de documentos gratuita de <a href="%{product_url}">%{product_name}</a>.'
sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature: Firme documentos con un certificado de confianza proporcionado por DocuSeal. Sus documentos y datos nunca se comparten con DocuSeal. Se proporciona un checksum de PDF para generar una firma de confianza. sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature: Firme documentos con un certificado de confianza proporcionado por DocuSeal. Sus documentos y datos nunca se comparten con DocuSeal. Se proporciona un checksum de PDF para generar una firma de confianza.
hi_there: Hola hi_there: Hola
thanks: Gracias thanks: Gracias
@@ -1158,6 +1217,57 @@ es: &es
opened: Abierto opened: Abierto
sent: Enviado sent: Enviado
awaiting: Pendiente awaiting: Pendiente
document_id: ID del Documento
envelope_id: ID del Sobre
event_log: Registro de Eventos
verify: Verificar
testing_log_not_for_production_use: Registro de Pruebas - No para Uso en Producción
original_sha256: SHA256 Original
result_sha256: SHA256 del Resultado
generated_at: Generado el
email_verification: Verificación de correo electrónico
phone_verification: Verificación telefónica
session_id: ID de la Sesión
option: Opción
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
multiple_field: Campo Múltiple
radio_field: Campo de Grupo Radio
radio_field: Campo de Radio
cells_field: Campo de Celdas
stamp_field: Campo de Sello
payment_field: Campo de Pago
phone_field: Campo de Teléfono
paid_price: 'Pagado %{price}'
verified: Verificado
unverified: No verificado
document: Documento
completed_at: Completado el
edit_recipient: Editar Destinatario
update_recipient: Actualizar Destinatario
use_international_format_1xxx_: 'Usa el formato internacional: +1xxx...'
submitter_cannot_be_updated: El remitente no puede ser actualizado.
at_least_one_field_must_be_filled: Al menos un campo debe estar completo.
submission_event_names:
send_email_to_html: '<b>Correo electrónico enviado</b> a %{submitter_name}'
send_reminder_email_to_html: '<b>Correo de recordatorio enviado</b> a %{submitter_name}'
send_sms_to_html: '<b>SMS enviado</b> a %{submitter_name}'
send_2fa_sms_to_html: '<b>SMS de verificación enviado</b> a %{submitter_name}'
open_email_by_html: '<b>Correo electrónico abierto</b> por %{submitter_name}'
click_email_by_html: '<b>Enlace del correo electrónico clicado</b> por %{submitter_name}'
click_sms_by_html: '<b>Enlace del SMS clicado</b> por %{submitter_name}'
phone_verified_by_html: '<b>Teléfono verificado</b> por %{submitter_name}'
start_form_by_html: '<b>Envío iniciado</b> por %{submitter_name}'
view_form_by_html: '<b>Formulario visto</b> por %{submitter_name}'
invite_party_by_html: '<b>Invitado</b> %{invited_submitter_name} por %{submitter_name}'
complete_form_by_html: '<b>Envío completado</b> por %{submitter_name}'
api_complete_form_by_html: '<b>Envío completado vía API</b> por %{submitter_name}'
import_list: import_list:
select_worksheet: Seleccionar hoja de cálculo select_worksheet: Seleccionar hoja de cálculo
open: Abrir open: Abrir
@@ -1184,6 +1294,10 @@ it: &it
enforce_recipients_order: Aplicar el orden de los destinatarios enforce_recipients_order: Aplicar el orden de los destinatarios
first_party: 'Prima parte' first_party: 'Prima parte'
docuseal_trusted_signature: "Firma Fiduciaria DocuSeal" docuseal_trusted_signature: "Firma Fiduciaria DocuSeal"
hello_name: Ciao %{name}
you_are_invited_to_product_name: Sei stato invitato a %{product_name}
you_have_been_invited_to_account_name_product_name_please_sign_up_using_the_link_below_: 'Sei stato invitato a %{account_name} %{product_name}. Registrati utilizzando il link qui sotto:'
sent_using_product_name_free_document_signing_html: 'Inviato utilizzando la firma di documenti gratuita di <a href="%{product_url}">%{product_name}</a>.'
sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature: "Firma documenti con un certificato di fiducia fornito da DocuSeal. I tuoi documenti e i tuoi dati non vengono mai condivisi con DocuSeal. Il checksum PDF è fornito per generare una firma di fiducia." sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature: "Firma documenti con un certificato di fiducia fornito da DocuSeal. I tuoi documenti e i tuoi dati non vengono mai condivisi con DocuSeal. Il checksum PDF è fornito per generare una firma di fiducia."
hi_there: Ciao hi_there: Ciao
thanks: Grazie thanks: Grazie
@@ -1739,6 +1853,57 @@ it: &it
opened: Aperto opened: Aperto
sent: Inviato sent: Inviato
awaiting: In attesa awaiting: In attesa
document_id: ID del Documento
envelope_id: ID della Busta
event_log: Registro degli Eventi
verify: Verifica
testing_log_not_for_production_use: Registro di Test - Non per Uso in Produzione
original_sha256: SHA256 Originale
result_sha256: SHA256 del Risultato
generated_at: Generato il
email_verification: Verifica e-mail
phone_verification: Verifica telefono
session_id: ID della Sessione
option: Opzione
text_field: Campo di Testo
signature_field: Campo di Firma
initials_field: Campo di Iniziali
date_field: Campo Data
number_field: Campo Numero
image_field: Campo Immagine
file_field: Campo File
select_field: Campo di Selezione
checkbox_field: Campo di Checkbox
multiple_field: Campo Selezione Multipla
radio_field: Campo di Gruppo Radio
cells_field: Campo Celle
stamp_field: Campo Timbro
payment_field: Campo Pagamento
phone_field: Campo Telefono
paid_price: 'Pagato %{price}'
verified: Verificato
unverified: Non verificato
document: Documento
completed_at: Completato il
edit_recipient: Modifica Destinatario
update_recipient: Aggiorna Destinatario
use_international_format_1xxx_: 'Utilizza il formato internazionale: +1xxx...'
submitter_cannot_be_updated: Il mittente non può essere aggiornato.
at_least_one_field_must_be_filled: Almeno un campo deve essere compilato.
submission_event_names:
send_email_to_html: '<b>E-mail inviato</b> a %{submitter_name}'
send_reminder_email_to_html: '<b>E-mail di promemoria inviato</b> a %{submitter_name}'
send_sms_to_html: '<b>SMS inviato</b> a %{submitter_name}'
send_2fa_sms_to_html: '<b>SMS di verifica inviato</b> a %{submitter_name}'
open_email_by_html: '<b>E-mail aperta</b> da %{submitter_name}'
click_email_by_html: "<b>Link dell'e-mail cliccato</b> da %{submitter_name}"
click_sms_by_html: "<b>Link dell'SMS cliccato</b> da %{submitter_name}"
phone_verified_by_html: '<b>Telefono verificato</b> da %{submitter_name}'
start_form_by_html: '<b>Invio iniziato</b> da %{submitter_name}'
view_form_by_html: '<b>Modulo visualizzato</b> da %{submitter_name}'
invite_party_by_html: '<b>Invitato</b> %{invited_submitter_name} da %{submitter_name}'
complete_form_by_html: '<b>Invio completato</b> da %{submitter_name}'
api_complete_form_by_html: '<b>Invio completato tramite API</b> da %{submitter_name}'
import_list: import_list:
select_worksheet: Seleziona il foglio di lavoro select_worksheet: Seleziona il foglio di lavoro
open: Apri open: Apri
@@ -1765,6 +1930,10 @@ fr: &fr
enforce_recipients_order: "Respecter l'ordre des destinataires" enforce_recipients_order: "Respecter l'ordre des destinataires"
first_party: 'Première partie' first_party: 'Première partie'
docuseal_trusted_signature: Signature de confiance DocuSeal docuseal_trusted_signature: Signature de confiance DocuSeal
hello_name: Bonjour %{name}
you_are_invited_to_product_name: Vous êtes invité à %{product_name}
you_have_been_invited_to_account_name_product_name_please_sign_up_using_the_link_below_: 'Vous avez été invité à %{account_name} %{product_name}. Veuillez vous inscrire en utilisant le lien ci-dessous:'
sent_using_product_name_free_document_signing_html: 'Envoyé en utilisant la signature de documents gratuite de <a href="%{product_url}">%{product_name}</a>.'
sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature: Signez des documents avec un certificat de confiance fourni par DocuSeal. Vos documents et données ne sont jamais partagés avec DocuSeal. Un checksum PDF est fourni pour générer une signature de confiance. sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature: Signez des documents avec un certificat de confiance fourni par DocuSeal. Vos documents et données ne sont jamais partagés avec DocuSeal. Un checksum PDF est fourni pour générer une signature de confiance.
hi_there: Bonjour hi_there: Bonjour
thanks: Merci thanks: Merci
@@ -2321,6 +2490,57 @@ fr: &fr
opened: Ouvert opened: Ouvert
sent: Envoyé sent: Envoyé
awaiting: En attente awaiting: En attente
document_id: ID du document
envelope_id: ID de l'enveloppe
event_log: Journal d'événements
verify: Vérifier
testing_log_not_for_production_use: Journal de test - Ne pas utiliser en production
original_sha256: SHA256 original
result_sha256: SHA256 du résultat
generated_at: Généré le
email_verification: Vérification de l'e-mail
phone_verification: Vérification du téléphone
session_id: ID de session
option: Option
text_field: Champ de Texte
signature_field: Champ de Signature
initials_field: "Champ d'Initiales"
date_field: Champ de Date
number_field: Champ de Numéro
image_field: "Champ d'Image"
file_field: Champ de Fichier
select_field: Champ de Choix
checkbox_field: Champ de Coche
multiple_field: Champ de Choix Multiple
radio_field: Champ de Groupe Radio
cells_field: Champ de Cellules
stamp_field: Champ de Tampon
payment_field: Champ de Paiement
phone_field: Champ de Téléphone
paid_price: 'Payé %{price}'
verified: Vérifié
unverified: Non vérifié
document: Document
completed_at: Terminé le
edit_recipient: Modifica Destinatario
update_recipient: Aggiorna Destinatario
use_international_format_1xxx_: 'Utilizza il formato internazionale: +1xxx...'
submitter_cannot_be_updated: Il mittente non può essere aggiornato.
at_least_one_field_must_be_filled: Almeno un campo deve essere compilato.
submission_event_names:
send_email_to_html: '<b>E-mail envoyé</b> à %{submitter_name}'
send_reminder_email_to_html: '<b>E-mail de rappel envoyé</b> à %{submitter_name}'
send_sms_to_html: '<b>SMS envoyé</b> à %{submitter_name}'
send_2fa_sms_to_html: '<b>SMS de vérification envoyé</b> à %{submitter_name}'
open_email_by_html: '<b>E-mail ouvert</b> par %{submitter_name}'
click_email_by_html: "<b>Lien de l'e-mail cliqué</b> par %{submitter_name}"
click_sms_by_html: '<b>Lien du SMS cliqué</b> par %{submitter_name}'
phone_verified_by_html: '<b>Téléphone vérifié</b> par %{submitter_name}'
start_form_by_html: '<b>Soumission commencée</b> par %{submitter_name}'
view_form_by_html: '<b>Formulaire consulté</b> par %{submitter_name}'
invite_party_by_html: '<b>Invité</b> %{invited_submitter_name} par %{submitter_name}'
complete_form_by_html: '<b>Soumission terminée</b> par %{submitter_name}'
api_complete_form_by_html: "<b>Soumission terminée via l'API</b> par %{submitter_name}"
import_list: import_list:
select_worksheet: Sélectionner la feuille de calcul select_worksheet: Sélectionner la feuille de calcul
open: Ouvrir open: Ouvrir
@@ -2347,6 +2567,10 @@ pt: &pt
enforce_recipients_order: 'Forçar a ordem dos recipientes' enforce_recipients_order: 'Forçar a ordem dos recipientes'
first_party: 'Primeira Parte' first_party: 'Primeira Parte'
docuseal_trusted_signature: Assinatura Confiável DocuSeal docuseal_trusted_signature: Assinatura Confiável DocuSeal
hello_name: Olá %{name}
you_are_invited_to_product_name: Você está convidado para %{product_name}
you_have_been_invited_to_account_name_product_name_please_sign_up_using_the_link_below_: 'Você foi convidado para %{account_name} %{product_name}. Inscreva-se usando o link abaixo:'
sent_using_product_name_free_document_signing_html: 'Enviado usando a assinatura gratuita de documentos de <a href="%{product_url}">%{product_name}</a>.'
sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature: Assine documentos com certificado confiável fornecido pela DocuSeal. Seus documentos e dados nunca são compartilhados com a DocuSeal. O checksum do PDF é fornecido para gerar uma assinatura confiável. sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature: Assine documentos com certificado confiável fornecido pela DocuSeal. Seus documentos e dados nunca são compartilhados com a DocuSeal. O checksum do PDF é fornecido para gerar uma assinatura confiável.
hi_there: Olá hi_there: Olá
thanks: Obrigado thanks: Obrigado
@@ -2518,7 +2742,7 @@ pt: &pt
removing: Removendo removing: Removendo
delete: Excluir delete: Excluir
default: Padrão default: Padrão
are_you_sure_: Você tem certeza? are_you_sure_: Tem certeza?
timestamp_server: Servidor de carimbo de tempo timestamp_server: Servidor de carimbo de tempo
timeserver_url: URL do servidor de carimbo de tempo timeserver_url: URL do servidor de carimbo de tempo
url_of_the_trusted_rfc_3161_timeserver_to_be_used_to_generate_timestamp_signatures: URL do servidor de carimbo de tempo RFC 3161 confiável a ser usado para gerar assinaturas de carimbo de tempo. url_of_the_trusted_rfc_3161_timeserver_to_be_used_to_generate_timestamp_signatures: URL do servidor de carimbo de tempo RFC 3161 confiável a ser usado para gerar assinaturas de carimbo de tempo.
@@ -2696,9 +2920,9 @@ pt: &pt
draw_signature: Desenhar assinatura draw_signature: Desenhar assinatura
clear: Limpar clear: Limpar
signature_uploaded: Assinatura enviada signature_uploaded: Assinatura enviada
submission_deletion_is_irreversible_and_will_permanently_remove_all_associated_signed_documents_with_it_are_you_sure_: 'A exclusão da submissão é irreversível e removerá permanentemente todos os documentos assinados associados a ela. Você tem certeza?' submission_deletion_is_irreversible_and_will_permanently_remove_all_associated_signed_documents_with_it_are_you_sure_: 'A exclusão da submissão é irreversível e removerá permanentemente todos os documentos assinados associados a ela. Tem certeza?'
return_back_to_your_desktop_device_to_complete_the_form_or_continue_on_mobile_html: 'Volte para seu dispositivo desktop para concluir o formulário ou <a class="link" href="%{link}">continue no celular</a>' return_back_to_your_desktop_device_to_complete_the_form_or_continue_on_mobile_html: 'Volte para seu dispositivo desktop para concluir o formulário ou <a class="link" href="%{link}">continue no celular</a>'
template_deletion_is_irreversible_and_will_permanently_remove_all_associated_signed_documents_with_it_are_you_sure_: A exclusão do modelo é irreversível e removerá permanentemente todos os documentos assinados associados a ele. Você tem certeza? template_deletion_is_irreversible_and_will_permanently_remove_all_associated_signed_documents_with_it_are_you_sure_: A exclusão do modelo é irreversível e removerá permanentemente todos os documentos assinados associados a ele. Tem certeza?
rename_folder: Renomear pasta rename_folder: Renomear pasta
folder_name: Nome da pasta folder_name: Nome da pasta
rename: Renomear rename: Renomear
@@ -2902,6 +3126,57 @@ pt: &pt
opened: Aberto opened: Aberto
sent: Enviado sent: Enviado
awaiting: Aguardando awaiting: Aguardando
document_id: ID do Documento
envelope_id: ID do Envelope
event_log: Log de Eventos
verify: Verificar
testing_log_not_for_production_use: Log de Teste - Não para Uso em Produção
original_sha256: SHA256 Original
result_sha256: SHA256 do Resultado
generated_at: Gerado em
email_verification: Verificação de E-mail
phone_verification: Verificação de Telefone
session_id: ID da Sessão
option: Opção
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
paid_price: 'Pago %{price}'
verified: Verificado
unverified: Não verificado
document: Documento
completed_at: Concluído em
edit_recipient: Editar Destinatário
update_recipient: Atualizar Destinatário
use_international_format_1xxx_: 'Use o formato internacional: +1xxx...'
submitter_cannot_be_updated: O remetente não pode ser atualizado.
at_least_one_field_must_be_filled: Pelo menos um campo deve ser preenchido.
submission_event_names:
send_email_to_html: '<b>E-mail enviado</b> para %{submitter_name}'
send_reminder_email_to_html: '<b>E-mail de lembrete enviado</b> para %{submitter_name}'
send_sms_to_html: '<b>SMS enviado</b> para %{submitter_name}'
send_2fa_sms_to_html: '<b>SMS de verificação enviado</b> para %{submitter_name}'
open_email_by_html: '<b>E-mail aberto</b> por %{submitter_name}'
click_email_by_html: '<b>Link do e-mail clicado</b> por %{submitter_name}'
click_sms_by_html: '<b>Link do SMS clicado</b> por %{submitter_name}'
phone_verified_by_html: '<b>Telefone verificado</b> por %{submitter_name}'
start_form_by_html: '<b>Submissão iniciada</b> por %{submitter_name}'
view_form_by_html: '<b>Formulário visualizado</b> por %{submitter_name}'
invite_party_by_html: '<b>Convidado</b> %{invited_submitter_name} por %{submitter_name}'
complete_form_by_html: '<b>Submissão concluída</b> por %{submitter_name}'
api_complete_form_by_html: '<b>Submissão concluída via API</b> por %{submitter_name}'
import_list: import_list:
select_worksheet: Selecionar planilha select_worksheet: Selecionar planilha
open: Abrir open: Abrir
@@ -2928,6 +3203,10 @@ de: &de
enforce_recipients_order: 'Empfängerreihenfolge durchsetzen' enforce_recipients_order: 'Empfängerreihenfolge durchsetzen'
first_party: 'Erste Partei' first_party: 'Erste Partei'
docuseal_trusted_signature: DocuSeal-Vertrauenssignatur docuseal_trusted_signature: DocuSeal-Vertrauenssignatur
hello_name: Hallo %{name}
you_are_invited_to_product_name: Sie sind eingeladen zu %{product_name}
you_have_been_invited_to_account_name_product_name_please_sign_up_using_the_link_below_: 'Sie wurden zu %{account_name} %{product_name} eingeladen. Bitte registrieren Sie sich über den folgenden Link:'
sent_using_product_name_free_document_signing_html: 'Gesendet mit der kostenlosen Dokumentensignierung von <a href="%{product_url}">%{product_name}</a>.'
sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature: Unterzeichnen Sie Dokumente mit einem vertrauenswürdigen Zertifikat von DocuSeal. Ihre Dokumente und Daten werden niemals mit DocuSeal geteilt. Eine PDF-Prüfziffer wird bereitgestellt, um eine vertrauenswürdige Signatur zu generieren. sign_documents_with_trusted_certificate_provided_by_docu_seal_your_documents_and_data_are_never_shared_with_docu_seal_p_d_f_checksum_is_provided_to_generate_a_trusted_signature: Unterzeichnen Sie Dokumente mit einem vertrauenswürdigen Zertifikat von DocuSeal. Ihre Dokumente und Daten werden niemals mit DocuSeal geteilt. Eine PDF-Prüfziffer wird bereitgestellt, um eine vertrauenswürdige Signatur zu generieren.
hi_there: Hallo hi_there: Hallo
thanks: Danke thanks: Danke
@@ -3483,6 +3762,57 @@ de: &de
opened: Geöffnet opened: Geöffnet
sent: Gesendet sent: Gesendet
awaiting: Ausstehend awaiting: Ausstehend
document_id: Dokumenten-ID
envelope_id: Umschlag-ID
event_log: Ereignisprotokoll
verify: Überprüfen
testing_log_not_for_production_use: Testprotokoll - Nicht für den Produktionseinsatz
original_sha256: Original SHA256
result_sha256: Ergebnis SHA256
generated_at: Generiert am
email_verification: E-Mail-Verifizierung
phone_verification: Telefonverifizierung
session_id: Sitzungs-ID
option: Option
text_field: Textfeld
signature_field: Unterschriftsfeld
initials_field: Initialenfeld
date_field: Datumsfeld
number_field: Zahlenfeld
image_field: Bildfeld
file_field: Dateifeld
select_field: Auswahlfeld
checkbox_field: Checkbox-Feld
multiple_field: Mehrfach-Auswahlfeld
radio_field: Radio-Gruppenfeld
cells_field: Zellenfeld
stamp_field: Stempelfeld
payment_field: Zahlungsfeld
phone_field: Telefonfeld
paid_price: 'Bezahlt %{price}'
verified: Verifiziert
unverified: Nicht verifiziert
document: Dokument
completed_at: Abgeschlossen am
edit_recipient: Empfänger bearbeiten
update_recipient: Empfänger aktualisieren
use_international_format_1xxx_: 'Verwenden Sie das internationale Format: +1xxx...'
submitter_cannot_be_updated: Der Absender kann nicht aktualisiert werden.
at_least_one_field_must_be_filled: Mindestens ein Feld muss ausgefüllt werden.
submission_event_names:
send_email_to_html: '<b>E-Mail gesendet</b> an %{submitter_name}'
send_reminder_email_to_html: '<b>Erinnerungs-E-Mail gesendet</b> an %{submitter_name}'
send_sms_to_html: '<b>SMS gesendet</b> an %{submitter_name}'
send_2fa_sms_to_html: '<b>Verifizierungs-SMS gesendet</b> an %{submitter_name}'
open_email_by_html: '<b>E-Mail geöffnet</b> von %{submitter_name}'
click_email_by_html: '<b>E-Mail-Link angeklickt</b> von %{submitter_name}'
click_sms_by_html: '<b>SMS-Link angeklickt</b> von %{submitter_name}'
phone_verified_by_html: '<b>Telefon verifiziert</b> von %{submitter_name}'
start_form_by_html: '<b>Einreichung gestartet</b> von %{submitter_name}'
view_form_by_html: '<b>Formular angesehen</b> von %{submitter_name}'
invite_party_by_html: '<b>Eingeladen</b> %{invited_submitter_name} von %{submitter_name}'
complete_form_by_html: '<b>Einreichung abgeschlossen</b> von %{submitter_name}'
api_complete_form_by_html: '<b>Einreichung über API abgeschlossen</b> von %{submitter_name}'
import_list: import_list:
select_worksheet: Arbeitsblatt auswählen select_worksheet: Arbeitsblatt auswählen
open: Öffnen open: Öffnen
+1
View File
@@ -69,6 +69,7 @@ Rails.application.routes.draw do
resources :submissions_archived, only: %i[index], path: 'submissions/archived' resources :submissions_archived, only: %i[index], path: 'submissions/archived'
resources :submissions, only: %i[index], controller: 'submissions_dashboard' resources :submissions, only: %i[index], controller: 'submissions_dashboard'
resources :submissions, only: %i[show destroy] resources :submissions, only: %i[show destroy]
resources :submitters, only: %i[edit update]
resources :console_redirect, only: %i[index] resources :console_redirect, only: %i[index]
resources :upgrade, only: %i[index], controller: 'console_redirect' resources :upgrade, only: %i[index], controller: 'console_redirect'
resources :manage, only: %i[index], controller: 'console_redirect' resources :manage, only: %i[index], controller: 'console_redirect'
+1
View File
@@ -1,6 +1,7 @@
queues: queues:
- [default, 1] - [default, 1]
- [webhooks, 1] - [webhooks, 1]
- [images, 1]
- [mailers, 1] - [mailers, 1]
- [recurrent, 1] - [recurrent, 1]
- [rollbar, 1] - [rollbar, 1]
+1
View File
@@ -11,6 +11,7 @@ const configs = generateWebpackConfig({
}, },
optimization: { optimization: {
runtimeChunk: false, runtimeChunk: false,
concatenateModules: !process.env.BUNDLE_ANALYZE,
splitChunks: { splitChunks: {
chunks (chunk) { chunks (chunk) {
return chunk.name !== 'rollbar' return chunk.name !== 'rollbar'
+3 -3
View File
@@ -43,9 +43,9 @@ module ActionMailerConfigsInterceptor
address: value['host'], address: value['host'],
port: value['port'], port: value['port'],
domain: value['domain'], domain: value['domain'],
openssl_verify_mode: OpenSSL::SSL::VERIFY_NONE, openssl_verify_mode: value['security'] == 'noverify' ? OpenSSL::SSL::VERIFY_NONE : nil,
authentication: value.fetch('authentication', 'plain'), authentication: value['password'].present? ? value.fetch('authentication', 'plain') : nil,
enable_starttls_auto: true, enable_starttls_auto: value['security'] != 'tls',
open_timeout: OPEN_TIMEOUT, open_timeout: OPEN_TIMEOUT,
read_timeout: READ_TIMEOUT, read_timeout: READ_TIMEOUT,
ssl: value['security'] == 'ssl', ssl: value['security'] == 'ssl',
+1 -1
View File
@@ -23,7 +23,7 @@ module ReplaceEmailVariables
# rubocop:disable Metrics # rubocop:disable Metrics
def call(text, submitter:, tracking_event_type: 'click_email', html_escape: false, sig: nil) def call(text, submitter:, tracking_event_type: 'click_email', html_escape: false, sig: nil)
text = replace(text, TEMPLATE_NAME, html_escape:) { submitter.template.name } text = replace(text, TEMPLATE_NAME, html_escape:) { (submitter.template || submitter.submission.template).name }
text = replace(text, TEMPLATE_ID, html_escape:) { submitter.template.id } text = replace(text, TEMPLATE_ID, html_escape:) { submitter.template.id }
text = replace(text, SUBMITTER_ID, html_escape:) { submitter.id } text = replace(text, SUBMITTER_ID, html_escape:) { submitter.id }
text = replace(text, SUBMITTER_SLUG, html_escape:) { submitter.slug } text = replace(text, SUBMITTER_SLUG, html_escape:) { submitter.slug }
-16
View File
@@ -3,22 +3,6 @@
module SubmissionEvents module SubmissionEvents
TRACKING_PARAM_LENGTH = 6 TRACKING_PARAM_LENGTH = 6
EVENT_NAMES = {
send_email: 'Email sent',
send_reminder_email: 'Reminder email sent',
send_sms: 'SMS sent',
send_2fa_sms: 'Verification SMS sent',
open_email: 'Email opened',
click_email: 'Email link clicked',
click_sms: 'SMS link clicked',
phone_verified: 'Phone verified',
start_form: 'Submission started',
view_form: 'Form viewed',
invite_party: 'Invited',
complete_form: 'Submission completed',
api_complete_form: 'Submission completed via API'
}.freeze
module_function module_function
def build_tracking_param(submitter, event_type = 'click_email') def build_tracking_param(submitter, event_type = 'click_email')
+3
View File
@@ -111,6 +111,9 @@ module Submissions
return email.downcase if email.to_s.include?('.mm') return email.downcase if email.to_s.include?('.mm')
return email.downcase if email.to_s.include?('.cm') return email.downcase if email.to_s.include?('.cm')
return email.downcase if email.to_s.include?('.et') return email.downcase if email.to_s.include?('.et')
return email.downcase if email.to_s.include?('.mo')
return email.downcase if email.to_s.include?('.nz')
return email.downcase if email.to_s.include?('.za')
return email.downcase unless email.to_s.include?('.') return email.downcase unless email.to_s.include?('.')
fixed_email = EmailTypo.call(email.delete_prefix('<')) fixed_email = EmailTypo.call(email.delete_prefix('<'))
+74 -63
View File
@@ -12,9 +12,6 @@ module Submissions
'Helvetica' 'Helvetica'
end end
VERIFIED_TEXT = 'Verified'
UNVERIFIED_TEXT = 'Unverified'
CURRENCY_SYMBOLS = { CURRENCY_SYMBOLS = {
'USD' => '$', 'USD' => '$',
'EUR' => '€', 'EUR' => '€',
@@ -32,35 +29,37 @@ module Submissions
# rubocop:disable Metrics # rubocop:disable Metrics
def call(submission) def call(submission)
document = build_audit_trail(submission)
account = submission.account account = submission.account
pkcs = Accounts.load_signing_pkcs(account) I18n.with_locale(account.locale) do
tsa_url = Accounts.load_timeserver_url(account) document = build_audit_trail(submission)
io = StringIO.new pkcs = Accounts.load_signing_pkcs(account)
tsa_url = Accounts.load_timeserver_url(account)
document.trailer.info[:Creator] = "#{Docuseal.product_name} (#{Docuseal::PRODUCT_URL})" io = StringIO.new
last_submitter = submission.submitters.select(&:completed_at).max_by(&:completed_at) document.trailer.info[:Creator] = "#{Docuseal.product_name} (#{Docuseal::PRODUCT_URL})"
sign_params = { last_submitter = submission.submitters.select(&:completed_at).max_by(&:completed_at)
reason: sign_reason,
**Submissions::GenerateResultAttachments.build_signing_params(last_submitter, pkcs, tsa_url)
}
document.sign(io, **sign_params) sign_params = {
reason: sign_reason,
**Submissions::GenerateResultAttachments.build_signing_params(last_submitter, pkcs, tsa_url)
}
Submissions::GenerateResultAttachments.maybe_enable_ltv(io, sign_params) document.sign(io, **sign_params)
ActiveStorage::Attachment.create!( Submissions::GenerateResultAttachments.maybe_enable_ltv(io, sign_params)
blob: ActiveStorage::Blob.create_and_upload!(
io: io.tap(&:rewind), filename: "Audit Log - #{submission.template.name}.pdf" ActiveStorage::Attachment.create!(
), blob: ActiveStorage::Blob.create_and_upload!(
name: 'audit_trail', io: io.tap(&:rewind), filename: "#{I18n.t('audit_log')} - #{submission.template.name}.pdf"
record: submission ),
) name: 'audit_trail',
record: submission
)
end
end end
def build_audit_trail(submission) def build_audit_trail(submission)
@@ -99,6 +98,12 @@ module Submissions
height: 1 height: 1
) )
configs = submission.account.account_configs.where(key: [AccountConfig::WITH_AUDIT_VALUES_KEY,
AccountConfig::WITH_SIGNATURE_ID])
with_signature_id = configs.find { |c| c.key == AccountConfig::WITH_SIGNATURE_ID }&.value == true
with_audit_values = configs.find { |c| c.key == AccountConfig::WITH_AUDIT_VALUES_KEY }&.value != false
composer.page_style(:default, page_size:) do |canvas, style| composer.page_style(:default, page_size:) do |canvas, style|
box = canvas.context.box(:media) box = canvas.context.box(:media)
canvas.save_graphics_state do canvas.save_graphics_state do
@@ -110,9 +115,6 @@ module Submissions
maybe_add_background(canvas, submission, page_size) maybe_add_background(canvas, submission, page_size)
end end
with_signature_id = submission.account.account_configs
.exists?(key: AccountConfig::WITH_SIGNATURE_ID, value: true)
if with_signature_id || submission.account.testing? if with_signature_id || submission.account.testing?
canvas.save_graphics_state do canvas.save_graphics_state do
document_id = Digest::MD5.hexdigest(submission.slug).upcase document_id = Digest::MD5.hexdigest(submission.slug).upcase
@@ -127,7 +129,7 @@ module Submissions
TESTING_FOOTER TESTING_FOOTER
end end
else else
"Document ID: #{document_id}" "#{I18n.t('document_id')}: #{document_id}"
end end
text = HexaPDF::Layout::TextFragment.create( text = HexaPDF::Layout::TextFragment.create(
@@ -155,17 +157,19 @@ module Submissions
composer.column(columns: 1) do |column| composer.column(columns: 1) do |column|
add_logo(column, submission) add_logo(column, submission)
column.text(account.testing? ? 'Testing Log - Not for Production Use' : 'Audit Log', column.text(account.testing? ? I18n.t('testing_log_not_for_production_use') : I18n.t('audit_log'),
font_size: 16, font_size: 16,
padding: [10, 0, 0, 0], padding: [10, 0, 0, 0],
position: :float, text_align: :right) position: :float, text_align: :right)
end end
composer.column(columns: 1) do |column| composer.column(columns: 1) do |column|
column.text("Envelope ID: #{submission.id}", font_size: 12, padding: [15, 0, 8, 0], position: :float) column.text("#{I18n.t('envelope_id')}: #{submission.id}", font_size: 12,
padding: [15, 0, 8, 0],
position: :float)
unless submission.source.in?(%w[embed api]) unless submission.source.in?(%w[embed api])
column.formatted_text([{ link: verify_url, text: 'Verify', style: :link }], column.formatted_text([{ link: verify_url, text: I18n.t('verify'), style: :link }],
font_size: 9, padding: [15, 0, 10, 0], position: :float, text_align: :right) font_size: 9, padding: [15, 0, 10, 0], position: :float, text_align: :right)
end end
end end
@@ -185,22 +189,19 @@ module Submissions
end end
end end
link =
ActiveStorage::Blob.proxy_url(document.blob)
[ [
composer.document.layout.formatted_text_box( composer.document.layout.formatted_text_box(
[{ text: document.filename.to_s, link: }] [{ text: document.filename.to_s }]
), ),
composer.document.layout.formatted_text_box( composer.document.layout.formatted_text_box(
[ [
{ text: "Original SHA256:\n", font: [FONT_NAME, { variant: :bold }] }, { text: "#{I18n.t('original_sha256')}:\n", font: [FONT_NAME, { variant: :bold }] },
original_documents.map { |d| d.metadata['sha256'] || d.checksum }.join("\n"), original_documents.map { |d| d.metadata['sha256'] || d.checksum }.join("\n"),
"\n", "\n",
{ text: "Result SHA256:\n", font: [FONT_NAME, { variant: :bold }] }, { text: "#{I18n.t('result_sha256')}:\n", font: [FONT_NAME, { variant: :bold }] },
document.metadata['sha256'] || document.checksum, document.metadata['sha256'] || document.checksum,
"\n", "\n",
{ text: 'Generated at: ', font: [FONT_NAME, { variant: :bold }] }, { text: "#{I18n.t('generated_at')}: ", font: [FONT_NAME, { variant: :bold }] },
"#{I18n.l(document.created_at.in_time_zone(account.timezone), format: :long, locale: account.locale)} " \ "#{I18n.l(document.created_at.in_time_zone(account.timezone), format: :long, locale: account.locale)} " \
"#{TimeUtils.timezone_abbr(account.timezone, document.created_at)}" "#{TimeUtils.timezone_abbr(account.timezone, document.created_at)}"
], line_spacing: 1.3 ], line_spacing: 1.3
@@ -247,13 +248,13 @@ module Submissions
composer.document.layout.formatted_text_box( composer.document.layout.formatted_text_box(
[ [
submitter.email && click_email_event && { submitter.email && click_email_event && {
text: "Email verification: #{VERIFIED_TEXT}\n" text: "#{I18n.t('email_verification')}: #{I18n.t('verified')}\n"
}, },
submitter.phone && is_phone_verified && { submitter.phone && is_phone_verified && {
text: "Phone verification: #{VERIFIED_TEXT}\n" text: "#{I18n.t('phone_verification')}: #{I18n.t('verified')}\n"
}, },
completed_event.data['ip'] && { text: "IP: #{completed_event.data['ip']}\n" }, completed_event.data['ip'] && { text: "IP: #{completed_event.data['ip']}\n" },
completed_event.data['sid'] && { text: "Session ID: #{completed_event.data['sid']}\n" }, completed_event.data['sid'] && { text: "#{I18n.t('session_id')}: #{completed_event.data['sid']}\n" },
completed_event.data['ua'] && { text: "User agent: #{completed_event.data['ua']}\n" }, completed_event.data['ua'] && { text: "User agent: #{completed_event.data['ua']}\n" },
"\n" "\n"
].compact_blank, line_spacing: 1.3, padding: [10, 20, 20, 0] ].compact_blank, line_spacing: 1.3, padding: [10, 20, 20, 0]
@@ -266,6 +267,7 @@ module Submissions
submission.template_fields.filter_map do |field| submission.template_fields.filter_map do |field|
next if field['submitter_uuid'] != submitter.uuid next if field['submitter_uuid'] != submitter.uuid
next if field['type'] == 'heading' next if field['type'] == 'heading'
next if !with_audit_values && !field['type'].in?(%w[signature initials])
submitter_field_counters[field['type']] += 1 submitter_field_counters[field['type']] += 1
@@ -280,7 +282,7 @@ module Submissions
[ [
{ {
text: TextUtils.maybe_rtl_reverse(field_name).upcase.presence || text: TextUtils.maybe_rtl_reverse(field_name).upcase.presence ||
"#{field['type']} Field #{submitter_field_counters[field['type']]}\n".upcase, "#{I18n.t("#{field['type']}_field")} #{submitter_field_counters[field['type']]}\n".upcase,
font_size: 6 font_size: 6
} }
].compact_blank, ].compact_blank,
@@ -312,7 +314,8 @@ module Submissions
price = ApplicationController.helpers.number_to_currency(field['preferences']['price'], unit:) price = ApplicationController.helpers.number_to_currency(field['preferences']['price'], unit:)
composer.formatted_text_box([{ text: "Paid #{price}\n" }], padding: [0, 0, 10, 0]) composer.formatted_text_box([{ text: "#{I18n.t('paid_price', price:)}\n" }],
padding: [0, 0, 10, 0])
end end
composer.formatted_text_box( composer.formatted_text_box(
@@ -332,7 +335,10 @@ module Submissions
value = TimeUtils.format_date_string(value, field.dig('preferences', 'format'), account.locale) value = TimeUtils.format_date_string(value, field.dig('preferences', 'format'), account.locale)
end end
value = NumberUtils.format_number(value, field.dig('preferences', 'format')) if field['type'] == 'number' if field['type'] == 'number'
value = NumberUtils.format_number(value,
field.dig('preferences', 'format'))
end
value = value.join(', ') if value.is_a?(Array) value = value.join(', ') if value.is_a?(Array)
@@ -342,37 +348,42 @@ module Submissions
end end
] ]
end end
composer.draw_box(divider)
end end
composer.text('Event Log', font_size: 12, padding: [10, 0, 20, 0]) composer.draw_box(divider)
composer.text(I18n.t('event_log'), font_size: 12, padding: [10, 0, 20, 0])
events_data = submission.submission_events.sort_by(&:event_timestamp).map do |event| events_data = submission.submission_events.sort_by(&:event_timestamp).map do |event|
submitter = submission.submitters.find { |e| e.id == event.submitter_id } submitter = submission.submitters.find { |e| e.id == event.submitter_id }
submitter_name =
if event.event_type.include?('sms') || event.event_type.include?('phone')
event.data['phone'] || submitter.phone
else
submitter.name || submitter.email || submitter.phone
end
text = SubmissionEvents::EVENT_NAMES[event.event_type.to_sym] text =
if event.event_type == 'invite_party' &&
(invited_submitter = submission.submitters.find { |e| e.uuid == event.data['uuid'] }) &&
(name = submission.template_submitters.find { |e| e['uuid'] == event.data['uuid'] }&.dig('name'))
invited_submitter_name = [invited_submitter.name || invited_submitter.email || invited_submitter.phone,
name].join(' ')
I18n.t('submission_event_names.invite_party_by_html', invited_submitter_name:,
submitter_name:)
elsif event.event_type.include?('send_')
I18n.t("submission_event_names.#{event.event_type}_to_html", submitter_name:)
else
I18n.t("submission_event_names.#{event.event_type}_by_html", submitter_name:)
end
if event.event_type == 'invite_party' && bold_text, normal_text = text.match(%r{<b>(.*?)</b>(.*)}).captures
(invited_submitter = submission.submitters.find { |e| e.uuid == event.data['uuid'] }) &&
(name = submission.template_submitters.find { |e| e['uuid'] == event.data['uuid'] }&.dig('name'))
text += ['', invited_submitter.name || invited_submitter.email || invited_submitter.phone, name].join(' ')
end
[ [
"#{I18n.l(event.event_timestamp.in_time_zone(account.timezone), format: :long, locale: account.locale)} " \ "#{I18n.l(event.event_timestamp.in_time_zone(account.timezone), format: :long, locale: account.locale)} " \
"#{TimeUtils.timezone_abbr(account.timezone, event.event_timestamp)}", "#{TimeUtils.timezone_abbr(account.timezone, event.event_timestamp)}",
composer.document.layout.formatted_text_box( composer.document.layout.formatted_text_box([{ text: bold_text, font: [FONT_NAME, { variant: :bold }] },
[ normal_text])
{ text:, font: [FONT_NAME, { variant: :bold }] },
event.event_type.include?('send_') ? ' to ' : ' by ',
if event.event_type.include?('sms') || event.event_type.include?('phone')
event.data['phone'] || submitter.phone
else
submitter.name || submitter.email || submitter.phone
end
]
)
] ]
end end
@@ -18,7 +18,7 @@ module Submissions
pdf.trailer.info[:Creator] = "#{Docuseal.product_name} (#{Docuseal::PRODUCT_URL})" pdf.trailer.info[:Creator] = "#{Docuseal.product_name} (#{Docuseal::PRODUCT_URL})"
sign_params = { sign_params = {
reason: sign_reason, reason: Submissions::GenerateResultAttachments.single_sign_reason(submitter),
**Submissions::GenerateResultAttachments.build_signing_params(submitter, pkcs, tsa_url) **Submissions::GenerateResultAttachments.build_signing_params(submitter, pkcs, tsa_url)
} }
@@ -38,7 +38,9 @@ module Submissions
def build_combined_pdf(submitter) def build_combined_pdf(submitter)
pdfs_index = Submissions::GenerateResultAttachments.generate_pdfs(submitter) pdfs_index = Submissions::GenerateResultAttachments.generate_pdfs(submitter)
audit_trail = Submissions::GenerateAuditTrail.build_audit_trail(submitter.submission) audit_trail = I18n.with_locale(submitter.account.locale) do
Submissions::GenerateAuditTrail.build_audit_trail(submitter.submission)
end
audit_trail.dispatch_message(:complete_objects) audit_trail.dispatch_message(:complete_objects)
@@ -56,9 +58,5 @@ module Submissions
result result
end end
def sign_reason
'Signed with DocuSeal.co'
end
end end
end end
+9 -8
View File
@@ -21,7 +21,7 @@ module Submissions
def rows_to_xlsx(rows) def rows_to_xlsx(rows)
workbook = RubyXL::Workbook.new workbook = RubyXL::Workbook.new
worksheet = workbook[0] worksheet = workbook[0]
worksheet.sheet_name = I18n.l(Time.current.to_date) worksheet.sheet_name = Time.current.to_date.to_s
headers = build_headers(rows) headers = build_headers(rows)
headers.each_with_index do |column_name, column_index| headers.each_with_index do |column_name, column_index|
@@ -79,7 +79,7 @@ module Submissions
submission_data += submitter.documents.map.with_index(1) do |attachment, index| submission_data += submitter.documents.map.with_index(1) do |attachment, index|
{ {
name: "Document #{index}", name: "#{I18n.t('document')} #{index}",
value: ActiveStorage::Blob.proxy_url(attachment.blob) value: ActiveStorage::Blob.proxy_url(attachment.blob)
} }
end end
@@ -92,20 +92,20 @@ module Submissions
def build_submission_data(submitter, submitter_name, submitters_count) def build_submission_data(submitter, submitter_name, submitters_count)
[ [
{ {
name: column_name('Name', submitter_name, submitters_count), name: column_name(I18n.t('name'), submitter_name, submitters_count),
value: submitter.name value: submitter.name
}, },
{ {
name: column_name('Email', submitter_name, submitters_count), name: column_name(I18n.t('email'), submitter_name, submitters_count),
value: submitter.email value: submitter.email
}, },
{ {
name: column_name('Phone', submitter_name, submitters_count), name: column_name(I18n.t('phone'), submitter_name, submitters_count),
value: submitter.phone value: submitter.phone
}, },
{ {
name: column_name('Completed At', submitter_name, submitters_count), name: column_name(I18n.t('completed_at'), submitter_name, submitters_count),
value: submitter.completed_at value: submitter.completed_at.to_s
} }
].reject { |e| e[:value].blank? } ].reject { |e| e[:value].blank? }
end end
@@ -127,7 +127,8 @@ module Submissions
template_field_type = template_field['type'] template_field_type = template_field['type']
template_field_counters[template_field_type] += 1 template_field_counters[template_field_type] += 1
template_field_name = template_field['name'].presence template_field_name = template_field['name'].presence
template_field_name ||= "#{template_field_type.titleize} Field #{template_field_counters[template_field_type]}" template_field_name ||=
"#{I18n.t("#{template_field_type}_field")} #{template_field_counters[template_field_type]}"
value = value =
if template_field_type.in?(%w[image signature]) if template_field_type.in?(%w[image signature])
+11 -7
View File
@@ -127,7 +127,7 @@ module Submissions
TESTING_FOOTER TESTING_FOOTER
end end
else else
"Document ID: #{document_id}" "#{I18n.t('document_id', locale: submitter.account.locale)}: #{document_id}"
end end
text = HexaPDF::Layout::TextFragment.create( text = HexaPDF::Layout::TextFragment.create(
@@ -224,11 +224,12 @@ module Submissions
reason_value = submitter.values[field.dig('preferences', 'reason_field_uuid')].presence reason_value = submitter.values[field.dig('preferences', 'reason_field_uuid')].presence
reason_string = reason_string =
"#{I18n.t('reason')}: #{reason_value || I18n.t('digitally_signed_by')} " \ I18n.with_locale(submitter.account.locale) do
"#{submitter.name}#{submitter.email.present? ? " <#{submitter.email}>" : ''}\n" \ "#{I18n.t('reason')}: #{reason_value || I18n.t('digitally_signed_by')} " \
"#{I18n.l(attachment.created_at.in_time_zone(submitter.account.timezone), "#{submitter.name}#{submitter.email.present? ? " <#{submitter.email}>" : ''}\n" \
format: :long, locale: submitter.account.locale)} " \ "#{I18n.l(attachment.created_at.in_time_zone(submitter.account.timezone), format: :long)} " \
"#{TimeUtils.timezone_abbr(submitter.account.timezone, attachment.created_at)}" "#{TimeUtils.timezone_abbr(submitter.account.timezone, attachment.created_at)}"
end
reason_text = HexaPDF::Layout::TextFragment.create(reason_string, reason_text = HexaPDF::Layout::TextFragment.create(reason_string,
font:, font:,
@@ -340,7 +341,8 @@ module Submissions
if field['type'].in?(%w[multiple radio]) if field['type'].in?(%w[multiple radio])
option = field['options']&.find { |o| o['uuid'] == area['option_uuid'] } option = field['options']&.find { |o| o['uuid'] == area['option_uuid'] }
option_name = option['value'].presence || "Option #{field['options'].index(option) + 1}" option_name = option['value'].presence
option_name ||= "#{I18n.t('option', locale: account.locale)} #{field['options'].index(option) + 1}"
value = Array.wrap(value).include?(option_name) value = Array.wrap(value).include?(option_name)
end end
@@ -457,6 +459,8 @@ module Submissions
**build_signing_params(submitter, pkcs, tsa_url) **build_signing_params(submitter, pkcs, tsa_url)
} }
pdf.pages.first[:Annots] = [] unless pdf.pages.first[:Annots].respond_to?(:<<)
begin begin
pdf.sign(io, write_options: { validate: false }, **sign_params) pdf.sign(io, write_options: { validate: false }, **sign_params)
rescue HexaPDF::MalformedPDFError => e rescue HexaPDF::MalformedPDFError => e
+12 -7
View File
@@ -8,6 +8,7 @@ module Submitters
AccountConfig::FORM_PREFILL_SIGNATURE_KEY, AccountConfig::FORM_PREFILL_SIGNATURE_KEY,
AccountConfig::WITH_SIGNATURE_ID, AccountConfig::WITH_SIGNATURE_ID,
AccountConfig::REQUIRE_SIGNING_REASON_KEY, AccountConfig::REQUIRE_SIGNING_REASON_KEY,
AccountConfig::REUSE_SIGNATURE_KEY,
AccountConfig::ALLOW_TYPED_SIGNATURE].freeze AccountConfig::ALLOW_TYPED_SIGNATURE].freeze
module_function module_function
@@ -20,16 +21,20 @@ module Submitters
with_typed_signature = find_safe_value(configs, AccountConfig::ALLOW_TYPED_SIGNATURE) != false with_typed_signature = find_safe_value(configs, AccountConfig::ALLOW_TYPED_SIGNATURE) != false
with_confetti = find_safe_value(configs, AccountConfig::FORM_WITH_CONFETTI_KEY) != false with_confetti = find_safe_value(configs, AccountConfig::FORM_WITH_CONFETTI_KEY) != false
prefill_signature = find_safe_value(configs, AccountConfig::FORM_PREFILL_SIGNATURE_KEY) != false prefill_signature = find_safe_value(configs, AccountConfig::FORM_PREFILL_SIGNATURE_KEY) != false
reuse_signature = find_safe_value(configs, AccountConfig::REUSE_SIGNATURE_KEY) != false
with_signature_id = find_safe_value(configs, AccountConfig::WITH_SIGNATURE_ID) == true with_signature_id = find_safe_value(configs, AccountConfig::WITH_SIGNATURE_ID) == true
require_signing_reason = find_safe_value(configs, AccountConfig::REQUIRE_SIGNING_REASON_KEY) == true require_signing_reason = find_safe_value(configs, AccountConfig::REQUIRE_SIGNING_REASON_KEY) == true
attrs = { completed_button:, attrs = {
with_typed_signature:, completed_button:,
with_confetti:, with_typed_signature:,
completed_message:, with_confetti:,
require_signing_reason:, reuse_signature:,
prefill_signature:, completed_message:,
with_signature_id: } require_signing_reason:,
prefill_signature:,
with_signature_id:
}
keys.each do |key| keys.each do |key|
attrs[key.to_sym] = configs.find { |e| e.key == key.to_s }&.value attrs[key.to_sym] = configs.find { |e| e.key == key.to_s }&.value
+1 -1
View File
@@ -220,7 +220,7 @@ module Submitters
end end
when 'date' when 'date'
if with_time if with_time
I18n.l(Time.current.in_time_zone(submission.account.timezone).to_date) Time.current.in_time_zone(submission.account.timezone).to_date.to_s
else else
e e
end end
+10 -6
View File
@@ -17,7 +17,7 @@ module Templates
module_function module_function
def call(attachment, data, extract_fields: false) def call(attachment, data, extract_fields: false, max_pages: MAX_NUMBER_OF_PAGES_PROCESSED)
if attachment.content_type == PDF_CONTENT_TYPE if attachment.content_type == PDF_CONTENT_TYPE
if extract_fields && data.size < MAX_FLATTEN_FILE_SIZE if extract_fields && data.size < MAX_FLATTEN_FILE_SIZE
pdf = HexaPDF::Document.new(io: StringIO.new(data)) pdf = HexaPDF::Document.new(io: StringIO.new(data))
@@ -25,7 +25,7 @@ module Templates
fields = Templates::FindAcroFields.call(pdf, attachment) fields = Templates::FindAcroFields.call(pdf, attachment)
end end
generate_pdf_preview_images(attachment, data, pdf) generate_pdf_preview_images(attachment, data, pdf, max_pages:)
attachment.metadata['pdf']['fields'] = fields if fields attachment.metadata['pdf']['fields'] = fields if fields
elsif attachment.image? elsif attachment.image?
@@ -56,7 +56,7 @@ module Templates
) )
end end
def generate_pdf_preview_images(attachment, data, pdf = nil) def generate_pdf_preview_images(attachment, data, pdf = nil, max_pages: MAX_NUMBER_OF_PAGES_PROCESSED)
ActiveStorage::Attachment.where(name: ATTACHMENT_NAME, record: attachment).destroy_all ActiveStorage::Attachment.where(name: ATTACHMENT_NAME, record: attachment).destroy_all
pdf ||= HexaPDF::Document.new(io: StringIO.new(data)) pdf ||= HexaPDF::Document.new(io: StringIO.new(data))
@@ -71,12 +71,16 @@ module Templates
attachment.save! attachment.save!
end end
max_pages_to_process = data.size < GENERATE_PREVIEW_SIZE_LIMIT ? MAX_NUMBER_OF_PAGES_PROCESSED : 1 max_pages_to_process = data.size < GENERATE_PREVIEW_SIZE_LIMIT ? max_pages : 1
pool = Concurrent::FixedThreadPool.new(CONCURRENCY) generate_document_preview_images(attachment, data, (0..[number_of_pages - 1, max_pages_to_process].min))
end
def generate_document_preview_images(attachment, data, range, concurrency: CONCURRENCY)
pool = Concurrent::FixedThreadPool.new(concurrency)
promises = promises =
(0..[number_of_pages - 1, max_pages_to_process].min).map do |page_number| range.map do |page_number|
Concurrent::Promise.execute(executor: pool) { build_and_upload_blob(data, page_number) } Concurrent::Promise.execute(executor: pool) { build_and_upload_blob(data, page_number) }
end end
+3 -1
View File
@@ -2,6 +2,8 @@
class UserMailerPreview < ActionMailer::Preview class UserMailerPreview < ActionMailer::Preview
def invitation_email def invitation_email
UserMailer.invitation_email(User.last) user = User.first
user.account.locale = I18n.locale
UserMailer.invitation_email(user)
end end
end end
-1
View File
@@ -50,7 +50,6 @@ RSpec.configure do |config|
config.include FactoryBot::Syntax::Methods config.include FactoryBot::Syntax::Methods
config.include Devise::Test::IntegrationHelpers config.include Devise::Test::IntegrationHelpers
config.include ActiveJob::TestHelper
config.before(:each, type: :system) do config.before(:each, type: :system) do
if ENV['HEADLESS'] == 'false' if ENV['HEADLESS'] == 'false'
+1 -1
View File
@@ -81,7 +81,7 @@ RSpec.describe 'Submit Form' do
expect do expect do
click_on 'Sign and Complete' click_on 'Sign and Complete'
end.to change(enqueued_jobs, :size).by(1) end.to change(ProcessSubmitterCompletionJob.jobs, :size).by(1)
end end
end end
end end