Compare commits

..
2 Commits
Author SHA1 Message Date
Pete Matsyburka e64f187f85 fix font url 2024-08-12 13:55:36 +03:00
Pete Matsyburka 416685334a adjust retries 2024-08-12 13:54:13 +03:00
389 changed files with 3241 additions and 17422 deletions

No files matched your search

+3 -5
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.3.4 ruby-version: 3.3.3
- 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.3.4 ruby-version: 3.3.3
- name: Cache gems - name: Cache gems
uses: actions/cache@v1 uses: actions/cache@v1
with: with:
@@ -100,15 +100,13 @@ jobs:
- name: Install Ruby - name: Install Ruby
uses: ruby/setup-ruby@v1 uses: ruby/setup-ruby@v1
with: with:
ruby-version: 3.3.4 ruby-version: 3.3.3
- name: Set up Node - name: Set up Node
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 20.9.0 node-version: 20.9.0
- name: Install Chrome - name: Install Chrome
uses: browser-actions/setup-chrome@latest uses: browser-actions/setup-chrome@latest
with:
chrome-version: 125
- name: Cache node_modules - name: Cache node_modules
uses: actions/cache@v1 uses: actions/cache@v1
with: with:
+5 -11
View File
@@ -34,7 +34,6 @@ Metrics/MethodLength:
Max: 30 Max: 30
Exclude: Exclude:
- 'db/migrate/**' - 'db/migrate/**'
- 'spec/**/*'
Metrics/CyclomaticComplexity: Metrics/CyclomaticComplexity:
Max: 15 Max: 15
@@ -47,29 +46,24 @@ Layout/LineLength:
Metrics/AbcSize: Metrics/AbcSize:
Max: 45 Max: 45
Exclude:
- spec/**/*
Metrics/ModuleLength: Metrics/ModuleLength:
Max: 1000 Max: 500
Metrics/ClassLength: Metrics/ClassLength:
Max: 1000 Max: 500
RSpec/NestedGroups: RSpec/NestedGroups:
Max: 6 Max: 6
RSpec/MultipleExpectations: RSpec/MultipleExpectations:
Max: 25 Max: 20
RSpec/ExampleLength: RSpec/ExampleLength:
Max: 50 Max: 40
RSpec/MultipleMemoizedHelpers: RSpec/MultipleMemoizedHelpers:
Max: 9 Max: 6
Metrics/BlockNesting:
Max: 4
Rails/I18nLocaleTexts: Rails/I18nLocaleTexts:
Enabled: false Enabled: false
+5 -6
View File
@@ -1,4 +1,4 @@
FROM ruby:3.3.4-alpine as fonts FROM ruby:3.3.3-alpine 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.3.4-alpine as webpack FROM ruby:3.3.3-alpine as webpack
ENV RAILS_ENV=production ENV RAILS_ENV=production
ENV NODE_ENV=production ENV NODE_ENV=production
@@ -32,7 +32,7 @@ COPY ./app/views ./app/views
RUN echo "gem 'shakapacker'" > Gemfile && ./bin/shakapacker RUN echo "gem 'shakapacker'" > Gemfile && ./bin/shakapacker
FROM ruby:3.3.4-alpine as app FROM ruby:3.3.3-alpine as app
ENV RAILS_ENV=production ENV RAILS_ENV=production
ENV BUNDLE_WITHOUT="development:test" ENV BUNDLE_WITHOUT="development:test"
@@ -41,7 +41,7 @@ ENV OPENSSL_CONF=/app/openssl_legacy.cnf
WORKDIR /app WORKDIR /app
RUN echo '@edge https://dl-cdn.alpinelinux.org/alpine/edge/community' >> /etc/apk/repositories && apk add --no-cache sqlite-dev libpq-dev mariadb-dev vips-dev vips-poppler poppler-utils redis libheif@edge vips-heif gcompat ttf-freefont && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf RUN apk add --no-cache sqlite-dev libpq-dev mariadb-dev vips-dev vips-poppler poppler-utils redis vips-heif gcompat ttf-freefont && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf
RUN echo $'.include = /etc/ssl/openssl.cnf\n\ RUN echo $'.include = /etc/ssl/openssl.cnf\n\
\n\ \n\
@@ -57,7 +57,7 @@ activate = 1' >> /app/openssl_legacy.cnf
COPY ./Gemfile ./Gemfile.lock ./ COPY ./Gemfile ./Gemfile.lock ./
RUN apk add --no-cache build-base && bundle install && apk del --no-cache build-base && rm -rf ~/.bundle /usr/local/bundle/cache && ruby -e "puts Dir['/usr/local/bundle/**/{spec,rdoc,resources/shared,resources/collation,resources/locales}']" | xargs rm -rf 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
@@ -68,7 +68,6 @@ COPY ./lib ./lib
COPY ./public ./public COPY ./public ./public
COPY ./tmp ./tmp COPY ./tmp ./tmp
COPY LICENSE README.md Rakefile config.ru .version ./ COPY LICENSE README.md Rakefile config.ru .version ./
COPY .version ./public/version
COPY --from=fonts /fonts/GoNotoKurrent-Regular.ttf /fonts/GoNotoKurrent-Bold.ttf /fonts/DancingScript-Regular.otf /fonts/OFL.txt /fonts COPY --from=fonts /fonts/GoNotoKurrent-Regular.ttf /fonts/GoNotoKurrent-Bold.ttf /fonts/DancingScript-Regular.otf /fonts/OFL.txt /fonts
COPY --from=fonts /fonts/FreeSans.ttf /usr/share/fonts/freefont COPY --from=fonts /fonts/FreeSans.ttf /usr/share/fonts/freefont
+2 -2
View File
@@ -2,7 +2,7 @@
source 'https://rubygems.org' source 'https://rubygems.org'
ruby '3.3.4' ruby '3.3.3'
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
@@ -39,7 +39,7 @@ gem 'ruby-vips'
gem 'rubyXL' gem 'rubyXL'
gem 'shakapacker' gem 'shakapacker'
gem 'sidekiq' gem 'sidekiq'
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
+188 -193
View File
@@ -1,101 +1,105 @@
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
actioncable (7.2.1.1) actioncable (7.1.3.4)
actionpack (= 7.2.1.1) actionpack (= 7.1.3.4)
activesupport (= 7.2.1.1) activesupport (= 7.1.3.4)
nio4r (~> 2.0) nio4r (~> 2.0)
websocket-driver (>= 0.6.1) websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6) zeitwerk (~> 2.6)
actionmailbox (7.2.1.1) actionmailbox (7.1.3.4)
actionpack (= 7.2.1.1) actionpack (= 7.1.3.4)
activejob (= 7.2.1.1) activejob (= 7.1.3.4)
activerecord (= 7.2.1.1) activerecord (= 7.1.3.4)
activestorage (= 7.2.1.1) activestorage (= 7.1.3.4)
activesupport (= 7.2.1.1) activesupport (= 7.1.3.4)
mail (>= 2.8.0) mail (>= 2.7.1)
actionmailer (7.2.1.1) net-imap
actionpack (= 7.2.1.1) net-pop
actionview (= 7.2.1.1) net-smtp
activejob (= 7.2.1.1) actionmailer (7.1.3.4)
activesupport (= 7.2.1.1) actionpack (= 7.1.3.4)
mail (>= 2.8.0) actionview (= 7.1.3.4)
activejob (= 7.1.3.4)
activesupport (= 7.1.3.4)
mail (~> 2.5, >= 2.5.4)
net-imap
net-pop
net-smtp
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
actionpack (7.2.1.1) actionpack (7.1.3.4)
actionview (= 7.2.1.1) actionview (= 7.1.3.4)
activesupport (= 7.2.1.1) activesupport (= 7.1.3.4)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
racc racc
rack (>= 2.2.4, < 3.2) rack (>= 2.2.4)
rack-session (>= 1.0.1) rack-session (>= 1.0.1)
rack-test (>= 0.6.3) rack-test (>= 0.6.3)
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6) rails-html-sanitizer (~> 1.6)
useragent (~> 0.16) actiontext (7.1.3.4)
actiontext (7.2.1.1) actionpack (= 7.1.3.4)
actionpack (= 7.2.1.1) activerecord (= 7.1.3.4)
activerecord (= 7.2.1.1) activestorage (= 7.1.3.4)
activestorage (= 7.2.1.1) activesupport (= 7.1.3.4)
activesupport (= 7.2.1.1)
globalid (>= 0.6.0) globalid (>= 0.6.0)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
actionview (7.2.1.1) actionview (7.1.3.4)
activesupport (= 7.2.1.1) activesupport (= 7.1.3.4)
builder (~> 3.1) builder (~> 3.1)
erubi (~> 1.11) erubi (~> 1.11)
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6) rails-html-sanitizer (~> 1.6)
activejob (7.2.1.1) activejob (7.1.3.4)
activesupport (= 7.2.1.1) activesupport (= 7.1.3.4)
globalid (>= 0.3.6) globalid (>= 0.3.6)
activemodel (7.2.1.1) activemodel (7.1.3.4)
activesupport (= 7.2.1.1) activesupport (= 7.1.3.4)
activerecord (7.2.1.1) activerecord (7.1.3.4)
activemodel (= 7.2.1.1) activemodel (= 7.1.3.4)
activesupport (= 7.2.1.1) activesupport (= 7.1.3.4)
timeout (>= 0.4.0) timeout (>= 0.4.0)
activestorage (7.2.1.1) activestorage (7.1.3.4)
actionpack (= 7.2.1.1) actionpack (= 7.1.3.4)
activejob (= 7.2.1.1) activejob (= 7.1.3.4)
activerecord (= 7.2.1.1) activerecord (= 7.1.3.4)
activesupport (= 7.2.1.1) activesupport (= 7.1.3.4)
marcel (~> 1.0) marcel (~> 1.0)
activesupport (7.2.1.1) activesupport (7.1.3.4)
base64 base64
bigdecimal bigdecimal
concurrent-ruby (~> 1.0, >= 1.3.1) concurrent-ruby (~> 1.0, >= 1.0.2)
connection_pool (>= 2.2.5) connection_pool (>= 2.2.5)
drb drb
i18n (>= 1.6, < 2) i18n (>= 1.6, < 2)
logger (>= 1.4.2)
minitest (>= 5.1) minitest (>= 5.1)
securerandom (>= 0.3) mutex_m
tzinfo (~> 2.0, >= 2.0.5) tzinfo (~> 2.0)
addressable (2.8.7) addressable (2.8.6)
public_suffix (>= 2.0.2, < 7.0) public_suffix (>= 2.0.2, < 6.0)
annotate (3.2.0) annotate (3.2.0)
activerecord (>= 3.2, < 8.0) activerecord (>= 3.2, < 8.0)
rake (>= 10.4, < 14.0) rake (>= 10.4, < 14.0)
arabic-letter-connector (0.1.1) arabic-letter-connector (0.1.1)
ast (2.4.2) ast (2.4.2)
aws-eventstream (1.3.0) aws-eventstream (1.3.0)
aws-partitions (1.985.0) aws-partitions (1.913.0)
aws-sdk-core (3.209.1) aws-sdk-core (3.191.6)
aws-eventstream (~> 1, >= 1.3.0) aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.651.0) aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.9) aws-sigv4 (~> 1.8)
jmespath (~> 1, >= 1.6.1) jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.94.0) aws-sdk-kms (1.79.0)
aws-sdk-core (~> 3, >= 3.207.0) aws-sdk-core (~> 3, >= 3.191.0)
aws-sigv4 (~> 1.5) aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.167.0) aws-sdk-s3 (1.146.1)
aws-sdk-core (~> 3, >= 3.207.0) aws-sdk-core (~> 3, >= 3.191.0)
aws-sdk-kms (~> 1) aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5) aws-sigv4 (~> 1.8)
aws-sdk-secretsmanager (1.108.0) aws-sdk-secretsmanager (1.91.0)
aws-sdk-core (~> 3, >= 3.207.0) aws-sdk-core (~> 3, >= 3.191.0)
aws-sigv4 (~> 1.5) aws-sigv4 (~> 1.1)
aws-sigv4 (1.10.0) aws-sigv4 (1.8.0)
aws-eventstream (~> 1, >= 1.0.2) aws-eventstream (~> 1, >= 1.0.2)
azure-storage-blob (2.0.3) azure-storage-blob (2.0.3)
azure-storage-common (~> 2.0) azure-storage-common (~> 2.0)
@@ -116,14 +120,14 @@ GEM
smart_properties smart_properties
bigdecimal (3.1.8) bigdecimal (3.1.8)
bindex (0.8.1) bindex (0.8.1)
bootsnap (1.18.4) bootsnap (1.18.3)
msgpack (~> 1.2) msgpack (~> 1.2)
builder (3.3.0) builder (3.3.0)
bullet (7.2.0) bullet (7.1.6)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
uniform_notifier (~> 1.11) uniform_notifier (~> 1.11)
camertron-eprun (1.1.1) camertron-eprun (1.1.1)
cancancan (3.6.1) cancancan (3.5.0)
capybara (3.40.0) capybara (3.40.0)
addressable addressable
matrix matrix
@@ -133,8 +137,7 @@ GEM
rack-test (>= 0.6.3) rack-test (>= 0.6.3)
regexp_parser (>= 1.5, < 3.0) regexp_parser (>= 1.5, < 3.0)
xpath (~> 3.2) xpath (~> 3.2)
childprocess (5.1.0) childprocess (5.0.0)
logger (~> 1.5)
chunky_png (1.4.0) chunky_png (1.4.0)
cldr-plurals-runtime-rb (1.1.0) cldr-plurals-runtime-rb (1.1.0)
cmdparse (3.0.7) cmdparse (3.0.7)
@@ -145,12 +148,12 @@ GEM
bigdecimal bigdecimal
rexml rexml
crass (1.0.6) crass (1.0.6)
css_parser (1.19.0) css_parser (1.17.1)
addressable addressable
csv (3.3.0) csv (3.3.0)
cuprite (0.15.1) cuprite (0.15)
capybara (~> 3.0) capybara (~> 3.0)
ferrum (~> 0.15.0) ferrum (~> 0.14.0)
date (3.3.4) date (3.3.4)
debug (1.9.2) debug (1.9.2)
irb (~> 1.10) irb (~> 1.10)
@@ -162,7 +165,7 @@ GEM
railties (>= 4.1.0) railties (>= 4.1.0)
responders responders
warden (~> 1.2.3) warden (~> 1.2.3)
devise-two-factor (6.0.0) devise-two-factor (5.0.0)
activesupport (~> 7.0) activesupport (~> 7.0)
devise (~> 4.0) devise (~> 4.0)
railties (~> 7.0) railties (~> 7.0)
@@ -170,26 +173,26 @@ GEM
diff-lcs (1.5.1) diff-lcs (1.5.1)
digest-crc (0.6.5) digest-crc (0.6.5)
rake (>= 12.0.0, < 14.0.0) rake (>= 12.0.0, < 14.0.0)
docile (1.4.1) docile (1.4.0)
dotenv (3.1.4) dotenv (3.1.0)
drb (2.2.1) drb (2.2.1)
email_typo (0.2.3) email_typo (0.2.3)
erb_lint (0.6.0) erb_lint (0.5.0)
activesupport activesupport
better_html (>= 2.0.1) better_html (>= 2.0.1)
parser (>= 2.7.1.4) parser (>= 2.7.1.4)
rainbow rainbow
rubocop (>= 1) rubocop
smart_properties smart_properties
erubi (1.13.0) erubi (1.13.0)
factory_bot (6.5.0) factory_bot (6.4.6)
activesupport (>= 5.0.0) activesupport (>= 5.0.0)
factory_bot_rails (6.4.3) factory_bot_rails (6.4.3)
factory_bot (~> 6.4) factory_bot (~> 6.4)
railties (>= 5.0.0) railties (>= 5.0.0)
faker (3.4.2) faker (3.3.1)
i18n (>= 1.8.11, < 2) i18n (>= 1.8.11, < 2)
faraday (1.10.4) faraday (1.10.3)
faraday-em_http (~> 1.0) faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0) faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1) faraday-excon (~> 1.1)
@@ -209,96 +212,93 @@ GEM
faraday-httpclient (1.0.1) faraday-httpclient (1.0.1)
faraday-multipart (1.0.4) faraday-multipart (1.0.4)
multipart-post (~> 2) multipart-post (~> 2)
faraday-net_http (1.0.2) faraday-net_http (1.0.1)
faraday-net_http_persistent (1.2.0) faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0) faraday-patron (1.0.0)
faraday-rack (1.0.0) faraday-rack (1.0.0)
faraday-retry (1.0.3) faraday-retry (1.0.3)
faraday_middleware (1.2.1) faraday_middleware (1.2.0)
faraday (~> 1.0) faraday (~> 1.0)
ferrum (0.15) ferrum (0.14)
addressable (~> 2.5) addressable (~> 2.5)
concurrent-ruby (~> 1.1) concurrent-ruby (~> 1.1)
webrick (~> 1.7) webrick (~> 1.7)
websocket-driver (~> 0.7) websocket-driver (>= 0.6, < 0.8)
ffi (1.17.0) ffi (1.16.3)
ffi (1.17.0-aarch64-linux-gnu)
ffi (1.17.0-x86_64-linux-gnu)
geom2d (0.4.1) geom2d (0.4.1)
globalid (1.2.1) globalid (1.2.1)
activesupport (>= 6.1) activesupport (>= 6.1)
google-apis-core (0.15.1) google-apis-core (0.14.1)
addressable (~> 2.5, >= 2.5.1) addressable (~> 2.5, >= 2.5.1)
googleauth (~> 1.9) googleauth (~> 1.9)
httpclient (>= 2.8.3, < 3.a) httpclient (>= 2.8.1, < 3.a)
mini_mime (~> 1.0) mini_mime (~> 1.0)
mutex_m
representable (~> 3.0) representable (~> 3.0)
retriable (>= 2.0, < 4.a) retriable (>= 2.0, < 4.a)
google-apis-iamcredentials_v1 (0.21.0) rexml
google-apis-core (>= 0.15.0, < 2.a) google-apis-iamcredentials_v1 (0.20.0)
google-apis-storage_v1 (0.46.0) google-apis-core (>= 0.14.0, < 2.a)
google-apis-core (>= 0.15.0, < 2.a) google-apis-storage_v1 (0.37.0)
google-cloud-core (1.7.1) google-apis-core (>= 0.14.0, < 2.a)
google-cloud-core (1.7.0)
google-cloud-env (>= 1.0, < 3.a) google-cloud-env (>= 1.0, < 3.a)
google-cloud-errors (~> 1.0) google-cloud-errors (~> 1.0)
google-cloud-env (2.2.1) google-cloud-env (2.1.1)
faraday (>= 1.0, < 3.a) faraday (>= 1.0, < 3.a)
google-cloud-errors (1.4.0) google-cloud-errors (1.4.0)
google-cloud-storage (1.52.0) google-cloud-storage (1.49.0)
addressable (~> 2.8) addressable (~> 2.8)
digest-crc (~> 0.4) digest-crc (~> 0.4)
google-apis-core (~> 0.13) google-apis-core (~> 0.13)
google-apis-iamcredentials_v1 (~> 0.18) google-apis-iamcredentials_v1 (~> 0.18)
google-apis-storage_v1 (~> 0.38) google-apis-storage_v1 (~> 0.33)
google-cloud-core (~> 1.6) google-cloud-core (~> 1.6)
googleauth (~> 1.9) googleauth (~> 1.9)
mini_mime (~> 1.0) mini_mime (~> 1.0)
googleauth (1.11.1) googleauth (1.11.0)
faraday (>= 1.0, < 3.a) faraday (>= 1.0, < 3.a)
google-cloud-env (~> 2.1) google-cloud-env (~> 2.1)
jwt (>= 1.4, < 3.0) jwt (>= 1.4, < 3.0)
multi_json (~> 1.11) multi_json (~> 1.11)
os (>= 0.9, < 2.0) os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a) signet (>= 0.16, < 2.a)
hashdiff (1.1.1) hashdiff (1.1.0)
hexapdf (0.47.0) hexapdf (0.42.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.6) i18n (1.14.5)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
image_processing (1.13.0) 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.14.1) irb (1.13.1)
rdoc (>= 4.0.0) rdoc (>= 4.0.0)
reline (>= 0.4.2) reline (>= 0.4.2)
jmespath (1.6.2) jmespath (1.6.2)
json (2.7.2) json (2.7.2)
jwt (2.9.3) jwt (2.8.1)
base64 base64
language_server-protocol (3.17.0.3) language_server-protocol (3.17.0.3)
launchy (3.0.1) launchy (3.0.0)
addressable (~> 2.8) addressable (~> 2.8)
childprocess (~> 5.0) childprocess (~> 5.0)
letter_opener (1.10.0) letter_opener (1.10.0)
launchy (>= 2.2, < 4) launchy (>= 2.2, < 4)
letter_opener_web (3.0.0) letter_opener_web (2.0.0)
actionmailer (>= 6.1) actionmailer (>= 5.2)
letter_opener (~> 1.9) letter_opener (~> 1.7)
railties (>= 6.1) railties (>= 5.2)
rexml rexml
logger (1.6.1)
lograge (0.14.0) lograge (0.14.0)
actionpack (>= 4) actionpack (>= 4)
activesupport (>= 4) activesupport (>= 4)
railties (>= 4) railties (>= 4)
request_store (~> 1.0) request_store (~> 1.0)
loofah (2.23.1) loofah (2.22.0)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.12.0) nokogiri (>= 1.12.0)
mail (2.8.1) mail (2.8.1)
@@ -309,51 +309,48 @@ GEM
marcel (1.0.4) marcel (1.0.4)
matrix (0.4.2) matrix (0.4.2)
method_source (1.1.0) method_source (1.1.0)
mini_magick (4.13.2) mini_magick (4.12.0)
mini_mime (1.1.5) mini_mime (1.1.5)
mini_portile2 (2.8.8) mini_portile2 (2.8.7)
minitest (5.25.1) minitest (5.24.1)
msgpack (1.7.3) msgpack (1.7.2)
multi_json (1.15.0) multi_json (1.15.0)
multipart-post (2.4.1) multipart-post (2.4.0)
mutex_m (0.2.0) mutex_m (0.2.0)
mysql2 (0.5.6) mysql2 (0.5.6)
net-http-persistent (4.0.4) net-http-persistent (4.0.2)
connection_pool (~> 2.2) connection_pool (~> 2.2)
net-imap (0.4.17) net-imap (0.4.14)
date date
net-protocol net-protocol
net-pop (0.1.2) net-pop (0.1.2)
net-protocol
net-protocol (0.2.2) net-protocol (0.2.2)
timeout timeout
net-smtp (0.5.0) net-smtp (0.5.0)
net-protocol net-protocol
nio4r (2.7.3) nio4r (2.7.1)
nokogiri (1.16.8) nokogiri (1.16.7)
mini_portile2 (~> 2.8.2) mini_portile2 (~> 2.8.2)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.16.8-aarch64-linux) nokogiri (1.16.7-aarch64-linux)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.16.8-x86_64-linux) nokogiri (1.16.7-x86_64-linux)
racc (~> 1.4) racc (~> 1.4)
oj (3.16.6) oj (3.16.3)
bigdecimal (>= 3.0) bigdecimal (>= 3.0)
ostruct (>= 0.2)
openssl (3.2.0) openssl (3.2.0)
orm_adapter (0.5.0) orm_adapter (0.5.0)
os (1.1.4) os (1.1.4)
ostruct (0.6.0)
package_json (0.1.0) package_json (0.1.0)
pagy (9.1.0) pagy (8.1.2)
parallel (1.26.3) parallel (1.25.1)
parser (3.3.5.0) parser (3.3.4.0)
ast (~> 2.4.1) ast (~> 2.4.1)
racc racc
pg (1.5.8) pg (1.5.6)
premailer (1.27.0) premailer (1.23.0)
addressable addressable
css_parser (>= 1.19.0) css_parser (>= 1.12.0)
htmlentities (>= 4.0.0) htmlentities (>= 4.0.0)
premailer-rails (1.12.0) premailer-rails (1.12.0)
actionmailer (>= 3) actionmailer (>= 3)
@@ -364,15 +361,15 @@ GEM
pry (0.14.2) pry (0.14.2)
coderay (~> 1.1) coderay (~> 1.1)
method_source (~> 1.0) method_source (~> 1.0)
pry-rails (0.3.11) pry-rails (0.3.9)
pry (>= 0.13.0) pry (>= 0.10.4)
psych (5.1.2) psych (5.1.2)
stringio stringio
public_suffix (6.0.1) public_suffix (5.0.5)
puma (6.4.3) puma (6.4.2)
nio4r (~> 2.0) nio4r (~> 2.0)
racc (1.8.1) racc (1.8.1)
rack (3.1.8) rack (3.1.7)
rack-proxy (0.7.7) rack-proxy (0.7.7)
rack rack
rack-session (2.0.0) rack-session (2.0.0)
@@ -382,27 +379,27 @@ GEM
rackup (2.1.0) rackup (2.1.0)
rack (>= 3) rack (>= 3)
webrick (~> 1.8) webrick (~> 1.8)
rails (7.2.1.1) rails (7.1.3.4)
actioncable (= 7.2.1.1) actioncable (= 7.1.3.4)
actionmailbox (= 7.2.1.1) actionmailbox (= 7.1.3.4)
actionmailer (= 7.2.1.1) actionmailer (= 7.1.3.4)
actionpack (= 7.2.1.1) actionpack (= 7.1.3.4)
actiontext (= 7.2.1.1) actiontext (= 7.1.3.4)
actionview (= 7.2.1.1) actionview (= 7.1.3.4)
activejob (= 7.2.1.1) activejob (= 7.1.3.4)
activemodel (= 7.2.1.1) activemodel (= 7.1.3.4)
activerecord (= 7.2.1.1) activerecord (= 7.1.3.4)
activestorage (= 7.2.1.1) activestorage (= 7.1.3.4)
activesupport (= 7.2.1.1) activesupport (= 7.1.3.4)
bundler (>= 1.15.0) bundler (>= 1.15.0)
railties (= 7.2.1.1) railties (= 7.1.3.4)
rails-dom-testing (2.2.0) rails-dom-testing (2.2.0)
activesupport (>= 5.0.0) activesupport (>= 5.0.0)
minitest minitest
nokogiri (>= 1.6) nokogiri (>= 1.6)
rails-html-sanitizer (1.6.1) rails-html-sanitizer (1.6.0)
loofah (~> 2.21) loofah (~> 2.21)
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) nokogiri (~> 1.14)
rails-i18n (7.0.9) rails-i18n (7.0.9)
i18n (>= 0.7, < 2) i18n (>= 0.7, < 2)
railties (>= 6.0.0, < 8) railties (>= 6.0.0, < 8)
@@ -410,101 +407,100 @@ GEM
actionview (> 3.1) actionview (> 3.1)
activesupport (> 3.1) activesupport (> 3.1)
railties (> 3.1) railties (> 3.1)
railties (7.2.1.1) railties (7.1.3.4)
actionpack (= 7.2.1.1) actionpack (= 7.1.3.4)
activesupport (= 7.2.1.1) activesupport (= 7.1.3.4)
irb (~> 1.13) irb
rackup (>= 1.0.0) rackup (>= 1.0.0)
rake (>= 12.2) rake (>= 12.2)
thor (~> 1.0, >= 1.2.2) thor (~> 1.0, >= 1.2.2)
zeitwerk (~> 2.6) zeitwerk (~> 2.6)
rainbow (3.1.1) rainbow (3.1.1)
rake (13.2.1) rake (13.2.1)
rdoc (6.7.0) rdoc (6.6.3.1)
psych (>= 4.0.0) psych (>= 4.0.0)
redis-client (0.22.2) redis-client (0.22.2)
connection_pool connection_pool
regexp_parser (2.9.2) regexp_parser (2.9.2)
reline (0.5.10) 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)
trailblazer-option (>= 0.1.1, < 0.2.0) trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0) uber (< 0.2.0)
request_store (1.7.0) request_store (1.6.0)
rack (>= 1.4) rack (>= 1.4)
responders (3.1.1) responders (3.1.1)
actionpack (>= 5.2) actionpack (>= 5.2)
railties (>= 5.2) railties (>= 5.2)
retriable (3.1.2) retriable (3.1.2)
rexml (3.3.9) rexml (3.3.3)
strscan
rotp (6.3.0) rotp (6.3.0)
rqrcode (2.2.0) rqrcode (2.2.0)
chunky_png (~> 1.0) chunky_png (~> 1.0)
rqrcode_core (~> 1.0) rqrcode_core (~> 1.0)
rqrcode_core (1.2.0) rqrcode_core (1.2.0)
rspec-core (3.13.1) rspec-core (3.13.0)
rspec-support (~> 3.13.0) rspec-support (~> 3.13.0)
rspec-expectations (3.13.3) rspec-expectations (3.13.0)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0) rspec-support (~> 3.13.0)
rspec-mocks (3.13.2) rspec-mocks (3.13.0)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0) rspec-support (~> 3.13.0)
rspec-rails (7.0.1) rspec-rails (6.1.2)
actionpack (>= 7.0) actionpack (>= 6.1)
activesupport (>= 7.0) activesupport (>= 6.1)
railties (>= 7.0) railties (>= 6.1)
rspec-core (~> 3.13) rspec-core (~> 3.13)
rspec-expectations (~> 3.13) rspec-expectations (~> 3.13)
rspec-mocks (~> 3.13) rspec-mocks (~> 3.13)
rspec-support (~> 3.13) rspec-support (~> 3.13)
rspec-support (3.13.1) rspec-support (3.13.1)
rubocop (1.66.1) rubocop (1.65.0)
json (~> 2.3) json (~> 2.3)
language_server-protocol (>= 3.17.0) language_server-protocol (>= 3.17.0)
parallel (~> 1.10) parallel (~> 1.10)
parser (>= 3.3.0.2) parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0) rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 2.4, < 3.0) regexp_parser (>= 2.4, < 3.0)
rubocop-ast (>= 1.32.2, < 2.0) rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.31.1, < 2.0)
ruby-progressbar (~> 1.7) ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0) unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.32.3) rubocop-ast (1.31.3)
parser (>= 3.3.1.0) parser (>= 3.3.1.0)
rubocop-performance (1.22.1) rubocop-performance (1.21.0)
rubocop (>= 1.48.1, < 2.0) rubocop (>= 1.48.1, < 2.0)
rubocop-ast (>= 1.31.1, < 2.0) rubocop-ast (>= 1.31.1, < 2.0)
rubocop-rails (2.26.2) rubocop-rails (2.24.1)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
rack (>= 1.1) rack (>= 1.1)
rubocop (>= 1.52.0, < 2.0) rubocop (>= 1.33.0, < 2.0)
rubocop-ast (>= 1.31.1, < 2.0) rubocop-ast (>= 1.31.1, < 2.0)
rubocop-rspec (3.1.0) rubocop-rspec (3.0.3)
rubocop (~> 1.61) rubocop (~> 1.61)
ruby-progressbar (1.13.0) ruby-progressbar (1.13.0)
ruby-vips (2.2.2) ruby-vips (2.2.1)
ffi (~> 1.12) ffi (~> 1.12)
logger
ruby2_keywords (0.0.5) ruby2_keywords (0.0.5)
rubyXL (3.4.27) rubyXL (3.4.27)
nokogiri (>= 1.10.8) nokogiri (>= 1.10.8)
rubyzip (>= 1.3.0) rubyzip (>= 1.3.0)
rubyzip (2.3.2) rubyzip (2.3.2)
securerandom (0.3.1)
semantic_range (3.0.0) semantic_range (3.0.0)
shakapacker (8.0.2) shakapacker (8.0.0)
activesupport (>= 5.2) activesupport (>= 5.2)
package_json package_json
rack-proxy (>= 0.6.1) rack-proxy (>= 0.6.1)
railties (>= 5.2) railties (>= 5.2)
semantic_range (>= 2.3.0) semantic_range (>= 2.3.0)
sidekiq (7.3.2) sidekiq (7.2.4)
concurrent-ruby (< 2) concurrent-ruby (< 2)
connection_pool (>= 2.3.0) connection_pool (>= 2.3.0)
logger
rack (>= 2.2.4) rack (>= 2.2.4)
redis-client (>= 0.22.2) redis-client (>= 0.19.0)
signet (0.19.0) signet (0.19.0)
addressable (~> 2.8) addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a) faraday (>= 0.17.5, < 3.a)
@@ -514,34 +510,33 @@ GEM
docile (~> 1.1) docile (~> 1.1)
simplecov-html (~> 0.11) simplecov-html (~> 0.11)
simplecov_json_formatter (~> 0.1) simplecov_json_formatter (~> 0.1)
simplecov-html (0.13.1) simplecov-html (0.12.3)
simplecov_json_formatter (0.1.4) simplecov_json_formatter (0.1.4)
smart_properties (1.17.0) smart_properties (1.17.0)
sqlite3 (2.1.0) sqlite3 (1.7.3)
mini_portile2 (~> 2.8.0) mini_portile2 (~> 2.8.0)
sqlite3 (2.1.0-aarch64-linux-gnu) stringio (3.1.0)
sqlite3 (2.1.0-x86_64-linux-gnu)
stringio (3.1.1)
strip_attributes (1.13.0) strip_attributes (1.13.0)
activemodel (>= 3.0, < 8.0) activemodel (>= 3.0, < 8.0)
thor (1.3.2) strscan (3.1.0)
thor (1.3.1)
timeout (0.4.1) timeout (0.4.1)
trailblazer-option (0.1.2) trailblazer-option (0.1.2)
turbo-rails (2.0.10) turbo-rails (2.0.5)
actionpack (>= 6.0.0) actionpack (>= 6.0.0)
activejob (>= 6.0.0)
railties (>= 6.0.0) railties (>= 6.0.0)
twitter_cldr (6.12.1) twitter_cldr (6.12.0)
camertron-eprun camertron-eprun
cldr-plurals-runtime-rb (~> 1.1) cldr-plurals-runtime-rb (~> 1.1)
tzinfo tzinfo
tzinfo (2.0.6) tzinfo (2.0.6)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
tzinfo-data (1.2024.2) tzinfo-data (1.2024.1)
tzinfo (>= 1.0.0) tzinfo (>= 1.0.0)
uber (0.1.0) uber (0.1.0)
unicode-display_width (2.6.0) unicode-display_width (2.5.0)
uniform_notifier (1.16.0) uniform_notifier (1.16.0)
useragent (0.16.10)
warden (1.2.9) warden (1.2.9)
rack (>= 2.0.9) rack (>= 2.0.9)
web-console (4.2.1) web-console (4.2.1)
@@ -549,17 +544,17 @@ GEM
activemodel (>= 6.0.0) activemodel (>= 6.0.0)
bindex (>= 0.4.0) bindex (>= 0.4.0)
railties (>= 6.0.0) railties (>= 6.0.0)
webmock (3.24.0) webmock (3.23.0)
addressable (>= 2.8.0) addressable (>= 2.8.0)
crack (>= 0.3.2) crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0) hashdiff (>= 0.4.0, < 2.0.0)
webrick (1.8.2) webrick (1.8.1)
websocket-driver (0.7.6) websocket-driver (0.7.6)
websocket-extensions (>= 0.1.0) websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5) websocket-extensions (0.1.5)
xpath (3.2.0) xpath (3.2.0)
nokogiri (~> 1.8) nokogiri (~> 1.8)
zeitwerk (2.7.0) zeitwerk (2.6.14)
PLATFORMS PLATFORMS
aarch64-linux aarch64-linux
@@ -628,7 +623,7 @@ DEPENDENCIES
webmock webmock
RUBY VERSION RUBY VERSION
ruby 3.3.4p94 ruby 3.3.3p89
BUNDLED WITH BUNDLED WITH
2.5.3 2.5.3
+14 -14
View File
@@ -1,6 +1,6 @@
<h1 align="center" style="border-bottom: none"> <h1 align="center" style="border-bottom: none">
<div> <div>
<a href="https://www.docuseal.com"> <a href="https://www.docuseal.co">
<img alt="DocuSeal" src="https://github.com/docusealco/docuseal/assets/5418788/c12cd051-81cd-4402-bc3a-92f2cfdc1b06" width="80" /> <img alt="DocuSeal" src="https://github.com/docusealco/docuseal/assets/5418788/c12cd051-81cd-4402-bc3a-92f2cfdc1b06" width="80" />
<br> <br>
</a> </a>
@@ -25,12 +25,12 @@
DocuSeal is an open source platform that provides secure and efficient digital document signing and processing. Create PDF forms to have them filled and signed online on any device with an easy-to-use, mobile-optimized web tool. DocuSeal is an open source platform that provides secure and efficient digital document signing and processing. Create PDF forms to have them filled and signed online on any device with an easy-to-use, mobile-optimized web tool.
</p> </p>
<h2 align="center"> <h2 align="center">
<a href="https://demo.docuseal.tech">✨ Live Demo</a> <a href="https://demo.docuseal.co">✨ Live Demo</a>
<span>|</span> <span>|</span>
<a href="https://docuseal.com/sign_up">☁️ Try in Cloud</a> <a href="https://docuseal.co/sign_up">☁️ Try in Cloud</a>
</h2> </h2>
[![Demo](https://github.com/docusealco/docuseal/assets/5418788/d8703ea3-361a-423f-8bfe-eff1bd9dbe14)](https://demo.docuseal.tech) [![Demo](https://github.com/docusealco/docuseal/assets/5418788/d8703ea3-361a-423f-8bfe-eff1bd9dbe14)](https://demo.docuseal.co)
## Features ## Features
- PDF form fields builder (WYSIWYG) - PDF form fields builder (WYSIWYG)
@@ -42,7 +42,7 @@ DocuSeal is an open source platform that provides secure and efficient digital d
- PDF signature verification - PDF signature verification
- Users management - Users management
- Mobile-optimized - Mobile-optimized
- 6 UI languages with signing available in 13 languages - Signing available in 13 languages
- API and Webhooks for integrations - API and Webhooks for integrations
- Easy to deploy in minutes - Easy to deploy in minutes
@@ -54,11 +54,11 @@ DocuSeal is an open source platform that provides secure and efficient digital d
- Conditional fields and formulas - Conditional fields and formulas
- Bulk send with CSV, XLSX spreadsheet import - Bulk send with CSV, XLSX spreadsheet import
- SSO / SAML - SSO / SAML
- Template creation with HTML API ([Guide](https://www.docuseal.com/guides/create-pdf-document-fillable-form-with-html-api)) - Template creation with HTML API ([Guide](https://www.docuseal.co/guides/create-pdf-document-fillable-form-with-html-api))
- Template creation with PDF or DOCX and field tags API ([Guide](https://www.docuseal.com/guides/use-embedded-text-field-tags-in-the-pdf-to-create-a-fillable-form)) - Template creation with PDF or DOCX and field tags API ([Guide](https://www.docuseal.co/guides/use-embedded-text-field-tags-in-the-pdf-to-create-a-fillable-form))
- Embedded signing form ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue), [Angular](https://github.com/docusealco/docuseal-angular) or [JavaScript](https://www.docuseal.com/docs/embedded)) - Embedded signing form ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue), [Angular](https://github.com/docusealco/docuseal-angular) or [JavaScript](https://www.docuseal.co/docs/embedded))
- Embedded document form builder ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue), [Angular](https://github.com/docusealco/docuseal-angular) or [JavaScript](https://www.docuseal.com/docs/embedded)) - Embedded document form builder ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue), [Angular](https://github.com/docusealco/docuseal-angular) or [JavaScript](https://www.docuseal.co/docs/embedded))
- [Learn more](https://www.docuseal.com/pricing) - [Learn more](https://www.docuseal.co/pricing)
## Deploy ## Deploy
@@ -93,7 +93,7 @@ sudo HOST=your-domain-name.com docker compose up
At DocuSeal we have expertise and technologies to make documents creation, filling, signing and processing seamlessly integrated with your product. We specialize in working with various industries, including **Banking, Healthcare, Transport, Real Estate, eCommerce, KYC, CRM, and other software products** that require bulk document signing. By leveraging DocuSeal, we can assist in reducing the overall cost of developing and processing electronic documents while ensuring security and compliance with local electronic document laws. At DocuSeal we have expertise and technologies to make documents creation, filling, signing and processing seamlessly integrated with your product. We specialize in working with various industries, including **Banking, Healthcare, Transport, Real Estate, eCommerce, KYC, CRM, and other software products** that require bulk document signing. By leveraging DocuSeal, we can assist in reducing the overall cost of developing and processing electronic documents while ensuring security and compliance with local electronic document laws.
[Book a Meeting](https://www.docuseal.com/contact) [Book a Meeting](https://www.docuseal.co/contact)
## License ## License
@@ -102,6 +102,6 @@ Unless otherwise noted, all files © 2023 DocuSeal LLC.
## Tools ## Tools
- [Signature Maker](https://www.docuseal.com/online-signature) - [Signature Maker](https://www.docuseal.co/online-signature)
- [Sign Document Online](https://www.docuseal.com/sign-documents-online) - [Sign Document Online](https://www.docuseal.co/sign-documents-online)
- [Fill PDF Online](https://www.docuseal.com/fill-pdf) - [Fill PDF Online](https://www.docuseal.co/fill-pdf)
+1 -1
View File
@@ -1,6 +1,6 @@
# Reporting a Vulnerability # Reporting a Vulnerability
If you come across any security concern or vulnarability, please report the information via email to security@docuseal.com 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.**
@@ -8,7 +8,6 @@ class AccountConfigsController < ApplicationController
AccountConfig::ALLOW_TYPED_SIGNATURE, AccountConfig::ALLOW_TYPED_SIGNATURE,
AccountConfig::FORCE_MFA, AccountConfig::FORCE_MFA,
AccountConfig::ALLOW_TO_RESUBMIT, AccountConfig::ALLOW_TO_RESUBMIT,
AccountConfig::ALLOW_TO_DECLINE_KEY,
AccountConfig::FORM_PREFILL_SIGNATURE_KEY, AccountConfig::FORM_PREFILL_SIGNATURE_KEY,
AccountConfig::ESIGNING_PREFERENCE_KEY, AccountConfig::ESIGNING_PREFERENCE_KEY,
AccountConfig::FORM_WITH_CONFETTI_KEY, AccountConfig::FORM_WITH_CONFETTI_KEY,
@@ -16,9 +15,7 @@ class AccountConfigsController < ApplicationController
AccountConfig::FORCE_SSO_AUTH_KEY, AccountConfig::FORCE_SSO_AUTH_KEY,
AccountConfig::FLATTEN_RESULT_PDF_KEY, AccountConfig::FLATTEN_RESULT_PDF_KEY,
AccountConfig::WITH_SIGNATURE_ID, AccountConfig::WITH_SIGNATURE_ID,
AccountConfig::COMBINE_PDF_RESULT_KEY, AccountConfig::REQUIRE_SIGNING_REASON_KEY
AccountConfig::REQUIRE_SIGNING_REASON_KEY,
AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY
].freeze ].freeze
InvalidKey = Class.new(StandardError) InvalidKey = Class.new(StandardError)
+18 -23
View File
@@ -4,11 +4,11 @@ class AccountsController < ApplicationController
LOCALE_OPTIONS = { LOCALE_OPTIONS = {
'en-US' => 'English (United States)', 'en-US' => 'English (United States)',
'en-GB' => 'English (United Kingdom)', 'en-GB' => 'English (United Kingdom)',
'fr-FR' => 'Français', 'fr-FR' => 'French (France)',
'es-ES' => 'Español', 'es-ES' => 'Spanish (Spain)',
'pt-PT' => 'Português', 'pt-PT' => 'Portuguese (Portugal)',
'de-DE' => 'Deutsch', 'de-DE' => 'German (Germany)',
'it-IT' => 'Italiano' 'it-IT' => 'Italian (Italy)'
}.freeze }.freeze
before_action :load_account before_action :load_account
@@ -19,25 +19,21 @@ class AccountsController < ApplicationController
def update def update
current_account.update!(account_params) current_account.update!(account_params)
unless Docuseal.multitenant? @encrypted_config = EncryptedConfig.find_or_initialize_by(account: current_account,
@encrypted_config = EncryptedConfig.find_or_initialize_by(account: current_account, key: EncryptedConfig::APP_URL_KEY)
key: EncryptedConfig::APP_URL_KEY) @encrypted_config.assign_attributes(app_url_params)
@encrypted_config.assign_attributes(app_url_params)
unless URI.parse(@encrypted_config.value.to_s).class.in?([URI::HTTP, URI::HTTPS]) unless URI.parse(@encrypted_config.value.to_s).class.in?([URI::HTTP, URI::HTTPS])
@encrypted_config.errors.add(:value, I18n.t('should_be_a_valid_url')) @encrypted_config.errors.add(:value, 'should be a valid URL')
return render :show, status: :unprocessable_entity return render :show, status: :unprocessable_entity
end
@encrypted_config.save!
Docuseal.refresh_default_url_options!
end end
with_locale do @encrypted_config.save!
redirect_to settings_account_path, notice: I18n.t('account_information_has_been_updated')
end Docuseal.refresh_default_url_options!
redirect_to settings_account_path, notice: 'Account information has been updated'
rescue ActiveRecord::RecordInvalid rescue ActiveRecord::RecordInvalid
render :show, status: :unprocessable_entity render :show, status: :unprocessable_entity
end end
@@ -47,12 +43,11 @@ class AccountsController < ApplicationController
true_user.update!(locked_at: Time.current) true_user.update!(locked_at: Time.current)
# rubocop:disable Layout/LineLength
render turbo_stream: turbo_stream.replace( render turbo_stream: turbo_stream.replace(
:account_delete_button, :account_delete_button,
html: helpers.tag.p(I18n.t('your_account_removal_request_will_be_processed_within_2_weeks_please_contact_us_if_you_want_to_keep_your_account')) html: helpers.tag.p('Your account removal request will be processed within 2 weeks. ' \
'Please contact us if you want to keep your account.')
) )
# rubocop:enable Layout/LineLength
end end
private private
@@ -38,12 +38,12 @@ module Api
private private
def authorization_check!(blob) def authorization_check!(blob)
attachment = blob.attachments.take is_authorized =
blob.attachments.all? do |a|
is_authorized = attachment.name.in?(%w[logo preview_images]) || a.name.in?(%w[logo preview_images]) ||
(current_user && attachment.record.account.id == current_user.account_id) || (current_user && a.record.account.id == current_user.account_id) ||
!attachment.record.account.account_configs !a.record.account.account_configs.find_or_initialize_by(key: AccountConfig::DOWNLOAD_LINKS_AUTH_KEY).value
.find_or_initialize_by(key: AccountConfig::DOWNLOAD_LINKS_AUTH_KEY).value end
return if is_authorized return if is_authorized
+2 -29
View File
@@ -25,9 +25,9 @@ module Api
render json: { error: 'Too many requests' }, status: :too_many_requests render json: { error: 'Too many requests' }, status: :too_many_requests
end end
unless Rails.env.development? if Rails.env.production?
rescue_from CanCan::AccessDenied do |e| rescue_from CanCan::AccessDenied do |e|
render json: { error: access_denied_error_message(e) }, status: :forbidden render json: { error: e.message }, status: :forbidden
end end
rescue_from JSON::ParserError do |e| rescue_from JSON::ParserError do |e|
@@ -39,33 +39,6 @@ module Api
private private
def access_denied_error_message(error)
return 'Not authorized' if request.headers['X-Auth-Token'].blank?
return 'Not authorized' unless error.subject.is_a?(ActiveRecord::Base)
return 'Not authorized' unless error.subject.respond_to?(:account_id)
linked_account_record_exists =
if current_user.account.testing?
current_user.account.linked_account_accounts.where(account_type: 'testing')
.exists?(account_id: error.subject.account_id)
else
current_user.account.testing_accounts.exists?(id: error.subject.account_id)
end
return 'Not authorized' unless linked_account_record_exists
object_name = error.subject.model_name.human
id = error.subject.id
if current_user.account.testing?
"#{object_name} #{id} not found using testing API key; Use production API key to " \
"access production #{object_name.downcase.pluralize}."
else
"#{object_name} #{id} not found using production API key; Use testing API key to " \
"access testing #{object_name.downcase.pluralize}."
end
end
def paginate(relation, field: :id) def paginate(relation, field: :id)
result = relation.order(field => :desc) result = relation.order(field => :desc)
.limit([params.fetch(:limit, DEFAULT_LIMIT).to_i, MAX_LIMIT].min) .limit([params.fetch(:limit, DEFAULT_LIMIT).to_i, MAX_LIMIT].min)
@@ -11,8 +11,7 @@ module Api
params[:before] = Time.zone.at(params[:before].to_i) if params[:before].present? params[:before] = Time.zone.at(params[:before].to_i) if params[:before].present?
submitters = paginate( submitters = paginate(
submitters.preload(template: :folder, submission: [:submitters, { audit_trail_attachment: :blob, submitters.preload(template: :folder, submission: [:submitters, { audit_trail_attachment: :blob }],
combined_document_attachment: :blob }],
documents_attachments: :blob, attachments_attachments: :blob), documents_attachments: :blob, attachments_attachments: :blob),
field: :completed_at field: :completed_at
) )
@@ -1,45 +0,0 @@
# frozen_string_literal: true
module Api
class SubmissionDocumentsController < ApiBaseController
load_and_authorize_resource :submission
def index
documents =
if @submission.submitters.all?(&:completed_at?)
last_submitter = @submission.submitters.max_by(&:completed_at)
if last_submitter.documents_attachments.blank?
last_submitter.documents_attachments = Submissions::EnsureResultGenerated.call(submitter)
end
last_submitter.documents_attachments
else
values_hash = Submissions::GeneratePreviewAttachments.build_values_hash(@submission)
if @submission.preview_documents.present? &&
@submission.preview_documents.all? { |s| s.metadata['values_hash'] == values_hash }
@submission.preview_documents
else
ApplicationRecord.no_touching do
@submission.preview_documents.each(&:destroy)
end
Submissions::GeneratePreviewAttachments.call(@submission, values_hash:)
end
end
ActiveRecord::Associations::Preloader.new(
records: documents,
associations: [:blob]
).call
render json: {
id: @submission.id,
documents: documents.map do |attachment|
{ name: attachment.filename.base, url: ActiveStorage::Blob.proxy_url(attachment.blob) }
end
}
end
end
end
@@ -1,52 +0,0 @@
# frozen_string_literal: true
module Api
class SubmissionEventsController < ApiBaseController
load_and_authorize_resource :submission, parent: false
def index
submissions = build_completed_query(@submissions)
params[:after] = Time.zone.at(params[:after].to_i) if params[:after].present?
params[:before] = Time.zone.at(params[:before].to_i) if params[:before].present?
submissions = paginate(submissions.preload(
:created_by_user, :submission_events,
template: :folder,
submitters: { documents_attachments: :blob, attachments_attachments: :blob },
audit_trail_attachment: :blob
),
field: :completed_at)
render json: {
data: submissions.map do |s|
{
event_type: 'submission.completed',
timestamp: s.completed_at,
data: Submissions::SerializeForApi.call(s, s.submitters)
}
end,
pagination: {
count: submissions.size,
next: submissions.last&.completed_at&.to_i,
prev: submissions.first&.completed_at&.to_i
}
}
end
private
def build_completed_query(submissions)
submissions = submissions.where(
Submitter.where(completed_at: nil).where(
Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id])
).select(1).arel.exists.not
)
submissions.joins(:submitters)
.group(:id)
.select(Submission.arel_table[Arel.star],
Submitter.arel_table[:completed_at].maximum.as('completed_at'))
end
end
end
+9 -22
View File
@@ -19,14 +19,10 @@ module Api
submissions = paginate(submissions.preload(:created_by_user, :submitters, submissions = paginate(submissions.preload(:created_by_user, :submitters,
template: :folder, template: :folder,
combined_document_attachment: :blob,
audit_trail_attachment: :blob)) audit_trail_attachment: :blob))
render json: { render json: {
data: submissions.map do |s| data: submissions.as_json(Submissions::SerializeForApi::SERIALIZE_PARAMS),
Submissions::SerializeForApi.call(s, s.submitters, params,
with_events: false, with_documents: false, with_values: false)
end,
pagination: { pagination: {
count: submissions.size, count: submissions.size,
next: submissions.last&.id, next: submissions.last&.id,
@@ -67,41 +63,32 @@ module Api
submissions = create_submissions(@template, params) submissions = create_submissions(@template, params)
WebhookUrls.for_account_id(@template.account_id, 'submission.created').each do |webhook_url| submissions.each do |submission|
submissions.each do |submission| SendSubmissionCreatedWebhookRequestJob.perform_async({ 'submission_id' => submission.id })
SendSubmissionCreatedWebhookRequestJob.perform_async('submission_id' => submission.id,
'webhook_url_id' => webhook_url.id)
end
end end
Submissions.send_signature_requests(submissions) Submissions.send_signature_requests(submissions)
submissions.each do |submission| submissions.each do |submission|
submission.submitters.each do |submitter| if submission.submitters.all?(&:completed_at?) && submission.submitters.last
next unless submitter.completed_at? ProcessSubmitterCompletionJob.perform_async({ 'submitter_id' => submission.submitters.last.id })
ProcessSubmitterCompletionJob.perform_async('submitter_id' => submitter.id, 'send_invitation_email' => false)
end end
end end
render json: build_create_json(submissions) render json: build_create_json(submissions)
rescue Submitters::NormalizeValues::BaseError, Submissions::CreateFromSubmitters::BaseError, rescue Submitters::NormalizeValues::BaseError => e
DownloadUtils::UnableToDownload => e
Rollbar.warning(e) if defined?(Rollbar) Rollbar.warning(e) if defined?(Rollbar)
render json: { error: e.message }, status: :unprocessable_entity render json: { error: e.message }, status: :unprocessable_entity
end end
def destroy def destroy
if params[:permanently].in?(['true', true]) if params[:permanently] == 'true'
@submission.destroy! @submission.destroy!
else else
@submission.update!(archived_at: Time.current) @submission.update!(archived_at: Time.current)
WebhookUrls.for_account_id(@submission.account_id, 'submission.archived').each do |webhook_url| SendSubmissionArchivedWebhookRequestJob.perform_async('submission_id' => @submission.id)
SendSubmissionArchivedWebhookRequestJob.perform_async('submission_id' => @submission.id,
'webhook_url_id' => webhook_url.id)
end
end end
render json: @submission.as_json(only: %i[id archived_at]) render json: @submission.as_json(only: %i[id archived_at])
@@ -167,7 +154,7 @@ module Api
: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, :value, :title, :description, fields: [:name, :uuid, :default_value, :value, :title, :description,
:readonly, :validation_pattern, :invalid_message, :readonly, :redacted, :validation_pattern, :invalid_message,
{ default_value: [], value: [], preferences: {} }] }]] { default_value: [], value: [], preferences: {} }] }]]
} }
] ]
@@ -13,10 +13,7 @@ module Api
SubmissionEvents.create_with_tracking_data(submitter, 'view_form', request) SubmissionEvents.create_with_tracking_data(submitter, 'view_form', request)
WebhookUrls.for_account_id(submitter.account_id, 'form.viewed').each do |webhook_url| SendFormViewedWebhookRequestJob.perform_async({ 'submitter_id' => submitter.id })
SendFormViewedWebhookRequestJob.perform_async('submitter_id' => submitter.id,
'webhook_url_id' => webhook_url.id)
end
render json: {} render json: {}
end end
+2 -7
View File
@@ -68,7 +68,7 @@ module Api
end end
if @submitter.completed_at? if @submitter.completed_at?
ProcessSubmitterCompletionJob.perform_async('submitter_id' => @submitter.id) ProcessSubmitterCompletionJob.perform_async({ 'submitter_id' => @submitter.id })
elsif normalized_params[:send_email] || normalized_params[:send_sms] elsif normalized_params[:send_email] || normalized_params[:send_sms]
Submitters.send_signature_requests([@submitter]) Submitters.send_signature_requests([@submitter])
end end
@@ -77,10 +77,6 @@ module Api
with_urls: true, with_urls: true,
with_events: false, with_events: false,
params:) params:)
rescue Submitters::NormalizeValues::BaseError, DownloadUtils::UnableToDownload => e
Rollbar.warning(e) if defined?(Rollbar)
render json: { error: e.message }, status: :unprocessable_entity
end end
def submitter_params def submitter_params
@@ -91,7 +87,7 @@ module Api
: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, :value, fields: [[:name, :uuid, :default_value, :value,
:readonly, :validation_pattern, :invalid_message, :readonly, :redacted, :validation_pattern, :invalid_message,
{ default_value: [], value: [], preferences: {} }]] } { default_value: [], value: [], preferences: {} }]] }
) )
end end
@@ -120,7 +116,6 @@ module Api
end&.dig('uuid') end&.dig('uuid')
submitter.email = Submissions.normalize_email(attrs[:email]) if attrs.key?(:email) submitter.email = Submissions.normalize_email(attrs[:email]) if attrs.key?(:email)
submitter.name = attrs[:name] if attrs.key?(:name)
if attrs.key?(:phone) if attrs.key?(:phone)
submitter.phone = attrs[:phone].to_s.gsub(/[^0-9+]/, '') submitter.phone = attrs[:phone].to_s.gsub(/[^0-9+]/, '')
@@ -23,14 +23,9 @@ module Api
cloned_template.source = :api cloned_template.source = :api
cloned_template.save! cloned_template.save!
schema_documents = Templates::CloneAttachments.call(template: cloned_template, schema_documents = Templates::CloneAttachments.call(template: cloned_template, original_template: @template)
original_template: @template,
documents: params[:documents])
WebhookUrls.for_account_id(cloned_template.account_id, 'template.created').each do |webhook_url| SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => cloned_template.id)
SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => cloned_template.id,
'webhook_url_id' => webhook_url.id)
end
render json: Templates::SerializeForApi.call(cloned_template, schema_documents) render json: Templates::SerializeForApi.call(cloned_template, schema_documents)
end end
+6 -10
View File
@@ -18,7 +18,7 @@ module Api
preview_image_attachments = preview_image_attachments =
ActiveStorage::Attachment.joins(:blob) ActiveStorage::Attachment.joins(:blob)
.where(blob: { filename: ['0.png', '0.jpg'] }) .where(blob: { filename: '0.jpg' })
.where(record_id: schema_documents.map(&:id), .where(record_id: schema_documents.map(&:id),
record_type: 'ActiveStorage::Attachment', record_type: 'ActiveStorage::Attachment',
name: :preview_images) name: :preview_images)
@@ -65,16 +65,13 @@ module Api
@template.update!(template_params) @template.update!(template_params)
WebhookUrls.for_account_id(@template.account_id, 'template.updated').each do |webhook_url| SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => @template.id)
SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => @template.id,
'webhook_url_id' => webhook_url.id)
end
render json: @template.as_json(only: %i[id updated_at]) render json: @template.as_json(only: %i[id updated_at])
end end
def destroy def destroy
if params[:permanently].in?(['true', true]) if params[:permanently] == 'true'
@template.destroy! @template.destroy!
else else
@template.update!(archived_at: Time.current) @template.update!(archived_at: Time.current)
@@ -87,7 +84,7 @@ module Api
def filter_templates(templates, params) def filter_templates(templates, params)
templates = Templates.search(templates, params[:q]) templates = Templates.search(templates, params[:q])
templates = params[:archived].in?(['true', true]) ? templates.archived : templates.active templates = params[:archived] ? templates.archived : templates.active
templates = templates.where(external_id: params[:application_key]) if params[:application_key].present? templates = templates.where(external_id: params[:application_key]) if params[:application_key].present?
templates = templates.where(external_id: params[:external_id]) if params[:external_id].present? templates = templates.where(external_id: params[:external_id]) if params[:external_id].present?
templates = templates.joins(:folder).where(folder: { name: params[:folder] }) if params[:folder].present? templates = templates.joins(:folder).where(folder: { name: params[:folder] }) if params[:folder].present?
@@ -98,14 +95,13 @@ module Api
def template_params def template_params
permitted_params = [ permitted_params = [
:name, :name,
:external_id,
{ {
submitters: [%i[name uuid is_requester invite_by_uuid linked_to_uuid email]], submitters: [%i[name uuid]],
fields: [[:uuid, :submitter_uuid, :name, :type, fields: [[:uuid, :submitter_uuid, :name, :type,
:required, :readonly, :default_value, :required, :readonly, :default_value,
:title, :description, :title, :description,
{ preferences: {}, { preferences: {},
conditions: [%i[field_uuid value action operation]], conditions: [%i[field_uuid value action]],
options: [%i[value uuid]], options: [%i[value uuid]],
validation: %i[message pattern], validation: %i[message pattern],
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]] areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]]
+4 -1
View File
@@ -20,7 +20,10 @@ module Api
pdf = HexaPDF::Document.new(io: StringIO.new(file)) pdf = HexaPDF::Document.new(io: StringIO.new(file))
trusted_certs = Accounts.load_trusted_certs(current_account) trusted_certs = Accounts.load_trusted_certs(current_account)
is_checksum_found = CompletedDocument.exists?(sha256: Base64.urlsafe_encode64(Digest::SHA256.digest(file)))
is_checksum_found = ActiveStorage::Attachment.joins(:blob)
.where(name: 'documents', record_type: 'Submitter')
.exists?(blob: { checksum: Digest::MD5.base64digest(file) })
render json: { render json: {
checksum_status: is_checksum_found ? 'verified' : 'not_found', checksum_status: is_checksum_found ? 'verified' : 'not_found',
+1 -1
View File
@@ -12,6 +12,6 @@ class ApiSettingsController < ApplicationController
current_user.access_token.save! current_user.access_token.save!
redirect_back(fallback_location: settings_api_index_path, notice: I18n.t('api_token_has_been_updated')) redirect_back(fallback_location: settings_api_index_path, notice: 'API token as been updated.')
end end
end end
+1 -29
View File
@@ -8,14 +8,12 @@ class ApplicationController < ActionController::Base
check_authorization unless: :devise_controller? check_authorization unless: :devise_controller?
around_action :with_locale
before_action :sign_in_for_demo, if: -> { Docuseal.demo? } before_action :sign_in_for_demo, if: -> { Docuseal.demo? }
before_action :maybe_redirect_to_setup, unless: :signed_in? before_action :maybe_redirect_to_setup, unless: :signed_in?
before_action :authenticate_user!, unless: :devise_controller? before_action :authenticate_user!, unless: :devise_controller?
helper_method :button_title, helper_method :button_title,
:current_account, :current_account,
:form_link_host,
:svg_icon :svg_icon
impersonates :user, with: ->(uuid) { User.find_by(uuid:) } impersonates :user, with: ->(uuid) { User.find_by(uuid:) }
@@ -39,10 +37,6 @@ class ApplicationController < ActionController::Base
end end
def default_url_options def default_url_options
if request.domain == 'docuseal.com'
return { host: 'docuseal.com', protocol: ENV['FORCE_SSL'].present? ? 'https' : 'http' }
end
Docuseal.default_url_options Docuseal.default_url_options
end end
@@ -57,18 +51,7 @@ class ApplicationController < ActionController::Base
private private
def with_locale(&)
return yield unless current_account
locale = params[:lang].presence if Rails.env.development?
locale ||= current_account.locale
I18n.with_locale(locale, &)
end
def with_browser_locale(&) def with_browser_locale(&)
return yield if I18n.locale != :'en-US' && I18n.locale != :en
locale = params[:lang].presence locale = params[:lang].presence
locale ||= request.env['HTTP_ACCEPT_LANGUAGE'].to_s[BROWSER_LOCALE_REGEXP].to_s locale ||= request.env['HTTP_ACCEPT_LANGUAGE'].to_s[BROWSER_LOCALE_REGEXP].to_s
@@ -96,8 +79,7 @@ class ApplicationController < ActionController::Base
redirect_to setup_index_path unless User.exists? redirect_to setup_index_path unless User.exists?
end end
def button_title(title: I18n.t('submit'), disabled_with: I18n.t('submitting'), title_class: '', icon: nil, def button_title(title: 'Submit', disabled_with: 'Submitting', title_class: '', icon: nil, icon_disabled: nil)
icon_disabled: nil)
render_to_string(partial: 'shared/button_title', render_to_string(partial: 'shared/button_title',
locals: { title:, disabled_with:, title_class:, icon:, icon_disabled: }) locals: { title:, disabled_with:, title_class:, icon:, icon_disabled: })
end end
@@ -105,14 +87,4 @@ class ApplicationController < ActionController::Base
def svg_icon(icon_name, class: '') def svg_icon(icon_name, class: '')
render_to_string(partial: "icons/#{icon_name}", locals: { class: }) render_to_string(partial: "icons/#{icon_name}", locals: { class: })
end end
def form_link_host
Docuseal.default_url_options[:host]
end
def maybe_redirect_com
return if request.domain != 'docuseal.co'
redirect_to request.url.gsub('.co/', '.com/'), allow_other_host: true, status: :moved_permanently
end
end end
+1 -1
View File
@@ -33,7 +33,7 @@ class DashboardController < ApplicationController
account_id: current_user.account_id, account_id: current_user.account_id,
key: AccountConfig::FORCE_MFA) key: AccountConfig::FORCE_MFA)
redirect_to mfa_setup_path, notice: I18n.t('setup_2fa_to_continue') redirect_to mfa_setup_path, notice: 'Setup 2FA to continue'
end end
def maybe_render_landing def maybe_render_landing
@@ -9,9 +9,9 @@ class EmailSmtpSettingsController < ApplicationController
def create def create
if @encrypted_config.update(email_configs) if @encrypted_config.update(email_configs)
SettingsMailer.smtp_successful_setup(@encrypted_config.value['from_email'] || current_user.email).deliver_now! SettingsMailer.smtp_successful_setup(@encrypted_config.value['from_email']).deliver_now!
redirect_to settings_email_index_path, notice: I18n.t('changes_have_been_saved') redirect_to settings_email_index_path, notice: 'Changes have been saved'
else else
render :index, status: :unprocessable_entity render :index, status: :unprocessable_entity
end end
+1 -1
View File
@@ -2,7 +2,7 @@
class ErrorsController < ActionController::Base class ErrorsController < ActionController::Base
ENTERPRISE_FEATURE_MESSAGE = ENTERPRISE_FEATURE_MESSAGE =
'This feature is available in Pro Edition: https://www.docuseal.com/pricing' 'This feature is available in Pro Edition: https://www.docuseal.co/pricing'
ENTERPRISE_PATHS = [ ENTERPRISE_PATHS = [
'/templates/html', '/templates/html',
+10 -18
View File
@@ -11,8 +11,6 @@ class EsignSettingsController < ApplicationController
end end
end end
prepend_before_action :maybe_redirect_com, only: %i[show]
before_action :load_encrypted_config before_action :load_encrypted_config
authorize_resource :encrypted_config, parent: false, only: %i[new create] authorize_resource :encrypted_config, parent: false, only: %i[new create]
authorize_resource :encrypted_config, only: %i[update destroy show] authorize_resource :encrypted_config, only: %i[update destroy show]
@@ -23,9 +21,9 @@ class EsignSettingsController < ApplicationController
default_pkcs = GenerateCertificate.load_pkcs(cert_data) if cert_data['cert'].present? default_pkcs = GenerateCertificate.load_pkcs(cert_data) if cert_data['cert'].present?
custom_pkcs_list = (cert_data['custom'] || []).map do |e| custom_pkcs_list = (cert_data['custom'] || []).map do |e|
pkcs = e['data'].present? ? OpenSSL::PKCS12.new(Base64.urlsafe_decode64(e['data']), e['password'].to_s) : nil { 'pkcs' => OpenSSL::PKCS12.new(Base64.urlsafe_decode64(e['data']), e['password'].to_s),
'name' => e['name'],
{ 'pkcs' => pkcs, 'name' => e['name'], 'status' => e['status'] } 'status' => e['status'] }
end end
@pkcs_list = [ @pkcs_list = [
@@ -50,7 +48,7 @@ class EsignSettingsController < ApplicationController
if (@encrypted_config.value && @encrypted_config.value['custom']&.any? { |e| e['name'] == @cert_record.name }) || if (@encrypted_config.value && @encrypted_config.value['custom']&.any? { |e| e['name'] == @cert_record.name }) ||
@cert_record.name == DEFAULT_CERT_NAME @cert_record.name == DEFAULT_CERT_NAME
@cert_record.errors.add(:name, I18n.t('already_exists')) @cert_record.errors.add(:name, 'already exists')
return render turbo_stream: turbo_stream.replace(:modal, template: 'esign_settings/new'), return render turbo_stream: turbo_stream.replace(:modal, template: 'esign_settings/new'),
status: :unprocessable_entity status: :unprocessable_entity
@@ -58,7 +56,7 @@ class EsignSettingsController < ApplicationController
save_new_cert!(@encrypted_config, @cert_record) save_new_cert!(@encrypted_config, @cert_record)
redirect_to settings_esign_path, notice: I18n.t('certificate_has_been_successfully_added') redirect_to settings_esign_path, notice: 'Certificate has been successfully added!'
rescue OpenSSL::PKCS12::PKCS12Error => e rescue OpenSSL::PKCS12::PKCS12Error => e
Rollbar.error(e) if defined?(Rollbar) Rollbar.error(e) if defined?(Rollbar)
@@ -68,20 +66,14 @@ class EsignSettingsController < ApplicationController
end end
def update def update
@encrypted_config.value['custom'].to_a.each { |e| e['status'] = 'validate' } @encrypted_config.value['custom'].each { |e| e['status'] = 'validate' }
custom_cert_data = @encrypted_config.value['custom'].to_a.find { |e| e['name'] == params[:name] } custom_cert_data = @encrypted_config.value['custom'].find { |e| e['name'] == params[:name] }
custom_cert_data['status'] = 'default' if custom_cert_data
if custom_cert_data
custom_cert_data['status'] = 'default'
elsif params[:name] == Docuseal::AATL_CERT_NAME
@encrypted_config.value['custom'] ||= []
@encrypted_config.value['custom'] << { 'name' => params[:name], 'status' => 'default' }
end
@encrypted_config.save! @encrypted_config.save!
redirect_to settings_esign_path, notice: I18n.t('default_certificate_has_been_selected') redirect_to settings_esign_path, notice: 'Default certificate has been selected'
end end
def destroy def destroy
@@ -89,7 +81,7 @@ class EsignSettingsController < ApplicationController
@encrypted_config.save! @encrypted_config.save!
redirect_to settings_esign_path, notice: I18n.t('certificate_has_been_removed') redirect_to settings_esign_path, notice: 'Certificate has been removed'
end end
private private
+5 -5
View File
@@ -18,11 +18,11 @@ class MfaSetupController < ApplicationController
current_user.otp_required_for_login = true current_user.otp_required_for_login = true
current_user.save! current_user.save!
redirect_to settings_profile_index_path, notice: I18n.t('2fa_has_been_configured') redirect_to settings_profile_index_path, notice: '2FA has been configured'
else else
@provision_url = current_user.otp_provisioning_uri(current_user.email, issuer: Docuseal.product_name) @provision_url = current_user.otp_provisioning_uri(current_user.email, issuer: Docuseal.product_name)
@error_message = I18n.t('code_is_invalid') @error_message = 'Code is invalid'
render turbo_stream: turbo_stream.replace(:mfa_form, partial: 'mfa_setup/form'), status: :unprocessable_entity render turbo_stream: turbo_stream.replace(:mfa_form, partial: 'mfa_setup/form'), status: :unprocessable_entity
end end
@@ -32,9 +32,9 @@ class MfaSetupController < ApplicationController
if current_user.validate_and_consume_otp!(params[:otp_attempt]) if current_user.validate_and_consume_otp!(params[:otp_attempt])
current_user.update!(otp_required_for_login: false, otp_secret: nil) current_user.update!(otp_required_for_login: false, otp_secret: nil)
redirect_to settings_profile_index_path, notice: I18n.t('2fa_has_been_removed') redirect_to settings_profile_index_path, notice: '2FA has been removed'
else else
@error_message = I18n.t('code_is_invalid') @error_message = 'Code is invalid'
render turbo_stream: turbo_stream.replace(:modal, template: 'mfa_setup/edit'), status: :unprocessable_entity render turbo_stream: turbo_stream.replace(:modal, template: 'mfa_setup/edit'), status: :unprocessable_entity
end end
@@ -43,7 +43,7 @@ class MfaSetupController < ApplicationController
private private
def set_provision_url def set_provision_url
return redirect_to root_path, alert: I18n.t('2fa_has_been_set_up_already') if current_user.otp_required_for_login return redirect_to root_path, alert: '2FA has been set up already' if current_user.otp_required_for_login
current_user.otp_secret ||= User.generate_otp_secret current_user.otp_secret ||= User.generate_otp_secret
@@ -13,9 +13,9 @@ class NotificationsSettingsController < ApplicationController
def create def create
if @account_config.value.present? ? @account_config.save : @account_config.delete if @account_config.value.present? ? @account_config.save : @account_config.delete
redirect_back fallback_location: settings_notifications_path, notice: I18n.t('changes_have_been_saved') redirect_back fallback_location: settings_notifications_path, notice: 'Changes have been saved'
else else
redirect_back fallback_location: settings_notifications_path, alert: I18n.t('unable_to_save') redirect_back fallback_location: settings_notifications_path, alert: 'Unable to save'
end end
end end
@@ -6,8 +6,7 @@ class PersonalizationSettingsController < ApplicationController
AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY,
AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY, AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY,
AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY, AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY,
AccountConfig::FORM_COMPLETED_MESSAGE_KEY, AccountConfig::FORM_COMPLETED_MESSAGE_KEY
*(Docuseal.multitenant? ? [] : [AccountConfig::POLICY_LINKS_KEY])
].freeze ].freeze
InvalidKey = Class.new(StandardError) InvalidKey = Class.new(StandardError)
@@ -31,7 +30,7 @@ class PersonalizationSettingsController < ApplicationController
@account_config.save! @account_config.save!
end end
redirect_back(fallback_location: settings_personalization_path, notice: I18n.t('settings_have_been_saved')) redirect_back(fallback_location: settings_personalization_path, notice: 'Settings have been saved.')
end end
private private
@@ -12,8 +12,7 @@ class PreviewDocumentPageController < ActionController::API
return head :not_found unless attachment return head :not_found unless attachment
preview_image = attachment.preview_images.joins(:blob) preview_image = attachment.preview_images.joins(:blob).find_by(blob: { filename: "#{params[:id]}#{FORMAT}" })
.find_by(blob: { filename: ["#{params[:id]}.png", "#{params[:id]}.jpg"] })
return redirect_to preview_image.url, allow_other_host: true if preview_image return redirect_to preview_image.url, allow_other_host: true if preview_image
+2 -2
View File
@@ -9,7 +9,7 @@ class ProfileController < ApplicationController
def update_contact def update_contact
if current_user.update(contact_params) if current_user.update(contact_params)
redirect_to settings_profile_index_path, notice: I18n.t('contact_information_has_been_update') redirect_to settings_profile_index_path, notice: 'Contact information has been updated'
else else
render :index, status: :unprocessable_entity render :index, status: :unprocessable_entity
end end
@@ -18,7 +18,7 @@ class ProfileController < ApplicationController
def update_password def update_password
if current_user.update(password_params) if current_user.update(password_params)
bypass_sign_in(current_user) bypass_sign_in(current_user)
redirect_to settings_profile_index_path, notice: I18n.t('password_has_been_changed') redirect_to settings_profile_index_path, notice: 'Password has been changed'
else else
render :index, status: :unprocessable_entity render :index, status: :unprocessable_entity
end end
@@ -7,8 +7,6 @@ class SendSubmissionEmailController < ApplicationController
skip_before_action :verify_authenticity_token skip_before_action :verify_authenticity_token
skip_authorization_check skip_authorization_check
SEND_DURATION = 30.minutes
def success; end def success; end
def create def create
@@ -25,10 +23,7 @@ class SendSubmissionEmailController < ApplicationController
RateLimit.call("send-email-#{@submitter.id}", limit: 2, ttl: 5.minutes) RateLimit.call("send-email-#{@submitter.id}", limit: 2, ttl: 5.minutes)
unless EmailEvent.exists?(tag: :submitter_documents_copy, email: @submitter.email, emailable: @submitter, SubmitterMailer.documents_copy_email(@submitter, sig: true).deliver_later!
event_type: :send, created_at: SEND_DURATION.ago..Time.current)
SubmitterMailer.documents_copy_email(@submitter, sig: true).deliver_later!
end
respond_to do |f| respond_to do |f|
f.html { redirect_to success_send_submission_email_index_path } f.html { redirect_to success_send_submission_email_index_path }
+1 -1
View File
@@ -12,7 +12,7 @@ class SessionsController < Devise::SessionsController
Rollbar.warning('Sign in new user') if defined?(Rollbar) Rollbar.warning('Sign in new user') if defined?(Rollbar)
return redirect_to new_registration_path(sign_up: true, user: sign_in_params.slice(:email)), return redirect_to new_registration_path(sign_up: true, user: sign_in_params.slice(:email)),
notice: I18n.t('create_a_new_account') notice: 'Create a new account'
end end
if User.exists?(email:, otp_required_for_login: true) && sign_in_params[:otp_attempt].blank? if User.exists?(email:, otp_required_for_login: true) && sign_in_params[:otp_attempt].blank?
+4 -4
View File
@@ -21,7 +21,7 @@ class SetupController < ApplicationController
@encrypted_config = EncryptedConfig.new(encrypted_config_params) @encrypted_config = EncryptedConfig.new(encrypted_config_params)
unless URI.parse(encrypted_config_params[:value].to_s).class.in?([URI::HTTP, URI::HTTPS]) unless URI.parse(encrypted_config_params[:value].to_s).class.in?([URI::HTTP, URI::HTTPS])
@encrypted_config.errors.add(:value, I18n.t('should_be_a_valid_url')) @encrypted_config.errors.add(:value, 'should be a valid URL')
return render :index, status: :unprocessable_entity return render :index, status: :unprocessable_entity
end end
@@ -56,7 +56,7 @@ class SetupController < ApplicationController
def account_params def account_params
return {} unless params[:account] return {} unless params[:account]
params.require(:account).permit(:name, :timezone, :locale) params.require(:account).permit(:name, :timezone)
end end
def encrypted_config_params def encrypted_config_params
@@ -66,10 +66,10 @@ class SetupController < ApplicationController
end end
def redirect_to_root_if_signed def redirect_to_root_if_signed
redirect_to root_path, notice: I18n.t('you_are_already_signed_in') redirect_to root_path, notice: 'You are already signed in'
end end
def ensure_first_user_not_created! def ensure_first_user_not_created!
redirect_to new_user_session_path, notice: I18n.t('please_sign_in') if User.exists? redirect_to new_user_session_path, notice: 'Please sign in.' if User.exists?
end end
end end
+15 -32
View File
@@ -7,41 +7,38 @@ class StartFormController < ApplicationController
skip_authorization_check skip_authorization_check
around_action :with_browser_locale, only: %i[show completed] around_action :with_browser_locale, only: %i[show completed]
before_action :maybe_redirect_com, only: %i[show completed]
before_action :load_template before_action :load_template
def show def show
@submitter = @template.submissions.new(account_id: @template.account_id) @submitter = @template.submissions.new(account_id: @template.account_id)
.submitters.new(uuid: (filter_undefined_submitters(@template).first || .submitters.new(uuid: @template.submitters.first['uuid'])
@template.submitters.first)['uuid'])
end end
def update def update
return redirect_to start_form_path(@template.slug) if @template.archived_at? return redirect_to start_form_path(@template.slug) if @template.archived_at?
@submitter = find_or_initialize_submitter(@template, submitter_params) @submitter = Submitter.where(submission: @template.submissions.where(expire_at: Time.current..)
.or(@template.submissions.where(expire_at: nil)).where(archived_at: nil))
.order(id: :desc)
.then { |rel| params[:resubmit].present? ? rel.where(completed_at: nil) : rel }
.find_or_initialize_by(**submitter_params.compact_blank)
if @submitter.completed_at? if @submitter.completed_at?
redirect_to start_form_completed_path(@template.slug, email: submitter_params[:email]) redirect_to start_form_completed_path(@template.slug, email: submitter_params[:email])
else else
if filter_undefined_submitters(@template).size > 1 && @submitter.new_record? if @template.submitters.to_a.size > 1 && @submitter.new_record?
@error_message = I18n.t('not_found') @error_message = 'Not found'
return render :show return render :show
end end
if (is_new_record = @submitter.new_record?) assign_submission_attributes(@submitter, @template) if @submitter.new_record?
assign_submission_attributes(@submitter, @template)
Submissions::AssignDefinedSubmitters.call(@submitter.submission) is_new_record = @submitter.new_record?
end
if @submitter.save if @submitter.save
if is_new_record if is_new_record
WebhookUrls.for_account_id(@submitter.account_id, 'submission.created').each do |webhook_url| SendSubmissionCreatedWebhookRequestJob.perform_async({ 'submission_id' => @submitter.submission.id })
SendSubmissionCreatedWebhookRequestJob.perform_async('submission_id' => @submitter.submission_id,
'webhook_url_id' => webhook_url.id)
end
end end
redirect_to submit_form_path(@submitter.slug) redirect_to submit_form_path(@submitter.slug)
@@ -59,21 +56,14 @@ class StartFormController < ApplicationController
private private
def find_or_initialize_submitter(template, submitter_params)
Submitter.where(submission: template.submissions.where(expire_at: Time.current..)
.or(template.submissions.where(expire_at: nil)).where(archived_at: nil))
.order(id: :desc)
.where(declined_at: nil)
.then { |rel| params[:resubmit].present? ? rel.where(completed_at: nil) : rel }
.find_or_initialize_by(**submitter_params.compact_blank)
end
def assign_submission_attributes(submitter, template) def assign_submission_attributes(submitter, template)
resubmit_submitter = resubmit_submitter =
(Submitter.where(submission: template.submissions).find_by(slug: params[:resubmit]) if params[:resubmit].present?) if params[:resubmit].present?
Submitter.where(submission: @template.submissions).find_by(slug: params[:resubmit])
end
submitter.assign_attributes( submitter.assign_attributes(
uuid: (filter_undefined_submitters(template).first || @template.submitters.first)['uuid'], uuid: template.submitters.first['uuid'],
ip: request.remote_ip, ip: request.remote_ip,
ua: request.user_agent, ua: request.user_agent,
values: resubmit_submitter&.preferences&.fetch('default_values', nil) || {}, values: resubmit_submitter&.preferences&.fetch('default_values', nil) || {},
@@ -90,18 +80,11 @@ class StartFormController < ApplicationController
submitter.submission ||= Submission.new(template:, submitter.submission ||= Submission.new(template:,
account_id: template.account_id, account_id: template.account_id,
template_submitters: template.submitters, template_submitters: template.submitters,
submitters: [submitter],
source: :link) source: :link)
submitter.account_id = submitter.submission.account_id
submitter submitter
end end
def filter_undefined_submitters(template)
Templates.filter_undefined_submitters(template)
end
def submitter_params def submitter_params
params.require(:submitter).permit(:email, :phone, :name).tap do |attrs| params.require(:submitter).permit(:email, :phone, :name).tap do |attrs|
attrs[:email] = Submissions.normalize_email(attrs[:email]) attrs[:email] = Submissions.normalize_email(attrs[:email])
@@ -11,7 +11,7 @@ class StorageSettingsController < ApplicationController
if @encrypted_config.update(storage_configs) if @encrypted_config.update(storage_configs)
LoadActiveStorageConfigs.reload LoadActiveStorageConfigs.reload
redirect_to settings_storage_index_path, notice: I18n.t('changes_have_been_saved') redirect_to settings_storage_index_path, notice: 'Changes have been saved'
else else
render :index, status: :unprocessable_entity render :index, status: :unprocessable_entity
end end
@@ -8,18 +8,8 @@ class SubmissionsArchivedController < ApplicationController
@submissions = @submissions.where.not(archived_at: nil) @submissions = @submissions.where.not(archived_at: nil)
.or(@submissions.where.not(templates: { archived_at: nil })) .or(@submissions.where.not(templates: { archived_at: nil }))
.preload(:created_by_user, template: :author) .preload(:created_by_user, template: :author)
@submissions = @submissions.preload(:template_accesses) unless current_user.role.in?(%w[admin superadmin])
@submissions = Submissions.search(@submissions, params[:q], search_template: true) @submissions = Submissions.search(@submissions, params[:q], search_template: true)
@submissions = Submissions::Filter.call(@submissions, current_user, params)
@submissions = if params[:completed_at_from].present? || params[:completed_at_to].present? @pagy, @submissions = pagy(@submissions.preload(:submitters).order(id: :desc))
@submissions.order(Submitter.arel_table[:completed_at].maximum.desc)
else
@submissions.order(id: :desc)
end
@pagy, @submissions = pagy(@submissions.preload(submitters: :start_form_submission_events))
end end
end end
+8 -27
View File
@@ -6,18 +6,9 @@ class SubmissionsController < ApplicationController
load_and_authorize_resource :submission, only: %i[show destroy] load_and_authorize_resource :submission, only: %i[show destroy]
prepend_before_action :maybe_redirect_com, only: %i[show]
def show def show
@submission = Submissions.preload_with_pages(@submission) @submission = Submissions.preload_with_pages(@submission)
unless @submission.submitters.all?(&:completed_at?)
ActiveRecord::Associations::Preloader.new(
records: [@submission],
associations: [submitters: :start_form_submission_events]
).call
end
render :show, layout: 'plain' render :show, layout: 'plain'
end end
@@ -52,28 +43,27 @@ class SubmissionsController < ApplicationController
params: params.merge('send_completed_email' => true)) params: params.merge('send_completed_email' => true))
end end
enqueue_submission_created_webhooks(@template, submissions) submissions.each do |submission|
SendSubmissionCreatedWebhookRequestJob.perform_async({ 'submission_id' => submission.id })
end
Submissions.send_signature_requests(submissions) Submissions.send_signature_requests(submissions)
redirect_to template_path(@template), notice: I18n.t('new_recipients_have_been_added') redirect_to template_path(@template), notice: 'New recipients have been added'
end end
def destroy def destroy
notice = notice =
if params[:permanently].in?(['true', true]) if params[:permanently].present?
@submission.destroy! @submission.destroy!
I18n.t('submission_has_been_removed') 'Submission has been removed'
else else
@submission.update!(archived_at: Time.current) @submission.update!(archived_at: Time.current)
WebhookUrls.for_account_id(@submission.account_id, 'submission.archived').each do |webhook_url| SendSubmissionArchivedWebhookRequestJob.perform_async('submission_id' => @submission.id)
SendSubmissionArchivedWebhookRequestJob.perform_async('submission_id' => @submission.id,
'webhook_url_id' => webhook_url.id)
end
I18n.t('submission_has_been_archived') 'Submission has been archived'
end end
redirect_back(fallback_location: template_path(@submission.template), notice:) redirect_back(fallback_location: template_path(@submission.template), notice:)
@@ -88,15 +78,6 @@ class SubmissionsController < ApplicationController
template.save! template.save!
end end
def enqueue_submission_created_webhooks(template, submissions)
WebhookUrls.for_account_id(template.account_id, 'submission.created').each do |webhook_url|
submissions.each do |submission|
SendSubmissionCreatedWebhookRequestJob.perform_async('submission_id' => submission.id,
'webhook_url_id' => webhook_url.id)
end
end
end
def submissions_params def submissions_params
params.permit(submission: { submitters: [%i[uuid email phone name]] }) params.permit(submission: { submitters: [%i[uuid email phone name]] })
end end
@@ -10,17 +10,11 @@ class SubmissionsDashboardController < ApplicationController
.where(templates: { archived_at: nil }) .where(templates: { archived_at: nil })
.preload(:created_by_user, template: :author) .preload(:created_by_user, template: :author)
@submissions = @submissions.preload(:template_accesses) unless current_user.role.in?(%w[admin superadmin])
@submissions = Submissions.search(@submissions, params[:q], search_template: true) @submissions = Submissions.search(@submissions, params[:q], search_template: true)
@submissions = Submissions::Filter.call(@submissions, current_user, params)
@submissions = if params[:completed_at_from].present? || params[:completed_at_to].present? @submissions = @submissions.pending if params[:status] == 'pending'
@submissions.order(Submitter.arel_table[:completed_at].maximum.desc) @submissions = @submissions.completed if params[:status] == 'completed'
else
@submissions.order(id: :desc)
end
@pagy, @submissions = pagy(@submissions.preload(submitters: :start_form_submission_events)) @pagy, @submissions = pagy(@submissions.preload(:submitters).order(id: :desc))
end end
end end
@@ -33,7 +33,7 @@ class SubmissionsDownloadController < ApplicationController
return head :not_found return head :not_found
end end
if params[:combined] == 'true' if params[:combined]
url = build_combined_url(submitter) url = build_combined_url(submitter)
if url if url
@@ -53,15 +53,8 @@ class SubmissionsDownloadController < ApplicationController
end end
def build_urls(submitter) def build_urls(submitter)
filename_format = AccountConfig.find_or_initialize_by(account_id: submitter.account_id,
key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value
Submitters.select_attachments_for_download(submitter).map do |attachment| Submitters.select_attachments_for_download(submitter).map do |attachment|
ActiveStorage::Blob.proxy_url( ActiveStorage::Blob.proxy_url(attachment.blob, expires_at: FILES_TTL.from_now.to_i)
attachment.blob,
expires_at: FILES_TTL.from_now.to_i,
filename: Submitters.build_document_filename(submitter, attachment.blob, filename_format)
)
end end
end end
@@ -72,13 +65,6 @@ class SubmissionsDownloadController < ApplicationController
attachment = submitter.submission.combined_document_attachment attachment = submitter.submission.combined_document_attachment
attachment ||= Submissions::GenerateCombinedAttachment.call(submitter) attachment ||= Submissions::GenerateCombinedAttachment.call(submitter)
filename_format = AccountConfig.find_or_initialize_by(account_id: submitter.account_id, ActiveStorage::Blob.proxy_url(attachment.blob, expires_at: FILES_TTL.from_now.to_i)
key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value
ActiveStorage::Blob.proxy_url(
attachment.blob,
expires_at: FILES_TTL.from_now.to_i,
filename: Submitters.build_document_filename(submitter, attachment.blob, filename_format)
)
end end
end end
@@ -1,18 +0,0 @@
# frozen_string_literal: true
class SubmissionsFiltersController < ApplicationController
ALLOWED_NAMES = %w[
author
completed_at
status
created_at
].freeze
skip_authorization_check
def show
return head :not_found unless ALLOWED_NAMES.include?(params[:name])
render params[:name]
end
end
@@ -4,8 +4,6 @@ class SubmissionsPreviewController < ApplicationController
skip_before_action :authenticate_user! skip_before_action :authenticate_user!
skip_authorization_check skip_authorization_check
prepend_before_action :maybe_redirect_com, only: %i[show completed]
TTL = 40.minutes TTL = 40.minutes
def show def show
@@ -20,8 +18,8 @@ class SubmissionsPreviewController < ApplicationController
@submission ||= Submission.find_by!(slug: params[:slug]) @submission ||= Submission.find_by!(slug: params[:slug])
if @submission.account.archived_at? || (!@submission.submitters.all?(&:completed_at?) && current_user.blank?) if !@submission.submitters.all?(&:completed_at?) && current_user.blank?
raise ActionController::RoutingError, I18n.t('not_found') raise ActionController::RoutingError, 'Not Found'
end end
if !submission_valid_ttl?(@submission) && !signature_valid if !submission_valid_ttl?(@submission) && !signature_valid
@@ -1,11 +0,0 @@
# frozen_string_literal: true
class SubmissionsUnarchiveController < ApplicationController
load_and_authorize_resource :submission
def create
@submission.update!(archived_at: nil)
redirect_to submission_path(@submission), notice: I18n.t('submission_has_been_unarchived')
end
end
+7 -25
View File
@@ -12,22 +12,16 @@ class SubmitFormController < ApplicationController
def show def show
@submitter = Submitter.find_by!(slug: params[:slug]) @submitter = Submitter.find_by!(slug: params[:slug])
submission = @submitter.submission
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at? return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
return render :archived if submission.template.archived_at? || return render :archived if @submitter.submission.template.archived_at? || @submitter.submission.archived_at?
submission.archived_at? || return render :expired if @submitter.submission.expired?
@submitter.account.archived_at?
return render :expired if submission.expired?
return render :declined if @submitter.declined_at?
return render :awaiting if submission.template.preferences['submitters_order'] == 'preserved' &&
!Submitters.current_submitter_order?(@submitter)
Submitters.preload_with_pages(@submitter) Submitters.preload_with_pages(@submitter)
Submitters::MaybeUpdateDefaultValues.call(@submitter, current_user) Submitters::MaybeUpdateDefaultValues.call(@submitter, current_user)
@attachments_index = build_attachments_index(submission) @attachments_index = ActiveStorage::Attachment.where(record: @submitter.submission.submitters, name: :attachments)
.preload(:blob).index_by(&:uuid)
@form_configs = Submitters::FormConfigs.call(@submitter, CONFIG_KEYS) @form_configs = Submitters::FormConfigs.call(@submitter, CONFIG_KEYS)
@@ -51,20 +45,15 @@ class SubmitFormController < ApplicationController
submitter = Submitter.find_by!(slug: params[:slug]) submitter = Submitter.find_by!(slug: params[:slug])
if submitter.completed_at? if submitter.completed_at?
return render json: { error: I18n.t('form_has_been_completed_already') }, status: :unprocessable_entity return render json: { error: 'Form has been completed already.' }, status: :unprocessable_entity
end end
if submitter.template.archived_at? || submitter.submission.archived_at? if submitter.template.archived_at? || submitter.submission.archived_at?
return render json: { error: I18n.t('form_has_been_archived') }, status: :unprocessable_entity return render json: { error: 'Form has been archived.' }, status: :unprocessable_entity
end end
if submitter.submission.expired? if submitter.submission.expired?
return render json: { error: I18n.t('form_has_been_expired') }, status: :unprocessable_entity return render json: { error: 'Form has been expired.' }, status: :unprocessable_entity
end
if submitter.declined_at?
return render json: { error: I18n.t('form_has_been_declined') },
status: :unprocessable_entity
end end
Submitters::SubmitValues.call(submitter, params, request) Submitters::SubmitValues.call(submitter, params, request)
@@ -79,11 +68,4 @@ class SubmitFormController < ApplicationController
end end
def success; end def success; end
private
def build_attachments_index(submission)
ActiveStorage::Attachment.where(record: submission.submitters, name: :attachments)
.preload(:blob).index_by(&:uuid)
end
end end
@@ -1,35 +0,0 @@
# frozen_string_literal: true
class SubmitFormDeclineController < ApplicationController
skip_before_action :authenticate_user!
skip_authorization_check
def create
submitter = Submitter.find_by!(slug: params[:submit_form_slug])
return redirect_to submit_form_path(submitter.slug) if submitter.declined_at? ||
submitter.completed_at? ||
submitter.submission.archived_at? ||
submitter.submission.expired? ||
submitter.submission.template.archived_at?
ApplicationRecord.transaction do
submitter.update!(declined_at: Time.current)
SubmissionEvents.create_with_tracking_data(submitter, 'decline_form', request, { reason: params[:reason] })
end
user = submitter.submission.created_by_user || submitter.template.author
if user.user_configs.find_by(key: UserConfig::RECEIVE_DECLINED_EMAIL)&.value != false
SubmitterMailer.declined_email(submitter, user).deliver_later!
end
WebhookUrls.for_account_id(submitter.account_id, 'form.declined').each do |webhook_url|
SendFormDeclinedWebhookRequestJob.perform_async('submitter_id' => submitter.id,
'webhook_url_id' => webhook_url.id)
end
redirect_to submit_form_path(submitter.slug)
end
end
@@ -1,37 +0,0 @@
# frozen_string_literal: true
class SubmitFormDownloadController < ApplicationController
skip_before_action :authenticate_user!
skip_authorization_check
FILES_TTL = 5.minutes
def index
submitter = Submitter.find_by!(slug: params[:submit_form_slug])
return redirect_to submitter_download_index_path(submitter.slug) if submitter.completed_at?
return head :unprocessable_entity if submitter.declined_at? ||
submitter.submission.archived_at? ||
submitter.submission.expired? ||
submitter.submission.template.archived_at?
last_completed_submitter = submitter.submission.submitters
.where.not(id: submitter.id)
.where.not(completed_at: nil)
.max_by(&:completed_at)
attachments =
if last_completed_submitter
Submitters.select_attachments_for_download(last_completed_submitter)
else
submitter.submission.template.schema_documents.preload(:blob)
end
urls = attachments.map do |attachment|
ActiveStorage::Blob.proxy_url(attachment.blob, expires_at: FILES_TTL.from_now.to_i)
end
render json: urls
end
end
@@ -1,58 +0,0 @@
# frozen_string_literal: true
class SubmitFormInviteController < ApplicationController
skip_before_action :authenticate_user!
skip_authorization_check
def create
submitter = Submitter.find_by!(slug: params[:submit_form_slug])
return head :unprocessable_entity unless can_invite?(submitter)
invite_submitters = filter_invite_submitters(submitter)
ApplicationRecord.transaction do
invite_submitters.each do |item|
attrs = submitters_attributes.find { |e| e[:uuid] == item['uuid'] }
next unless attrs
submitter.submission.submitters.create!(**attrs, account_id: submitter.account_id)
SubmissionEvents.create_with_tracking_data(submitter, 'invite_party', request, { uuid: submitter.uuid })
end
submitter.submission.update!(submitters_order: :preserved)
end
submitter.submission.submitters.reload
if invite_submitters.all? { |s| submitter.submission.submitters.any? { |e| e.uuid == s['uuid'] } }
Submitters::SubmitValues.call(submitter, ActionController::Parameters.new(completed: 'true'), request)
head :ok
else
head :unprocessable_entity
end
end
private
def can_invite?(submitter)
!submitter.declined_at? &&
!submitter.completed_at? &&
!submitter.submission.archived_at? &&
!submitter.submission.expired? &&
!submitter.submission.template.archived_at?
end
def filter_invite_submitters(submitter)
(submitter.submission.template_submitters || submitter.submission.template.submitters).select do |s|
s['invite_by_uuid'] == submitter.uuid && submitter.submission.submitters.none? { |e| e.uuid == s['uuid'] }
end
end
def submitters_attributes
params.require(:submission).permit(submitters: [%i[uuid email]]).fetch(:submitters, [])
end
end
@@ -7,10 +7,8 @@ class SubmitFormValuesController < ApplicationController
def index def index
submitter = Submitter.find_by!(slug: params[:submit_form_slug]) submitter = Submitter.find_by!(slug: params[:submit_form_slug])
return render json: {} if submitter.completed_at? || submitter.declined_at? return render json: {} if submitter.completed_at?
return render json: {} if submitter.submission.template.archived_at? || return render json: {} if submitter.submission.template.archived_at? || submitter.submission.archived_at?
submitter.submission.archived_at? ||
submitter.submission.expired?
value = submitter.values[params['field_uuid']] value = submitter.values[params['field_uuid']]
attachment = submitter.attachments.where(created_at: params[:after]..).find_by(uuid: value) if value.present? attachment = submitter.attachments.where(created_at: params[:after]..).find_by(uuid: value) if value.present?
@@ -9,8 +9,7 @@ class SubmittersAutocompleteController < ApplicationController
def index def index
submitters = search_submitters(@submitters) submitters = search_submitters(@submitters)
arel_columns = SELECT_COLUMNS.map { |col| Submitter.arel_table[col] } values = submitters.limit(LIMIT).group(SELECT_COLUMNS.join(', ')).pluck(SELECT_COLUMNS.join(', '))
values = submitters.limit(LIMIT).group(arel_columns).pluck(arel_columns)
attrs = values.map { |row| SELECT_COLUMNS.zip(row).to_h } attrs = values.map { |row| SELECT_COLUMNS.zip(row).to_h }
attrs = attrs.uniq { |e| e[params[:field]] } if params[:field].present? attrs = attrs.uniq { |e| e[params[:field]] } if params[:field].present?
-80
View File
@@ -1,80 +0,0 @@
# frozen_string_literal: true
class SubmittersController < ApplicationController
load_and_authorize_resource :submitter, only: %i[edit update]
def edit
@submitter_email_message =
if @submitter.preferences['email_message_uuid'].present?
@submitter.account.email_messages.find_by(uuid: @submitter.preferences['email_message_uuid'])
end
end
def update
submission = @submitter.submission
if @submitter.submission_events.exists?(event_type: 'start_form') || submission.archived_at? || submission.expired?
return redirect_back fallback_location: submission_path(submission), alert: I18n.t('submitter_cannot_be_updated')
end
if submitter_params.values.all?(&:blank?)
return redirect_back fallback_location: submission_path(submission),
alert: I18n.t('at_least_one_field_must_be_filled')
end
if params[:is_custom_message] != '1'
params.delete(:subject)
params.delete(:body)
end
submitter_preferences = Submitters.normalize_preferences(@submitter.account, current_user, params)
if submitter_preferences.key?('email_message_uuid')
@submitter.preferences['email_message_uuid'] = submitter_preferences['email_message_uuid']
end
assign_submitter_attrs(@submitter, submitter_params)
if @submitter.save
maybe_resend_email_sms(@submitter, params)
redirect_back fallback_location: submission_path(submission), notice: I18n.t('changes_have_been_saved')
else
redirect_back fallback_location: submission_path(submission), alert: I18n.t('unable_to_save')
end
end
private
def maybe_resend_email_sms(submitter, params)
if params[:send_email] == '1' && submitter.email.present?
is_sent_recently = Docuseal.multitenant? &&
EmailEvent.exists?(email: submitter.email,
tag: 'submitter_invitation',
emailable: submitter,
event_type: 'send',
created_at: 4.hours.ago..Time.current)
SendSubmitterInvitationEmailJob.perform_async('submitter_id' => submitter.id) unless is_sent_recently
end
return if submitter.phone.blank?
return unless params[:send_sms] == '1'
SendSubmitterInvitationSmsJob.perform_async('submitter_id' => submitter.id)
end
def assign_submitter_attrs(submitter, attrs)
submitter.phone = attrs[:phone].to_s.gsub(/[^0-9+]/, '') if attrs.key?(:phone)
submitter.email = Submissions.normalize_email(attrs[:email]) if attrs.key?(:email)
submitter.name = attrs[:name] if attrs.key?(:name)
submitter
end
def submitter_params
params.require(:submitter).permit(:email, :name, :phone).transform_values(&:strip)
end
end
@@ -6,11 +6,11 @@ class SubmittersSendEmailController < ApplicationController
def create def create
if Docuseal.multitenant? && SubmissionEvent.exists?(submitter: @submitter, if Docuseal.multitenant? && SubmissionEvent.exists?(submitter: @submitter,
event_type: 'send_email', event_type: 'send_email',
created_at: 10.hours.ago..Time.current) created_at: 24.hours.ago..Time.current)
Rollbar.warning("Already sent: #{@submitter.id}") if defined?(Rollbar) Rollbar.warning("Already sent: #{@submitter.id}") if defined?(Rollbar)
return redirect_back(fallback_location: submission_path(@submitter.submission), return redirect_back(fallback_location: submission_path(@submitter.submission),
alert: I18n.t('email_has_been_sent_already')) alert: 'Email has been sent already.')
end end
SendSubmitterInvitationEmailJob.perform_async('submitter_id' => @submitter.id) SendSubmitterInvitationEmailJob.perform_async('submitter_id' => @submitter.id)
@@ -18,6 +18,6 @@ class SubmittersSendEmailController < ApplicationController
@submitter.sent_at ||= Time.current @submitter.sent_at ||= Time.current
@submitter.save! @submitter.save!
redirect_back(fallback_location: submission_path(@submitter.submission), notice: I18n.t('email_has_been_sent')) redirect_back(fallback_location: submission_path(@submitter.submission), notice: 'Email has been sent')
end end
end end
@@ -5,7 +5,7 @@ class TemplateDocumentsController < ApplicationController
def create def create
if params[:blobs].blank? && params[:files].blank? if params[:blobs].blank? && params[:files].blank?
return render json: { error: I18n.t('file_is_missing') }, status: :unprocessable_entity return render json: { error: 'File is missing' }, status: :unprocessable_entity
end end
old_fields_hash = @template.fields.hash old_fields_hash = @template.fields.hash
@@ -28,6 +28,6 @@ class TemplateDocumentsController < ApplicationController
) )
} }
rescue Templates::CreateAttachments::PdfEncrypted rescue Templates::CreateAttachments::PdfEncrypted
render json: { error: 'PDF encrypted', status: 'pdf_encrypted' }, status: :unprocessable_entity render json: { error: 'PDF encrypted' }, status: :unprocessable_entity
end end
end end
@@ -4,11 +4,10 @@ class TemplateFoldersController < ApplicationController
load_and_authorize_resource :template_folder load_and_authorize_resource :template_folder
def show def show
@templates = @template_folder.templates.active.accessible_by(current_ability) @templates = @template_folder.templates.active.preload(:author).order(id: :desc)
.preload(:author, :template_accesses).order(id: :desc)
@templates = Templates.search(@templates, params[:q]) @templates = Templates.search(@templates, params[:q])
@pagy, @templates = pagy(@templates, limit: 12) @pagy, @templates = pagy(@templates, items: 12)
end end
def edit; end def edit; end
@@ -16,9 +15,9 @@ class TemplateFoldersController < ApplicationController
def update def update
if @template_folder != current_account.default_template_folder && if @template_folder != current_account.default_template_folder &&
@template_folder.update(template_folder_params) @template_folder.update(template_folder_params)
redirect_to folder_path(@template_folder), notice: I18n.t('folder_name_has_been_updated') redirect_to folder_path(@template_folder), notice: 'Folder name has been updated'
else else
redirect_to folder_path(@template_folder), alert: I18n.t('unable_to_rename_folder') redirect_to folder_path(@template_folder), alert: 'Unable to rename folder'
end end
end end
@@ -4,9 +4,9 @@ class TemplatesArchivedController < ApplicationController
load_and_authorize_resource :template, parent: false load_and_authorize_resource :template, parent: false
def index def index
@templates = @templates.where.not(archived_at: nil).preload(:author, :folder, :template_accesses).order(id: :desc) @templates = @templates.where.not(archived_at: nil).preload(:author, :folder).order(id: :desc)
@templates = Templates.search(@templates, params[:q]) @templates = Templates.search(@templates, params[:q])
@pagy, @templates = pagy(@templates, limit: 12) @pagy, @templates = pagy(@templates, items: 12)
end end
end end
@@ -7,15 +7,8 @@ class TemplatesArchivedSubmissionsController < ApplicationController
def index def index
@submissions = @submissions.where.not(archived_at: nil) @submissions = @submissions.where.not(archived_at: nil)
@submissions = Submissions.search(@submissions, params[:q], search_values: true) @submissions = Submissions.search(@submissions, params[:q], search_values: true)
@submissions = Submissions::Filter.call(@submissions, current_user, params)
@submissions = if params[:completed_at_from].present? || params[:completed_at_to].present? @pagy, @submissions = pagy(@submissions.preload(:submitters).order(id: :desc))
@submissions.order(Submitter.arel_table[:completed_at].maximum.desc)
else
@submissions.order(id: :desc)
end
@pagy, @submissions = pagy(@submissions.preload(submitters: :start_form_submission_events))
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound
redirect_to root_path redirect_to root_path
end end
+14 -37
View File
@@ -9,27 +9,19 @@ class TemplatesController < ApplicationController
submissions = @template.submissions.accessible_by(current_ability) submissions = @template.submissions.accessible_by(current_ability)
submissions = submissions.active if @template.archived_at.blank? submissions = submissions.active if @template.archived_at.blank?
submissions = Submissions.search(submissions, params[:q], search_values: true) submissions = Submissions.search(submissions, params[:q], search_values: true)
submissions = Submissions::Filter.call(submissions, current_user, params.except(:status))
@base_submissions = submissions @base_submissions = submissions
submissions = Submissions::Filter.filter_by_status(submissions, params) submissions = submissions.pending if params[:status] == 'pending'
submissions = submissions.completed if params[:status] == 'completed'
submissions = if params[:completed_at_from].present? || params[:completed_at_to].present? @pagy, @submissions = pagy(submissions.preload(:submitters).order(id: :desc))
submissions.order(Submitter.arel_table[:completed_at].maximum.desc)
else
submissions.order(id: :desc)
end
submissions = submissions.preload(:template_accesses) unless current_user.role.in?(%w[admin superadmin])
@pagy, @submissions = pagy(submissions.preload(submitters: :start_form_submission_events))
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound
redirect_to root_path redirect_to root_path
end end
def new def new
@template.name = "#{@base_template.name} (#{I18n.t('clone')})" if @base_template @template.name = "#{@base_template.name} (Clone)" if @base_template
end end
def edit def edit
@@ -74,7 +66,7 @@ class TemplatesController < ApplicationController
if @template.save if @template.save
Templates::CloneAttachments.call(template: @template, original_template: @base_template) if @base_template Templates::CloneAttachments.call(template: @template, original_template: @base_template) if @base_template
enqueue_template_created_webhooks(@template) SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => @template.id)
maybe_redirect_to_template(@template) maybe_redirect_to_template(@template)
else else
@@ -85,21 +77,21 @@ class TemplatesController < ApplicationController
def update def update
@template.update!(template_params) @template.update!(template_params)
enqueue_template_updated_webhooks(@template) SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => @template.id)
head :ok head :ok
end end
def destroy def destroy
notice = notice =
if params[:permanently].in?(['true', true]) if params[:permanently].present?
@template.destroy! @template.destroy!
I18n.t('template_has_been_removed') 'Template has been removed.'
else else
@template.update!(archived_at: Time.current) @template.update!(archived_at: Time.current)
I18n.t('template_has_been_archived') 'Template has been archived.'
end end
redirect_back(fallback_location: root_path, notice:) redirect_back(fallback_location: root_path, notice:)
@@ -110,13 +102,13 @@ class TemplatesController < ApplicationController
def template_params def template_params
params.require(:template).permit( params.require(:template).permit(
:name, :name,
{ schema: [[:attachment_uuid, :name, { conditions: [%i[field_uuid value action operation]] }]], { schema: [%i[attachment_uuid name]],
submitters: [%i[name uuid is_requester linked_to_uuid invite_by_uuid email]], submitters: [%i[name uuid]],
fields: [[:uuid, :submitter_uuid, :name, :type, fields: [[:uuid, :submitter_uuid, :name, :type,
:required, :readonly, :default_value, :required, :readonly, :default_value,
:title, :description, :title, :description,
{ preferences: {}, { preferences: {},
conditions: [%i[field_uuid value action operation]], conditions: [%i[field_uuid value action]],
options: [%i[value uuid]], options: [%i[value uuid]],
validation: %i[message pattern], validation: %i[message pattern],
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]] } areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]] }
@@ -124,29 +116,14 @@ class TemplatesController < ApplicationController
end end
def authorized_clone_account_id?(account_id) def authorized_clone_account_id?(account_id)
true_user.account_id.to_s == account_id.to_s || true_user.account_id.to_s == account_id.to_s || true_user.account.linked_accounts.exists?(id: account_id)
true_user.account.linked_accounts.accessible_by(current_ability).exists?(id: account_id)
end end
def maybe_redirect_to_template(template) def maybe_redirect_to_template(template)
if template.account == current_account if template.account == current_account
redirect_to(edit_template_path(@template)) redirect_to(edit_template_path(@template))
else else
redirect_back(fallback_location: root_path, notice: I18n.t('template_has_been_cloned')) redirect_back(fallback_location: root_path, notice: 'Template has been clonned')
end
end
def enqueue_template_created_webhooks(template)
WebhookUrls.for_account_id(template.account_id, 'template.created').each do |webhook_url|
SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => template.id,
'webhook_url_id' => webhook_url.id)
end
end
def enqueue_template_updated_webhooks(template)
WebhookUrls.for_account_id(template.account_id, 'template.updated').each do |webhook_url|
SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => template.id,
'webhook_url_id' => webhook_url.id)
end end
end end
@@ -9,9 +9,7 @@ class TemplatesDashboardController < ApplicationController
FOLDERS_PER_PAGE = 18 FOLDERS_PER_PAGE = 18
def index def index
@template_folders = @template_folders.where(id: @templates.active.select(:folder_id)).order(id: :desc) @template_folders = filter_template_folders(@template_folders)
@template_folders = TemplateFolders.search(@template_folders, params[:q])
@pagy, @template_folders = pagy( @pagy, @template_folders = pagy(
@template_folders, @template_folders,
@@ -25,21 +23,29 @@ class TemplatesDashboardController < ApplicationController
@template_folders = @template_folders.reject { |e| e.name == TemplateFolder::DEFAULT_NAME } @template_folders = @template_folders.reject { |e| e.name == TemplateFolder::DEFAULT_NAME }
@templates = filter_templates(@templates) @templates = filter_templates(@templates)
limit = items =
if @template_folders.size < 4 if @template_folders.size < 4
TEMPLATES_PER_PAGE TEMPLATES_PER_PAGE
else else
(@template_folders.size < 7 ? 9 : 6) (@template_folders.size < 7 ? 9 : 6)
end end
@pagy, @templates = pagy(@templates, limit:) @pagy, @templates = pagy(@templates, items:)
end end
end end
private private
def filter_template_folders(template_folders)
rel = template_folders.joins(:active_templates)
.order(id: :desc)
.distinct
TemplateFolders.search(rel, params[:q])
end
def filter_templates(templates) def filter_templates(templates)
rel = templates.active.preload(:author, :template_accesses).order(id: :desc) rel = templates.active.preload(:author).order(id: :desc)
if params[:q].blank? if params[:q].blank?
if Docuseal.multitenant? && !current_account.testing? if Docuseal.multitenant? && !current_account.testing?
@@ -9,9 +9,9 @@ class TemplatesFoldersController < ApplicationController
@template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:name]) @template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:name])
if @template.save if @template.save
redirect_back(fallback_location: template_path(@template), notice: I18n.t('document_template_has_been_moved')) redirect_back(fallback_location: template_path(@template), notice: 'Document template has been moved')
else else
redirect_back(fallback_location: template_path(@template), notice: I18n.t('unable_to_move_template_into_folder')) redirect_back(fallback_location: template_path(@template), notice: 'Unable to move template into folder')
end end
end end
@@ -9,7 +9,7 @@ class TemplatesPreferencesController < ApplicationController
authorize!(:update, @template) authorize!(:update, @template)
@template.preferences = @template.preferences.merge(template_params[:preferences]) @template.preferences = @template.preferences.merge(template_params[:preferences])
@template.preferences = @template.preferences.reject { |_, v| (v.is_a?(String) || v.is_a?(Hash)) && v.blank? } @template.preferences = @template.preferences.reject { |_, v| v.is_a?(String) && v.blank? }
@template.save! @template.save!
head :ok head :ok
@@ -22,19 +22,13 @@ class TemplatesPreferencesController < ApplicationController
preferences: %i[bcc_completed request_email_subject request_email_body preferences: %i[bcc_completed request_email_subject request_email_body
documents_copy_email_subject documents_copy_email_body documents_copy_email_subject documents_copy_email_body
documents_copy_email_enabled documents_copy_email_attach_audit documents_copy_email_enabled documents_copy_email_attach_audit
documents_copy_email_attach_documents
completed_notification_email_attach_documents completed_notification_email_attach_documents
completed_redirect_url
submitters_order
completed_notification_email_subject completed_notification_email_body completed_notification_email_subject completed_notification_email_body
completed_notification_email_enabled completed_notification_email_attach_audit] + completed_notification_email_enabled completed_notification_email_attach_audit]
[completed_message: %i[title body]]
).tap do |attrs| ).tap do |attrs|
attrs[:preferences] = attrs[:preferences].transform_values do |value| attrs[:preferences] = attrs[:preferences].transform_values do |value|
if %w[true false].include?(value) if %w[true false].include?(value)
value == 'true' value == 'true'
elsif value.respond_to?(:compact_blank)
value.compact_blank
else else
value value
end end
@@ -1,54 +0,0 @@
# frozen_string_literal: true
class TemplatesRecipientsController < ApplicationController
load_and_authorize_resource :template
def create
authorize!(:update, @template)
@template.submitters =
submitters_params.map { |s| s.reject { |_, v| v.is_a?(String) && v.blank? } }
@template.save!
render json: { submitters: @template.submitters }
end
private
def submitters_params
params.require(:template).permit(
submitters: [%i[name uuid is_requester invite_by_uuid linked_to_uuid email option]]
).fetch(:submitters, {}).values.filter_map do |s|
next if s[:uuid].blank?
if s[:is_requester] == '1' && s[:invite_by_uuid].blank?
s[:is_requester] = true
else
s.delete(:is_requester)
end
s.delete(:invite_by_uuid) if s[:invite_by_uuid].blank?
option = s.delete(:option)
if option.present?
case option
when 'is_requester'
s[:is_requester] = true
when 'not_set'
s.delete(:is_requester)
s.delete(:email)
s.delete(:linked_to_uuid)
s.delete(:invite_by_uuid)
when /\Alinked_to_(.*)\z/
s[:linked_to_uuid] = ::Regexp.last_match(-1)
when /\Ainvite_by_(.*)\z/
s[:invite_by_uuid] = ::Regexp.last_match(-1)
end
end
s
end
end
end
@@ -6,6 +6,6 @@ class TemplatesRestoreController < ApplicationController
def create def create
@template.update!(archived_at: nil) @template.update!(archived_at: nil)
redirect_to template_path(@template), notice: I18n.t('template_has_been_unarchived') redirect_to template_path(@template), notice: 'Template has been unarchived'
end end
end end
@@ -23,7 +23,7 @@ class TemplatesUploadsController < ApplicationController
@template.update!(schema:) @template.update!(schema:)
enqueue_template_created_webhooks(@template) SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => @template.id)
redirect_to edit_template_path(@template) redirect_to edit_template_path(@template)
rescue Templates::CreateAttachments::PdfEncrypted rescue Templates::CreateAttachments::PdfEncrypted
@@ -33,7 +33,7 @@ class TemplatesUploadsController < ApplicationController
raise if Rails.env.local? raise if Rails.env.local?
redirect_to root_path, alert: I18n.t('unable_to_update_file') redirect_to root_path, alert: 'Unable to upload file'
end end
private private
@@ -52,25 +52,23 @@ class TemplatesUploadsController < ApplicationController
def create_file_params_from_url def create_file_params_from_url
tempfile = Tempfile.new tempfile = Tempfile.new
tempfile.binmode tempfile.binmode
tempfile.write(DownloadUtils.call(params[:url]).body) tempfile.write(conn.get(Addressable::URI.parse(params[:url]).display_uri.to_s).body)
tempfile.rewind tempfile.rewind
filename = URI.decode_www_form_component(params[:filename]) if params[:filename].present?
filename ||= File.basename(URI.decode_www_form_component(params[:url]))
file = ActionDispatch::Http::UploadedFile.new( file = ActionDispatch::Http::UploadedFile.new(
tempfile:, tempfile:,
filename:, filename: File.basename(
URI.decode_www_form_component(params[:filename].presence || params[:url]), '.*'
),
type: Marcel::MimeType.for(tempfile) type: Marcel::MimeType.for(tempfile)
) )
{ files: [file] } { files: [file] }
end end
def enqueue_template_created_webhooks(template) def conn
WebhookUrls.for_account_id(template.account_id, 'template.created').each do |webhook_url| Faraday.new do |faraday|
SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => template.id, faraday.response :follow_redirects
'webhook_url_id' => webhook_url.id)
end end
end end
end end
@@ -4,8 +4,9 @@ class TestingApiSettingsController < ApplicationController
def index def index
authorize!(:manage, current_user.access_token) authorize!(:manage, current_user.access_token)
@webhook_url = current_account.webhook_urls.first_or_initialize @webhook_config =
current_account.encrypted_configs.find_or_initialize_by(key: EncryptedConfig::WEBHOOK_URL_KEY)
authorize!(:manage, @webhook_url) authorize!(:manage, @webhook_config)
end end
end end
@@ -14,12 +14,12 @@ class TimestampServerController < ApplicationController
test_timeserver_url(@encrypted_config.value) if @encrypted_config.value.present? test_timeserver_url(@encrypted_config.value) if @encrypted_config.value.present?
if @encrypted_config.value.present? ? @encrypted_config.save : @encrypted_config.delete if @encrypted_config.value.present? ? @encrypted_config.save : @encrypted_config.delete
redirect_back fallback_location: settings_notifications_path, notice: I18n.t('changes_have_been_saved') redirect_back fallback_location: settings_notifications_path, notice: 'Changes have been saved'
else else
redirect_back fallback_location: settings_notifications_path, alert: I18n.t('unable_to_save') redirect_back fallback_location: settings_notifications_path, alert: 'Unable to save'
end end
rescue SocketError, TimestampError, OpenSSL::Timestamp::TimestampError rescue SocketError, TimestampError, OpenSSL::Timestamp::TimestampError
redirect_back fallback_location: settings_notifications_path, alert: t('invalid_timeserver') redirect_back fallback_location: settings_notifications_path, alert: 'Invalid Timeserver'
end end
private private
+4 -4
View File
@@ -9,7 +9,7 @@ class UserInitialsController < ApplicationController
def update def update
file = params[:file] file = params[:file]
return redirect_to settings_profile_index_path, notice: I18n.t('unable_to_save_initials') if file.blank? return redirect_to settings_profile_index_path, notice: 'Unable to save initials' if file.blank?
blob = ActiveStorage::Blob.create_and_upload!(io: file.open, blob = ActiveStorage::Blob.create_and_upload!(io: file.open,
filename: file.original_filename, filename: file.original_filename,
@@ -22,16 +22,16 @@ class UserInitialsController < ApplicationController
) )
if @user_config.update(value: attachment.uuid) if @user_config.update(value: attachment.uuid)
redirect_to settings_profile_index_path, notice: I18n.t('initials_has_been_saved') redirect_to settings_profile_index_path, notice: 'Initials has been saved'
else else
redirect_to settings_profile_index_path, notice: I18n.t('unable_to_save_initials') redirect_to settings_profile_index_path, notice: 'Unable to save initials'
end end
end end
def destroy def destroy
@user_config.destroy @user_config.destroy
redirect_to settings_profile_index_path, notice: I18n.t('initials_has_been_removed') redirect_to settings_profile_index_path, notice: 'Initials has been removed'
end end
private private
@@ -9,7 +9,7 @@ class UserSignaturesController < ApplicationController
def update def update
file = params[:file] file = params[:file]
return redirect_to settings_profile_index_path, notice: I18n.t('unable_to_save_signature') if file.blank? return redirect_to settings_profile_index_path, notice: 'Unable to save signature' if file.blank?
blob = ActiveStorage::Blob.create_and_upload!(io: file.open, blob = ActiveStorage::Blob.create_and_upload!(io: file.open,
filename: file.original_filename, filename: file.original_filename,
@@ -22,16 +22,16 @@ class UserSignaturesController < ApplicationController
) )
if @user_config.update(value: attachment.uuid) if @user_config.update(value: attachment.uuid)
redirect_to settings_profile_index_path, notice: I18n.t('signature_has_been_saved') redirect_to settings_profile_index_path, notice: 'Signature has been saved'
else else
redirect_to settings_profile_index_path, notice: I18n.t('unable_to_save_signature') redirect_to settings_profile_index_path, notice: 'Unable to save signature'
end end
end end
def destroy def destroy
@user_config.destroy @user_config.destroy
redirect_to settings_profile_index_path, notice: I18n.t('signature_has_been_removed') redirect_to settings_profile_index_path, notice: 'Signature has been removed'
end end
private private
+22 -15
View File
@@ -9,11 +9,9 @@ class UsersController < ApplicationController
def index def index
@users = @users =
if params[:status] == 'archived' if params[:status] == 'archived'
@users.archived.where.not(role: 'integration') @users.archived
elsif params[:status] == 'integration'
@users.active.where(role: 'integration')
else else
@users.active.where.not(role: 'integration') @users.active
end end
@pagy, @users = pagy(@users.where(account: current_account).order(id: :desc)) @pagy, @users = pagy(@users.where(account: current_account).order(id: :desc))
@@ -24,25 +22,27 @@ class UsersController < ApplicationController
def edit; end def edit; end
def create def create
if User.accessible_by(current_ability).exists?(email: @user.email) existing_user = User.accessible_by(current_ability).find_by(email: @user.email)
@user.errors.add(:email, I18n.t('already_exists'))
return render turbo_stream: turbo_stream.replace(:modal, template: 'users/new'), status: :unprocessable_entity if existing_user
existing_user.archived_at = nil
existing_user.assign_attributes(user_params)
existing_user.account = current_account
@user = existing_user
end end
@user.role = User::ADMIN_ROLE unless role_valid?(@user.role)
if @user.save if @user.save
UserMailer.invitation_email(@user).deliver_later! UserMailer.invitation_email(@user).deliver_later!
redirect_back fallback_location: settings_users_path, notice: I18n.t('user_has_been_invited') redirect_back fallback_location: settings_users_path, notice: 'User has been invited'
else else
render turbo_stream: turbo_stream.replace(:modal, template: 'users/new'), status: :unprocessable_entity render turbo_stream: turbo_stream.replace(:modal, template: 'users/new'), status: :unprocessable_entity
end end
end end
def update def update
return redirect_to settings_users_path, notice: I18n.t('unable_to_update_user') if Docuseal.demo? return redirect_to settings_users_path, notice: 'Unable to update user.' if Docuseal.demo?
attrs = user_params.compact_blank.merge(user_params.slice(:archived_at)) attrs = user_params.compact_blank.merge(user_params.slice(:archived_at))
attrs.delete(:role) if !role_valid?(attrs[:role]) || current_user == @user attrs.delete(:role) if !role_valid?(attrs[:role]) || current_user == @user
@@ -56,7 +56,7 @@ class UsersController < ApplicationController
end end
if @user.update(attrs) if @user.update(attrs)
redirect_back fallback_location: settings_users_path, notice: I18n.t('user_has_been_updated') redirect_back fallback_location: settings_users_path, notice: 'User has been updated'
else else
render turbo_stream: turbo_stream.replace(:modal, template: 'users/edit'), status: :unprocessable_entity render turbo_stream: turbo_stream.replace(:modal, template: 'users/edit'), status: :unprocessable_entity
end end
@@ -64,12 +64,12 @@ class UsersController < ApplicationController
def destroy def destroy
if Docuseal.demo? || @user.id == current_user.id if Docuseal.demo? || @user.id == current_user.id
return redirect_to settings_users_path, notice: I18n.t('unable_to_remove_user') return redirect_to settings_users_path, notice: 'Unable to remove user'
end end
@user.update!(archived_at: Time.current) @user.update!(archived_at: Time.current)
redirect_back fallback_location: settings_users_path, notice: I18n.t('user_has_been_removed') redirect_back fallback_location: settings_users_path, notice: 'User has been removed'
end end
private private
@@ -79,7 +79,14 @@ class UsersController < ApplicationController
end end
def build_user def build_user
@user = current_account.users.new(user_params) @user = current_account.users.find_by(email: user_params[:email])&.tap do |user|
user.assign_attributes(user_params)
user.archived_at = nil
end
@user ||= current_account.users.new(user_params)
@user
end end
def user_params def user_params
@@ -14,6 +14,6 @@ class VerifyPdfSignatureController < ApplicationController
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: })
rescue HexaPDF::MalformedPDFError rescue HexaPDF::MalformedPDFError
render turbo_stream: turbo_stream.replace('result', html: helpers.tag.div(I18n.t('invalid_pdf'), id: 'result')) render turbo_stream: turbo_stream.replace('result', html: helpers.tag.div('Invalid PDF', id: 'result'))
end end
end end
@@ -1,22 +1,36 @@
# frozen_string_literal: true # frozen_string_literal: true
class WebhookPreferencesController < ApplicationController class WebhookPreferencesController < ApplicationController
load_and_authorize_resource :webhook_url, parent: false EVENTS = %w[
form.viewed
form.started
form.completed
template.created
template.updated
submission.created
submission.archived
].freeze
def update before_action :load_account_config
webhook_preferences_params[:events].each do |event, val| authorize_resource :account_config, parent: false
@webhook_url.events.delete(event) if val == '0'
@webhook_url.events.push(event) if val == '1' && @webhook_url.events.exclude?(event)
end
@webhook_url.save! def create
@account_config.value[account_config_params[:event]] = account_config_params[:value] == '1'
@account_config.save!
head :ok head :ok
end end
private private
def webhook_preferences_params def load_account_config
params.require(:webhook_url).permit(events: {}) @account_config =
current_account.account_configs.find_or_initialize_by(key: AccountConfig::WEBHOOK_PREFERENCES_KEY)
@account_config.value ||= {}
end
def account_config_params
params.permit(:event, :value)
end end
end end
+16 -9
View File
@@ -1,22 +1,29 @@
# frozen_string_literal: true # frozen_string_literal: true
class WebhookSecretController < ApplicationController class WebhookSecretController < ApplicationController
load_and_authorize_resource :webhook_url, parent: false before_action :load_encrypted_config
authorize_resource :encrypted_config, parent: false
def show; end def index; end
def update def create
@webhook_url.update!(secret: { @encrypted_config.assign_attributes(value: {
webhook_secret_params[:key] => webhook_secret_params[:value] encrypted_config_params[:key] => encrypted_config_params[:value]
}.compact_blank) }.compact_blank)
redirect_back(fallback_location: settings_webhook_path(@webhook_url), @encrypted_config.value.present? ? @encrypted_config.save! : @encrypted_config.delete
notice: I18n.t('webhook_secret_has_been_saved'))
redirect_back(fallback_location: settings_webhooks_path, notice: 'Webhook Secret has been saved.')
end end
private private
def webhook_secret_params def load_encrypted_config
params.require(:webhook_url).permit(secret: %i[key value]).fetch(:secret, {}) @encrypted_config =
current_account.encrypted_configs.find_or_initialize_by(key: EncryptedConfig::WEBHOOK_SECRET_KEY)
end
def encrypted_config_params
params.require(:encrypted_config).permit(value: %i[key value]).fetch(:value, {})
end end
end end
+14 -38
View File
@@ -1,60 +1,36 @@
# frozen_string_literal: true # frozen_string_literal: true
class WebhookSettingsController < ApplicationController class WebhookSettingsController < ApplicationController
load_and_authorize_resource :webhook_url, parent: false, only: %i[index show new create update destroy] before_action :load_encrypted_config
load_and_authorize_resource :webhook_url, only: %i[resend], id_param: :webhook_id authorize_resource :encrypted_config, parent: false
def index
@webhook_urls = @webhook_urls.order(id: :desc)
@webhook_url = @webhook_urls.first_or_initialize
render @webhook_urls.size > 1 ? 'index' : 'show'
end
def show; end def show; end
def new; end
def create def create
@webhook_url.save! @encrypted_config.assign_attributes(encrypted_config_params)
redirect_to settings_webhooks_path, notice: I18n.t('webhook_url_has_been_saved') @encrypted_config.value.present? ? @encrypted_config.save! : @encrypted_config.delete
redirect_back(fallback_location: settings_webhooks_path, notice: 'Webhook URL has been saved.')
end end
def update def update
@webhook_url.update!(update_params)
redirect_back(fallback_location: settings_webhook_path(@webhook_url),
notice: I18n.t('webhook_url_has_been_updated'))
end
def destroy
@webhook_url.destroy!
redirect_to settings_webhooks_path, notice: I18n.t('webhook_url_has_been_deleted')
end
def resend
submitter = current_account.submitters.where.not(completed_at: nil).order(:id).last submitter = current_account.submitters.where.not(completed_at: nil).order(:id).last
if submitter.blank? || @webhook_url.blank? SendFormCompletedWebhookRequestJob.perform_async({ 'submitter_id' => submitter.id,
return redirect_back(fallback_location: settings_webhooks_path, 'encrypted_config_id' => @encrypted_config.id })
alert: I18n.t('unable_to_resend_webhook_request'))
end
SendFormCompletedWebhookRequestJob.perform_async('submitter_id' => submitter.id, redirect_back(fallback_location: settings_webhooks_path, notice: 'Webhook request has been sent.')
'webhook_url_id' => @webhook_url.id)
redirect_back(fallback_location: settings_webhooks_path, notice: I18n.t('webhook_request_has_been_sent'))
end end
private private
def create_params def load_encrypted_config
params.require(:webhook_url).permit(:url, events: []).reverse_merge(events: []) @encrypted_config =
current_account.encrypted_configs.find_or_initialize_by(key: EncryptedConfig::WEBHOOK_URL_KEY)
end end
def update_params def encrypted_config_params
params.require(:webhook_url).permit(:url) params.require(:encrypted_config).permit(:value)
end end
end end
+2 -30
View File
@@ -25,12 +25,6 @@ import PromptPassword from './elements/prompt_password'
import EmailsTextarea from './elements/emails_textarea' import EmailsTextarea from './elements/emails_textarea'
import ToggleOnSubmit from './elements/toggle_on_submit' import ToggleOnSubmit from './elements/toggle_on_submit'
import PasswordInput from './elements/password_input' import PasswordInput from './elements/password_input'
import SearchInput from './elements/search_input'
import ToggleAttribute from './elements/toggle_attribute'
import LinkedInput from './elements/linked_input'
import CheckboxGroup from './elements/checkbox_group'
import MaskedInput from './elements/masked_input'
import SetDateButton from './elements/set_date_button'
import * as TurboInstantClick from './lib/turbo_instant_click' import * as TurboInstantClick from './lib/turbo_instant_click'
@@ -93,17 +87,9 @@ safeRegisterElement('emails-textarea', EmailsTextarea)
safeRegisterElement('toggle-cookies', ToggleCookies) safeRegisterElement('toggle-cookies', ToggleCookies)
safeRegisterElement('toggle-on-submit', ToggleOnSubmit) safeRegisterElement('toggle-on-submit', ToggleOnSubmit)
safeRegisterElement('password-input', PasswordInput) safeRegisterElement('password-input', PasswordInput)
safeRegisterElement('search-input', SearchInput)
safeRegisterElement('toggle-attribute', ToggleAttribute)
safeRegisterElement('linked-input', LinkedInput)
safeRegisterElement('checkbox-group', CheckboxGroup)
safeRegisterElement('masked-input', MaskedInput)
safeRegisterElement('set-date-button', SetDateButton)
safeRegisterElement('template-builder', class extends HTMLElement { safeRegisterElement('template-builder', class extends HTMLElement {
connectedCallback () { connectedCallback () {
document.addEventListener('turbo:submit-end', this.onSubmit)
this.appElem = document.createElement('div') this.appElem = document.createElement('div')
this.appElem.classList.add('md:h-screen') this.appElem.classList.add('md:h-screen')
@@ -113,36 +99,23 @@ safeRegisterElement('template-builder', class extends HTMLElement {
backgroundColor: '#faf7f5', backgroundColor: '#faf7f5',
locale: this.dataset.locale, locale: this.dataset.locale,
withPhone: this.dataset.withPhone === 'true', withPhone: this.dataset.withPhone === 'true',
withVerification: ['true', 'false'].includes(this.dataset.withVerification) ? this.dataset.withVerification === 'true' : null,
withLogo: this.dataset.withLogo !== 'false', withLogo: this.dataset.withLogo !== 'false',
editable: this.dataset.editable !== 'false', editable: this.dataset.editable !== 'false',
authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content, authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content,
withPayment: this.dataset.withPayment === 'true', withPayment: this.dataset.withPayment === 'true',
isPaymentConnected: this.dataset.isPaymentConnected === 'true', isPaymentConnected: this.dataset.isPaymentConnected === 'true',
withFormula: this.dataset.withFormula === 'true', withFormula: this.dataset.withFormula === 'true',
withSendButton: this.dataset.withSendButton !== 'false',
withSignYourselfButton: this.dataset.withSignYourselfButton !== 'false',
withConditions: this.dataset.withConditions === 'true', withConditions: this.dataset.withConditions === 'true',
currencies: (this.dataset.currencies || '').split(',').filter(Boolean), currencies: (this.dataset.currencies || '').split(',').filter(Boolean),
acceptFileTypes: this.dataset.acceptFileTypes acceptFileTypes: this.dataset.acceptFileTypes
}) })
this.component = this.app.mount(this.appElem) this.app.mount(this.appElem)
this.appendChild(this.appElem) this.appendChild(this.appElem)
} }
onSubmit = (e) => {
if (e.detail.success && e.detail?.formSubmission?.formElement?.id === 'submitters_form') {
e.detail.fetchResponse.response.json().then((data) => {
this.component.template.submitters = data.submitters
})
}
}
disconnectedCallback () { disconnectedCallback () {
document.removeEventListener('turbo:submit-end', this.onSubmit)
this.app?.unmount() this.app?.unmount()
this.appElem?.remove() this.appElem?.remove()
} }
@@ -155,8 +128,7 @@ safeRegisterElement('import-list', class extends HTMLElement {
this.app = createApp(ImportList, { this.app = createApp(ImportList, {
template: JSON.parse(this.dataset.template), template: JSON.parse(this.dataset.template),
multitenant: this.dataset.multitenant === 'true', multitenant: this.dataset.multitenant === 'true',
authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content, authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content
i18n: JSON.parse(this.dataset.i18n || '{}')
}) })
this.app.mount(this.appElem) this.app.mount(this.appElem)
+6 -19
View File
@@ -97,27 +97,10 @@ button[disabled] .enabled {
bottom: auto; bottom: auto;
} }
.tooltip-bottom-start:before {
transform: translateX(-31%);
top: var(--tooltip-offset);
left: 100%;
right: auto;
bottom: auto;
}
.tooltip-bottom-start:after {
transform: translateX(-25%);
border-color: transparent transparent var(--tooltip-color) transparent;
top: var(--tooltip-tail-offset);
left: 50%;
right: auto;
bottom: auto;
}
.autocomplete { .autocomplete {
background: white; background: white;
z-index: 1000; z-index: 1000;
font: 16px/25px "-apple-system", BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; font: 14px/22px "-apple-system", BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
overflow: auto; overflow: auto;
box-sizing: border-box; box-sizing: border-box;
@apply border border-base-300 mt-1 rounded-md; @apply border border-base-300 mt-1 rounded-md;
@@ -128,7 +111,7 @@ button[disabled] .enabled {
} }
.autocomplete > div { .autocomplete > div {
@apply px-2 py-1.5 font-normal; @apply px-2 py-1 font-normal text-sm;
} }
.autocomplete .group { .autocomplete .group {
@@ -147,3 +130,7 @@ button[disabled] .enabled {
outline-offset: 3px; outline-offset: 3px;
outline-color: hsl(var(--bc) / 0.2); outline-color: hsl(var(--bc) / 0.2);
} }
select:required:invalid {
color: gray !important;
}
-4
View File
@@ -12,10 +12,6 @@ window.customElements.define('draw-signature', class extends HTMLElement {
this.pad = new SignaturePad(this.canvas) this.pad = new SignaturePad(this.canvas)
if (this.dataset.color) {
this.pad.penColor = this.dataset.color
}
this.pad.addEventListener('endStroke', () => { this.pad.addEventListener('endStroke', () => {
this.updateSubmitButtonVisibility() this.updateSubmitButtonVisibility()
}) })
-15
View File
@@ -1,15 +0,0 @@
export default class extends HTMLElement {
connectedCallback () {
this.items.forEach((item) => {
item.addEventListener('change', (e) => {
this.items.forEach((item) => {
item.checked = item === e.target && e.target.checked
})
})
})
}
get items () {
return this.querySelectorAll('input[type="checkbox"]')
}
}
-31
View File
@@ -1,31 +0,0 @@
export default class extends HTMLElement {
connectedCallback () {
if (this.target) {
this.input.value = this.target.value
this.target.addEventListener('input', (e) => {
this.input.value = e.target.value
})
this.target.addEventListener('linked-input.update', (e) => {
this.input.value = e.target.value
})
}
}
get input () {
return this.querySelector('input')
}
get target () {
if (this.dataset.targetId) {
const listItem = this.closest('[data-targets="dynamic-list.items"]')
if (listItem) {
return listItem.querySelector(`#${this.dataset.targetId}`)
} else {
return document.getElementById(this.dataset.targetId)
}
}
}
}
-18
View File
@@ -1,18 +0,0 @@
export default class extends HTMLElement {
connectedCallback () {
const maskedToken = this.input.value
this.input.addEventListener('focus', () => {
this.input.value = this.dataset.token
this.input.select()
})
this.input.addEventListener('focusout', () => {
this.input.value = maskedToken
})
}
get input () {
return this.querySelector('input')
}
}
-25
View File
@@ -1,25 +0,0 @@
export default class extends HTMLElement {
connectedCallback () {
this.input.addEventListener('focus', () => {
if (this.title) {
this.title.classList.add('hidden', 'md:block')
this.input.classList.add('w-60')
}
})
this.input.addEventListener('blur', (e) => {
if (this.title && !e.target.value) {
this.title.classList.remove('hidden')
this.input.classList.remove('w-60')
}
})
}
get input () {
return this.querySelector('input')
}
get title () {
return document.querySelector(this.dataset.title)
}
}
@@ -1,20 +0,0 @@
export default class extends HTMLElement {
connectedCallback () {
this.button.addEventListener('click', () => {
this.fromInput.value = this.dataset.fromValue || ''
this.toInput.value = this.dataset.toValue || ''
})
}
get button () {
return this.querySelector('button')
}
get fromInput () {
return document.getElementById(this.dataset.fromId)
}
get toInput () {
return document.getElementById(this.dataset.toId)
}
}
@@ -24,7 +24,6 @@ export default class extends HTMLElement {
if (input && item[field]) { if (input && item[field]) {
input.value = item[field] input.value = item[field]
input.dispatchEvent(new CustomEvent('linked-input.update', { bubbles: true }))
} }
if (textarea && item[field]) { if (textarea && item[field]) {
@@ -1,28 +0,0 @@
export default class extends HTMLElement {
connectedCallback () {
this.input.addEventListener('change', (event) => {
if (this.dataset.attribute) {
this.target[this.dataset.attribute] = event.target.checked
}
if (this.dataset.className) {
this.target.classList.toggle(this.dataset.className, event.target.value !== this.dataset.value)
if (this.dataset.className === 'hidden' && this.target.tagName === 'INPUT') {
this.target.disabled = event.target.value !== this.dataset.value
}
}
if (this.dataset.attribute === 'disabled') {
this.target.value = ''
}
})
}
get input () {
return this.querySelector('input[type="checkbox"]') || this.querySelector('select')
}
get target () {
return document.getElementById(this.dataset.targetId)
}
}
+1 -1
View File
@@ -22,7 +22,7 @@ export default actionable(class extends HTMLElement {
} }
onSubmit = (e) => { onSubmit = (e) => {
if (e.detail.success && e.detail?.formSubmission?.formElement?.dataset?.closeOnSubmit !== 'false') { if (e.detail.success) {
this.close() this.close()
} }
} }
-4
View File
@@ -14,22 +14,18 @@ safeRegisterElement('submission-form', class extends HTMLElement {
this.app = createApp(Form, { this.app = createApp(Form, {
submitter: JSON.parse(this.dataset.submitter), submitter: JSON.parse(this.dataset.submitter),
inviteSubmitters: JSON.parse(this.dataset.inviteSubmitters),
schema: JSON.parse(this.dataset.schema),
canSendEmail: this.dataset.canSendEmail === 'true', canSendEmail: this.dataset.canSendEmail === 'true',
previousSignatureValue: this.dataset.previousSignatureValue, previousSignatureValue: this.dataset.previousSignatureValue,
goToLast: this.dataset.goToLast === 'true', goToLast: this.dataset.goToLast === 'true',
isDemo: this.dataset.isDemo === 'true', isDemo: this.dataset.isDemo === 'true',
attribution: this.dataset.attribution !== 'false', attribution: this.dataset.attribution !== 'false',
scrollPadding: this.dataset.scrollPadding || '-80px', scrollPadding: this.dataset.scrollPadding || '-80px',
language: this.dataset.language,
dryRun: this.dataset.dryRun === 'true', dryRun: this.dataset.dryRun === 'true',
expand: ['true', 'false'].includes(this.dataset.expand) ? this.dataset.expand === 'true' : null, expand: ['true', 'false'].includes(this.dataset.expand) ? this.dataset.expand === 'true' : null,
withSignatureId: this.dataset.withSignatureId === 'true', withSignatureId: this.dataset.withSignatureId === 'true',
requireSigningReason: this.dataset.requireSigningReason === 'true', requireSigningReason: this.dataset.requireSigningReason === 'true',
withConfetti: this.dataset.withConfetti !== 'false', withConfetti: this.dataset.withConfetti !== 'false',
withDisclosure: this.dataset.withDisclosure === 'true', withDisclosure: this.dataset.withDisclosure === 'true',
reuseSignature: this.dataset.reuseSignature !== 'false',
withTypedSignature: this.dataset.withTypedSignature !== 'false', withTypedSignature: this.dataset.withTypedSignature !== 'false',
authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content, authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content,
rememberSignature: this.dataset.rememberSignature === 'true', rememberSignature: this.dataset.rememberSignature === 'true',
+4
View File
@@ -31,6 +31,10 @@ button[disabled] .enabled {
display: none; display: none;
} }
select:required:invalid {
@apply text-gray-300;
}
.input-bordered { .input-bordered {
@apply border-base-content/20; @apply border-base-content/20;
} }
+2 -8
View File
@@ -18,15 +18,9 @@ if (token) {
transform (payload) { transform (payload) {
payload.body.telemetry = [] payload.body.telemetry = []
if (payload.request.query_string) { const string = JSON.stringify(payload)
payload.request.query_string = ''
}
if (payload.request.url) { return JSON.parse(string.replace(/(\/[des]\/)\w{6}/g, (_, m) => m))
payload.request.url = payload.request.url.replace(/(\/[sdep]\/)(\w{5})[^/]+/, '$1$2')
}
return payload
}, },
payload: { payload: {
client: { client: {
+10 -18
View File
@@ -3,7 +3,7 @@
class="field-area 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="{ 'font-serif': field.preferences?.font === 'Times', 'text-[1.6vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars, 'cursor-default': !submittable, 'border border-red-100 bg-red-100 cursor-pointer': submittable, 'border border-red-100': !isActive && submittable, 'bg-opacity-80': !isActive && !isValueSet && submittable, 'field-area-active outline-red-500 outline-dashed outline-2 z-10': isActive && submittable, 'bg-opacity-40': (isActive || isValueSet) && submittable }" :class="{ 'text-[1.6vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars, 'cursor-default': !submittable, 'border border-red-100 bg-red-100 cursor-pointer': submittable, 'border border-red-100': !isActive && submittable, 'bg-opacity-80': !isActive && !isValueSet && submittable, 'field-area-active outline-red-500 outline-dashed outline-2 z-10': isActive && submittable, 'bg-opacity-40': (isActive || isValueSet) && submittable }"
> >
<div <div
v-if="(!withFieldPlaceholder || !field.name || field.type === 'cells') && !isActive && !isValueSet && field.type !== 'checkbox' && submittable && !area.option_uuid" v-if="(!withFieldPlaceholder || !field.name || field.type === 'cells') && !isActive && !isValueSet && field.type !== 'checkbox' && submittable && !area.option_uuid"
@@ -29,16 +29,17 @@
{{ optionValue(option) }} {{ optionValue(option) }}
</template> </template>
<template v-else> <template v-else>
{{ field.title || field.name || fieldNames[field.type] }} {{ field.name || fieldNames[field.type] }}
<template v-if="field.type === 'checkbox' && !field.name"> <template v-if="field.type === 'checkbox' && !field.name">
{{ fieldIndex + 1 }} {{ fieldIndex + 1 }}
</template> </template>
<template v-else-if="!field.required && field.type !== 'checkbox'"> <template v-else-if="!field.required && field.type !== 'checkbox'">
({{ t('optional') }}) (optional)
</template> </template>
</template> </template>
</div> </div>
<div <div
v-if="isActive"
ref="scrollToElem" ref="scrollToElem"
class="absolute" class="absolute"
:style="{ top: scrollPadding }" :style="{ top: scrollPadding }"
@@ -74,7 +75,7 @@
ID: {{ signature.uuid }} ID: {{ signature.uuid }}
</div> </div>
<div> <div>
<span v-if="values[field.preferences?.reason_field_uuid]">{{ t('reason') }}: </span>{{ values[field.preferences?.reason_field_uuid] || t('digitally_signed_by') }} {{ submitter.name }} {{ t('reason') }}: {{ values[field.preferences?.reason_field_uuid] || t('digitally_signed_by') }} {{ submitter.name }}
<template v-if="submitter.email"> <template v-if="submitter.email">
&lt;{{ submitter.email }}&gt; &lt;{{ submitter.email }}&gt;
</template> </template>
@@ -168,7 +169,6 @@
<div <div
v-else-if="field.type === 'cells'" v-else-if="field.type === 'cells'"
class="w-full flex items-center" class="w-full flex items-center"
:class="{ 'justify-end': field.preferences?.align === 'right' }"
> >
<div <div
v-for="(char, index) in modelValue" v-for="(char, index) in modelValue"
@@ -210,7 +210,7 @@
</template> </template>
<script> <script>
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconCheck, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp, IconSquareNumber1, IconId } from '@tabler/icons-vue' import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconCheck, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard, IconRubberStamp, IconSquareNumber1 } from '@tabler/icons-vue'
export default { export default {
name: 'FieldArea', name: 'FieldArea',
@@ -226,8 +226,7 @@ export default {
}, },
submitter: { submitter: {
type: Object, type: Object,
required: false, required: true
default: () => ({})
}, },
withSignatureId: { withSignatureId: {
type: Boolean, type: Boolean,
@@ -312,8 +311,7 @@ export default {
cells: this.t('cells'), cells: this.t('cells'),
stamp: this.t('stamp'), stamp: this.t('stamp'),
payment: this.t('payment'), payment: this.t('payment'),
phone: this.t('phone'), phone: this.t('phone')
verification: this.t('verify_id')
} }
}, },
alignClasses () { alignClasses () {
@@ -342,8 +340,7 @@ export default {
cells: IconColumns3, cells: IconColumns3,
multiple: IconChecks, multiple: IconChecks,
phone: IconPhoneCheck, phone: IconPhoneCheck,
payment: IconCreditCard, payment: IconCreditCard
verification: IconId
} }
}, },
image () { image () {
@@ -407,12 +404,7 @@ export default {
} }
if (this.field.preferences?.font_size) { if (this.field.preferences?.font_size) {
style.fontSize = `clamp(4pt, 1.6vw, ${parseInt(this.field.preferences.font_size) * 1.23}pt)` style.fontSize = this.field.preferences.font_size + 'pt'
style.lineHeight = `clamp(6pt, 2.0vw, ${parseInt(this.field.preferences.font_size) * 1.23 + 3}pt)`
}
if (this.field.preferences?.color) {
style.color = this.field.preferences.color
} }
return style return style
+4 -17
View File
@@ -21,14 +21,14 @@
:values="values" :values="values"
:field="field" :field="field"
:area="area" :area="area"
:submittable="submittable" :submittable="true"
:field-index="fieldIndex" :field-index="fieldIndex"
:scroll-padding="scrollPadding" :scroll-padding="scrollPadding"
:submitter="submitter" :submitter="submitter"
:with-field-placeholder="withFieldPlaceholder" :with-field-placeholder="withFieldPlaceholder"
:with-signature-id="withSignatureId" :with-signature-id="withSignatureId"
:is-active="currentStep === step" :is-active="currentStep === step"
:with-label="withLabel && !withFieldPlaceholder && step.length < 2" :with-label="withLabel && !withFieldPlaceholder"
:is-value-set="step.some((f) => f.uuid in values)" :is-value-set="step.some((f) => f.uuid in values)"
:attachments-index="attachmentsIndex" :attachments-index="attachmentsIndex"
@click="$emit('focus-step', stepIndex)" @click="$emit('focus-step', stepIndex)"
@@ -58,11 +58,6 @@ export default {
required: false, required: false,
default: false default: false
}, },
submittable: {
type: Boolean,
required: false,
default: true
},
submitter: { submitter: {
type: Object, type: Object,
required: true required: true
@@ -132,18 +127,10 @@ export default {
this.scrollInContainer(areaRef.$el) this.scrollInContainer(areaRef.$el)
} else { } else {
const targetRect = areaRef.$refs.scrollToElem.getBoundingClientRect() const targetRect = areaRef.$refs.scrollToElem.getBoundingClientRect()
const root = this.$root.$el?.parentNode?.classList?.contains('ds') ? this.$root.$el : document.body
const rootRect = root.getBoundingClientRect()
const scrollEl = this.scrollEl || window const scrollEl = this.scrollEl || window
let rootRect = {}
if (this.scrollEl === document.documentElement) {
rootRect = this.scrollEl.getBoundingClientRect()
} else {
const root = this.$root.$el?.parentNode?.classList?.contains('ds') ? this.$root.$el : document.body
rootRect = root.getBoundingClientRect()
}
scrollEl.scrollTo({ top: targetRect.top - rootRect.top, behavior: 'smooth' }) scrollEl.scrollTo({ top: targetRect.top - rootRect.top, behavior: 'smooth' })
} }
+5 -20
View File
@@ -1,6 +1,5 @@
<template> <template>
<div <div
id="form_completed"
class="mx-auto max-w-md flex flex-col" class="mx-auto max-w-md flex flex-col"
dir="auto" dir="auto"
> >
@@ -11,7 +10,7 @@
:height="30" :height="30"
/> />
<span> <span>
{{ completedMessage.title || (hasSignatureFields ? (hasMultipleDocuments ? t('documents_have_been_signed') : t('document_has_been_signed')) : t('form_has_been_completed')) }} {{ completedMessage.title || t('form_has_been_completed') }}
</span> </span>
</div> </div>
<div <div
@@ -76,7 +75,7 @@
</a> </a>
<a <a
v-if="isDemo" v-if="isDemo"
href="https://docuseal.com/sign_up" href="https://docuseal.co/sign_up"
class="white-button flex items-center space-x-1 w-full" class="white-button flex items-center space-x-1 w-full"
> >
<IconLogin /> <IconLogin />
@@ -89,9 +88,9 @@
v-if="attribution" v-if="attribution"
class="text-center mt-4" class="text-center mt-4"
> >
{{ t('powered_by') }} {{ t('signed_with') }}
<a <a
href="https://www.docuseal.com/start" href="https://www.docuseal.co/start"
target="_blank" target="_blank"
class="underline" class="underline"
>DocuSeal</a> - {{ t('open_source_documents_software') }} >DocuSeal</a> - {{ t('open_source_documents_software') }}
@@ -130,16 +129,6 @@ export default {
required: false, required: false,
default: true default: true
}, },
hasSignatureFields: {
type: Boolean,
required: false,
default: false
},
hasMultipleDocuments: {
type: Boolean,
required: false,
default: false
},
withDownloadButton: { withDownloadButton: {
type: Boolean, type: Boolean,
required: false, required: false,
@@ -192,10 +181,6 @@ export default {
spread: 140 spread: 140
}) })
} }
if (window.decline_button) {
window.decline_button.setAttribute('disabled', 'true')
}
}, },
methods: { methods: {
sendCopyToEmail () { sendCopyToEmail () {
@@ -223,7 +208,7 @@ export default {
this.downloadUrls(urls) this.downloadUrls(urls)
} }
} else { } else {
alert(this.t('failed_to_download_files')) alert('Failed to download files')
} }
}) })
}, },
@@ -37,7 +37,6 @@
<AppearsOn :field="field" /> <AppearsOn :field="field" />
<div class="text-center"> <div class="text-center">
<input <input
:id="field.uuid"
ref="input" ref="input"
v-model="value" v-model="value"
class="base-input !text-2xl text-center w-full" class="base-input !text-2xl text-center w-full"
@@ -46,7 +45,6 @@
:name="`values[${field.uuid}]`" :name="`values[${field.uuid}]`"
@keydown.enter="onEnter" @keydown.enter="onEnter"
@focus="$emit('focus')" @focus="$emit('focus')"
@paste="onPaste"
> >
</div> </div>
</div> </div>
@@ -100,25 +98,6 @@ export default {
this.$emit('submit') this.$emit('submit')
} }
}, },
onPaste (e) {
e.preventDefault()
let pasteData = e.clipboardData.getData('text').trim()
if (pasteData.match(/^\d{2}\.\d{2}\.\d{4}$/)) {
pasteData = pasteData.split('.').reverse().join('-')
}
const parsedDate = new Date(pasteData)
if (!isNaN(parsedDate)) {
const inputEl = this.$refs.input
inputEl.valueAsDate = new Date(parsedDate.getTime() - parsedDate.getTimezoneOffset() * 60000)
inputEl.dispatchEvent(new Event('input', { bubbles: true }))
}
},
setCurrentDate () { setCurrentDate () {
const inputEl = this.$refs.input const inputEl = this.$refs.input
@@ -1,6 +1,5 @@
<template> <template>
<div <div
id="dropzone"
class="flex h-32 w-full" class="flex h-32 w-full"
@dragover.prevent @dragover.prevent
@drop.prevent="onDropFiles" @drop.prevent="onDropFiles"
+67 -290
View File
@@ -11,58 +11,13 @@
:with-label="!isAnonymousChecboxes && showFieldNames" :with-label="!isAnonymousChecboxes && showFieldNames"
:current-step="currentStepFields" :current-step="currentStepFields"
:scroll-padding="scrollPadding" :scroll-padding="scrollPadding"
@focus-step="[saveStep(), goToStep($event, false, true), currentField.type !== 'checkbox' ? isFormVisible = true : '']" @focus-step="[saveStep(), currentField.type !== 'checkbox' ? isFormVisible = true : '', goToStep($event, false, true)]"
/>
<FieldAreas
:steps="readonlyConditionalFields.map((e) => [e])"
:values="readonlyConditionalFields.reduce((acc, f) => { acc[f.uuid] = (values[f.uuid] || f.default_value); return acc }, {})"
:submitter="submitter"
:attachments-index="attachmentsIndex"
:submittable="false"
/> />
<FormulaFieldAreas <FormulaFieldAreas
v-if="formulaFields.length" v-if="formulaFields.length"
:fields="formulaFields" :fields="formulaFields"
:values="values" :values="values"
/> />
<Teleport
v-if="completeButtonToRef"
:to="completeButtonToRef"
>
<span
v-if="(emptyValueRequiredStep && ((stepFields.length - 1) !== currentStep || currentStepFields !== emptyValueRequiredStep)) || isCompleted"
class="tooltip-left"
:class="{ tooltip: !isCompleted }"
:data-tip="t('fill_all_required_fields_to_complete')"
>
<button
class="btn btn-sm btn-neutral text-white px-4 w-full flex justify-center btn-disabled pointer-events-auto"
@click="[isFormVisible = true, !isCompleted && goToStep(stepFields.indexOf(emptyValueRequiredStep), true, false)]"
>
{{ t('complete') }}
</button>
</span>
<button
v-else
id="complete_form_button"
class="btn btn-sm btn-neutral text-white px-4 w-full flex justify-center"
form="steps_form"
type="submit"
name="completed"
value="true"
:disabled="isSubmittingComplete"
>
<span class="flex items-center">
<IconInnerShadowTop
v-if="isSubmittingComplete"
class="mr-1 animate-spin w-5 h-5"
/>
<span>
{{ t('complete') }}
</span>
</span>
</button>
</Teleport>
<button <button
v-if="!isFormVisible" v-if="!isFormVisible"
id="expand_form_button" id="expand_form_button"
@@ -77,11 +32,8 @@
<template v-else-if="alwaysMinimize"> <template v-else-if="alwaysMinimize">
{{ t('next') }} {{ t('next') }}
</template> </template>
<template v-else-if="isShowContinue">
{{ t('continue') }}
</template>
<template v-else> <template v-else>
{{ t('start_now') }} {{ t('submit_form') }}
</template> </template>
<IconArrowsDiagonal <IconArrowsDiagonal
class="absolute right-0 mr-4" class="absolute right-0 mr-4"
@@ -101,7 +53,7 @@
id="minimize_form_button" id="minimize_form_button"
class="absolute right-0 mr-2 mt-2 top-0 hidden md:block" class="absolute right-0 mr-2 mt-2 top-0 hidden md:block"
:title="t('minimize')" :title="t('minimize')"
@click.prevent="minimizeForm" @click.prevent="isFormVisible = false"
> >
<IconArrowsDiagonalMinimize2 <IconArrowsDiagonalMinimize2
:width="20" :width="20"
@@ -112,8 +64,7 @@
:class="{ 'md:px-4': isBreakpointMd }" :class="{ 'md:px-4': isBreakpointMd }"
> >
<form <form
v-if="!isCompleted && !isInvite" v-if="!isCompleted"
id="steps_form"
ref="form" ref="form"
:action="submitPath" :action="submitPath"
method="post" method="post"
@@ -155,7 +106,7 @@
v-model="values[currentField.uuid]" v-model="values[currentField.uuid]"
:show-field-names="showFieldNames" :show-field-names="showFieldNames"
:field="currentField" :field="currentField"
@submit="!isSubmitting && submitStep()" @submit="submitStep"
@focus="scrollIntoField(currentField)" @focus="scrollIntoField(currentField)"
/> />
<div v-else-if="currentField.type === 'select'"> <div v-else-if="currentField.type === 'select'">
@@ -192,7 +143,6 @@
dir="auto" dir="auto"
:required="currentField.required" :required="currentField.required"
class="select base-input !text-2xl w-full text-center font-normal" class="select base-input !text-2xl w-full text-center font-normal"
:class="{ 'text-gray-300': !values[currentField.uuid] }"
:name="`values[${currentField.uuid}]`" :name="`values[${currentField.uuid}]`"
@change="values[currentField.uuid] = $event.target.value" @change="values[currentField.uuid] = $event.target.value"
@focus="scrollIntoField(currentField)" @focus="scrollIntoField(currentField)"
@@ -200,7 +150,6 @@
<option <option
value="" value=""
:selected="!values[currentField.uuid]" :selected="!values[currentField.uuid]"
class="text-gray-300"
> >
{{ t('select_your_option') }} {{ t('select_your_option') }}
</option> </option>
@@ -209,7 +158,6 @@
:key="option.uuid" :key="option.uuid"
:selected="values[currentField.uuid] == option.value" :selected="values[currentField.uuid] == option.value"
:value="option.value" :value="option.value"
class="text-base-content"
> >
{{ option.value }} {{ option.value }}
</option> </option>
@@ -247,7 +195,7 @@
<span <span
@click="scrollIntoField(currentField)" @click="scrollIntoField(currentField)"
> >
{{ t('complete_hightlighted_checkboxes_and_click') }} <span class="font-semibold">{{ submitButtonText }}</span>. {{ t('complete_hightlighted_checkboxes_and_click') }} <span class="font-semibold">{{ stepFields.length === currentStep + 1 ? t('submit') : t('next') }}</span>.
</span> </span>
</div> </div>
<div <div
@@ -311,7 +259,7 @@
> >
<template v-if="isAnonymousChecboxes || !showFieldNames"> <template v-if="isAnonymousChecboxes || !showFieldNames">
<span class="text-xl"> <span class="text-xl">
{{ t('complete_hightlighted_checkboxes_and_click') }} <span class="font-semibold">{{ submitButtonText }}</span>. {{ t('complete_hightlighted_checkboxes_and_click') }} <span class="font-semibold">{{ stepFields.length === currentStep + 1 ? t('submit') : t('next') }}</span>.
</span> </span>
<input <input
v-for="field in currentStepFields" v-for="field in currentStepFields"
@@ -386,7 +334,7 @@
:remember-signature="rememberSignature" :remember-signature="rememberSignature"
:attachments-index="attachmentsIndex" :attachments-index="attachmentsIndex"
:require-signing-reason="requireSigningReason" :require-signing-reason="requireSigningReason"
:button-text="submitButtonText" :button-text="buttonText"
:dry-run="dryRun" :dry-run="dryRun"
:with-disclosure="withDisclosure" :with-disclosure="withDisclosure"
:with-qr-button="withQrButton" :with-qr-button="withQrButton"
@@ -395,7 +343,7 @@
@update:reason="values[currentField.preferences?.reason_field_uuid] = $event" @update:reason="values[currentField.preferences?.reason_field_uuid] = $event"
@attached="attachments.push($event)" @attached="attachments.push($event)"
@start="scrollIntoField(currentField)" @start="scrollIntoField(currentField)"
@minimize="minimizeForm" @minimize="isFormVisible = false"
/> />
<InitialsStep <InitialsStep
v-else-if="currentField.type === 'initials'" v-else-if="currentField.type === 'initials'"
@@ -411,7 +359,7 @@
@attached="attachments.push($event)" @attached="attachments.push($event)"
@start="scrollIntoField(currentField)" @start="scrollIntoField(currentField)"
@focus="scrollIntoField(currentField)" @focus="scrollIntoField(currentField)"
@minimize="minimizeForm" @minimize="isFormVisible = false"
/> />
<AttachmentStep <AttachmentStep
v-else-if="currentField.type === 'file'" v-else-if="currentField.type === 'file'"
@@ -435,7 +383,7 @@
:default-value="submitter.phone" :default-value="submitter.phone"
:submitter-slug="submitterSlug" :submitter-slug="submitterSlug"
@focus="scrollIntoField(currentField)" @focus="scrollIntoField(currentField)"
@submit="!isSubmitting && submitStep()" @submit="submitStep"
/> />
<PaymentStep <PaymentStep
v-else-if="currentField.type === 'payment'" v-else-if="currentField.type === 'payment'"
@@ -447,24 +395,11 @@
:values="values" :values="values"
@attached="attachments.push($event)" @attached="attachments.push($event)"
@focus="scrollIntoField(currentField)" @focus="scrollIntoField(currentField)"
@submit="!isSubmitting && submitStep()" @submit="submitStep"
/>
<VerificationStep
v-else-if="currentField.type === 'verification'"
ref="currentStep"
:key="currentField.uuid"
:locale="language?.toLowerCase() || browserLanguage"
:submitter="submitter"
:empty-value-required-step="emptyValueRequiredStep"
:field="currentField"
:submitter-slug="submitterSlug"
:values="values"
@focus="scrollIntoField(currentField)"
@submit="!isSubmitting && submitStep()"
/> />
</div> </div>
<div <div
v-if="(currentField.type !== 'payment' && currentField.type !== 'verification') || submittedValues[currentField.uuid]" v-if="currentField.type !== 'payment' || submittedValues[currentField.uuid]"
:class="currentField.type === 'signature' ? 'mt-2' : 'mt-6 md:mt-8'" :class="currentField.type === 'signature' ? 'mt-2' : 'mt-6 md:mt-8'"
> >
<button <button
@@ -480,7 +415,7 @@
class="mr-1 animate-spin" class="mr-1 animate-spin"
/> />
<span> <span>
{{ submitButtonText }} {{ buttonText }}
</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"
@@ -495,21 +430,10 @@
</div> </div>
</div> </div>
</form> </form>
<InviteForm
v-else-if="isInvite"
:submitters="inviteSubmitters"
:submitter-slug="submitterSlug"
:authenticity-token="authenticityToken"
:url="baseUrl + submitPath + '/invite'"
:style="{ maxWidth: isBreakpointMd ? '582px' : '' }"
@success="[isInvite = false, performComplete($event)]"
/>
<FormCompleted <FormCompleted
v-else v-else
:is-demo="isDemo" :is-demo="isDemo"
:attribution="attribution" :attribution="attribution"
:has-signature-fields="stepFields.some((fields) => fields.some((f) => ['signature', 'initials'].includes(f.type)))"
:has-multiple-documents="hasMultipleDocuments"
:completed-button="completedRedirectUrl ? {} : completedButton" :completed-button="completedRedirectUrl ? {} : completedButton"
:completed-message="completedRedirectUrl ? {} : completedMessage" :completed-message="completedRedirectUrl ? {} : completedMessage"
:with-send-copy-button="withSendCopyButton && !completedRedirectUrl" :with-send-copy-button="withSendCopyButton && !completedRedirectUrl"
@@ -551,12 +475,10 @@ import AttachmentStep from './attachment_step'
import MultiSelectStep from './multi_select_step' import MultiSelectStep from './multi_select_step'
import PhoneStep from './phone_step' import PhoneStep from './phone_step'
import PaymentStep from './payment_step' import PaymentStep from './payment_step'
import VerificationStep from './verification_step'
import TextStep from './text_step' import TextStep from './text_step'
import NumberStep from './number_step' import NumberStep from './number_step'
import DateStep from './date_step' import DateStep from './date_step'
import MarkdownContent from './markdown_content' import MarkdownContent from './markdown_content'
import InviteForm from './invite_form'
import FormCompleted from './completed' import FormCompleted from './completed'
import { IconInnerShadowTop, IconArrowsDiagonal, IconWritingSign, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue' import { IconInnerShadowTop, IconArrowsDiagonal, IconWritingSign, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
import AppearsOn from './appears_on' import AppearsOn from './appears_on'
@@ -594,8 +516,6 @@ export default {
IconWritingSign, IconWritingSign,
AttachmentStep, AttachmentStep,
InitialsStep, InitialsStep,
VerificationStep,
InviteForm,
MultiSelectStep, MultiSelectStep,
IconInnerShadowTop, IconInnerShadowTop,
DateStep, DateStep,
@@ -622,11 +542,6 @@ export default {
type: Object, type: Object,
required: true required: true
}, },
inviteSubmitters: {
type: Array,
required: false,
default: () => []
},
withSignatureId: { withSignatureId: {
type: Boolean, type: Boolean,
required: false, required: false,
@@ -647,16 +562,6 @@ export default {
required: false, required: false,
default: false default: false
}, },
completeButtonToRef: {
type: Object,
required: false,
default: null
},
schema: {
type: Array,
required: false,
default: () => []
},
attachments: { attachments: {
type: Array, type: Array,
required: false, required: false,
@@ -828,16 +733,13 @@ export default {
data () { data () {
return { return {
isCompleted: false, isCompleted: false,
isInvite: false,
isFormVisible: this.expand !== false, isFormVisible: this.expand !== false,
showFillAllRequiredFields: false, showFillAllRequiredFields: false,
currentStep: 0, currentStep: 0,
isShowContinue: false,
enableScrollIntoField: true, enableScrollIntoField: true,
phoneVerifiedValues: {}, phoneVerifiedValues: {},
orientation: screen?.orientation?.type, orientation: screen?.orientation?.type,
isSubmitting: false, isSubmitting: false,
isSubmittingComplete: false,
submittedValues: {}, submittedValues: {},
recalculateButtonDisabledKey: '' recalculateButtonDisabledKey: ''
} }
@@ -846,37 +748,9 @@ export default {
isMobile () { isMobile () {
return /android|iphone|ipad/i.test(navigator.userAgent) return /android|iphone|ipad/i.test(navigator.userAgent)
}, },
attachmentConditionsIndex () { buttonText () {
return this.schema.reduce((acc, item) => { if (this.alwaysMinimize || this.stepFields.length === this.currentStep + 1) {
if (item.conditions?.length) {
if (item.conditions.every((c) => this.fieldsUuidIndex[c.field_uuid])) {
acc[item.attachment_uuid] = this.checkFieldConditions(item)
} else {
acc[item.attachment_uuid] = true
}
} else {
acc[item.attachment_uuid] = true
}
return acc
}, {})
},
emptyValueRequiredStep () {
return this.stepFields.find((fields, index) => {
return fields.some((f) => {
return f.required && isEmpty(this.values[f.uuid])
})
})
},
submitButtonText () {
if (this.alwaysMinimize) {
return this.t('submit') return this.t('submit')
} else if (this.stepFields.length === this.currentStep + 1) {
if (this.currentField.type === 'signature') {
return this.t('sign_and_complete')
} else {
return this.t('complete')
}
} else { } else {
return this.t('next') return this.t('next')
} }
@@ -884,18 +758,6 @@ export default {
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)
}, },
hasMultipleDocuments () {
return Object.keys(
this.stepFields.reduce((acc, fields) => {
fields.forEach((f) => {
f.areas?.forEach((a) => {
acc[a.attachment_uuid] = 1
})
})
return acc
}, {})
).filter(Boolean).length > 1
},
currentStepFields () { currentStepFields () {
return this.stepFields[this.currentStep] || [] return this.stepFields[this.currentStep] || []
}, },
@@ -940,30 +802,11 @@ export default {
currentField () { currentField () {
return this.currentStepFields[0] return this.currentStepFields[0]
}, },
readonlyConditionalFields () {
return this.fields.filter((f) => f.readonly && f.conditions?.length && this.checkFieldConditions(f) && this.checkFieldDocumentsConditions(f))
},
stepFields () { stepFields () {
const verificationFields = [] return this.fields.filter((f) => !f.readonly).reduce((acc, f) => {
const sortedFields = this.fields.reduce((acc, f) => {
if (f.type === 'verification') {
verificationFields.push(f)
} else if (!f.readonly) {
acc.push(f)
}
return acc
}, [])
if (verificationFields.length) {
sortedFields.push(verificationFields.pop())
}
return sortedFields.reduce((acc, f) => {
const prevStep = acc[acc.length - 1] const prevStep = acc[acc.length - 1]
if (this.checkFieldConditions(f) && this.checkFieldDocumentsConditions(f)) { if (this.checkFieldConditions(f)) {
if (f.type === 'checkbox' && Array.isArray(prevStep) && prevStep[0].type === 'checkbox' && !f.description) { if (f.type === 'checkbox' && Array.isArray(prevStep) && prevStep[0].type === 'checkbox' && !f.description) {
prevStep.push(f) prevStep.push(f)
} else { } else {
@@ -996,23 +839,6 @@ export default {
if (isEmpty(value) && this.currentStep > 0) { if (isEmpty(value) && this.currentStep > 0) {
this.currentStep -= 1 this.currentStep -= 1
} }
},
attachmentConditionsIndex: {
deep: true,
immediate: true,
handler (value) {
this.$nextTick(() => {
const root = this.$root.$el.parentNode.getRootNode()
for (const key in value) {
const doc = root.querySelector(`[id="document-${key}"`)
if (doc) {
doc.classList.toggle('hidden', !value[key])
}
}
})
}
} }
}, },
beforeUnmount () { beforeUnmount () {
@@ -1021,7 +847,7 @@ export default {
mounted () { mounted () {
this.submittedValues = JSON.parse(JSON.stringify(this.values)) this.submittedValues = JSON.parse(JSON.stringify(this.values))
screen?.orientation?.addEventListener('change', this.onOrientationChange) screen?.orientation.addEventListener('change', this.onOrientationChange)
this.fields.forEach((field) => { this.fields.forEach((field) => {
if (field.default_value && !field.readonly) { if (field.default_value && !field.readonly) {
@@ -1053,11 +879,11 @@ export default {
} }
if (document.body?.clientWidth >= 768 && this.expand !== true && ['signature', 'initials', 'file', 'image'].includes(this.currentField?.type)) { if (document.body?.clientWidth >= 768 && this.expand !== true && ['signature', 'initials', 'file', 'image'].includes(this.currentField?.type)) {
this.minimizeForm() this.isFormVisible = false
} }
if (this.alwaysMinimize) { if (this.alwaysMinimize) {
this.minimizeForm() this.isFormVisible = false
} }
if (/iPhone|iPad|iPod/i.test(navigator.userAgent)) { if (/iPhone|iPad|iPod/i.test(navigator.userAgent)) {
@@ -1094,57 +920,33 @@ export default {
onOrientationChange (event) { onOrientationChange (event) {
this.orientation = event.target.type this.orientation = event.target.type
}, },
checkFieldDocumentsConditions (field) {
if (field.areas?.length) {
return field.areas.some((area) => {
return this.attachmentConditionsIndex[area.attachment_uuid]
})
} else {
return true
}
},
checkFieldConditions (field) { checkFieldConditions (field) {
if (field.conditions?.length) { if (field.conditions?.length) {
const result = field.conditions.reduce((acc, cond) => { return field.conditions.reduce((acc, c) => {
if (cond.operation === 'or') { const field = this.fieldsUuidIndex[c.field_uuid]
acc.push(acc.pop() || this.checkFieldCondition(cond))
} else { if (['not_empty', 'checked', 'equal', 'contains'].includes(c.action) && !this.checkFieldConditions(field)) {
acc.push(this.checkFieldCondition(cond)) return false
} }
return acc if (['empty', 'unchecked'].includes(c.action)) {
}, []) return acc && isEmpty(this.values[c.field_uuid])
} else if (['not_empty', 'checked'].includes(c.action)) {
return acc && !isEmpty(this.values[c.field_uuid])
} else if (['equal', 'contains'].includes(c.action)) {
const option = field.options.find((o) => o.uuid === c.value)
const values = [this.values[c.field_uuid]].flat()
return !result.includes(false) return acc && values.includes(this.optionValue(option, field.options.indexOf(option)))
} else { } else if (['not_equal', 'does_not_contain'].includes(c.action)) {
return true const option = field.options.find((o) => o.uuid === c.value)
} const values = [this.values[c.field_uuid]].flat()
},
checkFieldCondition (condition) {
const field = this.fieldsUuidIndex[condition.field_uuid]
if (['not_empty', 'checked', 'equal', 'contains'].includes(condition.action) && field && !this.checkFieldConditions(field)) { return acc && !values.includes(this.optionValue(option, field.options.indexOf(option)))
return false } else {
} return acc
}
if (['empty', 'unchecked'].includes(condition.action)) { }, true)
return isEmpty(this.values[condition.field_uuid])
} else if (['not_empty', 'checked'].includes(condition.action)) {
return !isEmpty(this.values[condition.field_uuid])
} else if (['equal', 'contains'].includes(condition.action) && field) {
if (field.options) {
const option = field.options.find((o) => o.uuid === condition.value)
const values = [this.values[condition.field_uuid]].flat()
return values.includes(this.optionValue(option, field.options.indexOf(option)))
} else {
return [this.values[condition.field_uuid]].flat().includes(condition.value)
}
} else if (['not_equal', 'does_not_contain'].includes(condition.action) && field) {
const option = field.options.find((o) => o.uuid === condition.value)
const values = [this.values[condition.field_uuid]].flat()
return !values.includes(this.optionValue(option, field.options.indexOf(option)))
} else { } else {
return true return true
} }
@@ -1252,10 +1054,6 @@ export default {
const currentFieldUuids = this.currentStepFields.map((f) => f.uuid) const currentFieldUuids = this.currentStepFields.map((f) => f.uuid)
const currentFieldType = this.currentField.type const currentFieldType = this.currentField.type
if (!formData && !this.$refs.form.checkValidity()) {
return
}
if (this.dryRun) { if (this.dryRun) {
currentFieldUuids.forEach((fieldUuid) => { currentFieldUuids.forEach((fieldUuid) => {
this.submittedValues[fieldUuid] = this.values[fieldUuid] this.submittedValues[fieldUuid] = this.values[fieldUuid]
@@ -1291,24 +1089,16 @@ export default {
scrollIntoArea (area) { scrollIntoArea (area) {
return this.$refs.areas.scrollIntoArea(area) return this.$refs.areas.scrollIntoArea(area)
}, },
async submitStep (e) { async submitStep () {
this.isSubmitting = true this.isSubmitting = true
const forceComplete = e?.submitter?.getAttribute('name') === 'completed' const stepPromise = ['signature', 'phone', 'initials', 'payment'].includes(this.currentField.type)
if (forceComplete) {
this.isSubmittingComplete = true
}
const submitStep = this.currentStep
const stepPromise = ['signature', 'phone', 'initials', 'payment', 'verification'].includes(this.currentField.type)
? this.$refs.currentStep.submit ? this.$refs.currentStep.submit
: () => Promise.resolve({}) : () => Promise.resolve({})
stepPromise().then(async () => { stepPromise().then(async () => {
const emptyRequiredField = this.stepFields.find((fields, index) => { const emptyRequiredField = this.stepFields.find((fields, index) => {
if (forceComplete ? index === submitStep : index >= submitStep) { if (index >= this.currentStep) {
return false return false
} }
@@ -1318,9 +1108,9 @@ export default {
}) })
const formData = new FormData(this.$refs.form) const formData = new FormData(this.$refs.form)
const isLastStep = (submitStep === this.stepFields.length - 1) || forceComplete const isLastStep = this.currentStep === this.stepFields.length - 1
if (isLastStep && !emptyRequiredField && !this.inviteSubmitters.length) { if (isLastStep && !emptyRequiredField) {
formData.append('completed', 'true') formData.append('completed', 'true')
} }
@@ -1336,22 +1126,22 @@ export default {
if (response.status === 422 || response.status === 500) { if (response.status === 422 || response.status === 500) {
const data = await response.json() const data = await response.json()
if (data.error) { const i18nError = data.error ? this.t(data.error.replace(/\s+/g, '_').toLowerCase()) : ''
const i18nKey = data.error.replace(/\s+/g, '_').toLowerCase()
alert(this.t(i18nKey) !== i18nKey ? this.t(i18nKey) : data.error) alert(i18nError !== data.error ? i18nError : (data.error || this.t('value_is_invalid')))
} else {
alert(this.t('value_is_invalid'))
}
return Promise.reject(new Error(data.error)) return Promise.reject(new Error(data.error))
} }
const nextStep = (isLastStep && emptyRequiredField) || (forceComplete ? null : this.stepFields[submitStep + 1]) if (isLastStep) {
this.isSecondWalkthrough = true
}
const nextStep = (isLastStep && emptyRequiredField) || this.stepFields[this.currentStep + 1]
if (nextStep) { if (nextStep) {
if (this.alwaysMinimize) { if (this.alwaysMinimize) {
this.minimizeForm() this.isFormVisible = false
} }
this.goToStep(this.stepFields.indexOf(nextStep), this.autoscrollFields) this.goToStep(this.stepFields.indexOf(nextStep), this.autoscrollFields)
@@ -1359,46 +1149,33 @@ export default {
if (emptyRequiredField === nextStep) { if (emptyRequiredField === nextStep) {
this.showFillAllRequiredFields = true this.showFillAllRequiredFields = true
} }
} else if (this.inviteSubmitters.length) {
this.isInvite = true
} else { } else {
this.performComplete(response) this.isCompleted = true
const respData = await response.text()
if (respData) {
this.onComplete(JSON.parse(respData))
}
if (this.completedRedirectUrl) {
window.location.href = this.completedRedirectUrl
}
} }
}).catch(error => { }).catch(error => {
console.error(error) console.error(error)
}).finally(() => { }).finally(() => {
this.isSubmitting = false this.isSubmitting = false
this.isSubmittingComplete = false
}) })
}).catch(error => { }).catch(error => {
if (error?.message === 'Image too small') { if (error?.message === 'Image too small') {
alert(this.t('signature_is_too_small_please_redraw')) alert('Signature is too small - please redraw.')
} else { } else {
console.log(error) console.log(error)
} }
}).finally(() => { }).finally(() => {
this.isSubmitting = false this.isSubmitting = false
this.isSubmittingComplete = false
}) })
},
minimizeForm () {
this.isFormVisible = false
this.isShowContinue = true
},
async performComplete (resp) {
this.isCompleted = true
if (resp?.text) {
const respData = await resp.text()
if (respData) {
this.onComplete(JSON.parse(respData))
}
}
if (this.completedRedirectUrl) {
window.location.href = this.completedRedirectUrl
}
} }
} }
} }
+53 -209
View File
@@ -1,10 +1,4 @@
const en = { const en = {
complete_all_required_fields_to_proceed_with_identity_verification: 'Complete all required fields to proceed with identity verification.',
verify_id: 'Verify ID',
identity_verification: 'Identity verification',
complete: 'Complete',
fill_all_required_fields_to_complete: 'Fill all required fields to complete',
sign_and_complete: 'Sign and Complete',
text: 'Text', text: 'Text',
by_clicking_you_agree_to_the: 'By clicking "{button}", you agree to the', by_clicking_you_agree_to_the: 'By clicking "{button}", you agree to the',
electronic_signature_disclosure: 'Electronic Signature Disclosure', electronic_signature_disclosure: 'Electronic Signature Disclosure',
@@ -16,8 +10,6 @@ const en = {
reviewed: 'Reviewed', reviewed: 'Reviewed',
other: 'Other', other: 'Other',
authored_by_me: 'Authored by me', authored_by_me: 'Authored by me',
invite: 'Invite',
email: 'Email',
approved_by: 'Approved by', approved_by: 'Approved by',
reviewed_by: 'Reviewed by', reviewed_by: 'Reviewed by',
authored_by: 'Authored by', authored_by: 'Authored by',
@@ -44,8 +36,7 @@ const en = {
minimize: 'Minimize', minimize: 'Minimize',
payment: 'Payment', payment: 'Payment',
phone: 'Phone', phone: 'Phone',
start_now: 'Start Now', submit_form: 'Submit Form',
continue: 'Continue',
sign_now: 'Sign Now', sign_now: 'Sign Now',
type_here_: 'Type here...', type_here_: 'Type here...',
optional: 'optional', optional: 'optional',
@@ -66,11 +57,9 @@ const en = {
type_signature_here: 'Type signature here', type_signature_here: 'Type signature here',
type_initial_here: 'Type initials here', type_initial_here: 'Type initials here',
form_has_been_completed: 'Form has been completed!', form_has_been_completed: 'Form has been completed!',
document_has_been_signed: 'Document has been signed!',
documents_have_been_signed: 'Documents have been signed!',
create_a_free_account: 'Create a Free Account', create_a_free_account: 'Create a Free Account',
powered_by: 'Powered by', signed_with: 'Signed with',
please_check_the_box_to_continue: 'Please check the box to continue.', please_check_the_box_to_continue: 'Please check the box to continue',
open_source_documents_software: 'open source documents software', open_source_documents_software: 'open source documents software',
verified_phone_number: 'Verify Phone Number', verified_phone_number: 'Verify Phone Number',
use_international_format: 'Use international format: +1xxx', use_international_format: 'Use international format: +1xxx',
@@ -92,19 +81,10 @@ const en = {
pay_with_strip: 'Pay with Stripe', pay_with_strip: 'Pay with Stripe',
reupload: 'Reupload', reupload: 'Reupload',
upload: 'Upload', upload: 'Upload',
files: 'Files', files: 'Files'
signature_is_too_small_please_redraw: 'Signature is too small. Please redraw.'
} }
const es = { const es = {
complete_all_required_fields_to_proceed_with_identity_verification: 'Complete todos los campos requeridos para continuar con la verificación de identidad.',
verify_id: 'Verificar ID',
identity_verification: 'Verificación de identidad',
complete: 'Completar',
fill_all_required_fields_to_complete: 'Complete todos los campos requeridos para finalizar',
sign_and_complete: 'Firmar y Completar',
invite: 'Invitar',
email: 'Correo electrónico',
approved: 'Aprobado', approved: 'Aprobado',
reviewed: 'Revisado', reviewed: 'Revisado',
other: 'Otro', other: 'Otro',
@@ -142,8 +122,7 @@ const es = {
payment: 'Pago', payment: 'Pago',
phone: 'Teléfono', phone: 'Teléfono',
take_photo: 'Tomar foto', take_photo: 'Tomar foto',
start_now: 'Comenzar ahora', submit_form: 'Enviar Formulario',
continue: 'Continuar',
sign_now: 'Firmar ahora', sign_now: 'Firmar ahora',
type_here_: 'Escribe aquí...', type_here_: 'Escribe aquí...',
optional: 'opcional', optional: 'opcional',
@@ -163,11 +142,9 @@ const es = {
type_signature_here: 'Escribe la firma aquí', type_signature_here: 'Escribe la firma aquí',
type_initial_here: 'Escribe las iniciales aquí', type_initial_here: 'Escribe las iniciales aquí',
form_has_been_completed: '¡El formulario ha sido completado!', form_has_been_completed: '¡El formulario ha sido completado!',
document_has_been_signed: '¡El documento ha sido firmado!',
documents_have_been_signed: '¡Los documentos han sido firmados!',
create_a_free_account: 'Crear una Cuenta Gratuita', create_a_free_account: 'Crear una Cuenta Gratuita',
powered_by: 'Desarrollado por', signed_with: 'Firmado con',
please_check_the_box_to_continue: 'Por favor marque la casilla para continuar.', please_check_the_box_to_continue: 'Por favor marque la casilla para continuar',
open_source_documents_software: 'software de documentos de código abierto', open_source_documents_software: 'software de documentos de código abierto',
verified_phone_number: 'Verificar número de teléfono', verified_phone_number: 'Verificar número de teléfono',
use_international_format: 'Usar formato internacional: +1xxx', use_international_format: 'Usar formato internacional: +1xxx',
@@ -189,19 +166,10 @@ const es = {
pay_with_strip: 'Pagar con Stripe', pay_with_strip: 'Pagar con Stripe',
reupload: 'Volver a subir', reupload: 'Volver a subir',
upload: 'Subir', upload: 'Subir',
files: 'Archivos', files: 'Archivos'
signature_is_too_small_please_redraw: 'La firma es demasiado pequeña. Por favor, dibújala de nuevo.'
} }
const it = { const it = {
complete_all_required_fields_to_proceed_with_identity_verification: "Compila tutti i campi obbligatori per procedere con la verifica dell'identità.",
verify_id: 'Verifica ID',
identity_verification: "Verifica dell'identità",
complete: 'Completa',
fill_all_required_fields_to_complete: 'Compila tutti i campi obbligatori per completare',
sign_and_complete: 'Firma e Completa',
invite: 'Invita',
email: 'Email',
approved: 'Approvato', approved: 'Approvato',
reviewed: 'Revisionato', reviewed: 'Revisionato',
other: 'Altro', other: 'Altro',
@@ -230,7 +198,7 @@ const it = {
pay: 'Pagamento', pay: 'Pagamento',
file: 'File', file: 'File',
select: 'Seleziona', select: 'Seleziona',
checkbox: 'Checkbox', checkbox: 'Casella di controllo',
multiple: 'Multiplo', multiple: 'Multiplo',
radio: 'Radio', radio: 'Radio',
cells: 'Celle', cells: 'Celle',
@@ -238,8 +206,7 @@ const it = {
payment: 'Pagamento', payment: 'Pagamento',
number_phone_is_invalid: '{number} telefono non è valido', number_phone_is_invalid: '{number} telefono non è valido',
phone: 'Telefono', phone: 'Telefono',
start_now: 'Inizia ora', submit_form: 'Invia Modulo',
continue: 'Continua',
sign_now: 'Firma ora', sign_now: 'Firma ora',
type_here_: 'Digita qui...', type_here_: 'Digita qui...',
optional: 'opzionale', optional: 'opzionale',
@@ -260,11 +227,9 @@ const it = {
type_signature_here: 'Scrivi la firma qui', type_signature_here: 'Scrivi la firma qui',
type_initial_here: 'Scrivi le iniziali qui', type_initial_here: 'Scrivi le iniziali qui',
form_has_been_completed: 'Il modulo è stato completato!', form_has_been_completed: 'Il modulo è stato completato!',
document_has_been_signed: 'Il documento è stato firmato!',
documents_have_been_signed: 'I documenti sono stati firmati!',
create_a_free_account: 'Crea un Account Gratuito', create_a_free_account: 'Crea un Account Gratuito',
powered_by: 'Desarrollado por', signed_with: 'Firmato con',
please_check_the_box_to_continue: 'Si prega di spuntare la casella per continuare.', please_check_the_box_to_continue: 'Si prega di spuntare la casella per continuare',
open_source_documents_software: 'software di documenti open source', open_source_documents_software: 'software di documenti open source',
verified_phone_number: 'Verifica numero di telefono', verified_phone_number: 'Verifica numero di telefono',
use_international_format: 'Usa formato internazionale: +1xxx', use_international_format: 'Usa formato internazionale: +1xxx',
@@ -286,19 +251,10 @@ const it = {
pay_with_strip: 'Paga con Stripe', pay_with_strip: 'Paga con Stripe',
reupload: 'Ricarica', reupload: 'Ricarica',
upload: 'Carica', upload: 'Carica',
files: 'File', files: 'File'
signature_is_too_small_please_redraw: 'La firma è troppo piccola. Ridisegnala per favore.'
} }
const de = { const de = {
complete_all_required_fields_to_proceed_with_identity_verification: 'Vervollständigen Sie alle erforderlichen Felder, um mit der Identitätsverifizierung fortzufahren.',
verify_id: 'ID überprüfen',
identity_verification: 'Identitätsüberprüfung',
complete: 'Abschließen',
fill_all_required_fields_to_complete: 'Alle erforderlichen Felder ausfüllen, um abzuschließen',
sign_and_complete: 'Signieren und Abschließen',
invite: 'Einladen',
email: 'E-Mail',
approved: 'Genehmigt', approved: 'Genehmigt',
reviewed: 'Überprüft', reviewed: 'Überprüft',
other: 'Andere', other: 'Andere',
@@ -335,8 +291,7 @@ const de = {
payment: 'Zahlung', payment: 'Zahlung',
number_phone_is_invalid: '{number} Telefonnummer ist ungültig', number_phone_is_invalid: '{number} Telefonnummer ist ungültig',
phone: 'Telefon', phone: 'Telefon',
start_now: 'Jetzt starten', submit_form: 'Formular absenden',
continue: 'Fortsetzen',
sign_now: 'Jetzt unterschreiben', sign_now: 'Jetzt unterschreiben',
type_here_: 'Hier eingeben...', type_here_: 'Hier eingeben...',
optional: 'optional', optional: 'optional',
@@ -357,11 +312,9 @@ const de = {
type_signature_here: 'Unterschrift hier eingeben', type_signature_here: 'Unterschrift hier eingeben',
type_initial_here: 'Initialen hier eingeben', type_initial_here: 'Initialen hier eingeben',
form_has_been_completed: 'Formular wurde ausgefüllt!', form_has_been_completed: 'Formular wurde ausgefüllt!',
document_has_been_signed: 'Dokument wurde unterschrieben!',
documents_have_been_signed: 'Dokumente wurden unterschrieben!',
create_a_free_account: 'Kostenloses Konto erstellen', create_a_free_account: 'Kostenloses Konto erstellen',
powered_by: 'Bereitgestellt von', signed_with: 'Unterschrieben mit',
please_check_the_box_to_continue: 'Bitte setzen Sie das Häkchen, um fortzufahren.', please_check_the_box_to_continue: 'Bitte setzen Sie das Häkchen, um fortzufahren',
open_source_documents_software: 'Open-Source-Dokumentensoftware', open_source_documents_software: 'Open-Source-Dokumentensoftware',
verified_phone_number: 'Telefonnummer überprüfen', verified_phone_number: 'Telefonnummer überprüfen',
use_international_format: 'Internationales Format verwenden: +1xxx', use_international_format: 'Internationales Format verwenden: +1xxx',
@@ -383,19 +336,10 @@ const de = {
pay_with_strip: 'Mit Stripe bezahlen', pay_with_strip: 'Mit Stripe bezahlen',
reupload: 'Erneut hochladen', reupload: 'Erneut hochladen',
upload: 'Hochladen', upload: 'Hochladen',
files: 'Dateien', files: 'Dateien'
signature_is_too_small_please_redraw: 'Die Unterschrift ist zu klein. Bitte erneut zeichnen.'
} }
const fr = { const fr = {
complete_all_required_fields_to_proceed_with_identity_verification: "Veuillez remplir tous les champs obligatoires pour continuer la vérification de l'identité.",
verif_id: "Vérification de l'ID",
verif_identite: "Vérification de l'identité",
complete: 'Terminer',
fill_all_required_fields_to_complete: 'Veuillez remplir tous les champs obligatoires pour compléter',
sign_and_complete: 'Signer et Terminer',
invite: 'Inviter',
email: 'Courriel',
approved: 'Approuvé', approved: 'Approuvé',
reviewed: 'Révisé', reviewed: 'Révisé',
other: 'Autre', other: 'Autre',
@@ -432,8 +376,7 @@ const fr = {
payment: 'Paiement', payment: 'Paiement',
number_phone_is_invalid: '{number} téléphone est invalide', number_phone_is_invalid: '{number} téléphone est invalide',
phone: 'Téléphone', phone: 'Téléphone',
start_now: 'Commencer maintenant', submit_form: 'Envoyer le Formulaire',
continue: 'Continuer',
sign_now: 'Signer maintenant', sign_now: 'Signer maintenant',
type_here_: 'Tapez ici...', type_here_: 'Tapez ici...',
optional: 'facultatif', optional: 'facultatif',
@@ -454,11 +397,9 @@ const fr = {
type_signature_here: 'Tapez la signature ici', type_signature_here: 'Tapez la signature ici',
type_initial_here: 'Tapez les initiales ici', type_initial_here: 'Tapez les initiales ici',
form_has_been_completed: 'Le formulaire a été complété !', form_has_been_completed: 'Le formulaire a été complété !',
document_has_been_signed: 'Le document a été signé!',
documents_have_been_signed: 'Les documents ont été signés!',
create_a_free_account: 'Créer un Compte Gratuit', create_a_free_account: 'Créer un Compte Gratuit',
powered_by: 'Propulsé par', signed_with: 'Signé avec',
please_check_the_box_to_continue: 'Veuillez cocher la case pour continuer.', please_check_the_box_to_continue: 'Veuillez cocher la case pour continuer',
open_source_documents_software: 'logiciel de documents open source', open_source_documents_software: 'logiciel de documents open source',
verified_phone_number: 'Vérifier le numéro de téléphone', verified_phone_number: 'Vérifier le numéro de téléphone',
use_international_format: 'Utiliser le format international : +1xxx', use_international_format: 'Utiliser le format international : +1xxx',
@@ -480,19 +421,10 @@ const fr = {
pay_with_strip: 'Paiement avec Stripe', pay_with_strip: 'Paiement avec Stripe',
reupload: 'Recharger', reupload: 'Recharger',
upload: 'Télécharger', upload: 'Télécharger',
files: 'Fichiers', files: 'Fichiers'
signature_is_too_small_please_redraw: 'La signature est trop petite. Veuillez la redessiner.'
} }
const pl = { const pl = {
complete_all_required_fields_to_proceed_with_identity_verification: 'Uzupełnij wszystkie wymagane pola, aby kontynuować weryfikację tożsamości.',
verify_id: 'Zweryfikuj ID',
identity_verification: 'Weryfikacja tożsamości',
complete: 'Zakończ',
fill_all_required_fields_to_complete: 'Uzupełnij wszystkie wymagane pola, aby zakończyć',
sign_and_complete: 'Podpisz i zakończ',
invite: 'Zaproś',
email: 'E-mail',
approved: 'Zaakceptowany', approved: 'Zaakceptowany',
reviewed: 'Przejrzany', reviewed: 'Przejrzany',
other: 'Inny', other: 'Inny',
@@ -529,8 +461,7 @@ const pl = {
payment: 'Płatność', payment: 'Płatność',
number_phone_is_invalid: '{number} telefon jest nieprawidłowy', number_phone_is_invalid: '{number} telefon jest nieprawidłowy',
phone: 'Telefon', phone: 'Telefon',
start_now: 'Zacznij teraz', submit_form: 'Wyślij Formularz',
continue: 'Kontynuuj',
sign_now: 'Podpisz teraz', sign_now: 'Podpisz teraz',
type_here_: 'Wpisz tutaj...', type_here_: 'Wpisz tutaj...',
optional: 'opcjonalny', optional: 'opcjonalny',
@@ -551,11 +482,9 @@ const pl = {
type_signature_here: 'Wpisz podpis tutaj', type_signature_here: 'Wpisz podpis tutaj',
type_initial_here: 'Wpisz inicjały tutaj', type_initial_here: 'Wpisz inicjały tutaj',
form_has_been_completed: 'Formularz został wypełniony!', form_has_been_completed: 'Formularz został wypełniony!',
document_has_been_signed: 'Dokument został podpisany!',
documents_have_been_signed: 'Dokumenty zostały podpisane!',
create_a_free_account: 'Utwórz darmowe konto', create_a_free_account: 'Utwórz darmowe konto',
powered_by: 'Napędzany przez', signed_with: 'Podpisane za pomocą',
please_check_the_box_to_continue: 'Proszę zaznaczyć pole, aby kontynuować.', please_check_the_box_to_continue: 'Proszę zaznaczyć pole, aby kontynuować',
open_source_documents_software: 'oprogramowanie do dokumentów open source', open_source_documents_software: 'oprogramowanie do dokumentów open source',
verified_phone_number: 'Zweryfikuj numer telefonu', verified_phone_number: 'Zweryfikuj numer telefonu',
use_international_format: 'Użyj międzynarodowego formatu: +1xxx', use_international_format: 'Użyj międzynarodowego formatu: +1xxx',
@@ -577,19 +506,10 @@ const pl = {
pay_with_strip: 'Płatność za pomocą Stripe', pay_with_strip: 'Płatność za pomocą Stripe',
reupload: 'Ponowne przesłanie', reupload: 'Ponowne przesłanie',
upload: 'Przesyłanie', upload: 'Przesyłanie',
files: 'Pliki', files: 'Pliki'
signature_is_too_small_please_redraw: 'Podpis jest zbyt mały. Proszę narysować go ponownie.'
} }
const uk = { const uk = {
complete_all_required_fields_to_proceed_with_identity_verification: "Заповніть всі обов'язкові поля, щоб продовжити перевірку особи.",
verify_id: 'Підтвердження ідентичності',
identity_verification: 'Ідентифікація особи',
complete: 'Завершити',
fill_all_required_fields_to_complete: "Заповніть всі обов'язкові поля для завершення",
sign_and_complete: 'Підписати і завершити',
invite: 'Запросити',
email: 'Електронна пошта',
approved: 'Затверджено', approved: 'Затверджено',
reviewed: 'Переглянуто', reviewed: 'Переглянуто',
other: 'Інше', other: 'Інше',
@@ -626,8 +546,7 @@ const uk = {
payment: 'Платіж', payment: 'Платіж',
number_phone_is_invalid: '{number} телефон недійсний', number_phone_is_invalid: '{number} телефон недійсний',
phone: 'Телефон', phone: 'Телефон',
start_now: 'Почати зараз', submit_form: 'Надіслати Форму',
continue: 'Продовжити',
sign_now: 'Підписати зараз', sign_now: 'Підписати зараз',
type_here_: 'Введіть тут', type_here_: 'Введіть тут',
optional: 'необов’язково', optional: 'необов’язково',
@@ -648,11 +567,9 @@ const uk = {
type_signature_here: 'Введіть підпис тут', type_signature_here: 'Введіть підпис тут',
type_initial_here: 'Введіть ініціали тут', type_initial_here: 'Введіть ініціали тут',
form_has_been_completed: 'Форму заповнено!', form_has_been_completed: 'Форму заповнено!',
document_has_been_signed: 'Документ був підписаний!',
documents_have_been_signed: 'Документи були підписані!',
create_a_free_account: 'Створити безкоштовний обліковий запис', create_a_free_account: 'Створити безкоштовний обліковий запис',
powered_by: 'Працює на базі', signed_with: 'Підписано за допомогою',
please_check_the_box_to_continue: 'Будь ласка, позначте прапорець, щоб продовжити.', please_check_the_box_to_continue: 'Будь ласка, позначте прапорець, щоб продовжити',
open_source_documents_software: 'відкритий програмний засіб для документів', open_source_documents_software: 'відкритий програмний засіб для документів',
verified_phone_number: 'Підтвердіть номер телефону', verified_phone_number: 'Підтвердіть номер телефону',
use_international_format: 'Використовуйте міжнародний формат: +1xxx', use_international_format: 'Використовуйте міжнародний формат: +1xxx',
@@ -674,19 +591,10 @@ const uk = {
pay_with_strip: 'Сплатити за допомогою Stripe', pay_with_strip: 'Сплатити за допомогою Stripe',
reupload: 'Перезавантажити', reupload: 'Перезавантажити',
upload: 'Завантажити', upload: 'Завантажити',
files: 'Файли', files: 'Файли'
signature_is_too_small_please_redraw: 'Підпис занадто малий. Будь ласка, перемалюйте його.'
} }
const cs = { const cs = {
complete_all_required_fields_to_proceed_with_identity_verification: 'Vyplňte všechna povinná pole, abyste mohli pokračovat v ověření identity.',
verify_id: 'Ověřit ID',
identity_verification: 'Ověření identity',
complete: 'Dokončit',
fill_all_required_fields_to_complete: 'Please complete all mandatory fields',
sign_and_complete: 'Podepsat a dokončit',
invite: 'Pozvat',
email: 'E-mail',
approved: 'Schváleno', approved: 'Schváleno',
reviewed: 'Zkontrolováno', reviewed: 'Zkontrolováno',
other: 'Jiné', other: 'Jiné',
@@ -723,8 +631,7 @@ const cs = {
payment: 'Platba', payment: 'Platba',
phone: 'Telefon', phone: 'Telefon',
number_phone_is_invalid: '{number} telefon je neplatný', number_phone_is_invalid: '{number} telefon je neplatný',
start_now: 'Začít nyní', submit_form: 'Odeslat formulář',
continue: 'Pokračovat',
sign_now: 'Podepsat nyní', sign_now: 'Podepsat nyní',
type_here_: 'Zadejte zde', type_here_: 'Zadejte zde',
optional: 'volitelné', optional: 'volitelné',
@@ -745,11 +652,9 @@ const cs = {
type_signature_here: 'Sem zadejte podpis', type_signature_here: 'Sem zadejte podpis',
type_initial_here: 'Sem zadejte iniciály', type_initial_here: 'Sem zadejte iniciály',
form_has_been_completed: 'Formulář byl dokončen!', form_has_been_completed: 'Formulář byl dokončen!',
document_has_been_signed: 'Dokument byl podepsán!',
documents_have_been_signed: 'Dokumenty byly podepsány!',
create_a_free_account: 'Vytvořit bezplatný účet', create_a_free_account: 'Vytvořit bezplatný účet',
powered_by: 'Poháněno', signed_with: 'Podepsáno pomocí',
please_check_the_box_to_continue: 'Prosím, zaškrtněte políčko pro pokračování.', please_check_the_box_to_continue: 'Prosím, zaškrtněte políčko pro pokračování',
open_source_documents_software: 'open source software pro dokumenty', open_source_documents_software: 'open source software pro dokumenty',
verified_phone_number: 'Ověřte telefonní číslo', verified_phone_number: 'Ověřte telefonní číslo',
use_international_format: 'Použijte mezinárodní formát: +1xxx', use_international_format: 'Použijte mezinárodní formát: +1xxx',
@@ -771,19 +676,10 @@ const cs = {
pay_with_strip: 'Zaplacení přes Stripe', pay_with_strip: 'Zaplacení přes Stripe',
reupload: 'Znovu nahrát', reupload: 'Znovu nahrát',
upload: 'Nahrát', upload: 'Nahrát',
files: 'Soubory', files: 'Soubory'
signature_is_too_small_please_redraw: 'Podpis je příliš malý. Prosím, překreslete ho.'
} }
const pt = { const pt = {
complete_all_required_fields_to_proceed_with_identity_verification: 'Preencha todos os campos obrigatórios para prosseguir com a verificação de identidade.',
verify_id: 'Verificar ID',
identity_verification: 'Verificação de identidade',
complete: 'Completar',
fill_all_required_fields_to_complete: 'Preencher todos os campos obrigatórios para concluir',
sign_and_complete: 'Assinar e Completar',
invite: 'Convidar',
email: 'E-mail',
approved: 'Aprovado', approved: 'Aprovado',
reviewed: 'Revisado', reviewed: 'Revisado',
other: 'Outro', other: 'Outro',
@@ -820,8 +716,7 @@ const pt = {
payment: 'Pagamento', payment: 'Pagamento',
number_phone_is_invalid: '{number} telefone é inválido', number_phone_is_invalid: '{number} telefone é inválido',
phone: 'Telefone', phone: 'Telefone',
start_now: 'Começar agora', submit_form: 'Enviar Formulário',
continue: 'Continuar',
sign_now: 'Assinar agora', sign_now: 'Assinar agora',
type_here_: 'Digite aqui', type_here_: 'Digite aqui',
optional: 'opcional', optional: 'opcional',
@@ -842,11 +737,9 @@ const pt = {
type_signature_here: 'Digite a assinatura aqui', type_signature_here: 'Digite a assinatura aqui',
type_initial_here: 'Digite as iniciais aqui', type_initial_here: 'Digite as iniciais aqui',
form_has_been_completed: 'O formulário foi concluído!', form_has_been_completed: 'O formulário foi concluído!',
document_has_been_signed: 'O documento foi assinado!',
documents_have_been_signed: 'Os documentos foram assinados!',
create_a_free_account: 'Criar uma Conta Gratuita', create_a_free_account: 'Criar uma Conta Gratuita',
powered_by: 'Desenvolvido por', signed_with: 'Assinado com',
please_check_the_box_to_continue: 'Por favor, marque a caixa para continuar.', please_check_the_box_to_continue: 'Por favor, marque a caixa para continuar',
open_source_documents_software: 'software de documentos de código aberto', open_source_documents_software: 'software de documentos de código aberto',
verified_phone_number: 'Verificar Número de Telefone', verified_phone_number: 'Verificar Número de Telefone',
use_international_format: 'Use formato internacional: +1xxx', use_international_format: 'Use formato internacional: +1xxx',
@@ -868,19 +761,10 @@ const pt = {
pay_with_strip: 'Pagar com Stripe', pay_with_strip: 'Pagar com Stripe',
reupload: 'Reenviar', reupload: 'Reenviar',
upload: 'Carregar', upload: 'Carregar',
files: 'Arquivos', files: 'Arquivos'
signature_is_too_small_please_redraw: 'A assinatura é muito pequena. Por favor, redesenhe-a.'
} }
const he = { const he = {
complete_all_required_fields_to_proceed_with_identity_verification: 'מלא את כל השדות הנדרשים כדי להמשיך עם אימות זהות.',
verify_id: 'אמת מזהה',
identity_verification: 'אימות זהות',
complete: 'השלם',
fill_all_required_fields_to_complete: 'נא למלא את כל השדות הנדרשים להשלמה',
sign_and_complete: 'חתום והשלם',
invite: 'הזמן',
email: 'דוא"ל',
approved: 'מאושר', approved: 'מאושר',
reviewed: 'נסקר', reviewed: 'נסקר',
other: 'אחר', other: 'אחר',
@@ -917,8 +801,7 @@ const he = {
payment: 'תשלום', payment: 'תשלום',
phone: 'טלפון', phone: 'טלפון',
number_phone_is_invalid: '{number} טלפון אינו חוקי', number_phone_is_invalid: '{number} טלפון אינו חוקי',
start_now: 'התחל עכשיו', submit_form: 'שלח טופס',
continue: 'המשך',
sign_now: 'חתום כעת', sign_now: 'חתום כעת',
type_here_: 'הקלד כאן', type_here_: 'הקלד כאן',
optional: 'אופציונלי', optional: 'אופציונלי',
@@ -940,11 +823,9 @@ const he = {
type_signature_here: 'הקלד חתימה כאן', type_signature_here: 'הקלד חתימה כאן',
type_initial_here: 'הקלד ציוני ראשי תיבות כאן', type_initial_here: 'הקלד ציוני ראשי תיבות כאן',
form_has_been_completed: 'הטופס הושלם', form_has_been_completed: 'הטופס הושלם',
document_has_been_signed: 'המסמך נחתם!',
documents_have_been_signed: 'המסמכים נחתמו!',
create_a_free_account: 'צור חשבון חינם', create_a_free_account: 'צור חשבון חינם',
powered_by: 'מופעל על ידי', signed_with: 'חתום באמצעות',
please_check_the_box_to_continue: 'אנא סמן את התיבה כדי להמשיך.', please_check_the_box_to_continue: 'אנא סמן את התיבה כדי להמשיך',
open_source_documents_software: 'תוכנה פתוחה למסמכים', open_source_documents_software: 'תוכנה פתוחה למסמכים',
verified_phone_number: 'אימות מספר טלפון', verified_phone_number: 'אימות מספר טלפון',
use_international_format: 'השתמש בפורמט בינלאומי: +1xxx', use_international_format: 'השתמש בפורמט בינלאומי: +1xxx',
@@ -966,19 +847,10 @@ const he = {
pay_with_strip: 'שלם עם סטרייפ', pay_with_strip: 'שלם עם סטרייפ',
reupload: 'העלה שוב', reupload: 'העלה שוב',
upload: 'העלאה', upload: 'העלאה',
files: 'קבצים', files: 'קבצים'
signature_is_too_small_please_redraw: 'החתימה קטנה מדי. אנא צייר מחדש.'
} }
const nl = { const nl = {
complete_all_required_fields_to_proceed_with_identity_verification: 'Vul alle verplichte velden in om door te gaan met de identiteitsverificatie.',
verify_id: 'Verifiëren ID',
identity_verification: 'Identiteitsverificatie',
complete: 'Voltooien',
fill_all_required_fields_to_complete: 'Vul alle verplichte velden in om te voltooien',
sign_and_complete: 'Ondertekenen en voltooien',
invite: 'Uitnodigen',
email: 'E-mail',
approved: 'Goedgekeurd', approved: 'Goedgekeurd',
reviewed: 'Beoordeeld', reviewed: 'Beoordeeld',
other: 'Anders', other: 'Anders',
@@ -1016,8 +888,7 @@ const nl = {
payment: 'Betaling', payment: 'Betaling',
phone: 'Telefoon', phone: 'Telefoon',
number_phone_is_invalid: '{number} telefoon is ongeldig', number_phone_is_invalid: '{number} telefoon is ongeldig',
start_now: 'Nu beginnen', submit_form: 'Formulier verzenden',
continue: 'Doorgaan',
sign_now: 'Nu ondertekenen', sign_now: 'Nu ondertekenen',
type_here_: 'Typ hier...', type_here_: 'Typ hier...',
optional: 'Optioneel', optional: 'Optioneel',
@@ -1038,11 +909,9 @@ const nl = {
type_signature_here: 'Typ hier uw handtekening', type_signature_here: 'Typ hier uw handtekening',
type_initial_here: 'Typ hier uw initialen', type_initial_here: 'Typ hier uw initialen',
form_has_been_completed: 'Formulier is voltooid!', form_has_been_completed: 'Formulier is voltooid!',
document_has_been_signed: 'Document is ondertekend!',
documents_have_been_signed: 'De documenten zijn ondertekend!',
create_a_free_account: 'Maak een gratis account aan', create_a_free_account: 'Maak een gratis account aan',
powered_by: 'Aangedreven door', signed_with: 'Ondertekend met',
please_check_the_box_to_continue: 'Vink het vakje aan om door te gaan.', please_check_the_box_to_continue: 'Vink het vakje aan om door te gaan',
open_source_documents_software: 'Open source documenten software', open_source_documents_software: 'Open source documenten software',
verified_phone_number: 'Geverifieerd telefoonnummer', verified_phone_number: 'Geverifieerd telefoonnummer',
use_international_format: 'Gebruik internationaal formaat: +1xxx', use_international_format: 'Gebruik internationaal formaat: +1xxx',
@@ -1064,19 +933,10 @@ const nl = {
pay_with_strip: 'Betalen met Stripe', pay_with_strip: 'Betalen met Stripe',
reupload: 'Opnieuw uploaden', reupload: 'Opnieuw uploaden',
upload: 'Uploaden', upload: 'Uploaden',
files: 'Bestanden', files: 'Bestanden'
signature_is_too_small_please_redraw: 'De handtekening is te klein. Teken deze opnieuw, alstublieft.'
} }
const ar = { const ar = {
complete_all_required_fields_to_proceed_with_identity_verification: 'أكمل جميع الحقول المطلوبة للمتابعة في التحقق من الهوية.',
verify_id: 'تحقق من الهوية',
identity_verification: 'التحقق من الهوية',
complete: 'اكتمال',
fill_all_required_fields_to_complete: 'يرجى ملء جميع الحقول المطلوبة لإكمال',
sign_and_complete: 'التوقيع والاكتمال',
invite: 'دعوة',
email: 'البريد الإلكتروني',
approved: 'موافق عليه', approved: 'موافق عليه',
reviewed: 'تمت مراجعته', reviewed: 'تمت مراجعته',
other: 'آخر', other: 'آخر',
@@ -1113,8 +973,7 @@ const ar = {
payment: 'الدفع', payment: 'الدفع',
phone: 'هاتف', phone: 'هاتف',
number_phone_is_invalid: '{number} هاتف غير صالح', number_phone_is_invalid: '{number} هاتف غير صالح',
start_now: 'ابدأ الآن', submit_form: 'إرسال النموذج',
continue: 'متابعة',
sign_now: 'وقع الآن', sign_now: 'وقع الآن',
type_here_: 'اكتب هنا', type_here_: 'اكتب هنا',
optional: 'اختياري', optional: 'اختياري',
@@ -1135,11 +994,9 @@ const ar = {
type_signature_here: 'اكتب التوقيع هنا', type_signature_here: 'اكتب التوقيع هنا',
type_initial_here: 'اكتب الاختصارات هنا', type_initial_here: 'اكتب الاختصارات هنا',
form_has_been_completed: 'تم إكمال النموذج!', form_has_been_completed: 'تم إكمال النموذج!',
document_has_been_signed: 'تم توقيع الوثيقة!',
documents_have_been_signed: 'تم توقيع الوثائق!',
create_a_free_account: 'إنشاء حساب مجاني', create_a_free_account: 'إنشاء حساب مجاني',
powered_by: 'مدعوم من', signed_with: 'تم التوقيع بواسطة',
please_check_the_box_to_continue: 'الرجاء التحقق من الخانة للمتابعة.', please_check_the_box_to_continue: 'الرجاء التحقق من الخانة للمتابعة',
open_source_documents_software: 'برنامج وثائق مفتوح المصدر', open_source_documents_software: 'برنامج وثائق مفتوح المصدر',
verified_phone_number: 'تحقق من رقم الهاتف', verified_phone_number: 'تحقق من رقم الهاتف',
use_international_format: 'استخدم الشكل الدولي: +1xxx', use_international_format: 'استخدم الشكل الدولي: +1xxx',
@@ -1161,19 +1018,10 @@ const ar = {
pay_with_strip: 'الدفع بواسطة Stripe', pay_with_strip: 'الدفع بواسطة Stripe',
reupload: 'إعادة التحميل', reupload: 'إعادة التحميل',
upload: 'تحميل', upload: 'تحميل',
files: 'الملفات', files: 'الملفات'
signature_is_too_small_please_redraw: 'التوقيع صغير جدًا. يرجى إعادة الرسم.'
} }
const ko = { const ko = {
complete_all_required_fields_to_proceed_with_identity_verification: '신원 확인을 진행하려면 모든 필수 필드를 작성하십시오.',
verify_id: '아이디 확인',
identity_verification: '신원 확인',
complete: '완료',
fill_all_required_fields_to_complete: '모든 필수 필드를 작성하여 완료하세요',
sign_and_complete: '서명하고 완료하기',
invite: '초대하기',
email: '이메일',
approved: '승인됨', approved: '승인됨',
reviewed: '검토됨', reviewed: '검토됨',
other: '기타', other: '기타',
@@ -1209,8 +1057,7 @@ const ko = {
payment: '결제', payment: '결제',
phone: '전화', phone: '전화',
number_phone_is_invalid: '{number} 전화번호가 유효하지 않습니다', number_phone_is_invalid: '{number} 전화번호가 유효하지 않습니다',
start_now: '지금 시작하기', submit_form: '양식 제출',
continue: '계속하다',
sign_now: '지금 서명', sign_now: '지금 서명',
type_here_: '여기에 입력...', type_here_: '여기에 입력...',
optional: '선택 사항', optional: '선택 사항',
@@ -1231,11 +1078,9 @@ const ko = {
type_signature_here: '여기에 서명 입력', type_signature_here: '여기에 서명 입력',
type_initial_here: '여기에 이니셜 입력', type_initial_here: '여기에 이니셜 입력',
form_has_been_completed: '양식이 완료되었습니다!', form_has_been_completed: '양식이 완료되었습니다!',
document_has_been_signed: '문서가 서명되었습니다!',
documents_have_been_signed: '문서가 서명되었습니다!',
create_a_free_account: '무료 계정 생성', create_a_free_account: '무료 계정 생성',
powered_by: '구동', signed_with: '다음으로 서명:',
please_check_the_box_to_continue: '계속하려면 확인란을 선택하십시오.', please_check_the_box_to_continue: '계속하려면 확인란을 선택하십시오',
open_source_documents_software: '오픈 소스 문서 소프트웨어', open_source_documents_software: '오픈 소스 문서 소프트웨어',
verified_phone_number: '전화번호 확인됨', verified_phone_number: '전화번호 확인됨',
use_international_format: '국제 포맷 사용: +1xxx', use_international_format: '국제 포맷 사용: +1xxx',
@@ -1257,8 +1102,7 @@ const ko = {
pay_with_strip: '스트라이프로 결제', pay_with_strip: '스트라이프로 결제',
reupload: '다시 업로드', reupload: '다시 업로드',
upload: '업로드', upload: '업로드',
files: '파일', files: '파일'
signature_is_too_small_please_redraw: '서명이 너무 작습니다. 다시 그려주세요.'
} }
const i18n = { en, es, it, de, fr, pl, uk, cs, pt, he, nl, ar, ko } const i18n = { en, es, it, de, fr, pl, uk, cs, pt, he, nl, ar, ko }
@@ -50,26 +50,6 @@
</span> </span>
</a> </a>
</span> </span>
<span
class="tooltip"
:data-tip="t('click_to_upload')"
>
<label
class="btn btn-outline btn-sm font-medium inline-flex flex-nowrap"
>
<IconUpload :width="16" />
<input
:key="uploadImageInputKey"
type="file"
hidden
accept="image/*"
@change="drawImage"
>
<span class="hidden sm:inline">
{{ t('upload') }}
</span>
</label>
</span>
<a <a
v-if="modelValue || computedPreviousValue" v-if="modelValue || computedPreviousValue"
href="#" href="#"
@@ -117,7 +97,7 @@
<img <img
v-if="modelValue || computedPreviousValue" v-if="modelValue || computedPreviousValue"
:src="attachmentsIndex[modelValue || computedPreviousValue].url" :src="attachmentsIndex[modelValue || computedPreviousValue].url"
class="mx-auto bg-white border border-base-300 rounded max-h-44" class="mx-auto bg-white border border-base-300 rounded max-h-72"
> >
<canvas <canvas
v-show="!modelValue && !computedPreviousValue" v-show="!modelValue && !computedPreviousValue"
@@ -140,18 +120,16 @@
<script> <script>
import { cropCanvasAndExportToPNG } from './crop_canvas' import { cropCanvasAndExportToPNG } from './crop_canvas'
import { IconReload, IconTextSize, IconUpload, IconSignature, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue' import { IconReload, IconTextSize, IconSignature, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
import SignaturePad from 'signature_pad' import SignaturePad from 'signature_pad'
import AppearsOn from './appears_on' import AppearsOn from './appears_on'
import MarkdownContent from './markdown_content' import MarkdownContent from './markdown_content'
import SignatureStep from './signature_step'
const scale = 3 const scale = 3
export default { export default {
name: 'InitialsStep', name: 'InitialsStep',
components: { components: {
IconUpload,
AppearsOn, AppearsOn,
IconReload, IconReload,
IconTextSize, IconTextSize,
@@ -200,8 +178,7 @@ export default {
return { return {
isInitialsStarted: !!this.previousValue, isInitialsStarted: !!this.previousValue,
isUsePreviousValue: true, isUsePreviousValue: true,
isDrawInitials: false, isDrawInitials: false
uploadImageInputKey: Math.random().toString()
} }
}, },
computed: { computed: {
@@ -226,10 +203,6 @@ export default {
if (this.$refs.canvas) { if (this.$refs.canvas) {
this.pad = new SignaturePad(this.$refs.canvas) this.pad = new SignaturePad(this.$refs.canvas)
if (this.field.preferences?.color) {
this.pad.penColor = this.field.preferences.color
}
this.pad.addEventListener('beginStroke', () => { this.pad.addEventListener('beginStroke', () => {
this.isInitialsStarted = true this.isInitialsStarted = true
@@ -256,15 +229,6 @@ export default {
this.intersectionObserver?.disconnect() this.intersectionObserver?.disconnect()
}, },
methods: { methods: {
drawOnCanvas: SignatureStep.methods.drawOnCanvas,
drawImage (event) {
this.remove()
this.isInitialsStarted = true
this.drawOnCanvas(event.target.files[0], this.$refs.canvas)
this.uploadImageInputKey = Math.random().toString()
},
remove () { remove () {
this.$emit('update:model-value', '') this.$emit('update:model-value', '')
@@ -1,117 +0,0 @@
<template>
<form
ref="form"
action="post"
method="post"
class="mx-auto"
@submit.prevent="submit"
>
<input
type="hidden"
name="authenticity_token"
:value="authenticityToken"
>
<div
v-for="(submitter, index) in submitters"
:key="submitter.uuid"
:class="{ 'mt-4': index !== 0 }"
>
<input
:value="submitter.uuid"
hidden
name="submission[submitters][][uuid]"
>
<label
:for="submitter.uuid"
dir="auto"
class="label text-2xl"
>
{{ t('invite') }} {{ submitter.name }}
</label>
<input
:id="submitter.uuid"
dir="auto"
class="base-input !text-2xl w-full"
:placeholder="t('email')"
type="email"
required
autofocus="true"
name="submission[submitters][][email]"
>
</div>
<div
class="mt-6 md:mt-8"
>
<button
type="submit"
class="base-button w-full flex justify-center"
:disabled="isSubmitting"
>
<span class="flex">
<IconInnerShadowTop
v-if="isSubmitting"
class="mr-1 animate-spin"
/>
<span>
{{ t('submit') }}
</span><span
v-if="isSubmitting"
class="w-6 flex justify-start mr-1"
><span>...</span></span>
</span>
</button>
</div>
</form>
</template>
<script>
import { IconInnerShadowTop } from '@tabler/icons-vue'
export default {
name: 'InviteForm',
components: {
IconInnerShadowTop
},
inject: ['t'],
props: {
submitters: {
type: Array,
required: true
},
url: {
type: String,
required: true
},
authenticityToken: {
type: String,
required: true
},
submitterSlug: {
type: String,
required: true
}
},
emits: ['success'],
data () {
return {
isSubmitting: false
}
},
methods: {
submit () {
this.isSubmitting = true
return fetch(this.url, {
method: 'POST',
body: new FormData(this.$refs.form)
}).then((response) => {
if (response.status === 200) {
this.$emit('success')
}
}).finally(() => {
this.isSubmitting = false
})
}
}
}
</script>
@@ -1,6 +1,7 @@
<template> <template>
<label <label
v-if="!modelValue && !sessionId" v-if="!modelValue && !sessionId"
:for="field.uuid"
class="label text-2xl mb-2" class="label text-2xl mb-2"
> >
<MarkdownContent <MarkdownContent
File diff suppressed because it is too large. Load diff
+21 -110
View File
@@ -61,59 +61,30 @@
</a> </a>
</div> </div>
</div> </div>
<div <input
v-show="!isCodeSent" v-show="!isCodeSent"
class="flex w-full rounded-full outline-neutral-content outline-2 outline-offset-2 focus-within:outline" :id="field.uuid"
ref="phone"
:value="modelValue || defaultValue"
:readonly="!!defaultValue"
class="base-input !text-2xl w-full"
autocomplete="tel"
pattern="^\+[0-9\s\-]+$"
type="tel"
inputmode="tel"
:required="field.required"
placeholder="+1 234 567-8900"
:name="`values[${field.uuid}]`"
@invalid="$event.target.value ? $event.target.setCustomValidity(`${t('use_international_format')}...`) : ''"
@input="[$event.target.setCustomValidity(''), $emit('update:model-value', $event.target.value)]"
@focus="$emit('focus')"
> >
<div
id="country_code"
class="relative inline-block"
>
<div class="btn bg-base-200 border border-neutral-300 text-2xl whitespace-nowrap font-normal rounded-l-full">
{{ selectedCountry.flag }} +{{ selectedCountry.dial }}
</div>
<select
id="country_code_select"
class="absolute top-0 bottom-0 right-0 left-0 opacity-0 w-full h-full cursor-pointer"
:disabled="!!defaultValue"
@change="onCountrySelect(countries.find((country) => country.flag === $event.target.value))"
>
<option
v-for="(country, index) in countries"
:key="index"
:value="country.flag"
>
{{ country.flag }} {{ country.name }}
</option>
</select>
</div>
<input
:name="`values[${field.uuid}]`"
:value="fullInternationalPhoneValue"
hidden
>
<input
:id="field.uuid"
ref="phone"
:value="phoneValue"
:readonly="!!defaultValue"
class="base-input !text-2xl !rounded-l-none !border-l-0 !outline-none w-full"
autocomplete="tel"
type="tel"
inputmode="tel"
:required="field.required"
placeholder="234 567-8900"
@input="onPhoneInput"
@focus="$emit('focus')"
>
</div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import MarkdownContent from './markdown_content' import MarkdownContent from './markdown_content'
import phoneData from './phone_data'
function throttle (func, delay) { function throttle (func, delay) {
let lastCallTime = 0 let lastCallTime = 0
@@ -173,73 +144,13 @@ export default {
data () { data () {
return { return {
isCodeSent: false, isCodeSent: false,
isResendLoading: false, isResendLoading: false
phoneValue: this.modelValue || this.defaultValue || '',
selectedCountry: {}
}
},
computed: {
countries () {
return phoneData.map(([iso, name, dial, flag, tz]) => {
return { iso, name, dial, flag, tz }
})
},
countriesDialIndex () {
return this.countries.reduce((acc, item) => {
acc[item.dial] ||= item
return acc
}, {})
},
dialCodesRegexp () {
const dialCodes = this.countries.map((country) => country.dial).sort((a, b) => b.length - a.length)
return new RegExp(`^\\+(${dialCodes.join('|')})`)
},
detectedPhoneValueDialCode () {
return (this.phoneValue || '').replace(/[^\d+]/g, '').match(this.dialCodesRegexp)?.[1]
},
fullInternationalPhoneValue () {
if (this.detectedPhoneValueDialCode) {
return this.phoneValue
} else if (this.phoneValue) {
return ['+', this.selectedCountry.dial, this.phoneValue].filter(Boolean).join('')
} else {
return ''
}
}
},
mounted () {
const browserTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone
if (this.detectedPhoneValueDialCode) {
this.selectedCountry = this.countriesDialIndex[this.detectedPhoneValueDialCode]
} else if (browserTimeZone) {
const tz = browserTimeZone.split('/')[1]
this.selectedCountry = this.countries.find((country) => country.tz.includes(tz)) || this.countries[0]
} }
}, },
methods: { methods: {
emitSubmit: throttle(function (e) { emitSubmit: throttle(function (e) {
this.$emit('submit') this.$emit('submit')
}, 1000), }, 1000),
onCountrySelect (country) {
if (this.selectedCountry.flag !== country.flag) {
this.phoneValue = this.phoneValue.replace(`+${this.selectedCountry.dial}`, `+${country.dial}`)
}
this.selectedCountry = country
this.$refs.phone.focus()
},
onPhoneInput (e) {
this.phoneValue = e.target.value
if (this.detectedPhoneValueDialCode) {
this.selectedCountry = this.countriesDialIndex[this.detectedPhoneValueDialCode]
}
},
onInputCode (e) { onInputCode (e) {
if (e.target.value.length === 6) { if (e.target.value.length === 6) {
this.emitSubmit() this.emitSubmit()
@@ -260,14 +171,14 @@ export default {
body: JSON.stringify({ body: JSON.stringify({
submitter_slug: this.submitterSlug, submitter_slug: this.submitterSlug,
locale: this.locale, locale: this.locale,
phone: this.fullInternationalPhoneValue phone: this.$refs.phone.value
}), }),
headers: { 'Content-Type': 'application/json' } headers: { 'Content-Type': 'application/json' }
}).then(async (resp) => { }).then(async (resp) => {
if (resp.status === 422) { if (resp.status === 422) {
const data = await resp.json() const data = await resp.json()
alert(this.t('number_phone_is_invalid').replace('{number}', this.fullInternationalPhoneValue)) alert(this.t('number_phone_is_invalid').replace('{number}', this.$refs.phone.value))
return Promise.reject(new Error(data.error)) return Promise.reject(new Error(data.error))
} }
@@ -278,13 +189,13 @@ export default {
return Promise.resolve({}) return Promise.resolve({})
} }
if (!this.fullInternationalPhoneValue.toString().startsWith('+')) { if (!this.$refs.phone.value.toString().startsWith('+')) {
alert(this.t('use_international_format')) alert(this.t('use_international_format'))
return Promise.reject(new Error('phone invalid')) return Promise.reject(new Error('phone invalid'))
} else if (!this.isCodeSent) { } else if (!this.isCodeSent) {
return this.sendVerificationCode().then(() => { return this.sendVerificationCode().then(() => {
this.$emit('update:model-value', this.fullInternationalPhoneValue) this.$emit('update:model-value', this.$refs.phone.value)
this.isCodeSent = true this.isCodeSent = true
@@ -145,10 +145,6 @@
{{ t('clear') }} {{ t('clear') }}
</a> </a>
</div> </div>
<div
v-if="isTextSignature"
class="absolute top-0 right-0 left-0 bottom-0"
/>
<canvas <canvas
v-show="!modelValue && !computedPreviousValue" v-show="!modelValue && !computedPreviousValue"
ref="canvas" ref="canvas"
@@ -201,7 +197,6 @@
<select <select
v-if="requireSigningReason && !isOtherReason" v-if="requireSigningReason && !isOtherReason"
class="select base-input !text-2xl w-full mt-6 text-center" class="select base-input !text-2xl w-full mt-6 text-center"
:class="{ 'text-gray-300': !reason }"
required required
:name="`values[${field.preferences.reason_field_uuid}]`" :name="`values[${field.preferences.reason_field_uuid}]`"
@change="$event.target.value === 'other' ? [reason = '', isOtherReason = true] : $emit('update:reason', $event.target.value)" @change="$event.target.value === 'other' ? [reason = '', isOtherReason = true] : $emit('update:reason', $event.target.value)"
@@ -210,7 +205,6 @@
value="" value=""
disabled disabled
:selected="!reason" :selected="!reason"
class="text-gray-300"
> >
{{ t('select_a_reason') }} {{ t('select_a_reason') }}
</option> </option>
@@ -219,14 +213,10 @@
:key="option" :key="option"
:value="option" :value="option"
:selected="reason === option" :selected="reason === option"
class="text-base-content"
> >
{{ label }} {{ label }}
</option> </option>
<option <option value="other">
value="other"
class="text-base-content"
>
{{ t('other') }} {{ t('other') }}
</option> </option>
</select> </select>
@@ -259,7 +249,7 @@
class="text-base-content/60 text-xs text-center w-full mt-1" class="text-base-content/60 text-xs text-center w-full mt-1"
> >
{{ t('by_clicking_you_agree_to_the').replace('{button}', buttonText.charAt(0).toUpperCase() + buttonText.slice(1)) }} <a {{ t('by_clicking_you_agree_to_the').replace('{button}', buttonText.charAt(0).toUpperCase() + buttonText.slice(1)) }} <a
href="https://www.docuseal.com/esign-disclosure" href="https://www.docuseal.co/esign-disclosure"
target="_blank" target="_blank"
> >
<span class="inline md:hidden"> <span class="inline md:hidden">
@@ -423,10 +413,6 @@ export default {
if (this.$refs.canvas) { if (this.$refs.canvas) {
this.pad = new SignaturePad(this.$refs.canvas) this.pad = new SignaturePad(this.$refs.canvas)
if (this.field.preferences?.color) {
this.pad.penColor = this.field.preferences.color
}
this.pad.addEventListener('endStroke', () => { this.pad.addEventListener('endStroke', () => {
this.isSignatureStarted = true this.isSignatureStarted = true
@@ -583,11 +569,8 @@ export default {
this.remove() this.remove()
this.isSignatureStarted = true this.isSignatureStarted = true
this.drawOnCanvas(event.target.files[0], this.$refs.canvas) const file = event.target.files[0]
this.uploadImageInputKey = Math.random().toString()
},
drawOnCanvas (file, canvas) {
if (file && file.type.match('image.*')) { if (file && file.type.match('image.*')) {
const reader = new FileReader() const reader = new FileReader()
@@ -597,6 +580,7 @@ export default {
img.src = event.target.result img.src = event.target.result
img.onload = () => { img.onload = () => {
const canvas = this.$refs.canvas
const context = canvas.getContext('2d') const context = canvas.getContext('2d')
const aspectRatio = img.width / img.height const aspectRatio = img.width / img.height
@@ -631,6 +615,8 @@ export default {
} }
reader.readAsDataURL(file) reader.readAsDataURL(file)
this.uploadImageInputKey = Math.random().toString()
} }
}, },
maybeSetSignedUuid (signedUuid) { maybeSetSignedUuid (signedUuid) {
+2 -4
View File
@@ -114,9 +114,7 @@ export default {
const area = this.field.areas?.[0] const area = this.field.areas?.[0]
if (area) { if (area) {
const num = area.w / area.cell_w return parseInt(area.w / area.cell_w) + 1
return (num % 1) > 0.2 ? parseInt(num) + 1 : parseInt(num)
} else { } else {
return null return null
} }
@@ -149,7 +147,7 @@ export default {
const textarea = this.$refs.textarea const textarea = this.$refs.textarea
textarea.style.height = 'auto' textarea.style.height = 'auto'
textarea.style.height = Math.min(250, textarea.scrollHeight) + 'px' textarea.style.height = textarea.scrollHeight + 'px'
}, },
toggleTextArea () { toggleTextArea () {
this.isTextArea = true this.isTextArea = true
@@ -1,169 +0,0 @@
<template>
<label
class="label text-2xl mb-2"
>
<MarkdownContent
v-if="field.title"
:string="field.title"
/>
<template v-else>{{ field.name || t('identity_verification') }}</template>
</label>
<div
v-if="field.description"
dir="auto"
class="mb-4 px-1"
>
<MarkdownContent :string="field.description" />
</div>
<div
v-if="emptyValueRequiredStep && emptyValueRequiredStep[0] !== field"
class="px-1"
>
{{ t('complete_all_required_fields_to_proceed_with_identity_verification') }}
</div>
<div v-else>
<div
v-if="isLoading"
class="w-full flex space-x-2 justify-center mb-2"
>
<IconInnerShadowTop
width="40"
class="animate-spin h-10"
/>
</div>
<div
ref="widgetContainer"
/>
</div>
</template>
<script>
import MarkdownContent from './markdown_content'
import { IconInnerShadowTop } from '@tabler/icons-vue'
import phoneData from './phone_data'
export default {
name: 'VerificationStep',
components: {
MarkdownContent,
IconInnerShadowTop
},
inject: ['baseUrl', 't'],
props: {
modelValue: {
type: String,
required: false,
default: ''
},
submitter: {
type: Object,
required: true
},
field: {
type: Object,
required: true
},
emptyValueRequiredStep: {
type: Object,
required: false,
default: null
},
values: {
type: Object,
required: true
},
locale: {
type: String,
required: false,
default: 'en'
},
submitterSlug: {
type: String,
required: true
}
},
emits: ['focus', 'submit', 'update:model-value', 'attached'],
data () {
return {
isCreatingCheckout: false,
isMathLoaded: false,
isLoading: false,
eidEasyData: {}
}
},
computed: {
countryCode () {
const browserTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone
const browserTz = browserTimeZone.split('/')[1]
const country = phoneData.find(([a, b, c, e, tz]) => tz.includes(browserTz))
return country[0]
},
browserCountry () {
return (navigator.language || navigator.userLanguage || 'en').split('-')[1]
},
widgetSettings () {
return {
clientId: this.eidEasyData.client_id,
docId: this.eidEasyData.doc_id,
language: this.locale,
countryCode: this.countryCode,
enabledMethods: {
signature: this.eidEasyData.available_methods
},
selectedMethod: null,
enabledCountries: 'all',
onSuccess: (data) => {
this.$emit('submit')
},
onFail: (error) => {
console.error(error)
}
}
}
},
async mounted () {
this.isLoading = true
Promise.all([
import('@eid-easy/eideasy-widget'),
this.start()
]).finally(() => {
this.isLoading = false
})
},
methods: {
start () {
return fetch(this.baseUrl + `/api/identity_verification/${this.field.uuid}`, {
method: 'PUT',
body: JSON.stringify({
submitter_slug: this.submitterSlug
}),
headers: { 'Content-Type': 'application/json' }
}).then(async (resp) => {
this.eidEasyData = await resp.json()
const eidEasyWidget = document.createElement('eideasy-widget')
for (const key in this.widgetSettings) {
eidEasyWidget[key] = this.widgetSettings[key]
}
this.$refs.widgetContainer.innerHTML = ''
this.$refs.widgetContainer.appendChild(eidEasyWidget)
})
},
async submit () {
return fetch(this.baseUrl + '/api/identity_verification', {
method: 'POST',
body: JSON.stringify({
submitter_slug: this.submitterSlug
}),
headers: { 'Content-Type': 'application/json' }
}).then(async (resp) => {
return resp
})
}
}
}
</script>
Loaded 100 of 389 files, more files were not shown because too many files have changed in this diff. Show more