Compare commits

...
72 Commits
Author SHA1 Message Date
Pete Matsyburka 53d924be0b fix build 2024-05-20 20:07:17 +03:00
Pete Matsyburka d6cf027d5d fix erblint 2024-05-19 19:44:38 +03:00
Alex Turchyn ec332bab88 flatten pdf for preview images 2024-05-19 19:25:53 +03:00
Alex Turchyn 8b3b90a7e2 replace emojis 2024-05-19 19:12:17 +03:00
Pete Matsyburka 9047e3bed3 add dry run prop 2024-05-18 22:54:26 +03:00
Pete Matsyburka 72b7391245 add documents copy email to template preferences 2024-05-18 22:03:20 +03:00
Pete Matsyburka 2df05f0327 adjust initials height 2024-05-18 17:51:22 +03:00
Pete Matsyburka 95f3eb0e5d display filters 2024-05-18 13:58:21 +03:00
Pete Matsyburka ce8d964ce6 fix ensure result generated 2024-05-17 16:07:08 +03:00
Pete Matsyburka f0d5fc4e2d add submission created webhook event 2024-05-17 13:28:10 +03:00
Pete Matsyburka b4b17d847e add view archived to template page 2024-05-17 12:08:46 +03:00
Alex Turchyn fd537e7ac0 upgrade rack 3 2024-05-16 23:26:30 +03:00
Pete Matsyburka 8c271104e1 single digital signature by default 2024-05-16 23:18:48 +03:00
Pete Matsyburka 51edd60d96 update gem 2024-05-16 15:16:59 +03:00
Pete Matsyburka a265ca1716 remove pdf processor 2024-05-16 13:37:23 +03:00
Pete Matsyburka a773f63a65 remove pdf reader 2024-05-16 11:56:41 +03:00
Pete Matsyburka 576c8a2e24 fix ensure result generated 2024-05-15 23:49:36 +03:00
Pete Matsyburka 5829de9ee4 adjust required fields alert 2024-05-15 11:42:51 +03:00
Pete Matsyburka b202941b79 fix tsa settings 2024-05-15 00:21:52 +03:00
Pete Matsyburka 6f6fb74988 adjust plans settings nav 2024-05-14 23:32:11 +03:00
Pete Matsyburka 0137bdbe4c optimize tsa settings controller 2024-05-12 19:32:09 +03:00
Pete Matsyburka 55d9511c01 add disclosure message 2024-05-12 12:35:20 +03:00
Pete Matsyburka 929d29576d add template webhooks 2024-05-12 11:37:33 +03:00
Pete Matsyburka 6f7128a51c add number format 2024-05-12 11:12:07 +03:00
Pete Matsyburka fc5c4ff2fc adjust reminder options 2024-05-11 16:24:13 +03:00
Pete Matsyburka a81595db2b allow legacy algos 2024-05-10 22:55:21 +03:00
Pete Matsyburka a5615d500d fix missing field steps 2024-05-10 22:24:44 +03:00
Pete Matsyburka da9266befc fix time format 2024-05-10 09:24:09 +03:00
Pete Matsyburka 5b3cffeb73 fix submission archived webhook 2024-05-08 09:53:37 +03:00
Pete Matsyburka 8f7b8a5fdf fix signature size 2024-05-08 09:38:16 +03:00
Pete Matsyburka 6d93116993 fix checkbox add on missclick 2024-05-05 18:18:07 +03:00
Pete Matsyburka a0f4916d3b add field settings dropdown to area 2024-05-05 16:58:19 +03:00
Pete Matsyburka ace897eaf9 update SECURITY.md 2024-05-04 23:15:04 +03:00
Pete Matsyburka b8667c5408 fix audit log on dev 2024-05-04 11:09:48 +03:00
Pete Matsyburka d87d66632e add field-area-active class 2024-05-02 23:33:44 +03:00
Alex Turchyn 3015c2d156 convert BMP images to PNG before uploading in the signing form 2024-05-02 22:10:23 +03:00
Pete Matsyburka 52b9e9ee8e fix underscore field name 2024-05-02 16:33:41 +03:00
Pete Matsyburka 5bfad0271c fix audit log font 2024-05-02 13:50:43 +03:00
Pete Matsyburka 58797891c7 fix trusted cert env 2024-05-02 13:39:47 +03:00
Pete Matsyburka 897750ba1e fix build 2024-05-02 12:43:13 +03:00
Pete Matsyburka 99341e773f refactor sign params 2024-05-01 23:40:09 +03:00
Pete Matsyburka 71319c1548 fix pdf generation 2024-05-01 11:51:40 +03:00
Pete Matsyburka c0a42f6bfc do now show default fields on preview 2024-04-29 15:42:11 +03:00
Pete Matsyburka d5abb46112 fix phone value assignment 2024-04-28 22:09:02 +03:00
Pete Matsyburka 2508b5fc8a adjust template account clone 2024-04-28 16:43:31 +03:00
Pete Matsyburka 4bd1ef0da3 fix params normalize 2024-04-28 16:33:11 +03:00
Pete Matsyburka 4efd748571 fix 2024-04-28 16:32:23 +03:00
Pete Matsyburka 859892d1fd adjust template preferences 2024-04-28 16:26:47 +03:00
Pete Matsyburka 7adeaf93cd only active linked accounts 2024-04-28 15:01:15 +03:00
Pete Matsyburka 5dbe155ecb shorten call 2024-04-28 14:44:33 +03:00
Pete Matsyburka 667d065bee populate linked accounts 2024-04-28 14:27:23 +03:00
Pete Matsyburka bb613059de add archived at to accounts 2024-04-28 13:43:53 +03:00
Pete Matsyburka 1b2dec4c2a adjust create submission resp 2024-04-27 23:08:16 +03:00
Pete Matsyburka 2c2ef97c30 validate params 2024-04-27 21:35:20 +03:00
Pete Matsyburka 6df1d68237 addept value param 2024-04-27 21:35:09 +03:00
Pete Matsyburka 20c9faedff update dep 2024-04-27 20:15:39 +03:00
Pete Matsyburka d931a40ea9 adjust log 2024-04-27 11:11:00 +03:00
Pete Matsyburka 1097b98b85 add log 2024-04-26 23:37:04 +03:00
Pete Matsyburka 2e597fcf43 ruby 3.3.1 2024-04-26 12:39:45 +03:00
Pete Matsyburka bb2fa6a261 add reuse signature prop 2024-04-25 17:51:36 +03:00
Pete Matsyburka 1dc8369487 fix template clone 2024-04-24 21:02:19 +03:00
Pete Matsyburka e6d60f1985 better error message 2024-04-24 13:18:57 +03:00
Pete Matsyburka 82e3cb8a4d fix audit log toggle 2024-04-24 01:19:07 +03:00
Pete Matsyburka aeec0cbf8e phone step langauge 2024-04-23 21:33:40 +03:00
Pete Matsyburka 7a2d1fc6c5 allow to remove personalization configs 2024-04-23 20:47:12 +03:00
Pete Matsyburka 68033ab7d2 fix files link position 2024-04-23 20:09:39 +03:00
Pete Matsyburka 7595c60ea8 remove pattern validation 2024-04-23 15:02:42 +03:00
Pete Matsyburka d9318c04f8 fix builder bg color 2024-04-23 01:57:45 +03:00
Pete Matsyburka 2d76198290 add flatten pdf toggle 2024-04-22 21:34:55 +03:00
Pete Matsyburka 02a7b6ea55 do not allow to skip required field with whitespace 2024-04-22 21:15:57 +03:00
Pete Matsyburka ab57076b8f add class 2024-04-22 20:01:30 +03:00
Pete Matsyburka 82fba47c98 rotate pdf 2024-04-22 19:10:58 +03:00
99 changed files with 2063 additions and 861 deletions

No files matched your search

+3 -3
View File
@@ -11,7 +11,7 @@ jobs:
- name: Install Ruby - name: Install Ruby
uses: ruby/setup-ruby@v1 uses: ruby/setup-ruby@v1
with: with:
ruby-version: 3.2.2 ruby-version: 3.3.1
- name: Cache gems - name: Cache gems
uses: actions/cache@v1 uses: actions/cache@v1
with: with:
@@ -35,7 +35,7 @@ jobs:
- name: Install Ruby - name: Install Ruby
uses: ruby/setup-ruby@v1 uses: ruby/setup-ruby@v1
with: with:
ruby-version: 3.2.2 ruby-version: 3.3.1
- name: Cache gems - name: Cache gems
uses: actions/cache@v1 uses: actions/cache@v1
with: with:
@@ -100,7 +100,7 @@ jobs:
- name: Install Ruby - name: Install Ruby
uses: ruby/setup-ruby@v1 uses: ruby/setup-ruby@v1
with: with:
ruby-version: 3.2.2 ruby-version: 3.3.1
- name: Set up Node - name: Set up Node
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
+1 -1
View File
@@ -10,7 +10,7 @@ AllCops:
- node_modules/**/* - node_modules/**/*
- bin/* - bin/*
- vendor/**/* - vendor/**/*
TargetRubyVersion: '3.2' TargetRubyVersion: '3.3'
Metrics/BlockLength: Metrics/BlockLength:
Exclude: Exclude:
+19 -5
View File
@@ -1,4 +1,4 @@
FROM ruby:3.2.2-alpine3.18 as fonts FROM ruby:3.3.1-alpine3.18 as fonts
WORKDIR /fonts WORKDIR /fonts
@@ -6,7 +6,7 @@ RUN apk --no-cache add fontforge wget && wget https://github.com/satbyy/go-noto-
RUN fontforge -lang=py -c 'font1 = fontforge.open("FreeSans.ttf"); font2 = fontforge.open("NotoSansSymbols2-Regular.ttf"); font1.mergeFonts(font2); font1.generate("FreeSans.ttf")' RUN fontforge -lang=py -c 'font1 = fontforge.open("FreeSans.ttf"); font2 = fontforge.open("NotoSansSymbols2-Regular.ttf"); font1.mergeFonts(font2); font1.generate("FreeSans.ttf")'
FROM ruby:3.2.2-alpine3.18 as webpack FROM ruby:3.3.1-alpine3.18 as webpack
ENV RAILS_ENV=production ENV RAILS_ENV=production
ENV NODE_ENV=production ENV NODE_ENV=production
@@ -32,18 +32,32 @@ COPY ./app/views ./app/views
RUN echo "gem 'shakapacker'" > Gemfile && ./bin/shakapacker RUN echo "gem 'shakapacker'" > Gemfile && ./bin/shakapacker
FROM ruby:3.2.2-alpine3.18 as app FROM ruby:3.3.1-alpine3.18 as app
ENV RAILS_ENV=production ENV RAILS_ENV=production
ENV BUNDLE_WITHOUT="development:test" ENV BUNDLE_WITHOUT="development:test"
ENV LD_PRELOAD=/lib/libgcompat.so.0
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 vips-heif libc6-compat ttf-freefont && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf RUN apk add --no-cache sqlite-dev libpq-dev mariadb-dev vips-dev vips-poppler poppler-utils vips-heif gcompat ttf-freefont && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf
RUN echo $'.include = /etc/ssl/openssl.cnf\n\
\n\
[provider_sect]\n\
default = default_sect\n\
legacy = legacy_sect\n\
\n\
[default_sect]\n\
activate = 1\n\
\n\
[legacy_sect]\n\
activate = 1' >> /app/openssl_legacy.cnf
COPY ./Gemfile ./Gemfile.lock ./ COPY ./Gemfile ./Gemfile.lock ./
RUN apk add --no-cache build-base && bundle update --bundler && 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 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
+3 -3
View File
@@ -2,13 +2,14 @@
source 'https://rubygems.org' source 'https://rubygems.org'
ruby '3.2.2' ruby '3.3.1'
gem 'arabic-letter-connector', require: 'arabic-letter-connector/logic' gem 'arabic-letter-connector', require: 'arabic-letter-connector/logic'
gem 'aws-sdk-s3', require: false gem 'aws-sdk-s3', require: false
gem 'azure-storage-blob', require: false gem 'azure-storage-blob', require: false
gem 'bootsnap', require: false gem 'bootsnap', require: false
gem 'cancancan' gem 'cancancan'
gem 'csv'
gem 'devise' gem 'devise'
gem 'devise-two-factor' gem 'devise-two-factor'
gem 'dotenv', require: false gem 'dotenv', require: false
@@ -23,7 +24,6 @@ gem 'lograge'
gem 'mysql2', require: false gem 'mysql2', require: false
gem 'oj' gem 'oj'
gem 'pagy' gem 'pagy'
gem 'pdf-reader'
gem 'pg', require: false gem 'pg', require: false
gem 'premailer-rails' gem 'premailer-rails'
gem 'pretender' gem 'pretender'
@@ -38,7 +38,7 @@ gem 'ruby-vips'
gem 'rubyXL' gem 'rubyXL'
gem 'shakapacker' gem 'shakapacker'
gem 'sidekiq', require: ENV.key?('REDIS_URL') gem 'sidekiq', require: ENV.key?('REDIS_URL')
gem 'sqlite3', require: false gem 'sqlite3', require: false, force_ruby_platform: true
gem 'strip_attributes' gem 'strip_attributes'
gem 'turbo-rails' gem 'turbo-rails'
gem 'twitter_cldr', require: false gem 'twitter_cldr', require: false
+27 -36
View File
@@ -1,7 +1,6 @@
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
Ascii85 (1.1.0)
actioncable (7.1.3.2) actioncable (7.1.3.2)
actionpack (= 7.1.3.2) actionpack (= 7.1.3.2)
activesupport (= 7.1.3.2) activesupport (= 7.1.3.2)
@@ -78,7 +77,6 @@ GEM
tzinfo (~> 2.0) tzinfo (~> 2.0)
addressable (2.8.6) addressable (2.8.6)
public_suffix (>= 2.0.2, < 6.0) public_suffix (>= 2.0.2, < 6.0)
afm (0.2.2)
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)
@@ -117,7 +115,7 @@ GEM
erubi (~> 1.4) erubi (~> 1.4)
parser (>= 2.4) parser (>= 2.4)
smart_properties smart_properties
bigdecimal (3.1.7) bigdecimal (3.1.8)
bindex (0.8.1) bindex (0.8.1)
bootsnap (1.18.3) bootsnap (1.18.3)
msgpack (~> 1.2) msgpack (~> 1.2)
@@ -149,6 +147,7 @@ GEM
crass (1.0.6) crass (1.0.6)
css_parser (1.17.1) css_parser (1.17.1)
addressable addressable
csv (3.3.0)
cuprite (0.15) cuprite (0.15)
capybara (~> 3.0) capybara (~> 3.0)
ferrum (~> 0.14.0) ferrum (~> 0.14.0)
@@ -261,21 +260,20 @@ GEM
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.0)
hashery (2.1.2) hexapdf (0.42.0)
hexapdf (0.40.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)
htmlentities (4.3.4) htmlentities (4.3.4)
httpclient (2.8.3) httpclient (2.8.3)
i18n (1.14.4) i18n (1.14.5)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
image_processing (1.12.2) image_processing (1.12.2)
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.12.0) irb (1.13.1)
rdoc 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)
@@ -311,7 +309,7 @@ GEM
mini_magick (4.12.0) mini_magick (4.12.0)
mini_mime (1.1.5) mini_mime (1.1.5)
mini_portile2 (2.8.6) mini_portile2 (2.8.6)
minitest (5.22.3) minitest (5.23.0)
msgpack (1.7.2) msgpack (1.7.2)
multi_json (1.15.0) multi_json (1.15.0)
multipart-post (2.4.0) multipart-post (2.4.0)
@@ -329,10 +327,12 @@ GEM
net-smtp (0.4.0) net-smtp (0.4.0)
net-protocol net-protocol
nio4r (2.7.1) nio4r (2.7.1)
nokogiri (1.16.4) nokogiri (1.16.5)
mini_portile2 (~> 2.8.2) mini_portile2 (~> 2.8.2)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.16.4-arm64-darwin) nokogiri (1.16.5-aarch64-linux)
racc (~> 1.4)
nokogiri (1.16.5-x86_64-linux)
racc (~> 1.4) racc (~> 1.4)
oj (3.16.3) oj (3.16.3)
bigdecimal (>= 3.0) bigdecimal (>= 3.0)
@@ -342,15 +342,9 @@ GEM
package_json (0.1.0) package_json (0.1.0)
pagy (8.1.2) pagy (8.1.2)
parallel (1.24.0) parallel (1.24.0)
parser (3.3.0.5) parser (3.3.1.0)
ast (~> 2.4.1) ast (~> 2.4.1)
racc racc
pdf-reader (2.12.0)
Ascii85 (~> 1.0)
afm (~> 0.2.1)
hashery (~> 2.0)
ruby-rc4
ttfunk
pg (1.5.6) pg (1.5.6)
premailer (1.23.0) premailer (1.23.0)
addressable addressable
@@ -372,17 +366,17 @@ GEM
public_suffix (5.0.5) public_suffix (5.0.5)
puma (6.4.2) puma (6.4.2)
nio4r (~> 2.0) nio4r (~> 2.0)
racc (1.7.3) racc (1.8.0)
rack (2.2.9) rack (3.0.11)
rack-proxy (0.7.7) rack-proxy (0.7.7)
rack rack
rack-session (1.0.2) rack-session (2.0.0)
rack (< 3) rack (>= 3.0.0)
rack-test (2.1.0) rack-test (2.1.0)
rack (>= 1.3) rack (>= 1.3)
rackup (1.0.0) rackup (2.1.0)
rack (< 3) rack (>= 3)
webrick webrick (~> 1.8)
rails (7.1.3.2) rails (7.1.3.2)
actioncable (= 7.1.3.2) actioncable (= 7.1.3.2)
actionmailbox (= 7.1.3.2) actionmailbox (= 7.1.3.2)
@@ -426,7 +420,7 @@ GEM
redis-client (0.22.0) redis-client (0.22.0)
connection_pool connection_pool
regexp_parser (2.9.0) regexp_parser (2.9.0)
reline (0.5.1) reline (0.5.7)
io-console (~> 0.5) io-console (~> 0.5)
representable (3.2.0) representable (3.2.0)
declarative (< 0.1.0) declarative (< 0.1.0)
@@ -461,7 +455,7 @@ GEM
rspec-mocks (~> 3.13) rspec-mocks (~> 3.13)
rspec-support (~> 3.13) rspec-support (~> 3.13)
rspec-support (3.13.1) rspec-support (3.13.1)
rubocop (1.63.1) rubocop (1.63.3)
json (~> 2.3) json (~> 2.3)
language_server-protocol (>= 3.17.0) language_server-protocol (>= 3.17.0)
parallel (~> 1.10) parallel (~> 1.10)
@@ -494,7 +488,6 @@ GEM
rubocop-rspec_rails (2.28.3) rubocop-rspec_rails (2.28.3)
rubocop (~> 1.40) rubocop (~> 1.40)
ruby-progressbar (1.13.0) ruby-progressbar (1.13.0)
ruby-rc4 (0.1.5)
ruby-vips (2.2.1) ruby-vips (2.2.1)
ffi (~> 1.12) ffi (~> 1.12)
ruby2_keywords (0.0.5) ruby2_keywords (0.0.5)
@@ -503,7 +496,7 @@ GEM
rubyzip (>= 1.3.0) rubyzip (>= 1.3.0)
rubyzip (2.3.2) rubyzip (2.3.2)
semantic_range (3.0.0) semantic_range (3.0.0)
shakapacker (7.2.3) shakapacker (8.0.0)
activesupport (>= 5.2) activesupport (>= 5.2)
package_json package_json
rack-proxy (>= 0.6.1) rack-proxy (>= 0.6.1)
@@ -528,15 +521,12 @@ GEM
smart_properties (1.17.0) smart_properties (1.17.0)
sqlite3 (1.7.3) sqlite3 (1.7.3)
mini_portile2 (~> 2.8.0) mini_portile2 (~> 2.8.0)
sqlite3 (1.7.3-arm64-darwin)
stringio (3.1.0) stringio (3.1.0)
strip_attributes (1.13.0) strip_attributes (1.13.0)
activemodel (>= 3.0, < 8.0) activemodel (>= 3.0, < 8.0)
thor (1.3.1) thor (1.3.1)
timeout (0.4.1) timeout (0.4.1)
trailblazer-option (0.1.2) trailblazer-option (0.1.2)
ttfunk (1.8.0)
bigdecimal (~> 3.1)
turbo-rails (2.0.5) turbo-rails (2.0.5)
actionpack (>= 6.0.0) actionpack (>= 6.0.0)
activejob (>= 6.0.0) activejob (>= 6.0.0)
@@ -569,11 +559,12 @@ GEM
websocket-extensions (0.1.5) websocket-extensions (0.1.5)
xpath (3.2.0) xpath (3.2.0)
nokogiri (~> 1.8) nokogiri (~> 1.8)
zeitwerk (2.6.13) zeitwerk (2.6.14)
PLATFORMS PLATFORMS
arm64-darwin-22 aarch64-linux
ruby ruby
x86_64-linux
DEPENDENCIES DEPENDENCIES
annotate annotate
@@ -585,6 +576,7 @@ DEPENDENCIES
bullet bullet
cancancan cancancan
capybara capybara
csv
cuprite cuprite
debug debug
devise devise
@@ -605,7 +597,6 @@ DEPENDENCIES
mysql2 mysql2
oj oj
pagy pagy
pdf-reader
pg pg
premailer-rails premailer-rails
pretender pretender
@@ -636,7 +627,7 @@ DEPENDENCIES
webmock webmock
RUBY VERSION RUBY VERSION
ruby 3.2.2p53 ruby 3.3.1p55
BUNDLED WITH BUNDLED WITH
2.5.3 2.5.3
+9
View File
@@ -3,3 +3,12 @@
If you come across any security concern or vulnarability, please report the information via email to security@docuseal.co instead of opening a GitHub issue. We will promptly respond and will collaborate with you to validate the issue, and resolve it ASAP. If you come across any security concern or vulnarability, please report the information via email to security@docuseal.co instead of opening a GitHub issue. We will promptly respond and will collaborate with you to validate the issue, and resolve it ASAP.
**We have a bug bounty program to reward security researchers.** **We have a bug bounty program to reward security researchers.**
Out of scope vulnerabilities:
- CSRF
- DNSSEC, CAA, CSP headers
- DNS or email security related
- Rate Limiting
Note: We reserve the right to classify any reported vulnerability as out of scope for the bug bounty program.
@@ -11,7 +11,8 @@ class AccountConfigsController < ApplicationController
AccountConfig::ESIGNING_PREFERENCE_KEY, AccountConfig::ESIGNING_PREFERENCE_KEY,
AccountConfig::FORM_WITH_CONFETTI_KEY, AccountConfig::FORM_WITH_CONFETTI_KEY,
AccountConfig::DOWNLOAD_LINKS_AUTH_KEY, AccountConfig::DOWNLOAD_LINKS_AUTH_KEY,
AccountConfig::FORCE_SSO_AUTH_KEY AccountConfig::FORCE_SSO_AUTH_KEY,
AccountConfig::FLATTEN_RESULT_PDF_KEY
].freeze ].freeze
InvalidKey = Class.new(StandardError) InvalidKey = Class.new(StandardError)
+16 -44
View File
@@ -22,7 +22,7 @@ module Api
audit_trail_attachment: :blob)) audit_trail_attachment: :blob))
render json: { render json: {
data: submissions.as_json(serialize_params), data: submissions.as_json(Submissions::SerializeForApi::SERIALIZE_PARAMS),
pagination: { pagination: {
count: submissions.size, count: submissions.size,
next: submissions.last&.id, next: submissions.last&.id,
@@ -44,33 +44,11 @@ module Api
@submission.audit_trail_attachment = Submissions::GenerateAuditTrail.call(@submission) @submission.audit_trail_attachment = Submissions::GenerateAuditTrail.call(@submission)
end end
serialized_submitters = submitters.map { |submitter| Submitters::SerializeForApi.call(submitter) } render json: Submissions::SerializeForApi.call(@submission, submitters)
json = @submission.as_json(
serialize_params.deep_merge(
include: { submission_events: { only: %i[id submitter_id event_type event_timestamp] } }
)
)
if submitters.all?(&:completed_at?)
last_submitter = submitters.max_by(&:completed_at)
json[:documents] = serialized_submitters.find { |e| e['id'] == last_submitter.id }['documents']
json[:status] = 'completed'
json[:completed_at] = last_submitter.completed_at
else
json[:documents] = []
json[:status] = 'pending'
json[:completed_at] = nil
end
json[:submitters] = serialized_submitters
render json:
end end
def create def create
Params::SubmissionCreateValidator.call(params, dry_run: !Docuseal.multitenant?) Params::SubmissionCreateValidator.call(params)
return render json: { error: 'Template not found' }, status: :unprocessable_entity if @template.nil? return render json: { error: 'Template not found' }, status: :unprocessable_entity if @template.nil?
@@ -85,6 +63,10 @@ module Api
submissions = create_submissions(@template, params) submissions = create_submissions(@template, params)
submissions.each do |submission|
SendSubmissionCreatedWebhookRequestJob.perform_later(submission)
end
Submissions.send_signature_requests(submissions) Submissions.send_signature_requests(submissions)
submissions.each do |submission| submissions.each do |submission|
@@ -93,7 +75,13 @@ module Api
end end
end end
render json: submissions.flat_map(&:submitters) json = submissions.flat_map do |submission|
submission.submitters.map do |s|
Submitters::SerializeForApi.call(s, with_documents: false, with_urls: true)
end
end
render json:
rescue Submitters::NormalizeValues::BaseError => e rescue Submitters::NormalizeValues::BaseError => e
Rollbar.warning(e) if defined?(Rollbar) Rollbar.warning(e) if defined?(Rollbar)
@@ -147,22 +135,6 @@ module Api
end end
end end
def serialize_params
{
only: %i[id source submitters_order created_at updated_at archived_at],
methods: %i[audit_log_url],
include: {
submitters: { only: %i[id slug uuid name email phone
completed_at opened_at sent_at
created_at updated_at external_id metadata],
methods: %i[status application_key] },
template: { only: %i[id name external_id created_at updated_at],
methods: %i[folder_name] },
created_by_user: { only: %i[id email first_name last_name] }
}
}
end
def submissions_params def submissions_params
permitted_attrs = [ permitted_attrs = [
:send_email, :send_sms, :bcc_completed, :completed_redirect_url, :reply_to, :go_to_last, :send_email, :send_sms, :bcc_completed, :completed_redirect_url, :reply_to, :go_to_last,
@@ -171,9 +143,9 @@ module Api
submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role, submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role,
:completed, :phone, :application_key, :external_id, :reply_to, :go_to_last, :completed, :phone, :application_key, :external_id, :reply_to, :go_to_last,
{ metadata: {}, values: {}, readonly_fields: [], message: %i[subject body], { metadata: {}, values: {}, readonly_fields: [], message: %i[subject body],
fields: [:name, :uuid, :default_value, :title, :description, fields: [:name, :uuid, :default_value, :value, :title, :description,
:readonly, :redacted, :validation_pattern, :invalid_message, :readonly, :redacted, :validation_pattern, :invalid_message,
{ default_value: [] }] }]] { default_value: [], value: [] }] }]]
} }
] ]
+34 -10
View File
@@ -71,7 +71,9 @@ module Api
Submitters.send_signature_requests([@submitter]) Submitters.send_signature_requests([@submitter])
end end
render json: Submitters::SerializeForApi.call(@submitter, with_template: false, with_events: false) render json: Submitters::SerializeForApi.call(@submitter, with_template: false,
with_urls: true,
with_events: false)
end end
def submitter_params def submitter_params
@@ -81,8 +83,9 @@ module Api
:send_email, :send_sms, :reply_to, :completed_redirect_url, :uuid, :name, :email, :role, :send_email, :send_sms, :reply_to, :completed_redirect_url, :uuid, :name, :email, :role,
:completed, :phone, :application_key, :external_id, :go_to_last, :completed, :phone, :application_key, :external_id, :go_to_last,
{ metadata: {}, values: {}, readonly_fields: [], message: %i[subject body], { metadata: {}, values: {}, readonly_fields: [], message: %i[subject body],
fields: [[:name, :uuid, :default_value, :readonly, :redacted, :validation_pattern, :invalid_message, fields: [[:name, :uuid, :default_value, :value,
{ default_value: [] }]] } :readonly, :redacted, :validation_pattern, :invalid_message,
{ default_value: [], value: [] }]] }
) )
end end
@@ -101,20 +104,41 @@ module Api
end end
def assign_submitter_attrs(submitter, attrs) def assign_submitter_attrs(submitter, attrs)
submitter.email = Submissions.normalize_email(attrs[:email]) if attrs.key?(:email) values = attrs[:values]&.to_unsafe_h || {}
submitter.phone = attrs[:phone].to_s.gsub(/[^0-9+]/, '') if attrs.key?(:phone)
submitter.values = submitter.values.merge(attrs[:values].to_unsafe_h) if attrs[:values].present?
submitter.completed_at = attrs[:completed] ? Time.current : submitter.completed_at
submitter.external_id = attrs[:application_key] if attrs.key?(:application_key)
submitter.external_id = attrs[:external_id] if attrs.key?(:external_id)
submitter.metadata = attrs[:metadata] if attrs.key?(:metadata)
assign_submission_fields(submitter.submission) assign_submission_fields(submitter.submission)
phone_field_uuid = submitter.submission.template_fields.find do |f|
values[f['uuid']].present? && f['type'] == 'phone'
end&.dig('uuid')
submitter.email = Submissions.normalize_email(attrs[:email]) if attrs.key?(:email)
if attrs.key?(:phone)
submitter.phone = attrs[:phone].to_s.gsub(/[^0-9+]/, '')
elsif values[phone_field_uuid].present?
submitter.phone = values[phone_field_uuid].to_s.gsub(/[^0-9+]/, '')
end
values = values.except(phone_field_uuid)
submitter.values = submitter.values.merge(values) if values.present?
submitter.completed_at = attrs[:completed] ? Time.current : submitter.completed_at
submitter.metadata = attrs[:metadata] if attrs.key?(:metadata)
assign_external_id(submitter, attrs)
assign_preferences(submitter, attrs) assign_preferences(submitter, attrs)
submitter submitter
end end
def assign_external_id(submitter, attrs)
submitter.external_id = attrs[:application_key] if attrs.key?(:application_key)
submitter.external_id = attrs[:external_id] if attrs.key?(:external_id)
submitter
end
def assign_submission_fields(submission) def assign_submission_fields(submission)
submission.template_fields ||= submission.template.fields submission.template_fields ||= submission.template.fields
submission.template_schema ||= submission.template.schema submission.template_schema ||= submission.template.schema
@@ -20,6 +20,8 @@ module Api
Templates::CloneAttachments.call(template: cloned_template, original_template: @template) Templates::CloneAttachments.call(template: cloned_template, original_template: @template)
SendTemplateCreatedWebhookRequestJob.perform_later(cloned_template)
render json: Templates::SerializeForApi.call(cloned_template) render json: Templates::SerializeForApi.call(cloned_template)
end end
end end
@@ -65,6 +65,8 @@ module Api
@template.update!(template_params) @template.update!(template_params)
SendTemplateUpdatedWebhookRequestJob.perform_later(@template)
render json: @template.as_json(only: %i[id updated_at]) render json: @template.as_json(only: %i[id updated_at])
end end
+10 -1
View File
@@ -15,6 +15,11 @@ class ErrorsController < ActionController::Base
'/api/templates/docx' '/api/templates/docx'
].freeze ].freeze
SAFE_ERROR_MESSAGE_CLASSES = [
ActionDispatch::Http::Parameters::ParseError,
JSON::ParserError
].freeze
def show def show
if request.original_fullpath.in?(ENTERPRISE_PATHS) && error_status_code == 404 if request.original_fullpath.in?(ENTERPRISE_PATHS) && error_status_code == 404
return render json: { status: 404, message: ENTERPRISE_FEATURE_MESSAGE }, status: :not_found return render json: { status: 404, message: ENTERPRISE_FEATURE_MESSAGE }, status: :not_found
@@ -24,7 +29,11 @@ class ErrorsController < ActionController::Base
f.json do f.json do
set_cors_headers set_cors_headers
render json: { status: error_status_code }, status: error_status_code exception = request.env['action_dispatch.exception']
error = exception.message if exception.class.in?(SAFE_ERROR_MESSAGE_CLASSES)
render json: { status: error_status_code, error: }.compact, status: error_status_code
end end
f.html { render error_status_code.to_s, status: error_status_code } f.html { render error_status_code.to_s, status: error_status_code }
+2 -2
View File
@@ -57,8 +57,8 @@ class EsignSettingsController < ApplicationController
save_new_cert!(@encrypted_config, @cert_record) save_new_cert!(@encrypted_config, @cert_record)
redirect_to settings_esign_path, notice: 'Certificate has been successfully added!' redirect_to settings_esign_path, notice: 'Certificate has been successfully added!'
rescue OpenSSL::PKCS12::PKCS12Error rescue OpenSSL::PKCS12::PKCS12Error => e
@cert_record.errors.add(:password, "is invalid. Make sure you're uploading a valid .p12 file") @cert_record.errors.add(:password, e.message)
render turbo_stream: turbo_stream.replace(:modal, template: 'esign_settings/new'), status: :unprocessable_entity render turbo_stream: turbo_stream.replace(:modal, template: 'esign_settings/new'), status: :unprocessable_entity
end end
@@ -18,6 +18,12 @@ class PersonalizationSettingsController < ApplicationController
end end
def create def create
if @account_config.value.is_a?(Hash)
@account_config.value = @account_config.value.reject do |_, v|
v.blank? && v != false
end
end
if @account_config.value != false && @account_config.value.blank? if @account_config.value != false && @account_config.value.blank?
@account_config.destroy! @account_config.destroy!
else else
+4
View File
@@ -33,7 +33,11 @@ class StartFormController < ApplicationController
assign_submission_attributes(@submitter, @template) if @submitter.new_record? assign_submission_attributes(@submitter, @template) if @submitter.new_record?
is_new_record = @submitter.new_record?
if @submitter.save if @submitter.save
SendSubmissionCreatedWebhookRequestJob.perform_later(@submitter.submission) if is_new_record
redirect_to submit_form_path(@submitter.slug) redirect_to submit_form_path(@submitter.slug)
else else
render :show render :show
@@ -44,6 +44,10 @@ class SubmissionsController < ApplicationController
params: params.merge('send_completed_email' => true)) params: params.merge('send_completed_email' => true))
end end
submissions.each do |submission|
SendSubmissionCreatedWebhookRequestJob.perform_later(submission)
end
Submissions.send_signature_requests(submissions) Submissions.send_signature_requests(submissions)
redirect_to template_path(@template), notice: 'New recipients have been added' redirect_to template_path(@template), notice: 'New recipients have been added'
@@ -7,6 +7,8 @@ class SubmittersSendEmailController < ApplicationController
if Docuseal.multitenant? && SubmissionEvent.exists?(submitter: @submitter, if Docuseal.multitenant? && SubmissionEvent.exists?(submitter: @submitter,
event_type: 'send_email', event_type: 'send_email',
created_at: 24.hours.ago..Time.current) created_at: 24.hours.ago..Time.current)
Rollbar.warning("Already sent: #{@submitter.id}") if defined?(Rollbar)
return redirect_back(fallback_location: submission_path(@submitter.submission), return redirect_back(fallback_location: submission_path(@submitter.submission),
alert: 'Email has been sent already.') alert: 'Email has been sent already.')
end end
+4
View File
@@ -61,6 +61,8 @@ class TemplatesController < ApplicationController
if @template.save if @template.save
Templates::CloneAttachments.call(template: @template, original_template: @base_template) if @base_template Templates::CloneAttachments.call(template: @template, original_template: @base_template) if @base_template
SendTemplateUpdatedWebhookRequestJob.perform_later(@template)
maybe_redirect_to_template(@template) maybe_redirect_to_template(@template)
else else
render turbo_stream: turbo_stream.replace(:modal, template: 'templates/new'), status: :unprocessable_entity render turbo_stream: turbo_stream.replace(:modal, template: 'templates/new'), status: :unprocessable_entity
@@ -70,6 +72,8 @@ class TemplatesController < ApplicationController
def update def update
@template.update!(template_params) @template.update!(template_params)
SendTemplateUpdatedWebhookRequestJob.perform_later(@template)
head :ok head :ok
end end
@@ -17,6 +17,9 @@ class TemplatesPreferencesController < ApplicationController
private private
def template_params def template_params
params.require(:template).permit(preferences: %i[bcc_completed request_email_subject request_email_body]) params.require(:template).permit(
preferences: %i[bcc_completed request_email_subject request_email_body
documents_copy_email_subject documents_copy_email_body]
)
end end
end end
@@ -18,6 +18,8 @@ class TemplatesUploadsController < ApplicationController
@template.update!(schema:) @template.update!(schema:)
SendTemplateCreatedWebhookRequestJob.perform_later(@template)
redirect_to edit_template_path(@template) redirect_to edit_template_path(@template)
rescue Templates::CreateAttachments::PdfEncrypted rescue Templates::CreateAttachments::PdfEncrypted
render turbo_stream: turbo_stream.append(params[:form_id], html: helpers.tag.prompt_password) render turbo_stream: turbo_stream.append(params[:form_id], html: helpers.tag.prompt_password)
+19 -11
View File
@@ -1,9 +1,13 @@
# frozen_string_literal: true # frozen_string_literal: true
class TimestampServerController < ApplicationController class TimestampServerController < ApplicationController
HASH_ALGORITHM = 'SHA256'
before_action :build_encrypted_config before_action :build_encrypted_config
authorize_resource :encrypted_config authorize_resource :encrypted_config
TimestampError = Class.new(StandardError)
def create def create
return head :not_found if Docuseal.multitenant? return head :not_found if Docuseal.multitenant?
@@ -14,25 +18,29 @@ class TimestampServerController < ApplicationController
else else
redirect_back fallback_location: settings_notifications_path, alert: 'Unable to save' redirect_back fallback_location: settings_notifications_path, alert: 'Unable to save'
end end
rescue HexaPDF::Error, SocketError, Submissions::TimestampHandler::TimestampError, OpenSSL::Timestamp::TimestampError rescue SocketError, TimestampError, OpenSSL::Timestamp::TimestampError
redirect_back fallback_location: settings_notifications_path, alert: 'Invalid Timeserver' redirect_back fallback_location: settings_notifications_path, alert: 'Invalid Timeserver'
end end
private private
def test_timeserver_url(url) def test_timeserver_url(url)
pdf = HexaPDF::Document.new req = OpenSSL::Timestamp::Request.new
pdf.pages.add req.algorithm = HASH_ALGORITHM
req.message_imprint = OpenSSL::Digest.digest(HASH_ALGORITHM, 'test')
pkcs = Accounts.load_signing_pkcs(current_account) uri = Addressable::URI.parse(url)
pdf.sign(StringIO.new, conn = Faraday.new(uri.origin) do |c|
reason: 'Test', c.basic_auth(uri.user, uri.password) if uri.password.present?
certificate: pkcs.certificate, end
key: pkcs.key,
signature_size: 10_000, response = conn.post(uri.path, req.to_der,
certificate_chain: pkcs.ca_certs || [], 'content-type' => 'application/timestamp-query')
timestamp_handler: Submissions::TimestampHandler.new(tsa_url: url))
raise TimestampError if response.status != 200 || response.body.blank?
response
end end
def load_encrypted_config def load_encrypted_config
+10 -5
View File
@@ -1,10 +1,10 @@
# frozen_string_literal: true # frozen_string_literal: true
class UsersController < ApplicationController class UsersController < ApplicationController
load_and_authorize_resource :user, only: %i[index edit new update destroy] load_and_authorize_resource :user, only: %i[index edit update destroy]
before_action :build_user, only: :create before_action :build_user, only: %i[new create]
authorize_resource :user, only: :create authorize_resource :user, only: %i[new create]
def index def index
@users = @users =
@@ -14,7 +14,7 @@ class UsersController < ApplicationController
@users.active @users.active
end end
@pagy, @users = pagy(@users.order(id: :desc)) @pagy, @users = pagy(@users.where(account: current_account).order(id: :desc))
end end
def new; end def new; end
@@ -81,6 +81,11 @@ class UsersController < ApplicationController
end end
def user_params def user_params
params.require(:user).permit(:email, :first_name, :last_name, :password, :role, :archived_at) if params.key?(:user)
params.require(:user).permit(:email, :first_name, :last_name, :password,
:role, :archived_at, :account_id)
else
{}
end
end end
end end
@@ -27,7 +27,8 @@ class VerifyPdfSignatureController < ApplicationController
trusted_certs = [default_pkcs.certificate, trusted_certs = [default_pkcs.certificate,
*default_pkcs.ca_certs, *default_pkcs.ca_certs,
*custom_certs.map(&:certificate), *custom_certs.map(&:certificate),
*custom_certs.flat_map(&:ca_certs).compact] *custom_certs.flat_map(&:ca_certs).compact,
*Docuseal.trusted_certs]
render turbo_stream: turbo_stream.replace('result', partial: 'result', render turbo_stream: turbo_stream.replace('result', partial: 'result',
locals: { pdfs:, files: params[:files], trusted_certs: }) locals: { pdfs:, files: params[:files], trusted_certs: })
@@ -5,6 +5,9 @@ class WebhookPreferencesController < ApplicationController
form.viewed form.viewed
form.started form.started
form.completed form.completed
template.created
template.updated
submission.created
submission.archived submission.archived
].freeze ].freeze
+1
View File
@@ -15,6 +15,7 @@ window.customElements.define('submission-form', class extends HTMLElement {
isDemo: this.dataset.isDemo === 'true', isDemo: this.dataset.isDemo === 'true',
attribution: this.dataset.attribution !== 'false', attribution: this.dataset.attribution !== 'false',
withConfetti: this.dataset.withConfetti !== 'false', withConfetti: this.dataset.withConfetti !== 'false',
withDisclosure: this.dataset.withDisclosure === 'true',
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,
values: reactive(JSON.parse(this.dataset.values)), values: reactive(JSON.parse(this.dataset.values)),
+17 -2
View File
@@ -1,9 +1,9 @@
<template> <template>
<div <div
class="flex absolute lg:text-base -outline-offset-1" class="field-area flex absolute lg:text-base -outline-offset-1"
dir="auto" dir="auto"
:style="computedStyle" :style="computedStyle"
:class="{ 'text-[1.5vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars, 'cursor-default': !submittable, 'border border-red-100 bg-red-100 cursor-pointer': submittable, 'border border-red-100': !isActive && submittable, 'bg-opacity-80': !isActive && !isValueSet && submittable, 'outline-red-500 outline-dashed outline-2 z-10': isActive && submittable, 'bg-opacity-40': (isActive || isValueSet) && submittable }" :class="{ 'text-[1.5vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars, 'cursor-default': !submittable, 'border border-red-100 bg-red-100 cursor-pointer': submittable, 'border border-red-100': !isActive && submittable, 'bg-opacity-80': !isActive && !isValueSet && submittable, 'field-area-active outline-red-500 outline-dashed outline-2 z-10': isActive && submittable, 'bg-opacity-40': (isActive || isValueSet) && submittable }"
> >
<div <div
v-if="!isActive && !isValueSet && field.type !== 'checkbox' && submittable && !area.option_uuid" v-if="!isActive && !isValueSet && field.type !== 'checkbox' && submittable && !area.option_uuid"
@@ -165,6 +165,9 @@
<span v-else-if="field.type === 'date'"> <span v-else-if="field.type === 'date'">
{{ formattedDate }} {{ formattedDate }}
</span> </span>
<span v-else-if="field.type === 'number'">
{{ formatNumber(modelValue, field.preferences?.format) }}
</span>
<span <span
v-else v-else
class="whitespace-pre-wrap" class="whitespace-pre-wrap"
@@ -373,6 +376,17 @@ export default {
} }
} }
}, },
formatNumber (number, format) {
if (format === 'comma') {
return new Intl.NumberFormat('en-US').format(number)
} else if (format === 'dot') {
return new Intl.NumberFormat('de-DE').format(number)
} else if (format === 'space') {
return new Intl.NumberFormat('fr-FR').format(number)
} else {
return number
}
},
formatDate (date, format) { formatDate (date, format) {
const monthFormats = { const monthFormats = {
M: 'numeric', M: 'numeric',
@@ -388,6 +402,7 @@ export default {
const yearFormats = { const yearFormats = {
YYYY: 'numeric', YYYY: 'numeric',
YYY: 'numeric',
YY: '2-digit' YY: '2-digit'
} }
+31 -1
View File
@@ -104,9 +104,13 @@ export default {
this.isLoading = true this.isLoading = true
return await Promise.all( return await Promise.all(
Array.from(files).map((file) => { Array.from(files).map(async (file) => {
const formData = new FormData() const formData = new FormData()
if (file.type === 'image/bmp') {
file = await this.convertBmpToPng(file)
}
formData.append('file', file) formData.append('file', file)
formData.append('submitter_slug', this.submitterSlug) formData.append('submitter_slug', this.submitterSlug)
formData.append('name', 'attachments') formData.append('name', 'attachments')
@@ -122,6 +126,32 @@ export default {
}).finally(() => { }).finally(() => {
this.isLoading = false this.isLoading = false
}) })
},
convertBmpToPng (bmpFile) {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = function (event) {
const img = new Image()
img.onload = function () {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
canvas.width = img.width
canvas.height = img.height
ctx.drawImage(img, 0, 0)
canvas.toBlob(function (blob) {
const newFile = new File([blob], bmpFile.name.replace(/\.\w+$/, '.png'), { type: 'image/png' })
resolve(newFile)
}, 'image/png')
}
img.src = event.target.result
}
reader.onerror = reject
reader.readAsDataURL(bmpFile)
})
} }
} }
} }
+50 -16
View File
@@ -324,6 +324,8 @@
:previous-value="previousSignatureValueFor(currentField)" :previous-value="previousSignatureValueFor(currentField)"
:with-typed-signature="withTypedSignature" :with-typed-signature="withTypedSignature"
:attachments-index="attachmentsIndex" :attachments-index="attachmentsIndex"
:button-text="buttonText"
:with-disclosure="withDisclosure"
:submitter-slug="submitterSlug" :submitter-slug="submitterSlug"
:show-field-names="showFieldNames" :show-field-names="showFieldNames"
@attached="attachments.push($event)" @attached="attachments.push($event)"
@@ -360,6 +362,7 @@
:key="currentField.uuid" :key="currentField.uuid"
v-model="values[currentField.uuid]" v-model="values[currentField.uuid]"
:field="currentField" :field="currentField"
:locale="language?.toLowerCase() || browserLanguage"
:show-field-names="showFieldNames" :show-field-names="showFieldNames"
:verified-value="phoneVerifiedValues[currentField.uuid]" :verified-value="phoneVerifiedValues[currentField.uuid]"
:default-value="submitter.phone" :default-value="submitter.phone"
@@ -381,7 +384,7 @@
</div> </div>
<div <div
v-if="currentField.type !== 'payment' || submittedValues[currentField.uuid]" v-if="currentField.type !== 'payment' || submittedValues[currentField.uuid]"
class="mt-6 md:mt-8" :class="withDisclosure && currentField.type === 'signature' ? 'mt-2' : 'mt-6 md:mt-8'"
> >
<button <button
id="submit_form_button" id="submit_form_button"
@@ -395,11 +398,8 @@
v-if="isSubmitting" v-if="isSubmitting"
class="mr-1 animate-spin" class="mr-1 animate-spin"
/> />
<span v-if="alwaysMinimize || stepFields.length === currentStep + 1"> <span>
{{ t('submit') }} {{ buttonText }}
</span>
<span v-else>
{{ t('next') }}
</span><span </span><span
v-if="isSubmitting" v-if="isSubmitting"
class="w-6 flex justify-start mr-1" class="w-6 flex justify-start mr-1"
@@ -471,10 +471,14 @@ import i18n from './i18n'
const isEmpty = (obj) => { const isEmpty = (obj) => {
if (obj == null) return true if (obj == null) return true
if (Array.isArray(obj) || typeof obj === 'string') { if (Array.isArray(obj)) {
return obj.length === 0 return obj.length === 0
} }
if (typeof obj === 'string') {
return obj.trim().length === 0
}
if (typeof obj === 'object') { if (typeof obj === 'object') {
return Object.keys(obj).length === 0 return Object.keys(obj).length === 0
} }
@@ -549,6 +553,16 @@ export default {
required: false, required: false,
default: false default: false
}, },
withDisclosure: {
type: Boolean,
required: false,
default: false
},
reuseSignature: {
type: Boolean,
required: false,
default: true
},
withConfetti: { withConfetti: {
type: Boolean, type: Boolean,
required: false, required: false,
@@ -604,6 +618,11 @@ export default {
required: false, required: false,
default: false default: false
}, },
dryRun: {
type: Boolean,
required: false,
default: false
},
attribution: { attribution: {
type: Boolean, type: Boolean,
required: false, required: false,
@@ -672,11 +691,18 @@ export default {
isMobile () { isMobile () {
return /android|iphone|ipad/i.test(navigator.userAgent) return /android|iphone|ipad/i.test(navigator.userAgent)
}, },
buttonText () {
if (this.alwaysMinimize || this.stepFields.length === this.currentStep + 1) {
return this.t('submit')
} else {
return this.t('next')
}
},
alwaysMinimize () { alwaysMinimize () {
return this.minimize || (this.orientation?.includes('landscape') && this.isMobile && parseInt(window.innerHeight) < 550) return this.minimize || (this.orientation?.includes('landscape') && this.isMobile && parseInt(window.innerHeight) < 550)
}, },
currentStepFields () { currentStepFields () {
return this.stepFields[this.currentStep] return this.stepFields[this.currentStep] || []
}, },
browserLanguage () { browserLanguage () {
return (navigator.language || navigator.userLanguage || 'en').split('-')[0] return (navigator.language || navigator.userLanguage || 'en').split('-')[0]
@@ -695,9 +721,13 @@ export default {
}, {}) }, {})
}, },
previousInitialsValue () { previousInitialsValue () {
const initialsField = [...this.fields].reverse().find((field) => field.type === 'initials' && !!this.values[field.uuid]) if (this.reuseSignature !== false) {
const initialsField = [...this.fields].reverse().find((field) => field.type === 'initials' && !!this.values[field.uuid])
return this.values[initialsField?.uuid] return this.values[initialsField?.uuid]
} else {
return null
}
}, },
isAnonymousChecboxes () { isAnonymousChecboxes () {
return this.currentField.type === 'checkbox' && this.currentStepFields.every((e) => !e.name && !e.required) && this.currentStepFields.length > 4 return this.currentField.type === 'checkbox' && this.currentStepFields.every((e) => !e.name && !e.required) && this.currentStepFields.length > 4
@@ -749,7 +779,7 @@ export default {
this.isFormVisible = value this.isFormVisible = value
}, },
currentStepFields (value) { currentStepFields (value) {
if (!value) { if (isEmpty(value)) {
this.currentStep -= 1 this.currentStep -= 1
} }
} }
@@ -925,11 +955,15 @@ export default {
}) })
}, },
previousSignatureValueFor (field) { previousSignatureValueFor (field) {
const signatureField = [...this.fields].reverse().find((f) => if (this.reuseSignature !== false) {
f.type === 'signature' && field.preferences?.format === f.preferences?.format && !!this.values[f.uuid] const signatureField = [...this.fields].reverse().find((f) =>
) f.type === 'signature' && field.preferences?.format === f.preferences?.format && !!this.values[f.uuid]
)
return this.values[signatureField?.uuid] return this.values[signatureField?.uuid]
} else {
return null
}
}, },
goToStep (step, scrollToArea = false, clickUpload = false) { goToStep (step, scrollToArea = false, clickUpload = false) {
this.currentStep = this.stepFields.indexOf(step) this.currentStep = this.stepFields.indexOf(step)
@@ -1001,7 +1035,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 = this.currentStep === this.stepFields.length - 1
if (isLastStep && !emptyRequiredField) { if (isLastStep && !emptyRequiredField && !this.dryRun) {
formData.append('completed', 'true') formData.append('completed', 'true')
} }
+39
View File
@@ -1,5 +1,8 @@
const en = { const en = {
text: 'Text', text: 'Text',
by_clicking_you_agree_to_the: 'By clicking "{button}", you agree to the',
electronic_signature_disclosure: 'Electronic Signature Disclosure',
esignature_disclosure: 'eSignature Disclosure',
signature: 'Signature', signature: 'Signature',
initials: 'Initials', initials: 'Initials',
date: 'Date', date: 'Date',
@@ -65,6 +68,9 @@ const en = {
} }
const es = { const es = {
by_clicking_you_agree_to_the: 'Al hacer clic en "{button}", usted acepta el',
electronic_signature_disclosure: 'Divulgación de Firma Electrónica',
esignature_disclosure: 'Divulgación de eFirma',
minimize: 'Minimizar', minimize: 'Minimizar',
text: 'Texto', text: 'Texto',
signature: 'Firma', signature: 'Firma',
@@ -130,6 +136,9 @@ const es = {
} }
const it = { const it = {
by_clicking_you_agree_to_the: 'Cliccando su "{button}", accetti il',
electronic_signature_disclosure: 'Divulgazione della Firma Elettronica',
esignature_disclosure: 'Divulgazione della eFirma',
minimize: 'Minimizza', minimize: 'Minimizza',
text: 'Testo', text: 'Testo',
signature: 'Firma', signature: 'Firma',
@@ -195,6 +204,9 @@ const it = {
} }
const de = { const de = {
by_clicking_you_agree_to_the: 'Durch Klicken auf "{button}" stimmen Sie zu, dass Sie die',
electronic_signature_disclosure: 'Elektronische Unterschriftenoffenlegung',
esignature_disclosure: 'eSignatur Offenlegung',
minimize: 'Minimieren', minimize: 'Minimieren',
text: 'Text', text: 'Text',
signature: 'Unterschrift', signature: 'Unterschrift',
@@ -260,6 +272,9 @@ const de = {
} }
const fr = { const fr = {
by_clicking_you_agree_to_the: 'En cliquant sur "{button}", vous acceptez la',
electronic_signature_disclosure: 'Divulgation de Signature Électronique',
esignature_disclosure: 'Divulgation de la eSignature',
minimize: 'Réduire', minimize: 'Réduire',
text: 'Texte', text: 'Texte',
signature: 'Signature', signature: 'Signature',
@@ -325,6 +340,9 @@ const fr = {
} }
const pl = { const pl = {
by_clicking_you_agree_to_the: 'Klikając na "{button}", zgadzasz się na',
electronic_signature_disclosure: 'Ujawnienie Elektronicznej Sygnatury',
esignature_disclosure: 'Ujawnienie ePodpisu',
minimize: 'Zminimalizuj', minimize: 'Zminimalizuj',
text: 'Tekst', text: 'Tekst',
signature: 'Podpis', signature: 'Podpis',
@@ -390,6 +408,9 @@ const pl = {
} }
const uk = { const uk = {
by_clicking_you_agree_to_the: 'Натиснувши на "{button}", ви погоджуєтеся з',
electronic_signature_disclosure: 'Розголошення Електронного Підпису',
esignature_disclosure: 'Розголошення еПідпису',
minimize: 'Зменшити', minimize: 'Зменшити',
text: 'Текст', text: 'Текст',
signature: 'Підпис', signature: 'Підпис',
@@ -455,6 +476,9 @@ const uk = {
} }
const cs = { const cs = {
by_clicking_you_agree_to_the: 'Kliknutím na "{button}" souhlasíte s',
electronic_signature_disclosure: 'Zveřejněním Elektronického Podpisu',
esignature_disclosure: 'Zveřejnění ePodpisu',
minimize: 'Minimalizovat', minimize: 'Minimalizovat',
text: 'Text', text: 'Text',
signature: 'Podpis', signature: 'Podpis',
@@ -520,6 +544,9 @@ const cs = {
} }
const pt = { const pt = {
by_clicking_you_agree_to_the: 'Ao clicar em "{button}", você concorda com o',
electronic_signature_disclosure: 'Divulgação de Assinatura Eletrônica',
esignature_disclosure: 'Divulgação da eAssinatura',
minimize: 'Minimizar', minimize: 'Minimizar',
text: 'Texto', text: 'Texto',
signature: 'Assinatura', signature: 'Assinatura',
@@ -585,6 +612,9 @@ const pt = {
} }
const he = { const he = {
by_clicking_you_agree_to_the: 'על ידי לחיצה על "{button}", אתה מסכים ל',
electronic_signature_disclosure: 'חשיפת חתימה אלקטרונית',
esignature_disclosure: 'חשיפת ה-eחתימה',
minimize: 'לקטן', minimize: 'לקטן',
text: 'טקסט', text: 'טקסט',
signature: 'חתימה', signature: 'חתימה',
@@ -651,6 +681,9 @@ const he = {
} }
const nl = { const nl = {
by_clicking_you_agree_to_the: 'Door op "{button}" te klikken, gaat u akkoord met de',
electronic_signature_disclosure: 'Openbaarmaking van Elektronische Handtekening',
esignature_disclosure: 'Openbaarmaking van eHandtekening',
minimize: 'Minimaliseren', minimize: 'Minimaliseren',
text: 'Tekst', text: 'Tekst',
signature: 'Handtekening', signature: 'Handtekening',
@@ -717,6 +750,9 @@ const nl = {
} }
const ar = { const ar = {
by_clicking_you_agree_to_the: 'بالنقر فوق "{button}"، أنت توافق على',
electronic_signature_disclosure: 'كشف التوقيع الإلكتروني',
esignature_disclosure: 'كشف التوقيع الإلكتروني',
text: 'نص', text: 'نص',
signature: 'توقيع', signature: 'توقيع',
initials: 'الاختصارات', initials: 'الاختصارات',
@@ -783,6 +819,9 @@ const ar = {
} }
const ko = { const ko = {
by_clicking_you_agree_to_the: '"{button}"를 클릭함으로써, 다음에 동의하게 됩니다',
electronic_signature_disclosure: '전자 서명 공개',
esignature_disclosure: '전자 서명 공개',
text: '텍스트', text: '텍스트',
signature: '서명', signature: '서명',
initials: '이니셜', initials: '이니셜',
@@ -189,7 +189,7 @@ export default {
this.$nextTick(() => { this.$nextTick(() => {
if (this.$refs.canvas) { if (this.$refs.canvas) {
this.$refs.canvas.width = this.$refs.canvas.parentNode.clientWidth * scale this.$refs.canvas.width = this.$refs.canvas.parentNode.clientWidth * scale
this.$refs.canvas.height = (this.$refs.canvas.parentNode.clientWidth / 3) * scale this.$refs.canvas.height = (this.$refs.canvas.parentNode.clientWidth / 4.5) * scale
this.$refs.canvas.getContext('2d').scale(scale, scale) this.$refs.canvas.getContext('2d').scale(scale, scale)
} }
@@ -208,7 +208,7 @@ export default {
entries.forEach(entry => { entries.forEach(entry => {
if (entry.isIntersecting) { if (entry.isIntersecting) {
this.$refs.canvas.width = this.$refs.canvas.parentNode.clientWidth * scale this.$refs.canvas.width = this.$refs.canvas.parentNode.clientWidth * scale
this.$refs.canvas.height = (this.$refs.canvas.parentNode.clientWidth / 3) * scale this.$refs.canvas.height = (this.$refs.canvas.parentNode.clientWidth / 4.5) * scale
this.$refs.canvas.getContext('2d').scale(scale, scale) this.$refs.canvas.getContext('2d').scale(scale, scale)
@@ -117,6 +117,11 @@ export default {
type: String, type: String,
required: true required: true
}, },
locale: {
type: String,
required: false,
default: 'en'
},
showFieldNames: { showFieldNames: {
type: Boolean, type: Boolean,
required: false, required: false,
@@ -163,6 +168,7 @@ export default {
method: 'POST', method: 'POST',
body: JSON.stringify({ body: JSON.stringify({
submitter_slug: this.submitterSlug, submitter_slug: this.submitterSlug,
locale: this.locale,
phone: this.$refs.phone.value phone: this.$refs.phone.value
}), }),
headers: { 'Content-Type': 'application/json' } headers: { 'Content-Type': 'application/json' }
@@ -1,5 +1,8 @@
<template> <template>
<div dir="auto"> <div
dir="auto"
class="relative"
>
<div <div
class="flex justify-between items-center w-full" class="flex justify-between items-center w-full"
:class="{ 'mb-2': !field.description }" :class="{ 'mb-2': !field.description }"
@@ -136,6 +139,23 @@
type="text" type="text"
@input="updateWrittenSignature" @input="updateWrittenSignature"
> >
<div
v-if="withDisclosure"
dir="auto"
class="text-base-content/60 text-xs text-center w-full mt-1"
>
{{ t('by_clicking_you_agree_to_the').replace('{button}', buttonText.charAt(0).toUpperCase() + buttonText.slice(1)) }} <a
href="https://www.docuseal.co/esign-disclosure"
target="_blank"
>
<span class="inline md:hidden">
{{ t('esignature_disclosure') }}
</span>
<span class="hidden md:inline">
{{ t('electronic_signature_disclosure') }}
</span>
</a>
</div>
</div> </div>
</template> </template>
@@ -176,6 +196,16 @@ export default {
required: false, required: false,
default: true default: true
}, },
withDisclosure: {
type: Boolean,
required: false,
default: false
},
buttonText: {
type: String,
required: false,
default: 'Submit'
},
withTypedSignature: { withTypedSignature: {
type: Boolean, type: Boolean,
required: false, required: false,
+120 -3
View File
@@ -70,7 +70,7 @@
@blur="onNameBlur" @blur="onNameBlur"
>{{ optionIndexText }} {{ (defaultField ? (field.title || field.name) : field.name) || defaultName }}</span> >{{ optionIndexText }} {{ (defaultField ? (field.title || field.name) : field.name) || defaultName }}</span>
<div <div
v-if="isNameFocus && !['checkbox', 'phone'].includes(field.type)" v-if="isSettingsFocus || (isNameFocus && !['checkbox', 'phone'].includes(field.type))"
class="flex items-center ml-1.5" class="flex items-center ml-1.5"
> >
<input <input
@@ -86,6 +86,47 @@
@click.prevent="field.required = !field.required" @click.prevent="field.required = !field.required"
@mousedown.prevent @mousedown.prevent
>{{ t('required') }}</label> >{{ t('required') }}</label>
<span
v-if="field.type !== 'payment'"
class="dropdown dropdown-end"
@mouseenter="renderDropdown = true"
@touchstart="renderDropdown = true"
>
<label
ref="settingsButton"
tabindex="0"
:title="t('settings')"
class="cursor-pointer flex items-center"
style="height: 25px"
@focus="isSettingsFocus = true"
@blur="maybeBlurSettings"
>
<IconDotsVertical class="w-5 h-5" />
</label>
<ul
v-if="renderDropdown"
ref="settingsDropdown"
tabindex="0"
class="dropdown-content menu menu-xs px-2 pb-2 pt-1 shadow rounded-box w-52 z-10 rounded-t-none"
:style="{ backgroundColor: 'white' }"
@dragstart.prevent.stop
@click="closeDropdown"
@focusout="maybeBlurSettings"
>
<FieldSettings
:field="field"
:default-field="defaultField"
:editable="editable"
:background-color="'white'"
:with-required="false"
:with-areas="false"
@click-formula="isShowFormulaModal = true"
@click-description="isShowDescriptionModal = true"
@click-condition="isShowConditionsModal = true"
@scroll-to="[selectedAreaRef.value = $event, $emit('scroll-to', $event)]"
/>
</ul>
</span>
</div> </div>
<button <button
v-else-if="editable" v-else-if="editable"
@@ -120,6 +161,10 @@
class="aspect-square mx-auto" class="aspect-square mx-auto"
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }" :class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
/> />
<span
v-else-if="field.type === 'number'"
class="whitespace-pre-wrap"
>{{ formatNumber(field.default_value, field.preferences?.format) }}</span>
<span <span
v-else v-else
class="whitespace-pre-wrap" class="whitespace-pre-wrap"
@@ -145,6 +190,38 @@
@mousedown.stop="startResize" @mousedown.stop="startResize"
@touchstart="startTouchResize" @touchstart="startTouchResize"
/> />
<Teleport
v-if="isShowFormulaModal"
:to="modalContainerEl"
>
<FormulaModal
:field="field"
:editable="editable && !defaultField"
:build-default-name="buildDefaultName"
@close="isShowFormulaModal = false"
/>
</Teleport>
<Teleport
v-if="isShowConditionsModal"
:to="modalContainerEl"
>
<ConditionsModal
:field="field"
:build-default-name="buildDefaultName"
@close="isShowConditionsModal = false"
/>
</Teleport>
<Teleport
v-if="isShowDescriptionModal"
:to="modalContainerEl"
>
<DescriptionModal
:field="field"
:editable="editable && !defaultField"
:build-default-name="buildDefaultName"
@close="isShowDescriptionModal = false"
/>
</Teleport>
</div> </div>
</template> </template>
@@ -152,7 +229,11 @@
import FieldSubmitter from './field_submitter' import FieldSubmitter from './field_submitter'
import FieldType from './field_type' import FieldType from './field_type'
import Field from './field' import Field from './field'
import { IconX, IconCheck } from '@tabler/icons-vue' import FieldSettings from './field_settings'
import FormulaModal from './formula_modal'
import ConditionsModal from './conditions_modal'
import DescriptionModal from './description_modal'
import { IconX, IconCheck, IconDotsVertical } from '@tabler/icons-vue'
import { v4 } from 'uuid' import { v4 } from 'uuid'
export default { export default {
@@ -160,6 +241,11 @@ export default {
components: { components: {
FieldType, FieldType,
IconCheck, IconCheck,
FieldSettings,
FormulaModal,
IconDotsVertical,
DescriptionModal,
ConditionsModal,
FieldSubmitter, FieldSubmitter,
IconX IconX
}, },
@@ -195,11 +281,16 @@ export default {
default: null default: null
} }
}, },
emits: ['start-resize', 'stop-resize', 'start-drag', 'stop-drag', 'remove'], emits: ['start-resize', 'stop-resize', 'start-drag', 'stop-drag', 'remove', 'scroll-to'],
data () { data () {
return { return {
isShowFormulaModal: false,
isShowConditionsModal: false,
isSettingsFocus: false,
isShowDescriptionModal: false,
isResize: false, isResize: false,
isDragged: false, isDragged: false,
renderDropdown: false,
isNameFocus: false, isNameFocus: false,
textOverflowChars: 0, textOverflowChars: 0,
dragFrom: { x: 0, y: 0 } dragFrom: { x: 0, y: 0 }
@@ -208,6 +299,9 @@ export default {
computed: { computed: {
fieldNames: FieldType.computed.fieldNames, fieldNames: FieldType.computed.fieldNames,
fieldIcons: FieldType.computed.fieldIcons, fieldIcons: FieldType.computed.fieldIcons,
modalContainerEl () {
return this.$el.getRootNode().querySelector('#docuseal_modal_container')
},
defaultName () { defaultName () {
return this.buildDefaultName(this.field, this.template.fields) return this.buildDefaultName(this.field, this.template.fields)
}, },
@@ -324,6 +418,25 @@ export default {
}, },
methods: { methods: {
buildDefaultName: Field.methods.buildDefaultName, buildDefaultName: Field.methods.buildDefaultName,
closeDropdown () {
document.activeElement.blur()
},
formatNumber (number, format) {
if (format === 'comma') {
return new Intl.NumberFormat('en-US').format(number)
} else if (format === 'dot') {
return new Intl.NumberFormat('de-DE').format(number)
} else if (format === 'space') {
return new Intl.NumberFormat('fr-FR').format(number)
} else {
return number
}
},
maybeBlurSettings (e) {
if (!e.relatedTarget || !this.$refs.settingsDropdown.contains(e.relatedTarget)) {
this.isSettingsFocus = false
}
},
onNameFocus (e) { onNameFocus (e) {
this.selectedAreaRef.value = this.area this.selectedAreaRef.value = this.area
@@ -379,6 +492,10 @@ export default {
}) })
}, },
onNameBlur (e) { onNameBlur (e) {
if (e.relatedTarget === this.$refs.settingsButton) {
this.isSettingsFocus = true
}
const text = this.$refs.name.innerText.trim() const text = this.$refs.name.innerText.trim()
this.isNameFocus = false this.isNameFocus = false
+15 -3
View File
@@ -857,7 +857,7 @@ export default {
type = this.fieldTypes[0] type = this.fieldTypes[0]
} }
if (type === 'checkbox' && !this.drawFieldType) { if (type === 'checkbox' && !this.drawFieldType && (this.template.fields[this.template.fields.length - 1]?.type === 'checkbox' || area.w)) {
const previousField = [...this.template.fields].reverse().find((f) => f.type === type) const previousField = [...this.template.fields].reverse().find((f) => f.type === type)
const previousArea = previousField?.areas?.[previousField.areas.length - 1] const previousArea = previousField?.areas?.[previousField.areas.length - 1]
@@ -1116,7 +1116,13 @@ export default {
if (!this.isAllRequiredFieldsAdded) { if (!this.isAllRequiredFieldsAdded) {
e.preventDefault() e.preventDefault()
return alert(this.t('add_all_required_fields_to_continue')) const fields = this.defaultRequiredFields?.filter((f) => {
return !this.template.fields?.some((field) => field.name === f.name)
})
if (fields?.length) {
return alert(this.t('add_all_required_fields_to_continue') + ': ' + fields.map((f) => f.name).join(', '))
}
} }
if (!this.template.fields.length) { if (!this.template.fields.length) {
@@ -1127,7 +1133,13 @@ export default {
}, },
onSaveClick () { onSaveClick () {
if (!this.isAllRequiredFieldsAdded) { if (!this.isAllRequiredFieldsAdded) {
return alert(this.t('add_all_required_fields_to_continue')) const fields = this.defaultRequiredFields?.filter((f) => {
return !this.template.fields?.some((field) => field.name === f.name)
})
if (fields?.length) {
return alert(this.t('add_all_required_fields_to_continue') + ': ' + fields.map((f) => f.name).join(', '))
}
} }
if (this.template.fields.length) { if (this.template.fields.length) {
@@ -14,10 +14,7 @@
@focus="$emit('focus', $event)" @focus="$emit('focus', $event)"
@blur="onBlur" @blur="onBlur"
> >
<template {{ value }}
v-for="(item, index) in (value || '').split(/(_)/)"
:key="index"
>{{ item }}<wbr></template>
</span> </span>
<span <span
v-if="withRequired" v-if="withRequired"
+4 -1
View File
@@ -17,6 +17,7 @@
:image="image" :image="image"
@drop-field="$emit('drop-field', {...$event, attachment_uuid: document.uuid })" @drop-field="$emit('drop-field', {...$event, attachment_uuid: document.uuid })"
@remove-area="$emit('remove-area', $event)" @remove-area="$emit('remove-area', $event)"
@scroll-to="scrollToArea"
@draw="$emit('draw', {...$event, attachment_uuid: document.uuid })" @draw="$emit('draw', {...$event, attachment_uuid: document.uuid })"
/> />
</div> </div>
@@ -125,7 +126,9 @@ export default {
}, },
methods: { methods: {
scrollToArea (area) { scrollToArea (area) {
this.pageRefs[area.page].areaRefs.find((e) => e.area === area).$el.scrollIntoView({ behavior: 'smooth', block: 'center' }) this.$nextTick(() => {
this.pageRefs[area.page].areaRefs.find((e) => e.area === area).$el.scrollIntoView({ behavior: 'smooth', block: 'center' })
})
}, },
setPageRefs (el) { setPageRefs (el) {
if (el) { if (el) {
+21 -355
View File
@@ -16,6 +16,8 @@
v-model="field.type" v-model="field.type"
:editable="editable && !defaultField" :editable="editable && !defaultField"
:button-width="20" :button-width="20"
:menu-classes="'mt-1.5'"
:menu-style="{ backgroundColor: dropdownBgColor }"
@update:model-value="[maybeUpdateOptions(), save()]" @update:model-value="[maybeUpdateOptions(), save()]"
@click="scrollToFirstArea" @click="scrollToFirstArea"
/> />
@@ -110,285 +112,23 @@
<ul <ul
v-if="renderDropdown" v-if="renderDropdown"
tabindex="0" tabindex="0"
class="mt-1.5 dropdown-content menu menu-xs p-2 shadow bg-base-100 rounded-box w-52 z-10" class="mt-1.5 dropdown-content menu menu-xs p-2 shadow rounded-box w-52 z-10"
:style="{ backgroundColor: dropdownBgColor }"
draggable="true" draggable="true"
@dragstart.prevent.stop @dragstart.prevent.stop
@click="closeDropdown" @click="closeDropdown"
> >
<div <FieldSettings
v-if="['number'].includes(field.type)" :field="field"
class="py-1.5 px-1 relative" :default-field="defaultField"
@click.stop :editable="editable"
> :background-color="dropdownBgColor"
<select @click-formula="isShowFormulaModal = true"
class="select select-bordered select-xs w-full max-w-xs h-7 !outline-0 font-normal" @click-description="isShowDescriptionModal = true"
@change="[field.preferences ||= {}, field.preferences.align = $event.target.value, save()]" @click-condition="isShowConditionsModal = true"
> @set-draw="$emit('set-draw', $event)"
<option @scroll-to="$emit('scroll-to', $event)"
v-for="value in ['left', 'right', 'center']" />
:key="value"
:selected="field.preferences?.align ? value === field.preferences.align : value === 'left'"
:value="value"
>
{{ t(value) }}
</option>
</select>
<label
:style="{ backgroundColor: backgroundColor }"
class="absolute -top-1 left-2.5 px-1 h-4"
style="font-size: 8px"
>
{{ t('align') }}
</label>
</div>
<div
v-if="['text', 'number'].includes(field.type) && !defaultField"
class="py-1.5 px-1 relative"
@click.stop
>
<input
v-model="field.default_value"
:placeholder="t('default_value')"
dir="auto"
:type="field.type"
class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0"
@blur="save"
>
<label
v-if="field.default_value"
:style="{ backgroundColor: backgroundColor }"
class="absolute -top-1 left-2.5 px-1 h-4"
style="font-size: 8px"
>
{{ t('default_value') }}
</label>
</div>
<div
v-if="field.type === 'date'"
class="py-1.5 px-1 relative"
@click.stop
>
<select
v-model="field.preferences.format"
:placeholder="t('format')"
class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0"
@change="save"
>
<option
v-for="format in dateFormats"
:key="format"
:value="format"
>
{{ formatDate(new Date(), format) }}
</option>
</select>
<label
:style="{ backgroundColor: backgroundColor }"
class="absolute -top-1 left-2.5 px-1 h-4"
style="font-size: 8px"
>
{{ t('format') }}
</label>
</div>
<div
v-if="field.type === 'signature'"
class="py-1.5 px-1 relative"
@click.stop
>
<select
:placeholder="t('format')"
class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0"
@change="[field.preferences.format = $event.target.value, save()]"
>
<option
value="any"
:selected="!field.preferences?.format || field.preferences.format === 'any'"
>
{{ t('any') }}
</option>
<option
value="drawn"
:selected="field.preferences?.format === 'drawn'"
>
{{ t('drawn') }}
</option>
<option
value="typed"
:selected="field.preferences?.format === 'typed'"
>
{{ t('typed') }}
</option>
</select>
<label
:style="{ backgroundColor: backgroundColor }"
class="absolute -top-1 left-2.5 px-1 h-4"
style="font-size: 8px"
>
{{ t('format') }}
</label>
</div>
<li
v-if="field.type != 'phone' && field.type != 'stamp'"
@click.stop
>
<label class="cursor-pointer py-1.5">
<input
v-model="field.required"
type="checkbox"
:disabled="!editable || defaultField"
class="toggle toggle-xs"
@update:model-value="save"
>
<span class="label-text">{{ t('required') }}</span>
</label>
</li>
<li
v-if="field.type == 'stamp'"
@click.stop
>
<label class="cursor-pointer py-1.5">
<input
:checked="field.preferences?.with_logo != false"
type="checkbox"
class="toggle toggle-xs"
@change="[field.preferences ||= {}, field.preferences.with_logo = field.preferences.with_logo == false, save()]"
>
<span class="label-text">{{ t('with_logo') }}</span>
</label>
</li>
<li
v-if="field.type == 'checkbox'"
@click.stop
>
<label class="cursor-pointer py-1.5">
<input
v-model="field.default_value"
type="checkbox"
class="toggle toggle-xs"
@update:model-value="[field.default_value = $event, field.readonly = $event, save()]"
>
<span class="label-text">{{ t('checked') }}</span>
</label>
</li>
<li
v-if="field.type == 'date'"
@click.stop
>
<label class="cursor-pointer py-1.5">
<input
v-model="field.readonly"
type="checkbox"
class="toggle toggle-xs"
@update:model-value="[field.default_value = $event ? '{{date}}' : null, field.readonly = $event, save()]"
>
<span class="label-text">{{ t('set_signing_date') }}</span>
</label>
</li>
<li
v-if="['text', 'number'].includes(field.type) && !defaultField"
@click.stop
>
<label class="cursor-pointer py-1.5">
<input
v-model="field.readonly"
type="checkbox"
class="toggle toggle-xs"
@update:model-value="save"
>
<span class="label-text">{{ t('read_only') }}</span>
</label>
</li>
<hr
v-if="field.type != 'stamp'"
class="pb-0.5 mt-0.5"
>
<li
v-if="field.type != 'stamp'"
>
<label
class="label-text cursor-pointer text-center w-full flex items-center"
@click="isShowDescriptionModal = !isShowDescriptionModal"
>
<IconInfoCircle
width="18"
/>
<span class="text-sm">
{{ t('description') }}
</span>
</label>
</li>
<li
v-if="field.type != 'stamp'"
>
<label
class="label-text cursor-pointer text-center w-full flex items-center"
@click="isShowConditionsModal = !isShowConditionsModal"
>
<IconRouteAltLeft
width="18"
/>
<span class="text-sm">
{{ t('condition') }}
</span>
</label>
</li>
<li v-if="field.type == 'number'">
<label
class="label-text cursor-pointer text-center w-full flex items-center"
@click="isShowFormulaModal = true"
>
<IconMathFunction
width="18"
/>
<span class="text-sm">
{{ t('formula') }}
</span>
</label>
</li>
<hr class="pb-0.5 mt-0.5">
<li
v-for="(area, index) in sortedAreas"
:key="index"
>
<a
href="#"
class="text-sm py-1 px-2"
@click.prevent="$emit('scroll-to', area)"
>
<IconShape
:width="20"
:stroke-width="1.6"
/>
{{ t('page') }}
<template v-if="template.schema.length > 1">{{ template.schema.findIndex((item) => item.attachment_uuid === area.attachment_uuid) + 1 }}-</template>{{ area.page + 1 }}
</a>
</li>
<li v-if="!field.areas?.length || !['radio', 'multiple'].includes(field.type)">
<a
href="#"
class="text-sm py-1 px-2"
@click.prevent="$emit('set-draw', { field })"
>
<IconNewSection
:width="20"
:stroke-width="1.6"
/>
{{ t('draw_new_area') }}
</a>
</li>
<li v-if="field.areas?.length === 1 && ['date', 'signature', 'initials', 'text', 'cells'].includes(field.type)">
<a
href="#"
class="text-sm py-1 px-2"
@click.prevent="copyToAllPages(field)"
>
<IconCopy
:width="20"
:stroke-width="1.6"
/>
{{ t('copy_to_all_pages') }}
</a>
</li>
</ul> </ul>
</span> </span>
<button <button
@@ -515,10 +255,11 @@
import Contenteditable from './contenteditable' import Contenteditable from './contenteditable'
import FieldType from './field_type' import FieldType from './field_type'
import PaymentSettings from './payment_settings' import PaymentSettings from './payment_settings'
import FieldSettings from './field_settings'
import FormulaModal from './formula_modal' import FormulaModal from './formula_modal'
import ConditionsModal from './conditions_modal' import ConditionsModal from './conditions_modal'
import DescriptionModal from './description_modal' import DescriptionModal from './description_modal'
import { IconInfoCircle, IconRouteAltLeft, IconMathFunction, IconShape, IconNewSection, IconTrashX, IconCopy, IconSettings } from '@tabler/icons-vue' import { IconRouteAltLeft, IconMathFunction, IconNewSection, IconTrashX, IconSettings } from '@tabler/icons-vue'
import { v4 } from 'uuid' import { v4 } from 'uuid'
export default { export default {
@@ -526,17 +267,15 @@ export default {
components: { components: {
Contenteditable, Contenteditable,
IconSettings, IconSettings,
IconShape, FieldSettings,
PaymentSettings, PaymentSettings,
IconNewSection, IconNewSection,
IconInfoCircle,
FormulaModal, FormulaModal,
DescriptionModal, DescriptionModal,
ConditionsModal, ConditionsModal,
IconRouteAltLeft, IconRouteAltLeft,
IconTrashX, IconTrashX,
IconMathFunction, IconMathFunction,
IconCopy,
FieldType FieldType
}, },
inject: ['template', 'save', 'backgroundColor', 'selectedAreaRef', 't'], inject: ['template', 'save', 'backgroundColor', 'selectedAreaRef', 't'],
@@ -569,6 +308,9 @@ export default {
}, },
computed: { computed: {
fieldNames: FieldType.computed.fieldNames, fieldNames: FieldType.computed.fieldNames,
dropdownBgColor () {
return ['', null, 'transparent'].includes(this.backgroundColor) ? 'white' : this.backgroundColor
},
schemaAttachmentsIndexes () { schemaAttachmentsIndexes () {
return (this.template.schema || []).reduce((acc, item, index) => { return (this.template.schema || []).reduce((acc, item, index) => {
acc[item.attachment_uuid] = index acc[item.attachment_uuid] = index
@@ -584,29 +326,6 @@ export default {
modalContainerEl () { modalContainerEl () {
return this.$el.getRootNode().querySelector('#docuseal_modal_container') return this.$el.getRootNode().querySelector('#docuseal_modal_container')
}, },
dateFormats () {
const formats = [
'MM/DD/YYYY',
'DD/MM/YYYY',
'YYYY-MM-DD',
'DD-MM-YYYY',
'DD.MM.YYYY',
'MMM D, YYYY',
'MMMM D, YYYY',
'D MMM YYYY',
'D MMMM YYYY'
]
if (Intl.DateTimeFormat().resolvedOptions().timeZone?.includes('Seoul') || navigator.language?.startsWith('ko')) {
formats.push('YYYY년 MM월 DD일')
}
if (this.field.preferences?.format && !formats.includes(this.field.preferences.format)) {
formats.unshift(this.field.preferences.format)
}
return formats
},
defaultName () { defaultName () {
return this.buildDefaultName(this.field, this.template.fields) return this.buildDefaultName(this.field, this.template.fields)
}, },
@@ -641,54 +360,6 @@ export default {
return `${this.fieldNames[field.type]} ${suffix} ${typeIndex + 1}` return `${this.fieldNames[field.type]} ${suffix} ${typeIndex + 1}`
} }
}, },
formatDate (date, format) {
const monthFormats = {
M: 'numeric',
MM: '2-digit',
MMM: 'short',
MMMM: 'long'
}
const dayFormats = {
D: 'numeric',
DD: '2-digit'
}
const yearFormats = {
YYYY: 'numeric',
YY: '2-digit'
}
const parts = new Intl.DateTimeFormat([], {
day: dayFormats[format.match(/D+/)],
month: monthFormats[format.match(/M+/)],
year: yearFormats[format.match(/Y+/)]
}).formatToParts(date)
return format
.replace(/D+/, parts.find((p) => p.type === 'day').value)
.replace(/M+/, parts.find((p) => p.type === 'month').value)
.replace(/Y+/, parts.find((p) => p.type === 'year').value)
},
copyToAllPages (field) {
const areaString = JSON.stringify(field.areas[0])
this.template.documents.forEach((attachment) => {
const numberOfPages = attachment.metadata?.pdf?.number_of_pages || attachment.preview_images.length
for (let page = 0; page <= numberOfPages - 1; page++) {
if (!field.areas.find((area) => area.attachment_uuid === attachment.uuid && area.page === page)) {
field.areas.push({ ...JSON.parse(areaString), attachment_uuid: attachment.uuid, page })
}
}
})
this.$nextTick(() => {
this.$emit('scroll-to', this.field.areas[this.field.areas.length - 1])
})
this.save()
},
onNameFocus (e) { onNameFocus (e) {
this.isNameFocus = true this.isNameFocus = true
@@ -764,11 +435,6 @@ export default {
this.isNameFocus = false this.isNameFocus = false
this.save()
},
removeArea (area) {
this.field.areas.splice(this.field.areas.indexOf(area), 1)
this.save() this.save()
} }
} }
@@ -0,0 +1,462 @@
<template>
<div
v-if="field.type === 'number'"
class="py-1.5 px-1 relative"
@click.stop
>
<select
:placeholder="t('format')"
class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0 bg-transparent"
@change="[field.preferences ||= {}, field.preferences.format = $event.target.value, save()]"
>
<option
v-for="format in numberFormats"
:key="format"
:value="format"
:selected="format === field.preferences?.format || (format === 'none' && !field.preferences?.format)"
>
{{ formatNumber(123456789.567, format) }}
</option>
</select>
<label
:style="{ backgroundColor }"
class="absolute -top-1 left-2.5 px-1 h-4"
style="font-size: 8px"
>
{{ t('format') }}
</label>
</div>
<div
v-if="['number'].includes(field.type)"
class="py-1.5 px-1 relative"
@click.stop
>
<select
class="select select-bordered select-xs w-full max-w-xs h-7 !outline-0 font-normal bg-transparent"
@change="[field.preferences ||= {}, field.preferences.align = $event.target.value, save()]"
>
<option
v-for="value in ['left', 'right', 'center']"
:key="value"
:selected="field.preferences?.align ? value === field.preferences.align : value === 'left'"
:value="value"
>
{{ t(value) }}
</option>
</select>
<label
:style="{ backgroundColor }"
class="absolute -top-1 left-2.5 px-1 h-4"
style="font-size: 8px"
>
{{ t('align') }}
</label>
</div>
<div
v-if="['text', 'number'].includes(field.type) && !defaultField"
class="py-1.5 px-1 relative"
@click.stop
>
<input
v-model="field.default_value"
:placeholder="t('default_value')"
dir="auto"
:type="field.type"
class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0 bg-transparent"
@blur="save"
>
<label
v-if="field.default_value"
:style="{ backgroundColor }"
class="absolute -top-1 left-2.5 px-1 h-4"
style="font-size: 8px"
>
{{ t('default_value') }}
</label>
</div>
<div
v-if="field.type === 'date'"
class="py-1.5 px-1 relative"
@click.stop
>
<select
v-model="field.preferences.format"
:placeholder="t('format')"
class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0 bg-transparent"
@change="save"
>
<option
v-for="format in dateFormats"
:key="format"
:value="format"
>
{{ formatDate(new Date(), format) }}
</option>
</select>
<label
:style="{ backgroundColor }"
class="absolute -top-1 left-2.5 px-1 h-4"
style="font-size: 8px"
>
{{ t('format') }}
</label>
</div>
<div
v-if="field.type === 'signature'"
class="py-1.5 px-1 relative"
@click.stop
>
<select
:placeholder="t('format')"
class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0 bg-transparent"
@change="[field.preferences.format = $event.target.value, save()]"
>
<option
value="any"
:selected="!field.preferences?.format || field.preferences.format === 'any'"
>
{{ t('any') }}
</option>
<option
value="drawn"
:selected="field.preferences?.format === 'drawn'"
>
{{ t('drawn') }}
</option>
<option
value="typed"
:selected="field.preferences?.format === 'typed'"
>
{{ t('typed') }}
</option>
</select>
<label
:style="{ backgroundColor }"
class="absolute -top-1 left-2.5 px-1 h-4"
style="font-size: 8px"
>
{{ t('format') }}
</label>
</div>
<li
v-if="withRequired && field.type != 'phone' && field.type != 'stamp'"
@click.stop
>
<label class="cursor-pointer py-1.5">
<input
v-model="field.required"
type="checkbox"
:disabled="!editable || defaultField"
class="toggle toggle-xs"
@update:model-value="save"
>
<span class="label-text">{{ t('required') }}</span>
</label>
</li>
<li
v-if="field.type == 'stamp'"
@click.stop
>
<label class="cursor-pointer py-1.5">
<input
:checked="field.preferences?.with_logo != false"
type="checkbox"
class="toggle toggle-xs"
@change="[field.preferences ||= {}, field.preferences.with_logo = field.preferences.with_logo == false, save()]"
>
<span class="label-text">{{ t('with_logo') }}</span>
</label>
</li>
<li
v-if="field.type == 'checkbox'"
@click.stop
>
<label class="cursor-pointer py-1.5">
<input
v-model="field.default_value"
type="checkbox"
class="toggle toggle-xs"
@update:model-value="[field.default_value = $event, field.readonly = $event, save()]"
>
<span class="label-text">{{ t('checked') }}</span>
</label>
</li>
<li
v-if="field.type == 'date'"
@click.stop
>
<label class="cursor-pointer py-1.5">
<input
v-model="field.readonly"
type="checkbox"
class="toggle toggle-xs"
@update:model-value="[field.default_value = $event ? '{{date}}' : null, field.readonly = $event, save()]"
>
<span class="label-text">{{ t('set_signing_date') }}</span>
</label>
</li>
<li
v-if="['text', 'number'].includes(field.type) && !defaultField"
@click.stop
>
<label class="cursor-pointer py-1.5">
<input
v-model="field.readonly"
type="checkbox"
class="toggle toggle-xs"
@update:model-value="save"
>
<span class="label-text">{{ t('read_only') }}</span>
</label>
</li>
<hr
v-if="field.type != 'stamp'"
class="pb-0.5 mt-0.5"
>
<li
v-if="field.type != 'stamp'"
>
<label
class="label-text cursor-pointer text-center w-full flex items-center"
@click="$emit('click-description')"
>
<IconInfoCircle
width="18"
/>
<span class="text-sm">
{{ t('description') }}
</span>
</label>
</li>
<li
v-if="field.type != 'stamp'"
>
<label
class="label-text cursor-pointer text-center w-full flex items-center"
@click="$emit('click-condition')"
>
<IconRouteAltLeft
width="18"
/>
<span class="text-sm">
{{ t('condition') }}
</span>
</label>
</li>
<li v-if="field.type == 'number'">
<label
class="label-text cursor-pointer text-center w-full flex items-center"
@click="$emit('click-formula')"
>
<IconMathFunction
width="18"
/>
<span class="text-sm">
{{ t('formula') }}
</span>
</label>
</li>
<hr class="pb-0.5 mt-0.5">
<template v-if="withAreas">
<li
v-for="(area, index) in sortedAreas"
:key="index"
>
<a
href="#"
class="text-sm py-1 px-2"
@click.prevent="$emit('scroll-to', area)"
>
<IconShape
:width="20"
:stroke-width="1.6"
/>
{{ t('page') }}
<template v-if="template.schema.length > 1">{{ template.schema.findIndex((item) => item.attachment_uuid === area.attachment_uuid) + 1 }}-</template>{{ area.page + 1 }}
</a>
</li>
<li v-if="!field.areas?.length || !['radio', 'multiple'].includes(field.type)">
<a
href="#"
class="text-sm py-1 px-2"
@click.prevent="$emit('set-draw', { field })"
>
<IconNewSection
:width="20"
:stroke-width="1.6"
/>
{{ t('draw_new_area') }}
</a>
</li>
</template>
<li v-if="field.areas?.length === 1 && ['date', 'signature', 'initials', 'text', 'cells'].includes(field.type)">
<a
href="#"
class="text-sm py-1 px-2"
@click.prevent="copyToAllPages(field)"
>
<IconCopy
:width="20"
:stroke-width="1.6"
/>
{{ t('copy_to_all_pages') }}
</a>
</li>
</template>
<script>
import { IconRouteAltLeft, IconShape, IconMathFunction, IconNewSection, IconInfoCircle, IconCopy } from '@tabler/icons-vue'
export default {
name: 'FieldSettings',
components: {
IconShape,
IconInfoCircle,
IconMathFunction,
IconRouteAltLeft,
IconCopy,
IconNewSection
},
inject: ['template', 'save', 't'],
props: {
field: {
type: Object,
required: true
},
backgroundColor: {
type: String,
required: false,
default: null
},
defaultField: {
type: Object,
required: false,
default: null
},
withRequired: {
type: Boolean,
required: false,
default: true
},
withAreas: {
type: Boolean,
required: false,
default: true
},
editable: {
type: Boolean,
required: false,
default: true
}
},
emits: ['set-draw', 'scroll-to', 'click-formula', 'click-description', 'click-condition'],
data () {
return {
}
},
computed: {
schemaAttachmentsIndexes () {
return (this.template.schema || []).reduce((acc, item, index) => {
acc[item.attachment_uuid] = index
return acc
}, {})
},
numberFormats () {
return [
'none',
'comma',
'dot',
'space'
]
},
dateFormats () {
const formats = [
'MM/DD/YYYY',
'DD/MM/YYYY',
'YYYY-MM-DD',
'DD-MM-YYYY',
'DD.MM.YYYY',
'MMM D, YYYY',
'MMMM D, YYYY',
'D MMM YYYY',
'D MMMM YYYY'
]
if (Intl.DateTimeFormat().resolvedOptions().timeZone?.includes('Seoul') || navigator.language?.startsWith('ko')) {
formats.push('YYYY년 MM월 DD일')
}
if (this.field.preferences?.format && !formats.includes(this.field.preferences.format)) {
formats.unshift(this.field.preferences.format)
}
return formats
},
sortedAreas () {
return (this.field.areas || []).sort((a, b) => {
return this.schemaAttachmentsIndexes[a.attachment_uuid] - this.schemaAttachmentsIndexes[b.attachment_uuid]
})
}
},
methods: {
copyToAllPages (field) {
const areaString = JSON.stringify(field.areas[0])
this.template.documents.forEach((attachment) => {
const numberOfPages = attachment.metadata?.pdf?.number_of_pages || attachment.preview_images.length
for (let page = 0; page <= numberOfPages - 1; page++) {
if (!field.areas.find((area) => area.attachment_uuid === attachment.uuid && area.page === page)) {
field.areas.push({ ...JSON.parse(areaString), attachment_uuid: attachment.uuid, page })
}
}
})
this.$emit('scroll-to', this.field.areas[this.field.areas.length - 1])
this.save()
},
formatNumber (number, format) {
if (format === 'comma') {
return new Intl.NumberFormat('en-US').format(number)
} else if (format === 'dot') {
return new Intl.NumberFormat('de-DE').format(number)
} else if (format === 'space') {
return new Intl.NumberFormat('fr-FR').format(number)
} else {
return number
}
},
formatDate (date, format) {
const monthFormats = {
M: 'numeric',
MM: '2-digit',
MMM: 'short',
MMMM: 'long'
}
const dayFormats = {
D: 'numeric',
DD: '2-digit'
}
const yearFormats = {
YYYY: 'numeric',
YY: '2-digit'
}
const parts = new Intl.DateTimeFormat([], {
day: dayFormats[format.match(/D+/)],
month: monthFormats[format.match(/M+/)],
year: yearFormats[format.match(/Y+/)]
}).formatToParts(date)
return format
.replace(/D+/, parts.find((p) => p.type === 'day').value)
.replace(/M+/, parts.find((p) => p.type === 'month').value)
.replace(/Y+/, parts.find((p) => p.type === 'year').value)
}
}
}
</script>
@@ -37,6 +37,7 @@
tabindex="0" tabindex="0"
class="rounded-md min-w-max mb-2" class="rounded-md min-w-max mb-2"
:class="menuClasses" :class="menuClasses"
:style="menuStyle"
@click="closeDropdown" @click="closeDropdown"
> >
<li <li
@@ -136,6 +137,7 @@
v-if="(editable || !compact) && renderDropdown" v-if="(editable || !compact) && renderDropdown"
tabindex="0" tabindex="0"
:class="menuClasses" :class="menuClasses"
:style="menuStyle"
@click="closeDropdown" @click="closeDropdown"
> >
<li <li
@@ -253,10 +255,15 @@ export default {
type: String, type: String,
required: true required: true
}, },
menuStyle: {
type: Object,
required: false,
default: () => ({})
},
menuClasses: { menuClasses: {
type: String, type: String,
required: false, required: false,
default: 'dropdown-content menu p-2 shadow bg-base-100 rounded-box w-full z-10' default: 'dropdown-content menu p-2 shadow rounded-box w-full z-10'
} }
}, },
emits: ['update:model-value', 'remove', 'new-submitter', 'name-change'], emits: ['update:model-value', 'remove', 'new-submitter', 'name-change'],
@@ -22,6 +22,7 @@
v-if="editable && renderDropdown" v-if="editable && renderDropdown"
tabindex="0" tabindex="0"
class="dropdown-content menu menu-xs p-2 shadow rounded-box w-52 z-10 mb-3" class="dropdown-content menu menu-xs p-2 shadow rounded-box w-52 z-10 mb-3"
:style="menuStyle"
:class="menuClasses" :class="menuClasses"
@click="closeDropdown" @click="closeDropdown"
> >
@@ -65,6 +66,11 @@ export default {
required: false, required: false,
default: 'mt-1.5 bg-base-100' default: 'mt-1.5 bg-base-100'
}, },
menuStyle: {
type: Object,
required: false,
default: () => ({})
},
buttonClasses: { buttonClasses: {
type: String, type: String,
required: false, required: false,
+6 -5
View File
@@ -3,8 +3,9 @@
<FieldSubmitter <FieldSubmitter
:model-value="selectedSubmitter.uuid" :model-value="selectedSubmitter.uuid"
class="roles-dropdown w-full rounded-lg" class="roles-dropdown w-full rounded-lg"
:class="{ 'bg-base-100': withStickySubmitters }" :style="withStickySubmitters ? { backgroundColor } : {}"
:submitters="submitters" :submitters="submitters"
:menu-style="{ backgroundColor: ['', null, 'transparent'].includes(backgroundColor) ? 'white' : backgroundColor }"
:editable="editable && !defaultSubmitters.length" :editable="editable && !defaultSubmitters.length"
@new-submitter="save" @new-submitter="save"
@remove="removeSubmitter" @remove="removeSubmitter"
@@ -34,7 +35,7 @@
@set-draw="$emit('set-draw', $event)" @set-draw="$emit('set-draw', $event)"
/> />
</div> </div>
<div v-if="submitterDefaultFields.length"> <div v-if="submitterDefaultFields.length && editable">
<hr class="mb-2"> <hr class="mb-2">
<template v-if="isShowFieldSearch"> <template v-if="isShowFieldSearch">
<input <input
@@ -71,7 +72,7 @@
:key="field.name" :key="field.name"
> >
<div <div
:style="{ backgroundColor: backgroundColor }" :style="{ backgroundColor }"
draggable="true" draggable="true"
class="default-field border border-base-300 rounded rounded-tr-none relative group mb-2" class="default-field border border-base-300 rounded rounded-tr-none relative group mb-2"
@dragstart="onDragstart({ type: 'text', ...field })" @dragstart="onDragstart({ type: 'text', ...field })"
@@ -113,7 +114,7 @@
v-if="(fieldTypes.length === 0 || fieldTypes.includes(type)) && (withPhone || type != 'phone') && (withPayment || type != 'payment')" v-if="(fieldTypes.length === 0 || fieldTypes.includes(type)) && (withPhone || type != 'phone') && (withPayment || type != 'payment')"
draggable="true" draggable="true"
class="field-type-button group flex items-center justify-center border border-dashed w-full rounded relative" class="field-type-button group flex items-center justify-center border border-dashed w-full rounded relative"
:style="{ backgroundColor: backgroundColor }" :style="{ backgroundColor }"
:class="drawFieldType === type ? 'border-base-content/40' : 'border-base-300 hover:border-base-content/20'" :class="drawFieldType === type ? 'border-base-content/40' : 'border-base-300 hover:border-base-content/20'"
@dragstart="onDragstart({ type: type })" @dragstart="onDragstart({ type: type })"
@dragend="$emit('drag-end')" @dragend="$emit('drag-end')"
@@ -142,7 +143,7 @@
href="https://www.docuseal.co/pricing" href="https://www.docuseal.co/pricing"
target="_blank" target="_blank"
class="opacity-50 flex items-center justify-center border border-dashed border-base-300 w-full rounded relative" class="opacity-50 flex items-center justify-center border border-dashed border-base-300 w-full rounded relative"
:style="{ backgroundColor: backgroundColor }" :style="{ backgroundColor }"
> >
<div class="w-0 absolute left-0"> <div class="w-0 absolute left-0">
<IconLock <IconLock
@@ -24,6 +24,7 @@
:submitters="submitters" :submitters="submitters"
:editable="editable" :editable="editable"
:mobile-view="true" :mobile-view="true"
:menu-style="{ backgroundColor: ['', null, 'transparent'].includes(backgroundColor) ? 'white' : backgroundColor }"
@new-submitter="save" @new-submitter="save"
@remove="removeSubmitter" @remove="removeSubmitter"
@name-change="save" @name-change="save"
@@ -43,7 +44,7 @@ export default {
Field, Field,
FieldSubmitter FieldSubmitter
}, },
inject: ['save', 't'], inject: ['save', 't', 'backgroundColor'],
props: { props: {
drawField: { drawField: {
type: Object, type: Object,
+2 -1
View File
@@ -30,6 +30,7 @@
@start-drag="isMove = true" @start-drag="isMove = true"
@stop-drag="isMove = false" @stop-drag="isMove = false"
@remove="$emit('remove-area', item.area)" @remove="$emit('remove-area', item.area)"
@scroll-to="$emit('scroll-to', $event)"
/> />
<FieldArea <FieldArea
v-if="newArea" v-if="newArea"
@@ -116,7 +117,7 @@ export default {
required: true required: true
} }
}, },
emits: ['draw', 'drop-field', 'remove-area'], emits: ['draw', 'drop-field', 'remove-area', 'scroll-to'],
data () { data () {
return { return {
areaRefs: [], areaRefs: [],
@@ -29,9 +29,9 @@ class SendSubmissionArchivedWebhookRequestJob < ApplicationJob
end end
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS && if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
(!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan)) (!Docuseal.multitenant? || submission.account.account_configs.exists?(key: :plan))
SendSubmissionArchivedWebhookRequestJob.set(wait: (2**attempt).minutes) SendSubmissionArchivedWebhookRequestJob.set(wait: (2**attempt).minutes)
.perform_later(submitter, { .perform_later(submission, {
attempt: attempt + 1, attempt: attempt + 1,
last_status: resp&.status.to_i last_status: resp&.status.to_i
}) })
@@ -0,0 +1,40 @@
# frozen_string_literal: true
class SendSubmissionCreatedWebhookRequestJob < ApplicationJob
USER_AGENT = 'DocuSeal.co Webhook'
MAX_ATTEMPTS = 10
def perform(submission, params = {})
attempt = params[:attempt].to_i
url = Accounts.load_webhook_url(submission.account)
return if url.blank?
preferences = Accounts.load_webhook_preferences(submission.account)
return if preferences['submission.created'].blank?
resp = begin
Faraday.post(url,
{
event_type: 'submission.created',
timestamp: Time.current,
data: Submissions::SerializeForApi.call(submission)
}.to_json,
'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT)
rescue Faraday::Error
nil
end
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
(!Docuseal.multitenant? || submission.account.account_configs.exists?(key: :plan))
SendSubmissionCreatedWebhookRequestJob.set(wait: (2**attempt).minutes)
.perform_later(submission, {
attempt: attempt + 1,
last_status: resp&.status.to_i
})
end
end
end
@@ -0,0 +1,40 @@
# frozen_string_literal: true
class SendTemplateCreatedWebhookRequestJob < ApplicationJob
USER_AGENT = 'DocuSeal.co Webhook'
MAX_ATTEMPTS = 10
def perform(template, params = {})
attempt = params[:attempt].to_i
url = Accounts.load_webhook_url(template.account)
return if url.blank?
preferences = Accounts.load_webhook_preferences(template.account)
return if preferences['template.created'].blank?
resp = begin
Faraday.post(url,
{
event_type: 'template.created',
timestamp: Time.current,
data: Templates::SerializeForApi.call(template)
}.to_json,
'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT)
rescue Faraday::Error
nil
end
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
(!Docuseal.multitenant? || template.account.account_configs.exists?(key: :plan))
SendTemplateCreatedWebhookRequestJob.set(wait: (2**attempt).minutes)
.perform_later(template, {
attempt: attempt + 1,
last_status: resp&.status.to_i
})
end
end
end
@@ -0,0 +1,40 @@
# frozen_string_literal: true
class SendTemplateUpdatedWebhookRequestJob < ApplicationJob
USER_AGENT = 'DocuSeal.co Webhook'
MAX_ATTEMPTS = 10
def perform(template, params = {})
attempt = params[:attempt].to_i
url = Accounts.load_webhook_url(template.account)
return if url.blank?
preferences = Accounts.load_webhook_preferences(template.account)
return if preferences['template.updated'].blank?
resp = begin
Faraday.post(url,
{
event_type: 'template.updated',
timestamp: Time.current,
data: Templates::SerializeForApi.call(template)
}.to_json,
'Content-Type' => 'application/json',
'User-Agent' => USER_AGENT)
rescue Faraday::Error
nil
end
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
(!Docuseal.multitenant? || template.account.account_configs.exists?(key: :plan))
SendTemplateUpdatedWebhookRequestJob.set(wait: (2**attempt).minutes)
.perform_later(template, {
attempt: attempt + 1,
last_status: resp&.status.to_i
})
end
end
end
+8 -2
View File
@@ -76,9 +76,15 @@ class SubmitterMailer < ApplicationMailer
submitter, with_audit_log: @email_config.nil? || @email_config.value['attach_audit_log'] != false submitter, with_audit_log: @email_config.nil? || @email_config.value['attach_audit_log'] != false
) )
@subject = @submitter.template.preferences['documents_copy_email_subject'].presence
@subject ||= @email_config.value['subject'] if @email_config
@body = @submitter.template.preferences['documents_copy_email_body'].presence
@body ||= @email_config.value['body'] if @email_config
subject = subject =
if @email_config if @subject.present?
ReplaceEmailVariables.call(@email_config.value['subject'], submitter:) ReplaceEmailVariables.call(@subject, submitter:)
else else
'Your document copy' 'Your document copy'
end end
+10 -7
View File
@@ -4,13 +4,14 @@
# #
# Table name: accounts # Table name: accounts
# #
# id :bigint not null, primary key # id :bigint not null, primary key
# locale :string not null # archived_at :datetime
# name :string not null # locale :string not null
# timezone :string not null # name :string not null
# uuid :string not null # timezone :string not null
# created_at :datetime not null # uuid :string not null
# updated_at :datetime not null # created_at :datetime not null
# updated_at :datetime not null
# #
# Indexes # Indexes
# #
@@ -49,6 +50,8 @@ class Account < ApplicationRecord
attribute :timezone, :string, default: 'UTC' attribute :timezone, :string, default: 'UTC'
attribute :locale, :string, default: 'en-US' attribute :locale, :string, default: 'en-US'
scope :active, -> { where(archived_at: nil) }
def testing? def testing?
linked_account_account&.testing? linked_account_account&.testing?
end end
+1
View File
@@ -36,6 +36,7 @@ class AccountConfig < ApplicationRecord
WEBHOOK_PREFERENCES_KEY = 'webhook_preferences' WEBHOOK_PREFERENCES_KEY = 'webhook_preferences'
DOWNLOAD_LINKS_AUTH_KEY = 'download_links_auth' DOWNLOAD_LINKS_AUTH_KEY = 'download_links_auth'
FORCE_SSO_AUTH_KEY = 'force_sso_auth' FORCE_SSO_AUTH_KEY = 'force_sso_auth'
FLATTEN_RESULT_PDF_KEY = 'flatten_result_pdf'
DEFAULT_VALUES = { DEFAULT_VALUES = {
SUBMITTER_INVITATION_EMAIL_KEY => { SUBMITTER_INVITATION_EMAIL_KEY => {
+1 -1
View File
@@ -73,7 +73,7 @@ class User < ApplicationRecord
end end
def active_for_authentication? def active_for_authentication?
super && !archived_at? super && !archived_at? && !account.archived_at?
end end
def remember_me def remember_me
+2 -2
View File
@@ -3,7 +3,7 @@
<div class="flex-grow max-w-xl mx-auto"> <div class="flex-grow max-w-xl mx-auto">
<h1 class="text-4xl font-bold mb-4">Account</h1> <h1 class="text-4xl font-bold mb-4">Account</h1>
<%= form_for '', url: settings_account_path, method: :patch, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %> <%= form_for '', url: settings_account_path, method: :patch, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %>
<%= f.fields_for current_account do |ff| %> <%= fields_for current_account do |ff| %>
<div class="form-control"> <div class="form-control">
<%= ff.label :name, 'Company Name', class: 'label' %> <%= ff.label :name, 'Company Name', class: 'label' %>
<%= ff.text_field :name, required: true, class: 'base-input', dir: 'auto' %> <%= ff.text_field :name, required: true, class: 'base-input', dir: 'auto' %>
@@ -23,7 +23,7 @@
<% end %> <% end %>
<% encrypted_config = @encrypted_config || EncryptedConfig.find_or_initialize_by(account: current_account, key: EncryptedConfig::APP_URL_KEY) %> <% encrypted_config = @encrypted_config || EncryptedConfig.find_or_initialize_by(account: current_account, key: EncryptedConfig::APP_URL_KEY) %>
<% if !Docuseal.multitenant? && can?(:manage, encrypted_config) && !current_account.testing? %> <% if !Docuseal.multitenant? && can?(:manage, encrypted_config) && !current_account.testing? %>
<%= f.fields_for encrypted_config do |ff| %> <%= fields_for encrypted_config do |ff| %>
<div class="form-control"> <div class="form-control">
<%= ff.label :value, 'App URL', class: 'label' %> <%= ff.label :value, 'App URL', class: 'label' %>
<%= ff.text_field :value, autocomplete: 'off', class: 'base-input' %> <%= ff.text_field :value, autocomplete: 'off', class: 'base-input' %>
@@ -1,10 +1,12 @@
<div class="text-center"> <div class="text-center">
<div class="join"> <div class="join">
<a href="https://docuseal.co<%= request.fullpath.gsub('docuseal.eu', 'docuseal.co') %>" class="btn bg-base-200 join-item w-32 <%= 'bg-base-300' if request.host == 'docuseal.co' %>"> <a href="https://docuseal.co<%= request.fullpath.gsub('docuseal.eu', 'docuseal.co') %>" class="btn bg-base-200 join-item w-32 <%= 'bg-base-300' if request.host == 'docuseal.co' %>">
🌎 Global <%= svg_icon 'world', class: 'w-5 h-5' %>
Global
</a> </a>
<a href="https://docuseal.eu<%= request.fullpath.gsub('docuseal.co', 'docuseal.eu') %>" class="btn bg-base-200 join-item w-32 <%= 'bg-base-300' if request.host == 'docuseal.eu' %>"> <a href="https://docuseal.eu<%= request.fullpath.gsub('docuseal.co', 'docuseal.eu') %>" class="btn bg-base-200 join-item w-32 <%= 'bg-base-300' if request.host == 'docuseal.eu' %>">
🇪🇺 Europe <%= svg_icon 'eu_flag', class: 'w-5 h-5' %>
Europe
</a> </a>
</div> </div>
</div> </div>
+13 -1
View File
@@ -123,7 +123,7 @@
<% end %> <% end %>
</div> </div>
<% end %> <% end %>
<% account_config = AccountConfig.where(account: current_account, key: AccountConfig::ESIGNING_PREFERENCE_KEY).first_or_initialize(value: 'multiple') %> <% account_config = AccountConfig.where(account: current_account, key: AccountConfig::ESIGNING_PREFERENCE_KEY).first_or_initialize(value: 'single') %>
<% if can?(:manage, account_config) %> <% if can?(:manage, account_config) %>
<div class="px-1 mt-8 max-w-xl"> <div class="px-1 mt-8 max-w-xl">
<div class="flex justify-between items-end mb-4 mt-8"> <div class="flex justify-between items-end mb-4 mt-8">
@@ -140,6 +140,18 @@
</div> </div>
<% end %> <% end %>
<% end %> <% end %>
<% account_config = AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::FLATTEN_RESULT_PDF_KEY) %>
<% if can?(:manage, account_config) %>
<%= form_for account_config, url: account_configs_path, method: :post do |f| %>
<%= f.hidden_field :key %>
<div class="flex items-center justify-between py-2.5">
<span>
Remove PDF form fillable fields from the signed PDF (flatten form)
</span>
<%= f.check_box :value, { class: 'toggle', checked: account_config.value != false, onchange: 'this.form.requestSubmit()' } %>
</div>
<% end %>
<% end %>
</div> </div>
<% end %> <% end %>
</div> </div>
+125
View File
@@ -0,0 +1,125 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" version="1.1" viewBox="0 0 1000 1000">
<defs />
<path fill="#039" d="M8,195.25 C178.83,110.03 349.03,140.83 521.26,167.28 C676.47,191.12 833.42,211.85 992,132.75 L992,804.75 C821.17,889.97 650.97,859.17 478.74,832.72 C323.53,808.88 166.58,788.15 8,867.25 L8,195.25" />
<path fill="#fc0" d="M500,238.67 L488.74,272.43 L506.07,280.88 L500,238.67" />
<path fill="#fc0" d="M500,238.67 L511.26,275.92 L493.93,279 L500,238.67" />
<path fill="#fc0" d="M534.66,269.79 L498.22,264.19 L498.22,282.85 L534.66,269.79" />
<path fill="#fc0" d="M534.66,269.79 L505.18,287.21 L494.47,270.45 L534.66,269.79" />
<path fill="#fc0" d="M521.42,309.51 L510.16,272.27 L492.83,275.36 L521.42,309.51" />
<path fill="#fc0" d="M521.42,309.51 L491.94,283.01 L502.65,269.57 L521.42,309.51" />
<path fill="#fc0" d="M465.34,259.13 L501.78,264.74 L501.78,283.41 L465.34,259.13" />
<path fill="#fc0" d="M465.34,259.13 L494.82,285.61 L505.53,272.16 L465.34,259.13" />
<path fill="#fc0" d="M478.58,302.9 L489.84,269.13 L507.17,277.58 L478.58,302.9" />
<path fill="#fc0" d="M478.58,302.9 L508.06,285.51 L497.35,268.75 L478.58,302.9" />
<path fill="#fc0" d="M500,686.67 L488.74,720.43 L506.07,728.88 L500,686.67" />
<path fill="#fc0" d="M500,686.67 L511.26,723.92 L493.93,727 L500,686.67" />
<path fill="#fc0" d="M534.66,717.79 L498.22,712.19 L498.22,730.85 L534.66,717.79" />
<path fill="#fc0" d="M534.66,717.79 L505.18,735.21 L494.47,718.45 L534.66,717.79" />
<path fill="#fc0" d="M521.42,757.51 L510.16,720.27 L492.83,723.36 L521.42,757.51" />
<path fill="#fc0" d="M521.42,757.51 L491.94,731.01 L502.65,717.57 L521.42,757.51" />
<path fill="#fc0" d="M465.34,707.13 L501.78,712.74 L501.78,731.41 L465.34,707.13" />
<path fill="#fc0" d="M465.34,707.13 L494.82,733.61 L505.53,720.16 L465.34,707.13" />
<path fill="#fc0" d="M478.58,750.9 L489.84,717.13 L507.17,725.58 L478.58,750.9" />
<path fill="#fc0" d="M478.58,750.9 L508.06,733.51 L497.35,716.75 L478.58,750.9" />
<path fill="#fc0" d="M281.33,438.24 L270.07,473.54 L287.4,479.69 L281.33,438.24" />
<path fill="#fc0" d="M281.33,438.24 L292.6,474.11 L275.27,479.39 L281.33,438.24" />
<path fill="#fc0" d="M315.99,465.59 C303.97,464.84 291.94,464.29 279.55,464 L279.55,482.66 L315.99,465.59" />
<path fill="#fc0" d="M315.99,465.59 L286.51,486.13 L275.8,470.76 C289.06,468.76 302.33,467.06 315.99,465.59" />
<path fill="#fc0" d="M302.76,506.59 L291.49,470.59 L274.16,475.89 L302.76,506.59" />
<path fill="#fc0" d="M302.76,506.59 L273.27,483.67 L283.98,468.81 L302.76,506.59" />
<path fill="#fc0" d="M246.67,463.91 C258.7,463.69 270.72,463.75 283.12,464.09 L283.12,482.75 C271.09,476.27 259.06,470.03 246.67,463.91" />
<path fill="#fc0" d="M246.67,463.91 L276.16,485.87 L286.87,471.04 C273.6,468.37 260.34,465.98 246.67,463.91" />
<path fill="#fc0" d="M259.91,505.52 L271.17,470.08 L288.5,476.25 L259.91,505.52" />
<path fill="#fc0" d="M259.91,505.52 L289.4,484.08 L278.69,468.67 L259.91,505.52" />
<path fill="#fc0" d="M369.24,317.99 L398.73,299.53 L388.02,283.1 L369.24,317.99" />
<path fill="#fc0" d="M369.24,317.99 L380.51,283.75 L397.84,291.62 L369.24,317.99" />
<path fill="#fc0" d="M356.01,274.87 L385.49,300.04 L396.2,286.25 L356.01,274.87" />
<path fill="#fc0" d="M356.01,274.87 L392.45,278.94 L392.45,297.61 L356.01,274.87" />
<path fill="#fc0" d="M390.67,252.93 L379.4,287.09 L396.73,294.96 L390.67,252.93" />
<path fill="#fc0" d="M390.67,252.93 L401.93,289.85 L384.6,293.47 L390.67,252.93" />
<path fill="#fc0" d="M412.09,323.22 L382.6,297.55 L393.31,283.75 L412.09,323.22" />
<path fill="#fc0" d="M412.09,323.22 L400.83,286.23 L383.5,289.87 L412.09,323.22" />
<path fill="#fc0" d="M425.33,283.29 L395.84,301.31 L385.13,284.9 L425.33,283.29" />
<path fill="#fc0" d="M425.33,283.29 L388.88,278.51 L388.88,297.17 L425.33,283.29" />
<path fill="#fc0" d="M332.05,396.47 L320.79,360.14 L303.46,364.88 L332.05,396.47" />
<path fill="#fc0" d="M332.05,396.47 L302.57,372.63 L313.28,358.12 L332.05,396.47" />
<path fill="#fc0" d="M289.21,394.02 L318.69,373.56 L307.98,357.82 L289.21,394.02" />
<path fill="#fc0" d="M289.21,394.02 L300.47,358.97 L317.8,365.71 L289.21,394.02" />
<path fill="#fc0" d="M275.97,351.92 L305.45,374.93 L316.16,360.45 C302.9,357.36 289.63,354.51 275.97,351.92" />
<path fill="#fc0" d="M275.97,351.92 C288,352.16 300.02,352.64 312.41,353.37 L312.41,372.04 L275.97,351.92" />
<path fill="#fc0" d="M345.29,355.85 L315.81,375.53 L305.1,359.81 C318.36,358.25 331.62,356.95 345.29,355.85" />
<path fill="#fc0" d="M345.29,355.85 L308.85,353.17 L308.85,371.83 L345.29,355.85" />
<path fill="#fc0" d="M310.63,327.47 L321.89,363.69 L304.56,368.42 L310.63,327.47" />
<path fill="#fc0" d="M310.63,327.47 L299.37,362.4 L316.7,369.12 L310.63,327.47" />
<path fill="#fc0" d="M345.29,579.85 L308.85,577.17 L308.85,595.83 L345.29,579.85" />
<path fill="#fc0" d="M345.29,579.85 L315.81,599.53 L305.1,583.81 C318.36,582.25 331.62,580.95 345.29,579.85" />
<path fill="#fc0" d="M332.05,620.47 L320.79,584.14 L303.46,588.88 L332.05,620.47" />
<path fill="#fc0" d="M332.05,620.47 L302.57,596.63 L313.28,582.12 L332.05,620.47" />
<path fill="#fc0" d="M289.21,618.02 L318.69,597.56 L307.98,581.82 L289.21,618.02" />
<path fill="#fc0" d="M289.21,618.02 L300.47,582.97 L317.8,589.71 L289.21,618.02" />
<path fill="#fc0" d="M310.63,551.47 L321.89,587.69 L304.56,592.42 L310.63,551.47" />
<path fill="#fc0" d="M310.63,551.47 L299.37,586.4 L316.7,593.12 L310.63,551.47" />
<path fill="#fc0" d="M275.97,575.92 C288,576.16 300.02,576.64 312.41,577.37 L312.41,596.04 L275.97,575.92" />
<path fill="#fc0" d="M275.97,575.92 L305.45,598.93 L316.16,584.45 C302.9,581.36 289.63,578.51 275.97,575.92" />
<path fill="#fc0" d="M425.33,671.27 L388.88,666.49 L388.88,685.15 L425.33,671.27" />
<path fill="#fc0" d="M425.33,671.27 L395.84,689.29 L385.13,672.88 L425.33,671.27" />
<path fill="#fc0" d="M412.09,711.2 L400.83,674.21 L383.5,677.84 L412.09,711.2" />
<path fill="#fc0" d="M412.09,711.2 L382.6,685.53 L393.31,671.73 L412.09,711.2" />
<path fill="#fc0" d="M369.24,705.97 L398.73,687.51 L388.02,671.08 L369.24,705.97" />
<path fill="#fc0" d="M369.24,705.97 L380.51,671.73 L397.84,679.6 L369.24,705.97" />
<path fill="#fc0" d="M390.67,640.91 L401.93,677.83 L384.6,681.45 L390.67,640.91" />
<path fill="#fc0" d="M390.67,640.91 L379.4,675.07 L396.73,682.94 L390.67,640.91" />
<path fill="#fc0" d="M356.01,662.85 L392.45,666.92 L392.45,685.59 L356.01,662.85" />
<path fill="#fc0" d="M356.01,662.85 L385.49,688.02 L396.2,674.23 L356.01,662.85" />
<path fill="#fc0" d="M718.67,487.09 L729.93,522.81 L712.6,528.19 L718.67,487.09" />
<path fill="#fc0" d="M718.67,487.09 L707.4,522.24 L724.73,528.5 L718.67,487.09" />
<path fill="#fc0" d="M684.01,511.33 C696.03,512.09 708.06,512.63 720.45,512.93 L720.45,531.6 L684.01,511.33" />
<path fill="#fc0" d="M684.01,511.33 L713.49,534.68 L724.2,519.85 C710.94,517.34 697.67,514.52 684.01,511.33" />
<path fill="#fc0" d="M697.24,553.82 L708.51,518.8 L725.84,525.04 L697.24,553.82" />
<path fill="#fc0" d="M697.24,553.82 L726.73,532.85 L716.02,517.51 L697.24,553.82" />
<path fill="#fc0" d="M753.33,513.02 C741.3,513.24 729.28,513.17 716.88,512.84 L716.88,531.51 C728.91,525.67 740.93,519.59 753.33,513.02" />
<path fill="#fc0" d="M753.33,513.02 L723.84,534.95 L713.13,519.57 C726.4,517.73 739.66,515.59 753.33,513.02" />
<path fill="#fc0" d="M740.09,554.89 L728.83,519.32 L711.5,524.68 L740.09,554.89" />
<path fill="#fc0" d="M740.09,554.89 L710.6,532.44 L721.31,517.64 L740.09,554.89" />
<path fill="#fc0" d="M630.76,354.44 L601.27,329.01 L611.98,315.24 L630.76,354.44" />
<path fill="#fc0" d="M630.76,354.44 L619.49,317.67 L602.16,321.33 L630.76,354.44" />
<path fill="#fc0" d="M643.99,314.07 L614.51,332.79 L603.8,316.38 L643.99,314.07" />
<path fill="#fc0" d="M643.99,314.07 L607.55,310 L607.55,328.67 L643.99,314.07" />
<path fill="#fc0" d="M609.33,284.42 L620.6,321.27 L603.27,324.95 L609.33,284.42" />
<path fill="#fc0" d="M609.33,284.42 L598.07,318.52 L615.4,326.43 L609.33,284.42" />
<path fill="#fc0" d="M587.91,349.21 L617.4,330.99 L606.69,314.59 L587.91,349.21" />
<path fill="#fc0" d="M587.91,349.21 L599.17,315.18 L616.5,323.09 L587.91,349.21" />
<path fill="#fc0" d="M574.67,305.66 L604.16,331.52 L614.87,317.73 L574.67,305.66" />
<path fill="#fc0" d="M574.67,305.66 L611.12,310.44 L611.12,329.11 L574.67,305.66" />
<path fill="#fc0" d="M667.95,439.93 L679.21,405.25 L696.54,412.05 L667.95,439.93" />
<path fill="#fc0" d="M667.95,439.93 L697.43,419.88 L686.72,404.19 L667.95,439.93" />
<path fill="#fc0" d="M710.79,442.39 L681.31,418.95 L692.02,404.5 L710.79,442.39" />
<path fill="#fc0" d="M710.79,442.39 L699.53,406.42 L682.2,411.22 L710.79,442.39" />
<path fill="#fc0" d="M724.03,401.01 L694.55,421.89 L683.84,406.16 C697.1,404.74 710.36,403.06 724.03,401.01" />
<path fill="#fc0" d="M724.03,401.01 C712,400.77 699.98,400.28 687.59,399.55 L687.59,418.22 L724.03,401.01" />
<path fill="#fc0" d="M654.71,397.08 L684.19,421.29 L694.9,406.8 C681.64,403.85 668.38,400.63 654.71,397.08" />
<path fill="#fc0" d="M654.71,397.08 L691.15,399.76 L691.15,418.42 L654.71,397.08" />
<path fill="#fc0" d="M689.37,373.86 L678.11,408.65 L695.44,415.47 L689.37,373.86" />
<path fill="#fc0" d="M689.37,373.86 L700.63,409.95 L683.3,414.76 L689.37,373.86" />
<path fill="#fc0" d="M654.71,621.08 L691.15,623.76 L691.15,642.42 L654.71,621.08" />
<path fill="#fc0" d="M654.71,621.08 L684.19,645.29 L694.9,630.8 C681.64,627.85 668.38,624.63 654.71,621.08" />
<path fill="#fc0" d="M667.95,663.93 L679.21,629.25 L696.54,636.05 L667.95,663.93" />
<path fill="#fc0" d="M667.95,663.93 L697.43,643.88 L686.72,628.19 L667.95,663.93" />
<path fill="#fc0" d="M710.79,666.39 L681.31,642.95 L692.02,628.5 L710.79,666.39" />
<path fill="#fc0" d="M710.79,666.39 L699.53,630.42 L682.2,635.22 L710.79,666.39" />
<path fill="#fc0" d="M689.37,597.86 L678.11,632.65 L695.44,639.47 L689.37,597.86" />
<path fill="#fc0" d="M689.37,597.86 L700.63,633.95 L683.3,638.76 L689.37,597.86" />
<path fill="#fc0" d="M724.03,625.01 C712,624.77 699.98,624.28 687.59,623.55 L687.59,642.22 L724.03,625.01" />
<path fill="#fc0" d="M724.03,625.01 L694.55,645.89 L683.84,630.16 C697.1,628.74 710.36,627.06 724.03,625.01" />
<path fill="#fc0" d="M574.67,693.64 L611.12,698.42 L611.12,717.09 L574.67,693.64" />
<path fill="#fc0" d="M574.67,693.64 L604.16,719.5 L614.87,705.71 L574.67,693.64" />
<path fill="#fc0" d="M587.91,737.19 L599.17,703.16 L616.5,711.07 L587.91,737.19" />
<path fill="#fc0" d="M587.91,737.19 L617.4,718.97 L606.69,702.57 L587.91,737.19" />
<path fill="#fc0" d="M630.76,742.42 L601.27,716.99 L611.98,703.22 L630.76,742.42" />
<path fill="#fc0" d="M630.76,742.42 L619.49,705.65 L602.16,709.31 L630.76,742.42" />
<path fill="#fc0" d="M609.33,672.41 L598.07,706.49 L615.4,714.41 L609.33,672.41" />
<path fill="#fc0" d="M609.33,672.41 L620.6,709.25 L603.27,712.93 L609.33,672.41" />
<path fill="#fc0" d="M643.99,702.05 L607.55,697.98 L607.55,716.65 L643.99,702.05" />
<path fill="#fc0" d="M643.99,702.05 L614.51,720.77 L603.8,704.36 L643.99,702.05" />
<path fill="#1A1A1A" opacity="0.2" d="M8,195.25 C178.83,110.03 349.03,140.83 521.26,167.28 C676.47,191.12 833.42,211.85 992,132.75 L992,804.75 C821.17,889.97 650.97,859.17 478.74,832.72 C323.53,808.88 166.58,788.15 8,867.25 L8,195.25 M39.25,214.64 L39.25,819.14 C345.81,690.88 650.43,915.18 960.75,785.36 L960.75,180.86 C654.19,309.12 349.57,84.82 39.25,214.64" />
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

+50
View File
@@ -0,0 +1,50 @@
<svg version="1.1" class="<%= local_assigns[:class] %>" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
y="0px" viewBox="0 0 128 128" style="enable-background:new 0 0 128 128;" xml:space="preserve">
<g>
<g>
<path style="fill:#FFCA28;" d="M17.57,62.68c-2.79-4.01-3.91-7.79-1.18-10.08c2.31-1.94,5.71-2.31,9.91,2.54
c0,0,12.55,14.58,16.87,18.63c0.98,0.91,2.2,1.05,3.31-0.1c0.94-0.98,0.6-1.83-0.2-3c0,0-17.69-26.3-20.01-29.51
c-3.87-5.37-2.38-8.84-0.59-10.49c2.49-2.31,6.87-2.77,10.94,2.81l21.42,28.67c0.65,0.69,1.7,0.79,2.47,0.25
c0.1-0.07,0.2-0.14,0.3-0.21c0.79-0.56,1.02-1.63,0.54-2.47c-3.75-6.53-18.67-32.55-20.86-37.29c-2.52-5.47-1.44-8.25,1.23-9.86
c3.17-1.91,6.15-1.77,9.71,3.52c3.86,5.76,18.85,30.01,22.66,36.53c0.5,0.85,1.57,1.17,2.45,0.74c0.01-0.01,0.03-0.01,0.04-0.02
c0.84-0.41,1.6-1.24,1.25-2.42c-2.08-6.86-12.29-28.22-14.43-33.13c-2.93-6.71-1.5-8.99,1.53-10.53c3.18-1.61,6.49-0.34,8.74,4.14
c1.52,3.04,28.21,51.61,28.21,51.61c-0.39-7.24,1.44-12.37,3-17.52c2.84-9.37,9.71-14.24,14.69-12.05
c2.59,1.14,3.06,3.41,2.78,5.15c-0.56,3.38-2.94,13.85-3.4,22.05c-1.08,19.22,4.73,41.37-16.92,55.29
c-14.49,9.32-30.02,7.68-40.28,0.51C49.31,107.75,19.85,65.96,17.57,62.68z" />
<path style="fill:#EDA600;" d="M117.68,51.77c-1.81,7.61-2.05,16.95-1.99,20.93c0.24,15.51,0.16,28.93-15.39,41.19
c-1.91,1.51-7.9,5.19-14.87,7.11c-2.24,0.61-1.39,1.33-0.01,1.17c7.61-0.88,13.38-4.16,16.59-6.23
c21.64-13.92,16.32-35.92,17.4-55.14c0.46-8.2,2.96-21.1,2.92-22.21C122.28,37.48,119.49,44.16,117.68,51.77z" />
<path style="fill:#EDA600;" d="M77.53,52.57c0,0-1.55,0.01-3.02-2.04C70.1,44.4,56.27,21.25,52.29,15.57
c-4.54-6.48-8.77-4-9.73-3.48c0,0,3.48,0.12,5.4,2.97c3.92,5.81,18.78,31.85,23.47,37.44C74.46,56.11,77.53,52.57,77.53,52.57z" />
<path style="fill:#EDA600;" d="M22.71,54.54c1.36,1.46,13.3,15.63,17.7,19.58c3.78,3.39,6.6-0.93,6.6-0.93s-1.39,0.19-3.29-1.24
c-4.72-3.57-15.9-16.3-18.42-18.98c-3.74-3.98-7.22-1.49-7.96-1.04C17.35,51.93,19.49,51.1,22.71,54.54z" />
<path style="fill:#EDA600;" d="M61.59,61.26c0,0-1.28,0.93-3.63-1.82c-1.71-2.01-21.34-25.96-21.34-25.96
c-4.92-6.01-8.79-4.22-9.59-3.76c0,0,2.47-0.13,5.36,3.2c1.32,1.52,22.27,28.23,23.11,29.14C58.57,65.41,61.41,62.53,61.59,61.26z
" />
<path style="fill:#EDA600;" d="M101.8,57.83c0,0-26.01-43.61-27.67-46.58c-3.69-6.59-7.98-4.57-8.71-4.24c0,0,2.39-0.68,5.2,4.1
c1.5,2.54,20.61,36.9,26.71,47.88c0.48,2.05,0.44,4.72-1.1,6.44c-5.52,6.19-12.56,12.51-10.49,28.18
c0.52,3.97,1.99,7.73,3.08,9.54c1.38,2.27,2.97,1.41,2.27-0.29c-0.47-1.15-1.22-3.86-1.37-5.02c-0.7-5.4-3.06-14.84,7.52-26.16
C99.03,69.79,103.58,64.08,101.8,57.83z" />
</g>
<g>
<path style="fill:#B0BEC5;" d="M103.49,30.96c-1.39-4.93-3.55-9.45-6.35-13.37c-2.52-3.53-5.57-6.57-9.05-9.01
c-0.44-0.31-1-1.27-0.52-2.2s1.52-0.81,1.91-0.65c4.35,1.79,7.91,4.88,10.85,9.12c3.49,5.04,5.22,9.79,6.11,15.1
c0.1,0.58,0.08,1.8-1.16,2.12C104.05,32.39,103.63,31.44,103.49,30.96z" />
</g>
<g>
<path style="fill:#B0BEC5;" d="M96.69,36.25c-1.39-4.93-3.55-9.45-6.35-13.37c-2.52-3.53-5.57-6.57-9.05-9.01
c-0.44-0.31-1-1.27-0.52-2.2c0.48-0.94,1.52-0.81,1.91-0.65c4.35,1.79,7.91,4.88,10.85,9.12c3.49,5.04,5.22,9.79,6.11,15.1
c0.1,0.58,0.08,1.8-1.16,2.12C97.26,37.68,96.83,36.74,96.69,36.25z" />
</g>
<g>
<path style="fill:#B0BEC5;" d="M8.56,77.46c1.72,4.83,4.17,9.19,7.22,12.92c2.75,3.35,5.99,6.19,9.62,8.4
c0.46,0.28,1.08,1.2,0.66,2.16c-0.42,0.97-1.46,0.91-1.87,0.78c-4.46-1.5-8.22-4.35-11.43-8.39c-3.81-4.8-5.86-9.42-7.1-14.66
c-0.14-0.58-0.2-1.79,1.02-2.19C7.9,76.08,8.39,76.99,8.56,77.46z" />
</g>
<g>
<path style="fill:#B0BEC5;" d="M14.99,71.73c1.72,4.83,4.17,9.19,7.22,12.92c2.75,3.35,5.99,6.19,9.62,8.4
c0.46,0.28,1.08,1.2,0.66,2.16c-0.42,0.97-1.46,0.91-1.87,0.78c-4.46-1.5-8.22-4.35-11.43-8.39c-3.81-4.8-5.86-9.42-7.1-14.66
c-0.14-0.58-0.2-1.79,1.02-2.19C14.33,70.34,14.82,71.26,14.99,71.73z" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

+8
View File
@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0" />
<path d="M3.6 9h16.8" />
<path d="M3.6 15h16.8" />
<path d="M11.5 3a17 17 0 0 0 0 18" />
<path d="M12.5 3a17 17 0 0 1 0 18" />
</svg>

After

Width:  |  Height:  |  Size: 470 B

+4 -1
View File
@@ -1,5 +1,8 @@
<div class="max-w-xl mx-auto px-2"> <div class="max-w-xl mx-auto px-2">
<h1 class="text-4xl font-bold text-center my-8">👋 Welcome to <%= Docuseal.product_name %></h1> <h1 class="flex text-4xl font-bold items-center justify-center my-8 space-x-2">
<%= svg_icon('waving_hand', class: 'h-10 w-10') %>
<span>Welcome to <%= Docuseal.product_name %></span>
</h1>
<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put, class: 'space-y-6' }) do |f| %> <%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put, class: 'space-y-6' }) do |f| %>
<div class="space-y-2"> <div class="space-y-2">
<%= render 'devise/shared/error_messages', resource: %> <%= render 'devise/shared/error_messages', resource: %>
@@ -2,23 +2,24 @@
<%= f.hidden_field :key %> <%= f.hidden_field :key %>
<div class="form-control"> <div class="form-control">
<% record = Struct.new(:first_duration, :second_duration, :third_duration).new(*(f.object.value || {}).values_at('first_duration', 'second_duration', 'third_duration')) %> <% record = Struct.new(:first_duration, :second_duration, :third_duration).new(*(f.object.value || {}).values_at('first_duration', 'second_duration', 'third_duration')) %>
<% durations = Docuseal.multitenant? ? AccountConfigs::REMINDER_DURATIONS.except('one_hour', 'two_hours').invert : AccountConfigs::REMINDER_DURATIONS.invert %>
<div class="flex flex-col md:flex-row gap-4"> <div class="flex flex-col md:flex-row gap-4">
<div class="w-full"> <div class="w-full">
<%= f.fields_for :value, record do |ff| %> <%= f.fields_for :value, record do |ff| %>
<%= ff.label :first_duration, 'First reminder in', class: 'label' %> <%= ff.label :first_duration, 'First reminder in', class: 'label' %>
<%= ff.select :first_duration, AccountConfigs::REMINDER_DURATIONS.invert, { include_blank: 'None' }, class: 'base-select' %> <%= ff.select :first_duration, durations, { include_blank: 'None' }, class: 'base-select' %>
<% end %> <% end %>
</div> </div>
<div class="w-full"> <div class="w-full">
<%= f.fields_for :value, record do |ff| %> <%= f.fields_for :value, record do |ff| %>
<%= ff.label :second_duration, 'Second reminder in', class: 'label' %> <%= ff.label :second_duration, 'Second reminder in', class: 'label' %>
<%= ff.select :second_duration, AccountConfigs::REMINDER_DURATIONS.invert, { include_blank: 'None' }, class: 'base-select' %> <%= ff.select :second_duration, durations, { include_blank: 'None' }, class: 'base-select' %>
<% end %> <% end %>
</div> </div>
<div class="w-full"> <div class="w-full">
<%= f.fields_for :value, record do |ff| %> <%= f.fields_for :value, record do |ff| %>
<%= ff.label :third_duration, 'Third reminder in', class: 'label' %> <%= ff.label :third_duration, 'Third reminder in', class: 'label' %>
<%= ff.select :third_duration, AccountConfigs::REMINDER_DURATIONS.invert, { include_blank: 'None' }, class: 'base-select' %> <%= ff.select :third_duration, durations, { include_blank: 'None' }, class: 'base-select' %>
<% end %> <% end %>
</div> </div>
</div> </div>
+11 -8
View File
@@ -1,8 +1,11 @@
<div class="max-w-xl mx-auto px-2"> <div class="max-w-xl mx-auto px-2">
<h1 class="text-4xl font-bold text-center my-8">Initial Setup 👋</h1> <h1 class="flex text-4xl font-bold items-center justify-center my-8 space-x-2">
<span>Initial Setup</span>
<%= svg_icon('waving_hand', class: 'h-10 w-10') %>
</h1>
<%= form_for '', html: { class: 'space-y-6' }, url: setup_index_path do |f| %> <%= form_for '', html: { class: 'space-y-6' }, url: setup_index_path do |f| %>
<div class="space-y-2"> <div class="space-y-2">
<%= f.fields_for @user do |ff| %> <%= fields_for @user do |ff| %>
<div class="grid gap-2 md:grid-cols-2 md:gap-4"> <div class="grid gap-2 md:grid-cols-2 md:gap-4">
<div class="form-control"> <div class="form-control">
<%= ff.label :first_name, class: 'label' %> <%= ff.label :first_name, class: 'label' %>
@@ -14,30 +17,30 @@
</div> </div>
</div> </div>
<% end %> <% end %>
<%= f.fields_for @user do |ff| %> <%= fields_for @user do |ff| %>
<div class="form-control"> <div class="form-control">
<%= ff.label :email, class: 'label' %> <%= ff.label :email, class: 'label' %>
<%= ff.email_field :email, required: true, class: 'base-input' %> <%= ff.email_field :email, required: true, class: 'base-input' %>
</div> </div>
<% end %> <% end %>
<%= f.fields_for @account do |ff| %> <%= fields_for @account do |ff| %>
<set-timezone data-input-id="_account_timezone"></set-timezone> <set-timezone data-input-id="account_timezone"></set-timezone>
<%= ff.hidden_field :timezone %> <%= ff.hidden_field :timezone %>
<div class="form-control"> <div class="form-control">
<%= ff.label :name, 'Company name', class: 'label' %> <%= ff.label :name, 'Company name', class: 'label' %>
<%= ff.text_field :name, required: true, class: 'base-input' %> <%= ff.text_field :name, required: true, class: 'base-input' %>
</div> </div>
<% end %> <% end %>
<%= f.fields_for @user do |ff| %> <%= fields_for @user do |ff| %>
<div class="form-control"> <div class="form-control">
<%= ff.label :password, class: 'label' %> <%= ff.label :password, class: 'label' %>
<%= ff.password_field :password, required: true, placeholder: '************', class: 'base-input' %> <%= ff.password_field :password, required: true, placeholder: '************', class: 'base-input' %>
</div> </div>
<% end %> <% end %>
<%= f.fields_for @encrypted_config do |ff| %> <%= fields_for @encrypted_config do |ff| %>
<div class="form-control"> <div class="form-control">
<% if @encrypted_config.value.blank? %> <% if @encrypted_config.value.blank? %>
<set-origin-url data-input-id="_encrypted_config_value"></set-origin-url> <set-origin-url data-input-id="encrypted_config_value"></set-origin-url>
<% end %> <% end %>
<%= ff.label :value, 'App URL', class: 'label' %> <%= ff.label :value, 'App URL', class: 'label' %>
<%= ff.text_field :value, required: true, class: 'base-input' %> <%= ff.text_field :value, required: true, class: 'base-input' %>
+7 -7
View File
@@ -37,11 +37,17 @@
<%= link_to 'E-Signature', settings_esign_path, class: 'text-base hover:bg-base-300' %> <%= link_to 'E-Signature', settings_esign_path, class: 'text-base hover:bg-base-300' %>
</li> </li>
<% end %> <% end %>
<% if can?(:read, AccountConfig) %>
<li>
<%= link_to 'Personalization', settings_personalization_path, class: 'text-base hover:bg-base-300' %>
</li>
<% end %>
<% if can?(:read, User) %> <% if can?(:read, User) %>
<li> <li>
<%= link_to 'Users', settings_users_path, class: 'text-base hover:bg-base-300' %> <%= link_to 'Users', settings_users_path, class: 'text-base hover:bg-base-300' %>
</li> </li>
<% end %> <% end %>
<%= render 'shared/settings_nav_extra' %>
<% if Docuseal.demo? || !Docuseal.multitenant? %> <% if Docuseal.demo? || !Docuseal.multitenant? %>
<% if can?(:read, AccessToken) %> <% if can?(:read, AccessToken) %>
<li> <li>
@@ -54,13 +60,7 @@
</li> </li>
<% end %> <% end %>
<% end %> <% end %>
<% if can?(:read, AccountConfig) %> <% if !Docuseal.demo? && can?(:manage, EncryptedConfig) && (current_user != true_user || !current_account.linked_account_account) %>
<li>
<%= link_to 'Personalization', settings_personalization_path, class: 'text-base hover:bg-base-300' %>
</li>
<% end %>
<%= render 'shared/settings_nav_extra' %>
<% if !Docuseal.demo? && can?(:manage, EncryptedConfig) && (current_user != true_user || !current_account.testing?) %>
<li> <li>
<%= link_to Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CLOUD_URL}/sign_up?#{{ redir: "#{Docuseal::CONSOLE_URL}/on_premise" }.to_query}", class: 'text-base hover:bg-base-300', data: { prefetch: false } do %> <%= link_to Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CLOUD_URL}/sign_up?#{{ redir: "#{Docuseal::CONSOLE_URL}/on_premise" }.to_query}", class: 'text-base hover:bg-base-300', data: { prefetch: false } do %>
Plans Plans
+5
View File
@@ -39,6 +39,11 @@
<% value = Time.current.in_time_zone(local_assigns[:timezone]).to_date.to_s if value == '{{date}}' %> <% value = Time.current.in_time_zone(local_assigns[:timezone]).to_date.to_s if value == '{{date}}' %>
<%= TimeUtils.format_date_string(value, field.dig('preferences', 'format'), local_assigns[:locale]) %> <%= TimeUtils.format_date_string(value, field.dig('preferences', 'format'), local_assigns[:locale]) %>
</div> </div>
<% elsif field['type'] == 'number' %>
<autosize-field></autosize-field>
<div class="flex items-center px-0.5 whitespace-nowrap">
<%= NumberUtils.format_number(value, field.dig('preferences', 'format')) %>
</div>
<% else %> <% else %>
<autosize-field></autosize-field> <autosize-field></autosize-field>
<div class="flex items-center px-0.5 whitespace-pre-wrap"><%= Array.wrap(value).join(', ') %></div> <div class="flex items-center px-0.5 whitespace-pre-wrap"><%= Array.wrap(value).join(', ') %></div>
@@ -1,4 +1,4 @@
<% is_show_tabs = @pagy.pages > 1 || params[:status].present? %> <% is_show_tabs = @pagy.count >= 5 || params[:status].present? %>
<% if Docuseal.demo? %><%= render 'shared/demo_alert' %><% end %> <% if Docuseal.demo? %><%= render 'shared/demo_alert' %><% end %>
<div class="flex justify-between mb-4 items-center"> <div class="flex justify-between mb-4 items-center">
<div class="flex items-center"> <div class="flex items-center">
@@ -56,7 +56,7 @@
<%= render partial: 'templates/submission', collection: @submissions, locals: { with_template: true } %> <%= render partial: 'templates/submission', collection: @submissions, locals: { with_template: true } %>
</div> </div>
<% end %> <% end %>
<% if params[:q].blank? && @pagy.count < 5 %> <% if params[:q].blank? && params[:status].blank? && @pagy.count < 5 %>
<%= render 'templates/dropzone' %> <%= render 'templates/dropzone' %>
<% end %> <% end %>
<% if @submissions.present? || params[:q].blank? %> <% if @submissions.present? || params[:q].blank? %>
@@ -1,5 +1,5 @@
<% if @email_config %> <% if @body.present? %>
<%= render 'custom_content', content: @email_config.value['body'], submitter: @submitter, sig: @sig %> <%= render 'custom_content', content: @body, submitter: @submitter, sig: @sig %>
<% else %> <% else %>
<p>Hi there,</p> <p>Hi there,</p>
<p>Please check the copy of your "<%= @submitter.submission.template.name %>" submission in the email attachments.</p> <p>Please check the copy of your "<%= @submitter.submission.template.name %>" submission in the email attachments.</p>
+1 -1
View File
@@ -37,7 +37,7 @@
</div> </div>
<% if !template.archived_at? %> <% if !template.archived_at? %>
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<% if can?(:update, template) && (Docuseal.multitenant? || current_account.testing? || current_account.id == 1) %> <% if can?(:update, template) %>
<div class="tooltip" data-tip="Preferences"> <div class="tooltip" data-tip="Preferences">
<%= link_to template_preferences_path(template), class: 'btn border border-base-200 bg-base-200 hover:bg-base-300 hover:border-base-300 btn-sm flex-1 hidden md:flex', data: { turbo_frame: :modal } do %> <%= link_to template_preferences_path(template), class: 'btn border border-base-200 bg-base-200 hover:bg-base-300 hover:border-base-300 btn-sm flex-1 hidden md:flex', data: { turbo_frame: :modal } do %>
<span class="flex items-center justify-center space-x-2"> <span class="flex items-center justify-center space-x-2">
+2 -2
View File
@@ -3,9 +3,9 @@
<% if @base_template %> <% if @base_template %>
<%= hidden_field_tag :base_template_id, @base_template.id %> <%= hidden_field_tag :base_template_id, @base_template.id %>
<% end %> <% end %>
<% if @base_template && (can?(:manage, :tenants) || true_user != current_user) && true_user.account.account_linked_accounts.exists? %> <% if @base_template && (can?(:manage, :tenants) || true_user != current_user) && true_user.account.linked_accounts.active.exists? %>
<div class="form-control -mb-2 mt-2"> <div class="form-control -mb-2 mt-2">
<%= select_tag :account_id, options_for_select([true_user.account, *true_user.account.linked_accounts].uniq.map { |e| [e.name, e.id] }, current_account.id), required: true, class: 'base-select' %> <%= select_tag :account_id, options_for_select([true_user.account, *true_user.account.linked_accounts.active].uniq.map { |e| [e.name, e.id] }, current_account.id), required: true, class: 'base-select' %>
</div> </div>
<% end %> <% end %>
<div class="form-control mt-6"> <div class="form-control mt-6">
+14 -14
View File
@@ -57,20 +57,6 @@
<div class="space-y-4"> <div class="space-y-4">
<%= render partial: 'submission', collection: @submissions, locals: { template: @template } %> <%= render partial: 'submission', collection: @submissions, locals: { template: @template } %>
</div> </div>
<% view_archived_html = capture do %>
<% if @template.submissions.where.not(archived_at: nil).exists? && !@template.archived_at? %>
<div>
<a href="<%= template_archived_index_path(@template) %>" class="link text-sm">View Archived</a>
</div>
<% end %>
<% end %>
<% if @pagy.pages > 1 %>
<%= render 'shared/pagination', pagy: @pagy, items_name: 'submissions', left_additional_html: view_archived_html %>
<% else %>
<div class="mt-2">
<%= view_archived_html %>
</div>
<% end %>
<% else %> <% else %>
<div class="card bg-base-200"> <div class="card bg-base-200">
<div class="card-body text-center py-16"> <div class="card-body text-center py-16">
@@ -102,3 +88,17 @@
</div> </div>
</div> </div>
<% end %> <% end %>
<% view_archived_html = capture do %>
<% if @template.submissions.where.not(archived_at: nil).exists? && !@template.archived_at? %>
<div>
<a href="<%= template_archived_index_path(@template) %>" class="link text-sm">View Archived</a>
</div>
<% end %>
<% end %>
<% if @pagy.pages > 1 %>
<%= render 'shared/pagination', pagy: @pagy, items_name: 'submissions', left_additional_html: view_archived_html %>
<% else %>
<div class="mt-2">
<%= view_archived_html %>
</div>
<% end %>
+120 -70
View File
@@ -1,44 +1,22 @@
<%= render 'shared/turbo_modal_large', title: 'Preferences', close_after_submit: false do %> <%= render 'shared/turbo_modal_large', title: 'Preferences', close_after_submit: false do %>
<% options = [['General', 'general'], ['API and Embedding', 'api']] %> <% show_api = Docuseal.multitenant? || current_account.testing? || !current_account.linked_account_account %>
<toggle-visible data-element-ids="<%= options.map(&:last).to_json %>" class="relative text-center mt-3 block"> <% options = [%w[General general]] %>
<div class="join"> <% options << ['API and Embedding', 'api'] if show_api %>
<% options.each_with_index do |(label, value), index| %> <% if options.size > 1 %>
<span> <toggle-visible data-element-ids="<%= options.map(&:last).to_json %>" class="relative text-center mt-3 block">
<%= radio_button_tag 'option', value, value == 'general', class: 'peer hidden', data: { action: 'change:toggle-visible#trigger' } %> <div class="join">
<label for="option_<%= value %>" class="<%= '!rounded-s-full' if index.zero? %> btn btn-focus btn-sm join-item md:w-48 peer-checked:btn-active normal-case <%= 'px-8 md:px-0' if value == 'general' %>"> <% options.each_with_index do |(label, value), index| %>
<%= label %> <span>
</label> <%= radio_button_tag 'option', value, value == 'general', class: 'peer hidden', data: { action: 'change:toggle-visible#trigger' } %>
</span> <label for="option_<%= value %>" class="<%= '!rounded-s-full' if index.zero? %> btn btn-focus btn-sm join-item md:w-48 peer-checked:btn-active normal-case <%= 'px-8 md:px-0' if value == 'general' %>">
<% end %> <%= label %>
</div> </label>
</toggle-visible> </span>
<div id="general" class="px-5 mb-4"> <% end %>
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-1' } do |f| %>
<toggle-on-submit data-element-id="email_saved_alert"></toggle-on-submit>
<%= f.fields_for :preferences, Struct.new(:request_email_subject, :request_email_body).new(*(@template.preferences.values_at('request_email_subject', 'request_email_body').compact_blank.presence || AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY).value.values_at('subject', 'body'))) do |ff| %>
<div class="form-control">
<%= ff.label :request_email_subject, 'Email subject', class: 'label' %>
<%= ff.text_field :request_email_subject, required: true, class: 'base-input', dir: 'auto' %>
</div>
<div class="form-control">
<div class="flex items-center">
<%= ff.label :request_email_body, 'Email body', class: 'label' %>
<span class="tooltip tooltip-right" data-tip="Use following placeholders text: <%= AccountConfig::DEFAULT_VALUES.dig(AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY, 'body').scan(/{{.*?}}/).join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<autoresize-textarea>
<%= ff.text_area :request_email_body, required: true, class: 'base-input w-full py-2', dir: 'auto' %>
</autoresize-textarea>
</div>
<% end %>
<div class="form-control pt-2">
<%= f.button button_title(title: 'Save', disabled_with: 'Saving'), class: 'base-button' %>
<div class="flex justify-center">
<span id="email_saved_alert" class="text-sm invisible font-normal mt-0.5">Changes have been saved</span>
</div>
</div> </div>
<% end %> </toggle-visible>
<% end %>
<div id="general" class="px-5 mb-4">
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-2' } do |f| %> <%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-2' } do |f| %>
<toggle-on-submit data-element-id="bcc_saved_alert"></toggle-on-submit> <toggle-on-submit data-element-id="bcc_saved_alert"></toggle-on-submit>
<%= f.fields_for :preferences, Struct.new(:bcc_completed).new(@template.preferences['bcc_completed']) do |ff| %> <%= f.fields_for :preferences, Struct.new(:bcc_completed).new(@template.preferences['bcc_completed']) do |ff| %>
@@ -61,40 +39,112 @@
</div> </div>
<% end %> <% end %>
<%= render 'templates_code_modal/preferences' %> <%= render 'templates_code_modal/preferences' %>
<div class="join join-vertical w-full !rounded-2xl mb-1.5 mt-2.5">
<div class="collapse collapse-arrow join-item border border-base-300">
<input type="checkbox" name="accordion">
<div class="collapse-title text-xl font-medium">
Signature request email
</div>
<div class="collapse-content">
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-1' } do |f| %>
<toggle-on-submit data-element-id="email_saved_alert1"></toggle-on-submit>
<%= f.fields_for :preferences, Struct.new(:request_email_subject, :request_email_body).new(*(@template.preferences.values_at('request_email_subject', 'request_email_body').compact_blank.presence || AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY).value.values_at('subject', 'body'))) do |ff| %>
<div class="form-control">
<%= ff.label :request_email_subject, 'Email subject', class: 'label' %>
<%= ff.text_field :request_email_subject, required: true, class: 'base-input', dir: 'auto' %>
</div>
<div class="form-control">
<div class="flex items-center">
<%= ff.label :request_email_body, 'Email body', class: 'label' %>
<span class="tooltip tooltip-right" data-tip="Use following placeholders text: <%= AccountConfig::DEFAULT_VALUES.dig(AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY, 'body').scan(/{{.*?}}/).join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<autoresize-textarea>
<%= ff.text_area :request_email_body, required: true, class: 'base-input w-full py-2', dir: 'auto' %>
</autoresize-textarea>
</div>
<% end %>
<div class="form-control pt-2">
<%= f.button button_title(title: 'Save', disabled_with: 'Saving'), class: 'base-button' %>
<div class="flex justify-center">
<span id="email_saved_alert1" class="text-sm invisible font-normal mt-0.5">Changes have been saved</span>
</div>
</div>
<% end %>
</div>
</div>
<div class="collapse collapse-arrow join-item border border-base-300">
<input type="checkbox" name="accordion">
<div class="collapse-title text-xl font-medium">
Documents copy email
</div>
<div class="collapse-content">
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-1' } do |f| %>
<toggle-on-submit data-element-id="email_saved_alert2"></toggle-on-submit>
<%= f.fields_for :preferences, Struct.new(:documents_copy_email_subject, :documents_copy_email_body).new(*(@template.preferences.values_at('documents_copy_email_subject', 'documents_copy_email_body').compact_blank.presence || AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY).value.values_at('subject', 'body'))) do |ff| %>
<div class="form-control">
<%= ff.label :documents_copy_email_subject, 'Email subject', class: 'label' %>
<%= ff.text_field :documents_copy_email_subject, required: true, class: 'base-input', dir: 'auto' %>
</div>
<div class="form-control">
<div class="flex items-center">
<%= ff.label :documents_copy_email_body, 'Email body', class: 'label' %>
<span class="tooltip tooltip-right" data-tip="Use following placeholders text: <%= AccountConfig::DEFAULT_VALUES.dig(AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY, 'body').scan(/{{.*?}}/).join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<autoresize-textarea>
<%= ff.text_area :documents_copy_email_body, required: true, class: 'base-input w-full py-2', dir: 'auto' %>
</autoresize-textarea>
</div>
<% end %>
<div class="form-control pt-2">
<%= f.button button_title(title: 'Save', disabled_with: 'Saving'), class: 'base-button' %>
<div class="flex justify-center">
<span id="email_saved_alert2" class="text-sm invisible font-normal mt-0.5">Changes have been saved</span>
</div>
</div>
<% end %>
</div>
</div>
</div>
</div> </div>
<div id="api" class="hidden mt-2 mb-4 px-5"> <% if show_api %>
<div> <div id="api" class="hidden mt-2 mb-4 px-5">
<label class="text-sm font-semibold" for="template_id"> <div>
Template ID <label class="text-sm font-semibold" for="template_id">
</label> Template ID
<div class="flex gap-2 mb-4 mt-2"> </label>
<input id="template_id" type="text" value="<%= @template.id %>" class="base-input w-full" autocomplete="off" readonly> <div class="flex gap-2 mb-4 mt-2">
<%= render 'shared/clipboard_copy', icon: 'copy', text: @template.id, class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy', copied_title: 'Copied' %> <input id="template_id" type="text" value="<%= @template.id %>" class="base-input w-full" autocomplete="off" readonly>
<%= render 'shared/clipboard_copy', icon: 'copy', text: @template.id, class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy', copied_title: 'Copied' %>
</div>
</div> </div>
</div> <div>
<div> <label class="text-sm font-semibold" for="embedding_url">
<label class="text-sm font-semibold" for="embedding_url"> Embedding URL
Embedding URL </label>
</label> <div class="flex gap-2 mb-4 mt-2">
<div class="flex gap-2 mb-4 mt-2"> <input id="embedding_url" type="text" value="<%= start_form_url(slug: @template.slug) %>" class="base-input w-full" autocomplete="off" readonly>
<input id="embedding_url" type="text" value="<%= start_form_url(slug: @template.slug) %>" class="base-input w-full" autocomplete="off" readonly> <%= render 'shared/clipboard_copy', icon: 'copy', text: start_form_url(slug: @template.slug), class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy', copied_title: 'Copied' %>
<%= render 'shared/clipboard_copy', icon: 'copy', text: start_form_url(slug: @template.slug), class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy', copied_title: 'Copied' %> </div>
</div> </div>
</div> <%= render 'templates_code_modal/placeholder' %>
<%= render 'templates_code_modal/placeholder' %> <%= render 'templates/embedding', template: @template %>
<%= render 'templates/embedding', template: @template %> <% if can?(:manage, TemplateSharing.new(template: @template)) %>
<% if can?(:manage, TemplateSharing.new(template: @template)) %> <%= form_for '', url: template_sharings_testing_index_path, method: :post, html: { class: 'mt-1' } do |f| %>
<%= form_for '', url: template_sharings_testing_index_path, method: :post, html: { class: 'mt-1' } do |f| %> <%= f.hidden_field :template_id, value: @template.id %>
<%= f.hidden_field :template_id, value: @template.id %> <div class="flex items-center justify-between">
<div class="flex items-center justify-between"> <span>
<span> Share template with Test Environment
Share template with Test Environment </span>
</span> <%= f.check_box :value, class: 'toggle', checked: @template.template_sharings.exists?(account_id: current_account.testing_accounts), onchange: 'this.form.requestSubmit()' %>
<%= f.check_box :value, class: 'toggle', checked: @template.template_sharings.exists?(account_id: current_account.testing_accounts), onchange: 'this.form.requestSubmit()' %> </div>
<% end %>
<div class="mb-4">
</div> </div>
<% end %> <% end %>
<div class="mb-4"> </div>
</div> <% end %>
<% end %>
</div>
<% end %> <% end %>
+17 -15
View File
@@ -15,22 +15,24 @@
</div> </div>
<% end %> <% end %>
<% preference = current_account.account_configs.find_by(key: AccountConfig::WEBHOOK_PREFERENCES_KEY)&.value || {} %> <% preference = current_account.account_configs.find_by(key: AccountConfig::WEBHOOK_PREFERENCES_KEY)&.value || {} %>
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 mt-2 gap-y-2"> <% WebhookPreferencesController::EVENTS.group_by { |e| e.include?('form') }.each do |_, events| %>
<% WebhookPreferencesController::EVENTS.each do |event| %> <div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 mt-2 gap-y-2">
<%= form_for '', url: webhook_preferences_path, method: :post do |f| %> <% events.each do |event| %>
<%= f.hidden_field :event, value: event %> <%= form_for '', url: webhook_preferences_path, method: :post do |f| %>
<% uuid = SecureRandom.uuid %> <%= f.hidden_field :event, value: event %>
<div class="flex"> <% uuid = SecureRandom.uuid %>
<label for="<%= uuid %>" class="flex items-center space-x-2"> <div class="flex">
<%= f.check_box :value, class: 'base-checkbox', checked: preference[event] || (!preference.key?(event) && event.starts_with?('form.')), onchange: 'this.form.requestSubmit()', id: uuid %> <label for="<%= uuid %>" class="flex items-center space-x-2">
<span> <%= f.check_box :value, class: 'base-checkbox', checked: preference[event] || (!preference.key?(event) && event.starts_with?('form.')), onchange: 'this.form.requestSubmit()', id: uuid %>
<%= event %> <span>
</span> <%= event %>
</label> </span>
</div> </label>
</div>
<% end %>
<% end %> <% end %>
<% end %> </div>
</div> <% end %>
</div> </div>
</div> </div>
<% submitter = current_account.submitters.where.not(completed_at: nil).order(:id).last %> <% submitter = current_account.submitters.where.not(completed_at: nil).order(:id).last %>
+10 -2
View File
@@ -128,14 +128,22 @@ Rails.application.configure do
config.lograge.formatter = ->(data) { data.except(:path, :location).to_json } config.lograge.formatter = ->(data) { data.except(:path, :location).to_json }
config.lograge.custom_payload do |controller| config.lograge.custom_payload do |controller|
params = controller.request.try(:params) || {} params =
begin
controller.request.try(:params) || {}
rescue StandardError
{}
end
{ {
fwd: controller.request.ip, fwd: controller.request.ip,
params: { params: {
id: params[:id], id: params[:id],
sig: (params[:signed_uuid] || params[:signed_id]).to_s.split('--').first, sig: (params[:signed_uuid] || params[:signed_id]).to_s.split('--').first,
slug: (params[:slug] || params[:submission_slug] || params[:template_slug]).to_s.last(5) slug: (params[:slug] ||
params[:submitter_slug] ||
params[:submission_slug] ||
params[:template_slug]).to_s.last(5)
}.compact_blank, }.compact_blank,
host: controller.request.host, host: controller.request.host,
uid: controller.instance_variable_get(:@current_user).try(:id) uid: controller.instance_variable_get(:@current_user).try(:id)
+20
View File
@@ -1,6 +1,18 @@
# frozen_string_literal: true # frozen_string_literal: true
module HexaPDF module HexaPDF
module Encryption
class SecurityHandler
def encrypt_string(str, obj)
return str.dup if str.empty? || obj == document.trailer[:Encrypt] || obj.type == :XRef ||
(obj.type == :Sig && obj[:Contents].equal?(str))
key = object_key(obj.oid, obj.gen, string_algorithm)
string_algorithm.encrypt(key, str).dup
end
end
end
module Type module Type
# fix NoMethodError: undefined method `field_value' for #<HexaPDF::Type::AcroForm::Field # fix NoMethodError: undefined method `field_value' for #<HexaPDF::Type::AcroForm::Field
module AcroForm module AcroForm
@@ -8,6 +20,14 @@ module HexaPDF
def field_value def field_value
'' ''
end end
def terminal_field?
kids = self[:Kids]
# rubocop:disable Rails/Blank
kids.nil? || kids.empty? || kids.none? { |kid| kid&.key?(:T) }
# rubocop:enable Rails/Blank
end
end end
end end
+13
View File
@@ -1,5 +1,6 @@
en: &en en: &en
email: Email email: Email
verification_code_code: 'Verification code: %{code}'
digitally_signed_by: Digitally signed by digitally_signed_by: Digitally signed by
role: Role role: Role
provide_your_email_to_start: Provide your email to start provide_your_email_to_start: Provide your email to start
@@ -21,6 +22,7 @@ en: &en
es: &es es: &es
role: Rol role: Rol
verification_code_code: 'Código de verificación: %{code}'
email: Correo electrónico email: Correo electrónico
digitally_signed_by: Firmado digitalmente por digitally_signed_by: Firmado digitalmente por
role: Rol role: Rol
@@ -42,6 +44,7 @@ es: &es
it: it:
role: Rôle role: Rôle
verification_code_code: 'Codice di verifica: %{code}'
email: Email email: Email
digitally_signed_by: Firmato digitalmente da digitally_signed_by: Firmato digitalmente da
role: Ruolo role: Ruolo
@@ -63,6 +66,7 @@ it:
fr: &fr fr: &fr
email: Email email: Email
verification_code_code: 'Code de vérification: %{code}'
digitally_signed_by: Signé numériquement par digitally_signed_by: Signé numériquement par
role: Rôle role: Rôle
provide_your_email_to_start: Entrez votre adresse email pour commencer provide_your_email_to_start: Entrez votre adresse email pour commencer
@@ -84,6 +88,7 @@ fr: &fr
pt: &pt pt: &pt
role: Função role: Função
email: Email email: Email
verification_code_code: 'Código de verificação: %{code}'
digitally_signed_by: Assinado digitalmente por digitally_signed_by: Assinado digitalmente por
role: Função role: Função
provide_your_email_to_start: Forneça o seu email para começar provide_your_email_to_start: Forneça o seu email para começar
@@ -104,6 +109,7 @@ pt: &pt
de: &de de: &de
role: Rolle role: Rolle
verification_code_code: 'Verifizierungscode: %{code}'
email: E-Mail email: E-Mail
digitally_signed_by: Digital signiert von digitally_signed_by: Digital signiert von
role: Rolle role: Rolle
@@ -125,6 +131,7 @@ de: &de
pl: pl:
email: Email email: Email
verification_code_code: 'Kod weryfikacyjny: %{code}'
digitally_signed_by: Podpis cyfrowy przez digitally_signed_by: Podpis cyfrowy przez
role: Rola role: Rola
provide_your_email_to_start: Podaj swój adres email, aby rozpocząć provide_your_email_to_start: Podaj swój adres email, aby rozpocząć
@@ -146,6 +153,7 @@ pl:
uk: uk:
email: Email email: Email
digitally_signed_by: Цифровий підпис від digitally_signed_by: Цифровий підпис від
verification_code_code: 'Код підтвердження: %{code}'
role: Роль role: Роль
provide_your_email_to_start: Введіть свій email, щоб почати provide_your_email_to_start: Введіть свій email, щоб почати
start: Почати start: Почати
@@ -166,6 +174,7 @@ uk:
cs: cs:
email: Email email: Email
digitally_signed_by: Digitálně podepsáno uživatelem digitally_signed_by: Digitálně podepsáno uživatelem
verification_code_code: 'Ověřovací kód: %{code}'
role: Role role: Role
provide_your_email_to_start: Zadejte svůj email pro zahájení provide_your_email_to_start: Zadejte svůj email pro zahájení
start: Zahájit start: Zahájit
@@ -187,6 +196,7 @@ he:
email: דוא"ל email: דוא"ל
digitally_signed_by: חתום דיגיטלית על ידי digitally_signed_by: חתום דיגיטלית על ידי
role: תפקיד role: תפקיד
verification_code_code: 'קוד אימות: %{code}'
provide_your_email_to_start: ספק את כתובת הדוא"ל שלך כדי להתחיל provide_your_email_to_start: ספק את כתובת הדוא"ל שלך כדי להתחיל
start: התחל start: התחל
starting: מתחיל starting: מתחיל
@@ -207,6 +217,7 @@ nl:
email: E-mail email: E-mail
digitally_signed_by: Digitaal ondertekend door digitally_signed_by: Digitaal ondertekend door
role: Rol role: Rol
verification_code_code: 'Verificatiecode: %{code}'
provide_your_email_to_start: Geef uw e-mailadres om te beginnen provide_your_email_to_start: Geef uw e-mailadres om te beginnen
start: Start start: Start
starting: Starten starting: Starten
@@ -231,6 +242,7 @@ ar:
provide_your_email_to_start: قدم بريدك الإلكتروني للبدء provide_your_email_to_start: قدم بريدك الإلكتروني للبدء
start: بدء start: بدء
starting: بداية starting: بداية
verification_code_code: 'رمز التحقق: %{code}'
form_has_been_deleted_by_html: 'تم حذف الاستمارة بواسطة <span class="font-semibold">%{name}</span>.' form_has_been_deleted_by_html: 'تم حذف الاستمارة بواسطة <span class="font-semibold">%{name}</span>.'
invited_by_html: 'تمت الدعوة بواسطة <span class="font-semibold">%{name}</span>' invited_by_html: 'تمت الدعوة بواسطة <span class="font-semibold">%{name}</span>'
you_have_been_invited_to_submit_a_form: تمت دعوتك لتقديم استمارة you_have_been_invited_to_submit_a_form: تمت دعوتك لتقديم استمارة
@@ -255,6 +267,7 @@ ko:
form_has_been_deleted_by_html: '<span class="font-semibold">%{name}</span>에 의해 양식이 삭제되었습니다.' form_has_been_deleted_by_html: '<span class="font-semibold">%{name}</span>에 의해 양식이 삭제되었습니다.'
invited_by_html: '<span class="font-semibold">%{name}</span>에 의해 초대되었습니다.' invited_by_html: '<span class="font-semibold">%{name}</span>에 의해 초대되었습니다.'
you_have_been_invited_to_submit_a_form: 양식을 제출하도록 초대되었습니다. you_have_been_invited_to_submit_a_form: 양식을 제출하도록 초대되었습니다.
verification_code_code: '확인 코드: %{code}'
signed_on_time: '%{time}에 서명됨' signed_on_time: '%{time}에 서명됨'
form_has_been_submitted_already: 양식이 이미 제출되었습니다. form_has_been_submitted_already: 양식이 이미 제출되었습니다.
send_copy_to_email: 이메일로 사본 보내기 send_copy_to_email: 이메일로 사본 보내기
+2 -2
View File
@@ -1,8 +1,8 @@
const { globalMutableWebpackConfig, merge } = require('shakapacker') const { generateWebpackConfig, merge } = require('shakapacker')
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
const { VueLoaderPlugin } = require('vue-loader') const { VueLoaderPlugin } = require('vue-loader')
const configs = merge(globalMutableWebpackConfig, { const configs = generateWebpackConfig({
resolve: { resolve: {
extensions: ['.css', '.scss', '.vue'] extensions: ['.css', '.scss', '.vue']
}, },
@@ -0,0 +1,7 @@
# frozen_string_literal: true
class AddArchivedAtToAccounts < ActiveRecord::Migration[7.1]
def change
add_column :accounts, :archived_at, :datetime
end
end
@@ -0,0 +1,28 @@
# frozen_string_literal: true
class PopulateAccountLinkedAccounts < ActiveRecord::Migration[7.1]
class MigrationAccount < ApplicationRecord
self.table_name = 'accounts'
end
class MigrationAccountLinkedAccount < ApplicationRecord
self.table_name = 'account_linked_accounts'
end
def up
return if Docuseal.multitenant?
MigrationAccount.order(:id).each do |account|
next if account.id == 1
next if MigrationAccountLinkedAccount.exists?(linked_account_id: account.id)
MigrationAccountLinkedAccount.create!(account_id: 1,
linked_account_id: account.id,
account_type: :linked)
end
end
def down
nil
end
end
+2 -1
View File
@@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.1].define(version: 2024_04_16_170023) do ActiveRecord::Schema[7.1].define(version: 2024_04_28_112400) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
@@ -52,6 +52,7 @@ ActiveRecord::Schema[7.1].define(version: 2024_04_16_170023) do
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.string "uuid", null: false t.string "uuid", null: false
t.datetime "archived_at"
t.index ["uuid"], name: "index_accounts_on_uuid", unique: true t.index ["uuid"], name: "index_accounts_on_uuid", unique: true
end end
+5 -1
View File
@@ -90,7 +90,11 @@ module Accounts
def load_signing_pkcs(account) def load_signing_pkcs(account)
cert_data = cert_data =
if Docuseal.multitenant? if Docuseal.multitenant?
EncryptedConfig.find_by(account:, key: EncryptedConfig::ESIGN_CERTS_KEY)&.value || Docuseal::CERTS data = EncryptedConfig.find_by(account:, key: EncryptedConfig::ESIGN_CERTS_KEY)&.value
return Docuseal.default_pkcs if data.blank?
data
else else
EncryptedConfig.find_by(account:, key: EncryptedConfig::ESIGN_CERTS_KEY)&.value || EncryptedConfig.find_by(account:, key: EncryptedConfig::ESIGN_CERTS_KEY)&.value ||
EncryptedConfig.find_by(key: EncryptedConfig::ESIGN_CERTS_KEY).value EncryptedConfig.find_by(key: EncryptedConfig::ESIGN_CERTS_KEY).value
+11
View File
@@ -60,6 +60,17 @@ module Docuseal
ENV['ACTIVE_STORAGE_PUBLIC'] == 'true' ENV['ACTIVE_STORAGE_PUBLIC'] == 'true'
end end
def default_pkcs
@default_pkcs ||= GenerateCertificate.load_pkcs(Docuseal::CERTS)
end
def trusted_certs
@trusted_certs ||=
ENV['TRUSTED_CERTS'].to_s.gsub('\\n', "\n").split("\n\n").map do |base64|
OpenSSL::X509::Certificate.new(base64)
end
end
def default_url_options def default_url_options
return DEFAULT_URL_OPTIONS if multitenant? return DEFAULT_URL_OPTIONS if multitenant?
+5 -1
View File
@@ -3,6 +3,8 @@
module GenerateCertificate module GenerateCertificate
SIZE = 2**11 SIZE = 2**11
Pkcs12Struct = Struct.new(:certificate, :ca_certs, keyword_init: true)
module_function module_function
def call(name = Docuseal.product_name) def call(name = Docuseal.product_name)
@@ -89,10 +91,12 @@ module GenerateCertificate
def load_pkcs(cert_data) def load_pkcs(cert_data)
cert = OpenSSL::X509::Certificate.new(cert_data['cert']) cert = OpenSSL::X509::Certificate.new(cert_data['cert'])
key = OpenSSL::PKey::RSA.new(cert_data['key']) key = OpenSSL::PKey::RSA.new(cert_data['key']) if cert_data['key'].present?
sub_ca = OpenSSL::X509::Certificate.new(cert_data['sub_ca']) sub_ca = OpenSSL::X509::Certificate.new(cert_data['sub_ca'])
root_ca = OpenSSL::X509::Certificate.new(cert_data['root_ca']) root_ca = OpenSSL::X509::Certificate.new(cert_data['root_ca'])
return Pkcs12Struct.new(certificate: cert, ca_certs: [sub_ca, root_ca]) unless key
OpenSSL::PKCS12.create( OpenSSL::PKCS12.create(
'', '',
'', '',
+21
View File
@@ -0,0 +1,21 @@
# frozen_string_literal: true
module NumberUtils
FORMAT_LOCALES = {
'dot' => 'de',
'space' => 'fr',
'comma' => 'en'
}.freeze
module_function
def format_number(number, format)
locale = FORMAT_LOCALES[format]
if locale
ApplicationController.helpers.number_with_delimiter(number, locale:)
else
number
end
end
end
-96
View File
@@ -1,96 +0,0 @@
# frozen_string_literal: true
class PdfProcessor < HexaPDF::Content::Processor
attr_accessor :handler, :serializer
class ParseTextHandler
attr_accessor :pos, :num, :search_chars, :handler
def initialize(handler)
@num = 0
@pos = 0
@handler = handler
@search_chars = handler.search_chars
end
TJS = %i[TJ Tj].freeze
def call(processor, operator, operands)
return unless TJS.include?(operator)
processor.send(:decode_text, *operands).chars.each do |char|
handler.tokens << [char, [@num, @pos]] if search_chars.include?(char)
@pos += 1
end
@pos = 0
@num += 1
rescue HexaPDF::Error => e
Rails.logger.error(e.message)
@pos = 0
@num += 1
end
end
def initialize(page)
super
@serializer = HexaPDF::Serializer.new
end
def serialize(operator, operands)
operators[operator].serialize(serializer, *operands)
end
def process(operator, operands = [])
super
handler.call(self, operator, operands)
end
def self.call(data, process_handler, result_handler, acc = {}, remove_tags: true)
doc = HexaPDF::Document.new(io: StringIO.new(data))
doc.pages.each do |page|
processor = PdfProcessor.new(page)
process_handler_instance = process_handler.new
processor.handler = ParseTextHandler.new(process_handler_instance)
page.process_contents(processor)
if process_handler_instance.tokens?
processor = PdfProcessor.new(page)
processor.handler = process_handler_instance
page.process_contents(processor)
page.contents = process_handler_instance.contents if process_handler_instance.result.present? && remove_tags
end
page[:Annots].to_a.each do |annot|
next unless annot
text = annot[:Contents].to_s.squish
next unless text.starts_with?('{{') && text.ends_with?('}}')
result_handler.call({ text:, rect: annot[:Rect] }, page, acc)
page[:Annots].delete(annot)
end
process_handler_instance.result.each do |item|
result_handler.call(item, page, acc)
end
end
new_io = StringIO.new
doc.write(new_io, validate: false)
[new_io.tap(&:rewind).read, acc]
end
end
+8 -2
View File
@@ -150,15 +150,21 @@ module Submissions
def build_submitter(submission:, attrs:, uuid:, is_order_sent:, mark_as_sent:, user:, preferences:) def build_submitter(submission:, attrs:, uuid:, is_order_sent:, mark_as_sent:, user:, preferences:)
email = Submissions.normalize_email(attrs[:email]) email = Submissions.normalize_email(attrs[:email])
submitter_preferences = Submitters.normalize_preferences(submission.account, user, attrs) submitter_preferences = Submitters.normalize_preferences(submission.account, user, attrs)
values = attrs[:values] || {}
phone_field_uuid =
(submission.template_fields || submission.template.fields).find do |f|
values[f['uuid']].present? && f['type'] == 'phone'
end&.dig('uuid')
submission.submitters.new( submission.submitters.new(
email:, email:,
phone: attrs[:phone].to_s.gsub(/[^0-9+]/, ''), phone: (attrs[:phone] || values[phone_field_uuid]).to_s.gsub(/[^0-9+]/, ''),
name: attrs[:name], name: attrs[:name],
external_id: attrs[:external_id].presence || attrs[:application_key], external_id: attrs[:external_id].presence || attrs[:application_key],
completed_at: attrs[:completed].present? ? Time.current : nil, completed_at: attrs[:completed].present? ? Time.current : nil,
sent_at: mark_as_sent && email.present? && is_order_sent ? Time.current : nil, sent_at: mark_as_sent && email.present? && is_order_sent ? Time.current : nil,
values: attrs[:values] || {}, values: values.except(phone_field_uuid),
metadata: attrs[:metadata] || {}, metadata: attrs[:metadata] || {},
preferences: preferences.merge(submitter_preferences) preferences: preferences.merge(submitter_preferences)
.merge({ default_values: attrs[:values] }.compact_blank) .merge({ default_values: attrs[:values] }.compact_blank)
+2 -1
View File
@@ -11,6 +11,7 @@ module Submissions
module_function module_function
def call(submitter) def call(submitter)
return [] unless submitter
return submitter.documents if ApplicationRecord.uncached { submitter.document_generation_events.complete.exists? } return submitter.documents if ApplicationRecord.uncached { submitter.document_generation_events.complete.exists? }
events = events =
@@ -54,7 +55,7 @@ module Submissions
DocumentGenerationEvent.where(submitter:).order(:created_at).last DocumentGenerationEvent.where(submitter:).order(:created_at).last
end end
break submitter.documents if last_event.event_name.in?(%w[complete fail]) break submitter.documents.reload if last_event.event_name.in?(%w[complete fail])
raise WaitForCompleteTimeout if total_wait_time > CHECK_COMPLETE_TIMEOUT raise WaitForCompleteTimeout if total_wait_time > CHECK_COMPLETE_TIMEOUT
end end
+24 -23
View File
@@ -7,15 +7,10 @@ module Submissions
FONT_PATH = '/fonts/GoNotoKurrent-Regular.ttf' FONT_PATH = '/fonts/GoNotoKurrent-Regular.ttf'
FONT_BOLD_PATH = '/fonts/GoNotoKurrent-Bold.ttf' FONT_BOLD_PATH = '/fonts/GoNotoKurrent-Bold.ttf'
FONT_NAME = if File.exist?(FONT_PATH) FONT_NAME = if File.exist?(FONT_PATH)
FONT_PATH 'GoNotoKurrent'
else else
'Helvetica' 'Helvetica'
end end
FONT_BOLD_NAME = if File.exist?(FONT_BOLD_PATH)
FONT_BOLD_PATH
else
'Helvetica'
end
SIGN_REASON = 'Signed with DocuSeal.co' SIGN_REASON = 'Signed with DocuSeal.co'
VERIFIED_TEXT = 'Verified' VERIFIED_TEXT = 'Verified'
@@ -48,7 +43,19 @@ module Submissions
end end
composer = HexaPDF::Composer.new(skip_page_creation: true) composer = HexaPDF::Composer.new(skip_page_creation: true)
composer.document.fonts.add(FONT_BOLD_NAME, variant: :bold) composer.document.task(:pdfa) if FONT_NAME == 'GoNotoKurrent'
composer.document.config['font.map'] = {
'Helvetica' => {
none: FONT_PATH,
bold: FONT_BOLD_PATH
},
FONT_NAME => {
none: FONT_PATH,
bold: FONT_BOLD_PATH
}
}
composer.document.config['font.on_missing_glyph'] = composer.document.config['font.on_missing_glyph'] =
Submissions::GenerateResultAttachments.method(:on_missing_glyph).to_proc Submissions::GenerateResultAttachments.method(:on_missing_glyph).to_proc
@@ -80,7 +87,7 @@ 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.linked_account_account&.testing? ? 'Testing Log - Not for Production Use' : 'Audit Log', column.text(account.testing? ? 'Testing Log - Not for Production Use' : '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)
@@ -119,13 +126,13 @@ module Submissions
), ),
composer.document.layout.formatted_text_box( composer.document.layout.formatted_text_box(
[ [
{ text: "Original SHA256:\n", font: [FONT_BOLD_NAME, { variant: :bold }] }, { text: "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_BOLD_NAME, { variant: :bold }] }, { text: "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_BOLD_NAME, { variant: :bold }] }, { text: '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
@@ -162,7 +169,7 @@ module Submissions
composer.document.layout.formatted_text_box( composer.document.layout.formatted_text_box(
[ [
submission.template_submitters.size > 1 && { text: "#{item['name']}\n" }, submission.template_submitters.size > 1 && { text: "#{item['name']}\n" },
submitter.email && { text: "#{submitter.email}\n", font: [FONT_BOLD_NAME, { variant: :bold }] }, submitter.email && { text: "#{submitter.email}\n", font: [FONT_NAME, { variant: :bold }] },
submitter.name && { text: "#{TextUtils.maybe_rtl_reverse(submitter.name)}\n" }, submitter.name && { text: "#{TextUtils.maybe_rtl_reverse(submitter.name)}\n" },
submitter.phone && { text: "#{submitter.phone}\n" } submitter.phone && { text: "#{submitter.phone}\n" }
].compact_blank, line_spacing: 1.3, padding: [0, 20, 0, 0] ].compact_blank, line_spacing: 1.3, padding: [0, 20, 0, 0]
@@ -254,6 +261,8 @@ 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'
value = value.join(', ') if value.is_a?(Array) value = value.join(', ') if value.is_a?(Array)
composer.formatted_text_box([{ text: TextUtils.maybe_rtl_reverse(value.to_s.presence || 'n/a') }], composer.formatted_text_box([{ text: TextUtils.maybe_rtl_reverse(value.to_s.presence || 'n/a') }],
@@ -276,7 +285,7 @@ module Submissions
composer.document.layout.formatted_text_box( composer.document.layout.formatted_text_box(
[ [
{ text: SubmissionEvents::EVENT_NAMES[event.event_type.to_sym], { text: SubmissionEvents::EVENT_NAMES[event.event_type.to_sym],
font: [FONT_BOLD_NAME, { variant: :bold }] }, font: [FONT_NAME, { variant: :bold }] },
event.event_type.include?('send_') ? ' to ' : ' by ', event.event_type.include?('send_') ? ' to ' : ' by ',
if event.event_type.include?('sms') || event.event_type.include?('phone') if event.event_type.include?('sms') || event.event_type.include?('phone')
event.data['phone'] || submitter.phone event.data['phone'] || submitter.phone
@@ -296,17 +305,9 @@ module Submissions
sign_params = { sign_params = {
reason: SIGN_REASON, reason: SIGN_REASON,
certificate: pkcs.certificate, **Submissions::GenerateResultAttachments.build_signing_params(pkcs, tsa_url)
doc_mdp_permissions: :no_changes,
key: pkcs.key,
certificate_chain: pkcs.ca_certs || []
} }
if tsa_url
sign_params[:timestamp_handler] = Submissions::TimestampHandler.new(tsa_url:)
sign_params[:signature_size] = 10_000
end
composer.document.sign(io, **sign_params) composer.document.sign(io, **sign_params)
ActiveStorage::Attachment.create!( ActiveStorage::Attachment.create!(
@@ -324,7 +325,7 @@ module Submissions
column.formatted_text([{ text: 'DocuSeal', column.formatted_text([{ text: 'DocuSeal',
link: Docuseal::PRODUCT_URL }], link: Docuseal::PRODUCT_URL }],
font_size: 20, font_size: 20,
font: [FONT_BOLD_NAME, { variant: :bold }], font: [FONT_NAME, { variant: :bold }],
width: 100, width: 100,
padding: [5, 0, 0, 8], padding: [5, 0, 0, 8],
position: :float, text_align: :left) position: :float, text_align: :left)
+79 -29
View File
@@ -11,12 +11,12 @@ module Submissions
end end
SIGN_REASON = 'Signed by %<name>s with DocuSeal.co' SIGN_REASON = 'Signed by %<name>s with DocuSeal.co'
SIGN_SIGNLE_REASON = 'Digitally signed with DocuSeal.co'
RTL_REGEXP = TextUtils::RTL_REGEXP RTL_REGEXP = TextUtils::RTL_REGEXP
TEXT_LEFT_MARGIN = 1 TEXT_LEFT_MARGIN = 1
TEXT_TOP_MARGIN = 1 TEXT_TOP_MARGIN = 1
MAX_PAGE_ROTATE = 20
A4_SIZE = [595, 842].freeze A4_SIZE = [595, 842].freeze
SUPPORTED_IMAGE_TYPES = ['image/png', 'image/jpeg'].freeze SUPPORTED_IMAGE_TYPES = ['image/png', 'image/jpeg'].freeze
@@ -43,12 +43,16 @@ module Submissions
template = submitter.submission.template template = submitter.submission.template
is_flatten =
submitter.account.account_configs
.find_or_initialize_by(key: AccountConfig::FLATTEN_RESULT_PDF_KEY).value != false
account = submitter.account account = submitter.account
pkcs = Accounts.load_signing_pkcs(account) pkcs = Accounts.load_signing_pkcs(account)
tsa_url = Accounts.load_timeserver_url(account) tsa_url = Accounts.load_timeserver_url(account)
attachments_data_cache = {} attachments_data_cache = {}
pdfs_index = build_pdfs_index(submitter) pdfs_index = build_pdfs_index(submitter, flatten: is_flatten)
submitter.submission.template_fields.each do |field| submitter.submission.template_fields.each do |field|
next if field['submitter_uuid'] != submitter.uuid next if field['submitter_uuid'] != submitter.uuid
@@ -85,10 +89,12 @@ module Submissions
next if Array.wrap(value).compact_blank.blank? next if Array.wrap(value).compact_blank.blank?
begin if is_flatten
page.flatten_annotations begin
rescue StandardError => e page.flatten_annotations
Rollbar.error(e) if defined?(Rollbar) rescue StandardError => e
Rollbar.error(e) if defined?(Rollbar)
end
end end
canvas = page.canvas(type: :overlay) canvas = page.canvas(type: :overlay)
@@ -140,17 +146,22 @@ module Submissions
attachment_uuid = Array.wrap(value)[acc] attachment_uuid = Array.wrap(value)[acc]
attachment = submitter.attachments.find { |a| a.uuid == attachment_uuid } attachment = submitter.attachments.find { |a| a.uuid == attachment_uuid }
next_index = next_index = lines[(index + 1)..].index { |l| l.items.first.is_a?(HexaPDF::Layout::InlineBox) }
lines[(index + 1)..].index { |l| l.items.first.is_a?(HexaPDF::Layout::InlineBox) } || (lines.size - 1) next_index += index if next_index
next_index ||= lines.size - 1
diff = ((area['h'] * height) / 2) - (lines.sum(&:height) / 2)
page[:Annots] << pdf.add( page[:Annots] << pdf.add(
{ {
Type: :Annot, Subtype: :Link, Type: :Annot, Subtype: :Link,
Rect: [ Rect: [
(area['x'] * width) + TEXT_LEFT_MARGIN, (area['x'] * width) + TEXT_LEFT_MARGIN,
height - (area['y'] * height) - lines[...index].sum(&:height) + height_diff, height - (area['y'] * height) - lines[...index].sum(&:height) +
height_diff - (height_diff.zero? ? diff : 0),
(area['x'] * width) + (area['w'] * width) + TEXT_LEFT_MARGIN, (area['x'] * width) + (area['w'] * width) + TEXT_LEFT_MARGIN,
height - (area['y'] * height) - lines[..next_index].sum(&:height) + height_diff height - (area['y'] * height) - lines[..next_index].sum(&:height) +
height_diff - (height_diff.zero? ? diff : 0)
], ],
A: { Type: :Action, S: :URI, A: { Type: :Action, S: :URI,
URI: ActiveStorage::Blob.proxy_url(attachment.blob) } URI: ActiveStorage::Blob.proxy_url(attachment.blob) }
@@ -201,6 +212,8 @@ 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'
value = TextUtils.maybe_rtl_reverse(Array.wrap(value).join(', ')) value = TextUtils.maybe_rtl_reverse(Array.wrap(value).join(', '))
text = HexaPDF::Layout::TextFragment.create(value, font: pdf.fonts.add(FONT_NAME), text = HexaPDF::Layout::TextFragment.create(value, font: pdf.fonts.add(FONT_NAME),
@@ -292,16 +305,9 @@ module Submissions
if sign_reason if sign_reason
sign_params = { sign_params = {
reason: sign_reason, reason: sign_reason,
certificate: pkcs.certificate, **build_signing_params(pkcs, tsa_url)
key: pkcs.key,
certificate_chain: pkcs.ca_certs || []
} }
if tsa_url
sign_params[:timestamp_handler] = Submissions::TimestampHandler.new(tsa_url:)
sign_params[:signature_size] = 10_000
end
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
@@ -330,11 +336,26 @@ module Submissions
) )
end end
def build_signing_params(pkcs, tsa_url)
params = {
certificate: pkcs.certificate,
key: pkcs.key,
certificate_chain: pkcs.ca_certs || []
}
if tsa_url
params[:timestamp_handler] = Submissions::TimestampHandler.new(tsa_url:)
params[:signature_size] = 20_000
end
params
end
def images_pdf_uuid(attachments) def images_pdf_uuid(attachments)
Digest::UUID.uuid_v5(Digest::UUID::OID_NAMESPACE, attachments.map(&:uuid).sort.join(':')) Digest::UUID.uuid_v5(Digest::UUID::OID_NAMESPACE, attachments.map(&:uuid).sort.join(':'))
end end
def build_pdfs_index(submitter) def build_pdfs_index(submitter, flatten: true)
latest_submitter = find_last_submitter(submitter) latest_submitter = find_last_submitter(submitter)
Submissions::EnsureResultGenerated.call(latest_submitter) if latest_submitter Submissions::EnsureResultGenerated.call(latest_submitter) if latest_submitter
@@ -350,10 +371,14 @@ module Submissions
HexaPDF::Document.new(io: StringIO.new(attachment.download)) HexaPDF::Document.new(io: StringIO.new(attachment.download))
end end
begin pdf = maybe_rotate_pdf(pdf)
pdf.acro_form&.flatten
rescue StandardError => e if flatten
Rollbar.error(e) if defined?(Rollbar) begin
pdf.acro_form&.flatten
rescue StandardError => e
Rollbar.error(e) if defined?(Rollbar)
end
end end
pdf.config['font.on_missing_glyph'] = method(:on_missing_glyph).to_proc pdf.config['font.on_missing_glyph'] = method(:on_missing_glyph).to_proc
@@ -362,6 +387,26 @@ module Submissions
end end
end end
def maybe_rotate_pdf(pdf)
return pdf if pdf.pages.size > MAX_PAGE_ROTATE
is_rotated = pdf.pages.filter_map do |page|
page.rotate(0, flatten: true) if page[:Rotate] != 0
end.present?
return pdf unless is_rotated
io = StringIO.new
pdf.write(io, incremental: false, validate: false)
HexaPDF::Document.new(io:)
rescue StandardError => e
Rollbar.error(e) if defined?(Rollbar)
pdf
end
def on_missing_glyph(character, font_wrapper) def on_missing_glyph(character, font_wrapper)
Rollbar.error("Missing glyph: #{character}") if character.present? && defined?(Rollbar) Rollbar.error("Missing glyph: #{character}") if character.present? && defined?(Rollbar)
@@ -384,6 +429,7 @@ module Submissions
def build_pdf_from_image(attachment) def build_pdf_from_image(attachment)
pdf = HexaPDF::Document.new pdf = HexaPDF::Document.new
page = pdf.pages.add page = pdf.pages.add
scale = [A4_SIZE.first / attachment.metadata['width'].to_f, scale = [A4_SIZE.first / attachment.metadata['width'].to_f,
@@ -406,8 +452,10 @@ module Submissions
format(SIGN_REASON, name:) format(SIGN_REASON, name:)
end end
def single_sign_reason def single_sign_reason(submitter)
SIGN_SIGNLE_REASON signers = submitter.submission.submitters.sort_by(&:completed_at).map { |s| s.email || s.name || s.phone }
format(SIGN_REASON, name: signers.reverse.join(', '))
end end
def fetch_sign_reason(submitter) def fetch_sign_reason(submitter)
@@ -416,16 +464,18 @@ module Submissions
config = config =
if Docuseal.multitenant? if Docuseal.multitenant?
AccountConfig.where(account: submitter.account, key: AccountConfig::ESIGNING_PREFERENCE_KEY) AccountConfig.where(account: submitter.account, key: AccountConfig::ESIGNING_PREFERENCE_KEY)
.first_or_initialize(value: 'multiple') .first_or_initialize(value: 'single')
else else
AccountConfig.where(key: AccountConfig::ESIGNING_PREFERENCE_KEY) AccountConfig.where(key: AccountConfig::ESIGNING_PREFERENCE_KEY)
.first_or_initialize(value: 'multiple') .first_or_initialize(value: 'single')
end end
return sign_reason(reason_name) if config.value == 'multiple' return sign_reason(reason_name) if config.value == 'multiple'
return single_sign_reason if !submitter.submission.submitters.exists?(completed_at: nil) && if !submitter.submission.submitters.exists?(completed_at: nil) &&
submitter.completed_at == submitter.submission.submitters.maximum(:completed_at) submitter.completed_at == submitter.submission.submitters.maximum(:completed_at)
return single_sign_reason(submitter)
end
nil nil
end end
+1
View File
@@ -23,6 +23,7 @@ module Submissions
submitter_params[:fields]&.each do |f| submitter_params[:fields]&.each do |f|
default_values[f[:name].presence || f[:uuid]] = f[:default_value] if f.key?(:default_value) default_values[f[:name].presence || f[:uuid]] = f[:default_value] if f.key?(:default_value)
default_values[f[:name].presence || f[:uuid]] = f[:value] if f.key?(:value)
end end
return submitter_params if default_values.blank? return submitter_params if default_values.blank?
+49
View File
@@ -0,0 +1,49 @@
# frozen_string_literal: true
module Submissions
module SerializeForApi
SERIALIZE_PARAMS = {
only: %i[id source submitters_order created_at updated_at archived_at],
methods: %i[audit_log_url],
include: {
submitters: { only: %i[id slug uuid name email phone
completed_at opened_at sent_at
created_at updated_at external_id metadata],
methods: %i[status application_key] },
template: { only: %i[id name external_id created_at updated_at],
methods: %i[folder_name] },
created_by_user: { only: %i[id email first_name last_name] }
}
}.freeze
module_function
def call(submission, submitters = nil)
submitters ||= submission.submitters.preload(documents_attachments: :blob, attachments_attachments: :blob)
serialized_submitters = submitters.map { |submitter| Submitters::SerializeForApi.call(submitter) }
json = submission.as_json(
SERIALIZE_PARAMS.deep_merge(
include: { submission_events: { only: %i[id submitter_id event_type event_timestamp] } }
)
)
if submitters.all?(&:completed_at?)
last_submitter = submitters.max_by(&:completed_at)
json[:documents] = serialized_submitters.find { |e| e['id'] == last_submitter.id }['documents']
json[:status] = 'completed'
json[:completed_at] = last_submitter.completed_at
else
json[:documents] = []
json[:status] = 'pending'
json[:completed_at] = nil
end
json[:submitters] = serialized_submitters
json
end
end
end
+27 -21
View File
@@ -2,38 +2,44 @@
module Submitters module Submitters
module SerializeForApi module SerializeForApi
SERIALIZE_PARAMS = {
only: %i[id slug uuid name email phone completed_at external_id
submission_id metadata opened_at sent_at created_at updated_at],
methods: %i[status application_key]
}.freeze
module_function module_function
def call(submitter, with_template: false, with_events: false) def call(submitter, with_template: false, with_events: false, with_documents: true, with_urls: false)
ActiveRecord::Associations::Preloader.new( ActiveRecord::Associations::Preloader.new(
records: [submitter], records: [submitter],
associations: [documents_attachments: :blob, attachments_attachments: :blob] associations: if with_documents
[documents_attachments: :blob, attachments_attachments: :blob]
else
[attachments_attachments: :blob]
end
).call ).call
values = SerializeForWebhook.build_values_array(submitter) additional_attrs = {}
documents = SerializeForWebhook.build_documents_array(submitter)
submitter_name = (submitter.submission.template_submitters || additional_attrs['values'] = SerializeForWebhook.build_values_array(submitter)
submitter.submission.template.submitters).find { |e| e['uuid'] == submitter.uuid }['name'] additional_attrs['documents'] = SerializeForWebhook.build_documents_array(submitter) if with_documents
additional_attrs['preferences'] = submitter.preferences.except('default_values')
serialize_params = { additional_attrs['role'] =
include: {}, (submitter.submission.template_submitters ||
only: %i[id slug uuid name email phone completed_at external_id submitter.submission.template.submitters).find { |e| e['uuid'] == submitter.uuid }['name']
submission_id metadata opened_at sent_at created_at updated_at],
methods: %i[status application_key]
}
serialize_params[:include][:template] = { only: %i[id name created_at updated_at] } if with_template if with_urls
additional_attrs['embed_src'] =
if with_events Rails.application.routes.url_helpers.submit_form_url(slug: submitter.slug, **Docuseal.default_url_options)
serialize_params[:include][:submission_events] =
{ as: :events, only: %i[id submitter_id event_type event_timestamp] }
end end
submitter.as_json(serialize_params) include_params = {}
.merge('values' => values, include_params[:template] = { only: %i[id name created_at updated_at] } if with_template
'documents' => documents, include_params[:submission_events] = { only: %i[id submitter_id event_type event_timestamp] } if with_events
'role' => submitter_name)
submitter.as_json(SERIALIZE_PARAMS.merge(include: include_params)).merge(additional_attrs)
end end
end end
end end
+7 -4
View File
@@ -2,6 +2,12 @@
module Submitters module Submitters
module SerializeForWebhook module SerializeForWebhook
SERIALIZE_PARAMS = {
methods: %i[status application_key],
only: %i[id submission_id email phone name ua ip sent_at opened_at
completed_at created_at updated_at external_id metadata]
}.freeze
module_function module_function
def call(submitter) def call(submitter)
@@ -16,10 +22,7 @@ module Submitters
submitter_name = (submitter.submission.template_submitters || submitter_name = (submitter.submission.template_submitters ||
submitter.submission.template.submitters).find { |e| e['uuid'] == submitter.uuid }['name'] submitter.submission.template.submitters).find { |e| e['uuid'] == submitter.uuid }['name']
submitter.as_json(methods: %i[status application_key], submitter.as_json(SERIALIZE_PARAMS)
only: %i[id submission_id email phone name ua ip sent_at opened_at
completed_at created_at updated_at external_id metadata])
.except('uuid', 'values', 'slug')
.merge('role' => submitter_name, .merge('role' => submitter_name,
'preferences' => submitter.preferences.except('default_values'), 'preferences' => submitter.preferences.except('default_values'),
'values' => values, 'values' => values,
+22 -4
View File
@@ -4,10 +4,9 @@ module Templates
module Clone module Clone
module_function module_function
# rubocop:disable Metrics
def call(original_template, author:, external_id: nil, name: nil, folder_name: nil) def call(original_template, author:, external_id: nil, name: nil, folder_name: nil)
original_template_account = original_template.account template = original_template.account.templates.new
template = original_template_account.templates.new
template.external_id = external_id template.external_id = external_id
template.author = author template.author = author
@@ -19,6 +18,7 @@ module Templates
template.folder = TemplateFolders.find_or_create_by_name(author, folder_name) if folder_name.present? template.folder = TemplateFolders.find_or_create_by_name(author, folder_name) if folder_name.present?
submitter_uuids_replacements = {} submitter_uuids_replacements = {}
field_uuids_replacements = {}
cloned_submitters = original_template['submitters'].deep_dup cloned_submitters = original_template['submitters'].deep_dup
cloned_fields = original_template['fields'].deep_dup cloned_fields = original_template['fields'].deep_dup
@@ -31,13 +31,31 @@ module Templates
end end
cloned_fields.each do |field| cloned_fields.each do |field|
field['uuid'] = SecureRandom.uuid new_field_uuid = SecureRandom.uuid
field_uuids_replacements[field['uuid']] = new_field_uuid
field['uuid'] = new_field_uuid
field['submitter_uuid'] = submitter_uuids_replacements[field['submitter_uuid']] field['submitter_uuid'] = submitter_uuids_replacements[field['submitter_uuid']]
end end
replace_fields_regexp = Regexp.union(field_uuids_replacements.keys)
cloned_fields.each do |field|
Array.wrap(field['conditions']).each do |condition|
condition['field_uuid'] = field_uuids_replacements[condition['field_uuid']]
end
if field.dig('preferences', 'formula').present?
field['preferences']['formula'] =
field['preferences']['formula'].gsub(replace_fields_regexp, field_uuids_replacements)
end
end
template.assign_attributes(fields: cloned_fields, submitters: cloned_submitters) template.assign_attributes(fields: cloned_fields, submitters: cloned_submitters)
template template
end end
# rubocop:enable Metrics
end end
end end
+26 -1
View File
@@ -10,6 +10,7 @@ module Templates
Q = 35 Q = 35
MAX_WIDTH = 1400 MAX_WIDTH = 1400
MAX_NUMBER_OF_PAGES_PROCESSED = 15 MAX_NUMBER_OF_PAGES_PROCESSED = 15
MAX_FLATTEN_FILE_SIZE = 15.megabytes
module_function module_function
@@ -43,7 +44,11 @@ module Templates
def generate_pdf_preview_images(attachment, data) def generate_pdf_preview_images(attachment, data)
ActiveStorage::Attachment.where(name: ATTACHMENT_NAME, record: attachment).destroy_all ActiveStorage::Attachment.where(name: ATTACHMENT_NAME, record: attachment).destroy_all
number_of_pages = HexaPDF::Document.new(io: StringIO.new(data)).pages.size
pdf = HexaPDF::Document.new(io: StringIO.new(data))
number_of_pages = pdf.pages.size
data = maybe_flatten_form(data, pdf, attachment)
attachment.metadata['pdf'] ||= {} attachment.metadata['pdf'] ||= {}
attachment.metadata['pdf']['number_of_pages'] = number_of_pages attachment.metadata['pdf']['number_of_pages'] = number_of_pages
@@ -69,6 +74,26 @@ module Templates
end end
end end
def maybe_flatten_form(data, pdf, attachment)
return data if data.size > MAX_FLATTEN_FILE_SIZE
return data if pdf.acro_form.blank?
return data if attachment.record.account.account_configs
.find_or_initialize_by(key: AccountConfig::FLATTEN_RESULT_PDF_KEY).value == false
io = StringIO.new
pdf.acro_form&.flatten
pdf.write(io, incremental: false, validate: false)
io.string
rescue StandardError => e
raise if Rails.env.development?
Rollbar.error(e) if defined?(Rollbar)
data
end
def generate_pdf_preview_from_file(attachment, file_path, page_number) def generate_pdf_preview_from_file(attachment, file_path, page_number)
io = StringIO.new io = StringIO.new
+6 -4
View File
@@ -2,13 +2,15 @@
module Templates module Templates
module SerializeForApi module SerializeForApi
SERIALIZE_PARAMS = {
methods: %i[application_key folder_name],
include: { author: { only: %i[id email first_name last_name] } }
}.freeze
module_function module_function
def call(template, schema_documents = template.schema_documents, preview_image_attachments = []) def call(template, schema_documents = template.schema_documents, preview_image_attachments = [])
json = template.as_json( json = template.as_json(SERIALIZE_PARAMS)
methods: %i[application_key folder_name],
include: { author: { only: %i[id email first_name last_name] } }
)
json[:documents] = template.schema.map do |item| json[:documents] = template.schema.map do |item|
attachment = schema_documents.find { |e| e.uuid == item['attachment_uuid'] } attachment = schema_documents.find { |e| e.uuid == item['attachment_uuid'] }
+1
View File
@@ -15,6 +15,7 @@ module TimeUtils
YEAR_FORMATS = { YEAR_FORMATS = {
'YYYY' => '%Y', 'YYYY' => '%Y',
'YYY' => '%Y',
'YY' => '%y' 'YY' => '%y'
}.freeze }.freeze
+2 -1
View File
@@ -27,7 +27,7 @@
"postcss-loader": "^7.3.0", "postcss-loader": "^7.3.0",
"sass": "^1.62.1", "sass": "^1.62.1",
"sass-loader": "^13.2.2", "sass-loader": "^13.2.2",
"shakapacker": "7.2.3", "shakapacker": "8.0.0",
"signature_pad": "^4.1.5", "signature_pad": "^4.1.5",
"snarkdown": "^2.0.0", "snarkdown": "^2.0.0",
"tailwindcss": "^3.3.2", "tailwindcss": "^3.3.2",
@@ -42,6 +42,7 @@
"webpack-dev-server": "^4.15.0", "webpack-dev-server": "^4.15.0",
"webpack-merge": "5" "webpack-merge": "5"
}, },
"packageManager": "yarn@1.0",
"version": "0.1.0", "version": "0.1.0",
"scripts": { "scripts": {
"eslint": "./node_modules/eslint/bin/eslint.js \"app/javascript/**/*.{js,vue}\" --fix" "eslint": "./node_modules/eslint/bin/eslint.js \"app/javascript/**/*.{js,vue}\" --fix"
+7
View File
@@ -0,0 +1,7 @@
# frozen_string_literal: true
FactoryBot.define do
factory :account_config do
account
end
end
+7
View File
@@ -0,0 +1,7 @@
# frozen_string_literal: true
FactoryBot.define do
factory :user_config do
user
end
end
+100
View File
@@ -0,0 +1,100 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe 'Notifications Settings' do
let(:user) { create(:user, account: create(:account)) }
before do
sign_in(user)
end
it 'shows the notifications settings page' do
visit settings_notifications_path
expect(page).to have_content('Email Notifications')
expect(page).to have_content('Receive notification emails on completed submission')
expect(page).to have_content('Sign Request Email Reminders')
expect(page).to have_field('account_config[value]')
expect(page).to have_field('user_config[value]')
%w[first_duration second_duration third_duration].each do |duration|
expect(page).to have_field("account_config[value][#{duration}]")
end
end
context 'when changes email notifications settings' do
it 'updates BCC email address' do
visit settings_notifications_path
fill_in 'account_config[value]', with: 'john.doe@example.com'
all(:button, 'Save')[0].click
expect(page).to have_content('Changes have been saved')
account_config = AccountConfig.find_by(account: user.account, key: AccountConfig::BCC_EMAILS)
expect(account_config.value).to eq('john.doe@example.com')
end
it 'delete BCC email address' do
create(:account_config, account: user.account, key: AccountConfig::BCC_EMAILS, value: 'john.doe.bbc@example.com')
visit settings_notifications_path
expect(page).to have_field('account_config[value]', with: 'john.doe.bbc@example.com')
fill_in 'account_config[value]', with: ''
all(:button, 'Save')[0].click
expect(page).to have_content('Changes have been saved')
end
it 'disable receive notification emails on completed submission' do
visit settings_notifications_path
uncheck 'user_config[value]'
expect(UserConfig.find_by(user:, key: UserConfig::RECEIVE_COMPLETED_EMAIL).value).to be false
end
it 'enable receive notification emails on completed submission' do
create(:user_config, user:, key: UserConfig::RECEIVE_COMPLETED_EMAIL, value: false)
visit settings_notifications_path
check 'user_config[value]'
expect(UserConfig.find_by(user:, key: UserConfig::RECEIVE_COMPLETED_EMAIL).value).to be true
end
end
context 'when changes sign request email reminders settings' do
it 'updates first reminder duration' do
visit settings_notifications_path
selected_values = %w[first_duration second_duration third_duration].index_with do |_|
AccountConfigs::REMINDER_DURATIONS.keys.sample
end
selected_values.each do |duration, value|
selected_duration_name = AccountConfigs::REMINDER_DURATIONS[value]
select selected_duration_name, from: "account_config[value][#{duration}]"
end
expect do
all(:button, 'Save')[1].click
end.to change(AccountConfig, :count).by(1)
account_config = AccountConfig.find_by(account: user.account, key: AccountConfig::SUBMITTER_REMINDERS)
expect(page).to have_content('Changes have been saved')
selected_values.each do |duration, value|
expect(account_config.value[duration]).to eq(value)
end
end
end
end
@@ -0,0 +1,19 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe 'Personalization Settings', :js do
let!(:account) { create(:account) }
let!(:user) { create(:user, account:) }
before do
sign_in(user)
visit settings_personalization_path
end
it 'shows the notifications settings page' do
expect(page).to have_content('Email Templates')
expect(page).to have_content('Company Logo')
expect(page).to have_content('Submission Form')
end
end
+5 -6
View File
@@ -3126,7 +3126,7 @@ glob@7.1.6:
once "^1.3.0" once "^1.3.0"
path-is-absolute "^1.0.0" path-is-absolute "^1.0.0"
glob@^7.1.3, glob@^7.2.0: glob@^7.1.3:
version "7.2.3" version "7.2.3"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==
@@ -4882,12 +4882,11 @@ setprototypeof@1.2.0:
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424"
integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==
shakapacker@7.2.3: shakapacker@8.0.0:
version "7.2.3" version "8.0.0"
resolved "https://registry.yarnpkg.com/shakapacker/-/shakapacker-7.2.3.tgz#12613e8ca3139e47d42003f8d01846dea28f6c40" resolved "https://registry.yarnpkg.com/shakapacker/-/shakapacker-8.0.0.tgz#f29537c19078af7318758c92e7a1bca4cee96bdd"
integrity sha512-+qF74Y6+hEARqtsmJmBtdc2L0q6eXO21PlPsvtw6aqoxtqhM2VpCCDt51/5Cqnj/5IyEOEICZqStRCSO/q38hg== integrity sha512-HCdpITzIKXzGEyUWQhKzPbpwwOsgTamaPH+0kXdhM59VQxZ3NWnT5cL3DlJdAT3sGsWCJskEl3eMkQlnh9DjhA==
dependencies: dependencies:
glob "^7.2.0"
js-yaml "^4.1.0" js-yaml "^4.1.0"
path-complete-extname "^1.0.0" path-complete-extname "^1.0.0"