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
344 changed files with 2744 additions and 13442 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:
+1 -7
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,8 +46,6 @@ Layout/LineLength:
Metrics/AbcSize: Metrics/AbcSize:
Max: 45 Max: 45
Exclude:
- spec/**/*
Metrics/ModuleLength: Metrics/ModuleLength:
Max: 500 Max: 500
@@ -66,10 +63,7 @@ RSpec/ExampleLength:
Max: 40 Max: 40
RSpec/MultipleMemoizedHelpers: RSpec/MultipleMemoizedHelpers:
Max: 9 Max: 6
Metrics/BlockNesting:
Max: 4
Rails/I18nLocaleTexts: Rails/I18nLocaleTexts:
Enabled: false Enabled: false
+5 -5
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
+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
+181 -186
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,90 +212,87 @@ 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)
@@ -309,27 +309,26 @@ 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.7) 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.7) nokogiri (1.16.7)
mini_portile2 (~> 2.8.2) mini_portile2 (~> 2.8.2)
racc (~> 1.4) racc (~> 1.4)
@@ -337,23 +336,21 @@ GEM
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.16.7-x86_64-linux) nokogiri (1.16.7-x86_64-linux)
racc (~> 1.4) racc (~> 1.4)
oj (3.16.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,20 +379,20 @@ 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
@@ -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,7 +15,6 @@ 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
].freeze ].freeze
+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
+8 -19
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,24 +63,20 @@ 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
ProcessSubmitterCompletionJob.perform_async('submitter_id' => submitter.id) if submitter.completed_at? ProcessSubmitterCompletionJob.perform_async({ 'submitter_id' => submission.submitters.last.id })
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
@@ -96,10 +88,7 @@ module Api
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])
@@ -165,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+]/, '')
@@ -25,10 +25,7 @@ module Api
schema_documents = Templates::CloneAttachments.call(template: cloned_template, original_template: @template) schema_documents = Templates::CloneAttachments.call(template: cloned_template, original_template: @template)
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
+3 -7
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,10 +65,7 @@ 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
@@ -98,9 +95,8 @@ 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,
+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 -24
View File
@@ -8,7 +8,6 @@ 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?
@@ -38,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
@@ -56,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
@@ -95,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
@@ -104,10 +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 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 -16
View File
@@ -21,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 = [
@@ -48,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
@@ -56,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)
@@ -66,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
@@ -87,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
@@ -30,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
+7 -24
View File
@@ -9,13 +9,6 @@ class SubmissionsController < ApplicationController
def show def show
@submission = Submissions.preload_with_pages(@submission) @submission = Submissions.preload_with_pages(@submission)
unless @submission.submitters.all?(&:completed_at?)
ActiveRecord::Associations::Preloader.new(
records: [@submission],
associations: [submitters: :start_form_submission_events]
).call
end
render :show, layout: 'plain' render :show, layout: 'plain'
end end
@@ -50,11 +43,13 @@ 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
@@ -62,16 +57,13 @@ class SubmissionsController < ApplicationController
if params[:permanently].present? 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:)
@@ -86,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
@@ -15,6 +15,6 @@ class SubmissionsDashboardController < ApplicationController
@submissions = @submissions.pending if params[:status] == 'pending' @submissions = @submissions.pending if params[:status] == 'pending'
@submissions = @submissions.completed if params[:status] == 'completed' @submissions = @submissions.completed if params[:status] == 'completed'
@pagy, @submissions = pagy(@submissions.preload(submitters: :start_form_submission_events).order(id: :desc)) @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
@@ -4,8 +4,6 @@ class SubmissionsPreviewController < ApplicationController
skip_before_action :authenticate_user! skip_before_action :authenticate_user!
skip_authorization_check skip_authorization_check
before_action :maybe_redirect_com, only: %i[show completed]
TTL = 40.minutes TTL = 40.minutes
def show def show
@@ -21,7 +19,7 @@ class SubmissionsPreviewController < ApplicationController
@submission ||= Submission.find_by!(slug: params[:slug]) @submission ||= Submission.find_by!(slug: params[:slug])
if !@submission.submitters.all?(&:completed_at?) && current_user.blank? if !@submission.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
+6 -18
View File
@@ -7,27 +7,20 @@ class SubmitFormController < ApplicationController
skip_before_action :authenticate_user! skip_before_action :authenticate_user!
skip_authorization_check skip_authorization_check
before_action :maybe_redirect_com, only: %i[show completed]
CONFIG_KEYS = [].freeze CONFIG_KEYS = [].freeze
def show def show
@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? || submission.archived_at? return render :archived if @submitter.submission.template.archived_at? || @submitter.submission.archived_at?
return render :expired if submission.expired? return render :expired if @submitter.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 = ActiveStorage::Attachment.where(record: submission.submitters, name: :attachments) @attachments_index = ActiveStorage::Attachment.where(record: @submitter.submission.submitters, name: :attachments)
.preload(:blob).index_by(&:uuid) .preload(:blob).index_by(&:uuid)
@form_configs = Submitters::FormConfigs.call(@submitter, CONFIG_KEYS) @form_configs = Submitters::FormConfigs.call(@submitter, CONFIG_KEYS)
@@ -52,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)
@@ -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?
-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
@@ -10,7 +10,7 @@ class SubmittersSendEmailController < ApplicationController
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
@@ -7,7 +7,7 @@ class TemplateFoldersController < ApplicationController
@templates = @template_folder.templates.active.preload(:author).order(id: :desc) @templates = @template_folder.templates.active.preload(:author).order(id: :desc)
@templates = Templates.search(@templates, params[:q]) @templates = Templates.search(@templates, params[:q])
@pagy, @templates = pagy(@templates, limit: 12) @pagy, @templates = pagy(@templates, items: 12)
end end
def edit; end def edit; end
@@ -15,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
@@ -7,6 +7,6 @@ class TemplatesArchivedController < ApplicationController
@templates = @templates.where.not(archived_at: nil).preload(:author, :folder).order(id: :desc) @templates = @templates.where.not(archived_at: nil).preload(:author, :folder).order(id: :desc)
@templates = Templates.search(@templates, params[:q]) @templates = Templates.search(@templates, params[:q])
@pagy, @templates = pagy(@templates, limit: 12) @pagy, @templates = pagy(@templates, items: 12)
end end
end end
+9 -24
View File
@@ -15,13 +15,13 @@ class TemplatesController < ApplicationController
submissions = submissions.pending if params[:status] == 'pending' submissions = submissions.pending if params[:status] == 'pending'
submissions = submissions.completed if params[:status] == 'completed' submissions = submissions.completed if params[:status] == 'completed'
@pagy, @submissions = pagy(submissions.preload(submitters: :start_form_submission_events).order(id: :desc)) @pagy, @submissions = pagy(submissions.preload(:submitters).order(id: :desc))
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
@@ -66,7 +66,7 @@ class TemplatesController < ApplicationController
if @template.save if @template.save
Templates::CloneAttachments.call(template: @template, original_template: @base_template) if @base_template Templates::CloneAttachments.call(template: @template, original_template: @base_template) if @base_template
enqueue_template_created_webhooks(@template) SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => @template.id)
maybe_redirect_to_template(@template) maybe_redirect_to_template(@template)
else else
@@ -77,7 +77,7 @@ class TemplatesController < ApplicationController
def update def update
@template.update!(template_params) @template.update!(template_params)
enqueue_template_updated_webhooks(@template) SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => @template.id)
head :ok head :ok
end end
@@ -87,11 +87,11 @@ class TemplatesController < ApplicationController
if params[:permanently].present? 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:)
@@ -103,7 +103,7 @@ class TemplatesController < ApplicationController
params.require(:template).permit( params.require(:template).permit(
:name, :name,
{ schema: [%i[attachment_uuid name]], { 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,
@@ -116,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
@@ -23,14 +23,14 @@ class TemplatesDashboardController < ApplicationController
@template_folders = @template_folders.reject { |e| e.name == TemplateFolder::DEFAULT_NAME } @template_folders = @template_folders.reject { |e| e.name == TemplateFolder::DEFAULT_NAME }
@templates = filter_templates(@templates) @templates = filter_templates(@templates)
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
@@ -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
@@ -23,17 +23,12 @@ class TemplatesPreferencesController < ApplicationController
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
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,7 +52,7 @@ 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
file = ActionDispatch::Http::UploadedFile.new( file = ActionDispatch::Http::UploadedFile.new(
@@ -66,10 +66,9 @@ class TemplatesUploadsController < ApplicationController
{ 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
+7 -9
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))
@@ -37,14 +35,14 @@ class UsersController < ApplicationController
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
@@ -58,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
@@ -66,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
@@ -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 -8
View File
@@ -1,21 +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_webhooks_path, notice: I18n.t('webhook_secret_has_been_saved')) @encrypted_config.value.present? ? @encrypted_config.save! : @encrypted_config.delete
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
+13 -12
View File
@@ -1,35 +1,36 @@
# frozen_string_literal: true # frozen_string_literal: true
class WebhookSettingsController < ApplicationController class WebhookSettingsController < ApplicationController
before_action :load_webhook_url before_action :load_encrypted_config
authorize_resource :webhook_url, parent: false authorize_resource :encrypted_config, parent: false
def show; end def show; end
def create def create
@webhook_url.assign_attributes(webhook_params) @encrypted_config.assign_attributes(encrypted_config_params)
@webhook_url.url.present? ? @webhook_url.save! : @webhook_url.delete @encrypted_config.value.present? ? @encrypted_config.save! : @encrypted_config.delete
redirect_back(fallback_location: settings_webhooks_path, notice: I18n.t('webhook_url_has_been_saved')) redirect_back(fallback_location: settings_webhooks_path, notice: 'Webhook URL has been saved.')
end end
def update def update
submitter = current_account.submitters.where.not(completed_at: nil).order(:id).last submitter = current_account.submitters.where.not(completed_at: nil).order(:id).last
SendFormCompletedWebhookRequestJob.perform_async('submitter_id' => submitter.id, SendFormCompletedWebhookRequestJob.perform_async({ 'submitter_id' => submitter.id,
'webhook_url_id' => @webhook_url.id) 'encrypted_config_id' => @encrypted_config.id })
redirect_back(fallback_location: settings_webhooks_path, notice: I18n.t('webhook_request_has_been_sent')) redirect_back(fallback_location: settings_webhooks_path, notice: 'Webhook request has been sent.')
end end
private private
def load_webhook_url def load_encrypted_config
@webhook_url = current_account.webhook_urls.first_or_initialize @encrypted_config =
current_account.encrypted_configs.find_or_initialize_by(key: EncryptedConfig::WEBHOOK_URL_KEY)
end end
def webhook_params def encrypted_config_params
params.require(:webhook_url).permit(:url) params.require(:encrypted_config).permit(:value)
end end
end end
+2 -27
View File
@@ -25,11 +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 * as TurboInstantClick from './lib/turbo_instant_click' import * as TurboInstantClick from './lib/turbo_instant_click'
@@ -92,16 +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('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')
@@ -117,29 +105,17 @@ safeRegisterElement('template-builder', class extends HTMLElement {
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()
} }
@@ -152,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 -2
View File
@@ -100,7 +100,7 @@ button[disabled] .enabled {
.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;
@@ -111,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 {
@@ -130,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;
}
-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)
}
}
@@ -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()
} }
} }
-3
View File
@@ -14,21 +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),
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: {
+5 -7
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,12 +29,12 @@
{{ 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>
@@ -226,8 +226,7 @@ export default {
}, },
submitter: { submitter: {
type: Object, type: Object,
required: false, required: true
default: () => ({})
}, },
withSignatureId: { withSignatureId: {
type: Boolean, type: Boolean,
@@ -405,8 +404,7 @@ export default {
} }
if (this.field.preferences?.font_size) { if (this.field.preferences?.font_size) {
style.fontSize = `clamp(4pt, 1.6vw, ${this.field.preferences.font_size}pt)` style.fontSize = this.field.preferences.font_size + 'pt'
style.lineHeight = `clamp(6pt, 2.0vw, ${parseInt(this.field.preferences.font_size) + 3}pt)`
} }
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')
} }
}) })
}, },
@@ -45,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>
@@ -99,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
+41 -107
View File
@@ -13,12 +13,6 @@
:scroll-padding="scrollPadding" :scroll-padding="scrollPadding"
@focus-step="[saveStep(), currentField.type !== 'checkbox' ? isFormVisible = true : '', goToStep($event, false, 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] = f.default_value; return acc }, {})"
:submitter="submitter"
:submittable="false"
/>
<FormulaFieldAreas <FormulaFieldAreas
v-if="formulaFields.length" v-if="formulaFields.length"
:fields="formulaFields" :fields="formulaFields"
@@ -38,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"
@@ -62,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"
@@ -73,7 +64,7 @@
:class="{ 'md:px-4': isBreakpointMd }" :class="{ 'md:px-4': isBreakpointMd }"
> >
<form <form
v-if="!isCompleted && !isInvite" v-if="!isCompleted"
ref="form" ref="form"
:action="submitPath" :action="submitPath"
method="post" method="post"
@@ -115,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'">
@@ -152,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)"
@@ -160,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>
@@ -169,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>
@@ -207,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
@@ -271,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"
@@ -346,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"
@@ -355,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'"
@@ -371,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'"
@@ -395,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'"
@@ -407,7 +395,7 @@
:values="values" :values="values"
@attached="attachments.push($event)" @attached="attachments.push($event)"
@focus="scrollIntoField(currentField)" @focus="scrollIntoField(currentField)"
@submit="!isSubmitting && submitStep()" @submit="submitStep"
/> />
</div> </div>
<div <div
@@ -427,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"
@@ -442,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"
@@ -502,7 +479,6 @@ 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'
@@ -540,7 +516,6 @@ export default {
IconWritingSign, IconWritingSign,
AttachmentStep, AttachmentStep,
InitialsStep, InitialsStep,
InviteForm,
MultiSelectStep, MultiSelectStep,
IconInnerShadowTop, IconInnerShadowTop,
DateStep, DateStep,
@@ -567,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,
@@ -763,11 +733,9 @@ 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,
@@ -780,15 +748,9 @@ export default {
isMobile () { isMobile () {
return /android|iphone|ipad/i.test(navigator.userAgent) return /android|iphone|ipad/i.test(navigator.userAgent)
}, },
submitButtonText () { buttonText () {
if (this.alwaysMinimize) { if (this.alwaysMinimize || this.stepFields.length === this.currentStep + 1) {
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')
} }
@@ -796,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] || []
}, },
@@ -852,9 +802,6 @@ 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))
},
stepFields () { stepFields () {
return this.fields.filter((f) => !f.readonly).reduce((acc, f) => { return this.fields.filter((f) => !f.readonly).reduce((acc, f) => {
const prevStep = acc[acc.length - 1] const prevStep = acc[acc.length - 1]
@@ -900,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) {
@@ -932,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)) {
@@ -978,7 +925,7 @@ export default {
return field.conditions.reduce((acc, c) => { return field.conditions.reduce((acc, c) => {
const field = this.fieldsUuidIndex[c.field_uuid] const field = this.fieldsUuidIndex[c.field_uuid]
if (['not_empty', 'checked', 'equal', 'contains'].includes(c.action) && field && !this.checkFieldConditions(field)) { if (['not_empty', 'checked', 'equal', 'contains'].includes(c.action) && !this.checkFieldConditions(field)) {
return false return false
} }
@@ -986,12 +933,12 @@ export default {
return acc && isEmpty(this.values[c.field_uuid]) return acc && isEmpty(this.values[c.field_uuid])
} else if (['not_empty', 'checked'].includes(c.action)) { } else if (['not_empty', 'checked'].includes(c.action)) {
return acc && !isEmpty(this.values[c.field_uuid]) return acc && !isEmpty(this.values[c.field_uuid])
} else if (['equal', 'contains'].includes(c.action) && field) { } else if (['equal', 'contains'].includes(c.action)) {
const option = field.options.find((o) => o.uuid === c.value) const option = field.options.find((o) => o.uuid === c.value)
const values = [this.values[c.field_uuid]].flat() const values = [this.values[c.field_uuid]].flat()
return acc && values.includes(this.optionValue(option, field.options.indexOf(option))) return acc && values.includes(this.optionValue(option, field.options.indexOf(option)))
} else if (['not_equal', 'does_not_contain'].includes(c.action) && field) { } else if (['not_equal', 'does_not_contain'].includes(c.action)) {
const option = field.options.find((o) => o.uuid === c.value) const option = field.options.find((o) => o.uuid === c.value)
const values = [this.values[c.field_uuid]].flat() const values = [this.values[c.field_uuid]].flat()
@@ -1107,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]
@@ -1149,15 +1092,13 @@ export default {
async submitStep () { async submitStep () {
this.isSubmitting = true this.isSubmitting = true
const submitStep = this.currentStep
const stepPromise = ['signature', 'phone', 'initials', 'payment'].includes(this.currentField.type) const stepPromise = ['signature', 'phone', 'initials', 'payment'].includes(this.currentField.type)
? this.$refs.currentStep.submit ? this.$refs.currentStep.submit
: () => Promise.resolve({}) : () => Promise.resolve({})
stepPromise().then(async () => { stepPromise().then(async () => {
const emptyRequiredField = this.stepFields.find((fields, index) => { const emptyRequiredField = this.stepFields.find((fields, index) => {
if (index >= submitStep) { if (index >= this.currentStep) {
return false return false
} }
@@ -1167,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 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')
} }
@@ -1192,11 +1133,15 @@ export default {
return Promise.reject(new Error(data.error)) return Promise.reject(new Error(data.error))
} }
const nextStep = (isLastStep && emptyRequiredField) || 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)
@@ -1204,10 +1149,18 @@ 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)
@@ -1216,32 +1169,13 @@ export default {
}) })
}).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
}) })
},
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
}
} }
} }
} }
+27 -118
View File
@@ -1,6 +1,4 @@
const en = { const en = {
complete: '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',
@@ -12,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',
@@ -40,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',
@@ -62,10 +57,8 @@ 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',
@@ -92,10 +85,6 @@ const en = {
} }
const es = { const es = {
complete: 'Completar',
sign_and_complete: 'Firmar y Completar',
invite: 'Invitar',
email: 'Correo electrónico',
approved: 'Aprobado', approved: 'Aprobado',
reviewed: 'Revisado', reviewed: 'Revisado',
other: 'Otro', other: 'Otro',
@@ -133,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',
@@ -154,10 +142,8 @@ 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',
@@ -184,10 +170,6 @@ const es = {
} }
const it = { const it = {
complete: 'Completa',
sign_and_complete: 'Firma e Completa',
invite: 'Invita',
email: 'Email',
approved: 'Approvato', approved: 'Approvato',
reviewed: 'Revisionato', reviewed: 'Revisionato',
other: 'Altro', other: 'Altro',
@@ -216,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',
@@ -224,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',
@@ -246,10 +227,8 @@ 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',
@@ -276,10 +255,6 @@ const it = {
} }
const de = { const de = {
complete: 'Abschließ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',
@@ -316,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',
@@ -338,10 +312,8 @@ 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',
@@ -368,10 +340,6 @@ const de = {
} }
const fr = { const fr = {
complete: 'Terminer',
sign_and_complete: 'Signer et Terminer',
invite: 'Inviter',
email: 'Courriel',
approved: 'Approuvé', approved: 'Approuvé',
reviewed: 'Révisé', reviewed: 'Révisé',
other: 'Autre', other: 'Autre',
@@ -408,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',
@@ -430,10 +397,8 @@ 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',
@@ -460,10 +425,6 @@ const fr = {
} }
const pl = { const pl = {
complete: 'Zakończ',
sign_and_complete: 'Podpisz i zakończ',
invite: 'Zaproś',
email: 'E-mail',
approved: 'Zaakceptowany', approved: 'Zaakceptowany',
reviewed: 'Przejrzany', reviewed: 'Przejrzany',
other: 'Inny', other: 'Inny',
@@ -500,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',
@@ -522,10 +482,8 @@ 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',
@@ -552,10 +510,6 @@ const pl = {
} }
const uk = { const uk = {
complete: 'Завершити',
sign_and_complete: 'Підписати і завершити',
invite: 'Запросити',
email: 'Електронна пошта',
approved: 'Затверджено', approved: 'Затверджено',
reviewed: 'Переглянуто', reviewed: 'Переглянуто',
other: 'Інше', other: 'Інше',
@@ -592,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: 'необов’язково',
@@ -614,10 +567,8 @@ 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: 'Підтвердіть номер телефону',
@@ -644,10 +595,6 @@ const uk = {
} }
const cs = { const cs = {
complete: 'Dokončit',
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é',
@@ -684,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é',
@@ -706,10 +652,8 @@ 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',
@@ -736,10 +680,6 @@ const cs = {
} }
const pt = { const pt = {
complete: 'Completar',
sign_and_complete: 'Assinar e Completar',
invite: 'Convidar',
email: 'E-mail',
approved: 'Aprovado', approved: 'Aprovado',
reviewed: 'Revisado', reviewed: 'Revisado',
other: 'Outro', other: 'Outro',
@@ -776,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',
@@ -798,10 +737,8 @@ 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',
@@ -828,10 +765,6 @@ const pt = {
} }
const he = { const he = {
complete: 'השלם',
sign_and_complete: 'חתום והשלם',
invite: 'הזמן',
email: 'דוא"ל',
approved: 'מאושר', approved: 'מאושר',
reviewed: 'נסקר', reviewed: 'נסקר',
other: 'אחר', other: 'אחר',
@@ -868,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: 'אופציונלי',
@@ -891,10 +823,8 @@ 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: 'אימות מספר טלפון',
@@ -921,10 +851,6 @@ const he = {
} }
const nl = { const nl = {
complete: 'Voltooien',
sign_and_complete: 'Ondertekenen en voltooien',
invite: 'Uitnodigen',
email: 'E-mail',
approved: 'Goedgekeurd', approved: 'Goedgekeurd',
reviewed: 'Beoordeeld', reviewed: 'Beoordeeld',
other: 'Anders', other: 'Anders',
@@ -962,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',
@@ -984,10 +909,8 @@ 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',
@@ -1014,10 +937,6 @@ const nl = {
} }
const ar = { const ar = {
complete: 'اكتمال',
sign_and_complete: 'التوقيع والاكتمال',
invite: 'دعوة',
email: 'البريد الإلكتروني',
approved: 'موافق عليه', approved: 'موافق عليه',
reviewed: 'تمت مراجعته', reviewed: 'تمت مراجعته',
other: 'آخر', other: 'آخر',
@@ -1054,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: 'اختياري',
@@ -1076,10 +994,8 @@ 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: 'تحقق من رقم الهاتف',
@@ -1106,10 +1022,6 @@ const ar = {
} }
const ko = { const ko = {
complete: '완료',
sign_and_complete: '서명하고 완료하기',
invite: '초대하기',
email: '이메일',
approved: '승인됨', approved: '승인됨',
reviewed: '검토됨', reviewed: '검토됨',
other: '기타', other: '기타',
@@ -1145,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: '선택 사항',
@@ -1167,10 +1078,8 @@ 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: '전화번호 확인됨',
@@ -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: {
@@ -252,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>
File diff suppressed because it is too large. Load diff
+21 -106
View File
@@ -61,55 +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 class="relative inline-block">
<div class="btn bg-base-200 border border-neutral-300 text-2xl whitespace-nowrap font-normal rounded-l-full">
{{ selectedCountry.flag }} +{{ selectedCountry.dial }}
</div>
<select
class="absolute top-0 bottom-0 right-0 left-0 opacity-0 w-full h-full cursor-pointer"
:disabled="!!defaultValue"
@change="onCountrySelect(countries.find((country) => country.flag === $event.target.value))"
>
<option
v-for="(country, index) in countries"
:key="index"
:value="country.flag"
>
{{ country.flag }} {{ country.name }}
</option>
</select>
</div>
<input
:name="`values[${field.uuid}]`"
:value="fullInternationalPhoneValue"
hidden
>
<input
:id="field.uuid"
ref="phone"
:value="phoneValue"
:readonly="!!defaultValue"
class="base-input !text-2xl !rounded-l-none !border-l-0 !outline-none w-full"
autocomplete="tel"
type="tel"
inputmode="tel"
:required="field.required"
placeholder="234 567-8900"
@input="onPhoneInput"
@focus="$emit('focus')"
>
</div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import MarkdownContent from './markdown_content' import MarkdownContent from './markdown_content'
import phoneData from './phone_data'
function throttle (func, delay) { function throttle (func, delay) {
let lastCallTime = 0 let lastCallTime = 0
@@ -169,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(([name, dial, flag, tz]) => {
return { name, dial, flag, tz }
})
},
countriesDialIndex () {
return this.countries.reduce((acc, item) => {
acc[item.dial] ||= item
return acc
}, {})
},
dialCodesRegexp () {
const dialCodes = this.countries.map((country) => country.dial).sort((a, b) => b.length - a.length)
return new RegExp(`^\\+(${dialCodes.join('|')})`)
},
detectedPhoneValueDialCode () {
return (this.phoneValue || '').replace(/[^\d+]/g, '').match(this.dialCodesRegexp)?.[1]
},
fullInternationalPhoneValue () {
if (this.detectedPhoneValueDialCode) {
return this.phoneValue
} else if (this.phoneValue) {
return ['+', this.selectedCountry.dial, this.phoneValue].filter(Boolean).join('')
} else {
return ''
}
}
},
mounted () {
const browserTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone
if (this.detectedPhoneValueDialCode) {
this.selectedCountry = this.countriesDialIndex[this.detectedPhoneValueDialCode]
} else if (browserTimeZone) {
const tz = browserTimeZone.split('/')[1]
this.selectedCountry = this.countries.find((country) => country.tz.includes(tz)) || this.countries[0]
} }
}, },
methods: { methods: {
emitSubmit: throttle(function (e) { emitSubmit: throttle(function (e) {
this.$emit('submit') this.$emit('submit')
}, 1000), }, 1000),
onCountrySelect (country) {
if (this.selectedCountry.flag !== country.flag) {
this.phoneValue = this.phoneValue.replace(`+${this.selectedCountry.dial}`, `+${country.dial}`)
}
this.selectedCountry = country
this.$refs.phone.focus()
},
onPhoneInput (e) {
this.phoneValue = e.target.value
if (this.detectedPhoneValueDialCode) {
this.selectedCountry = this.countriesDialIndex[this.detectedPhoneValueDialCode]
}
},
onInputCode (e) { onInputCode (e) {
if (e.target.value.length === 6) { if (e.target.value.length === 6) {
this.emitSubmit() this.emitSubmit()
@@ -256,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))
} }
@@ -274,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">
@@ -579,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()
@@ -593,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
@@ -627,6 +615,8 @@ export default {
} }
reader.readAsDataURL(file) reader.readAsDataURL(file)
this.uploadImageInputKey = Math.random().toString()
} }
}, },
maybeSetSignedUuid (signedUuid) { maybeSetSignedUuid (signedUuid) {
+1 -1
View File
@@ -147,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
View File
@@ -360,7 +360,6 @@ export default {
}, },
computed: { computed: {
fieldNames: FieldType.computed.fieldNames, fieldNames: FieldType.computed.fieldNames,
fieldLabels: FieldType.computed.fieldLabels,
fieldIcons: FieldType.computed.fieldIcons, fieldIcons: FieldType.computed.fieldIcons,
isDefaultValuePresent () { isDefaultValuePresent () {
if (this.field?.type === 'radio' && this.field?.areas?.length > 1) { if (this.field?.type === 'radio' && this.field?.areas?.length > 1) {
+10 -60
View File
@@ -58,7 +58,6 @@
/> />
<template v-else> <template v-else>
<a <a
v-if="withSignYourselfButton"
:href="template.submitters.length > 1 ? `/templates/${template.id}/submissions/new?selfsign=true` : `/d/${template.slug}`" :href="template.submitters.length > 1 ? `/templates/${template.id}/submissions/new?selfsign=true` : `/d/${template.slug}`"
class="btn btn-primary btn-ghost text-base hidden md:flex" class="btn btn-primary btn-ghost text-base hidden md:flex"
:target="template.submitters.length > 1 ? '' : '_blank'" :target="template.submitters.length > 1 ? '' : '_blank'"
@@ -74,7 +73,6 @@
</span> </span>
</a> </a>
<a <a
v-if="withSendButton"
:href="`/templates/${template.id}/submissions/new?with_link=true`" :href="`/templates/${template.id}/submissions/new?with_link=true`"
data-turbo-frame="modal" data-turbo-frame="modal"
class="white-button md:!px-6" class="white-button md:!px-6"
@@ -131,18 +129,7 @@
class="flex items-center justify-center space-x-2" class="flex items-center justify-center space-x-2"
> >
<IconEye class="w-6 h-6 flex-shrink-0" /> <IconEye class="w-6 h-6 flex-shrink-0" />
<span class="whitespace-nowrap">{{ t('save_and_preview') }}</span> <span class="whitespace-nowrap">Save and Preview</span>
</a>
</li>
<li>
<a
:href="`/templates/${template.id}/preferences`"
data-turbo-frame="modal"
class="flex space-x-2"
@click="closeDropdown"
>
<IconAdjustments class="w-6 h-6 flex-shrink-0" />
<span class="whitespace-nowrap">{{ t('preferences') }}</span>
</a> </a>
</li> </li>
</ul> </ul>
@@ -332,7 +319,7 @@
> >
<div class="bg-base-200 rounded-lg p-5 text-center space-y-4"> <div class="bg-base-200 rounded-lg p-5 text-center space-y-4">
<p> <p>
{{ t('draw_field_on_the_document') }} {{ t('draw_field_on_the_document').replace('{field}', drawField?.name || '') }}
</p> </p>
<div> <div>
<button <button
@@ -415,11 +402,10 @@ import Contenteditable from './contenteditable'
import DocumentPreview from './preview' import DocumentPreview from './preview'
import DocumentControls from './controls' import DocumentControls from './controls'
import MobileFields from './mobile_fields' import MobileFields from './mobile_fields'
import FieldSubmitter from './field_submitter' import { IconPlus, IconUsersPlus, IconDeviceFloppy, IconChevronDown, IconEye, IconWritingSign, IconInnerShadowTop, IconInfoCircle } from '@tabler/icons-vue'
import { IconPlus, IconUsersPlus, IconDeviceFloppy, IconChevronDown, IconEye, IconWritingSign, IconInnerShadowTop, IconInfoCircle, IconAdjustments } from '@tabler/icons-vue'
import { v4 } from 'uuid' import { v4 } from 'uuid'
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import * as i18n from './i18n' import { en as i18nEn } from './i18n'
export default { export default {
name: 'TemplateBuilder', name: 'TemplateBuilder',
@@ -440,7 +426,6 @@ export default {
Contenteditable, Contenteditable,
IconUsersPlus, IconUsersPlus,
IconChevronDown, IconChevronDown,
IconAdjustments,
IconEye, IconEye,
IconDeviceFloppy IconDeviceFloppy
}, },
@@ -494,16 +479,6 @@ export default {
required: false, required: false,
default: true default: true
}, },
withSendButton: {
type: Boolean,
required: false,
default: true
},
withSignYourselfButton: {
type: Boolean,
required: false,
default: true
},
inputMode: { inputMode: {
type: Boolean, type: Boolean,
required: false, required: false,
@@ -559,11 +534,6 @@ export default {
required: false, required: false,
default: () => [] default: () => []
}, },
defineSubmitters: {
type: Array,
required: false,
default: () => []
},
acceptFileTypes: { acceptFileTypes: {
type: String, type: String,
required: false, required: false,
@@ -683,12 +653,8 @@ export default {
} }
}, },
computed: { computed: {
submitterDefaultNames: FieldSubmitter.computed.names,
selectedAreaRef: () => ref(), selectedAreaRef: () => ref(),
fieldsDragFieldRef: () => ref(), fieldsDragFieldRef: () => ref(),
language () {
return this.locale.split('-')[0].toLowerCase()
},
defaultDateFormat () { defaultDateFormat () {
const isUsBrowser = Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US') const isUsBrowser = Intl.DateTimeFormat().resolvedOptions().locale.endsWith('-US')
const isUsTimezone = new Intl.DateTimeFormat('en-US', { timeZoneName: 'short' }).format(new Date()).match(/\s(?:CST|CDT|PST|PDT|EST|EDT)$/) const isUsTimezone = new Intl.DateTimeFormat('en-US', { timeZoneName: 'short' }).format(new Date()).match(/\s(?:CST|CDT|PST|PDT|EST|EDT)$/)
@@ -700,7 +666,6 @@ export default {
'de-DE': 'DD.MM.YYYY', 'de-DE': 'DD.MM.YYYY',
'fr-FR': 'DD/MM/YYYY', 'fr-FR': 'DD/MM/YYYY',
'it-IT': 'DD/MM/YYYY', 'it-IT': 'DD/MM/YYYY',
'en-GB': 'DD/MM/YYYY',
'es-ES': 'DD/MM/YYYY' 'es-ES': 'DD/MM/YYYY'
} }
}, },
@@ -756,18 +721,6 @@ export default {
} }
}) })
this.defineSubmitters.forEach((name, index) => {
const submitter = (this.template.submitters[index] ||= {})
submitter.name = name || this.submitterDefaultNames[index]
if (existingSubmittersUuids.filter(Boolean).length) {
submitter.uuid = existingSubmittersUuids[index] || submitter.uuid || v4()
} else {
submitter.uuid ||= v4()
}
})
this.selectedSubmitter = this.template.submitters[0] this.selectedSubmitter = this.template.submitters[0]
}, },
mounted () { mounted () {
@@ -805,11 +758,8 @@ export default {
this.documentRefs = [] this.documentRefs = []
}, },
methods: { methods: {
closeDropdown () {
document.activeElement.blur()
},
t (key) { t (key) {
return this.i18n[key] || i18n[this.language]?.[key] || i18n.en[key] || key return this.i18n[key] || i18nEn[key] || key
}, },
removePendingFields () { removePendingFields () {
this.template.fields = this.template.fields.filter((f) => { this.template.fields = this.template.fields.filter((f) => {
@@ -1375,7 +1325,7 @@ export default {
this.save() this.save()
}, },
onDocumentRemove (item) { onDocumentRemove (item) {
if (window.confirm(this.t('are_you_sure_'))) { if (window.confirm(this.t('are_you_sure'))) {
this.template.schema.splice(this.template.schema.indexOf(item), 1) this.template.schema.splice(this.template.schema.indexOf(item), 1)
} }
@@ -1474,7 +1424,7 @@ export default {
if (!this.template.fields.length) { if (!this.template.fields.length) {
e.preventDefault() e.preventDefault()
alert(this.t('please_draw_fields_to_prepare_the_document')) alert('Please draw fields to prepare the document.')
} else { } else {
const submitterWithoutFields = const submitterWithoutFields =
this.template.submitters.find((submitter) => !this.template.fields.some((f) => f.submitter_uuid === submitter.uuid)) this.template.submitters.find((submitter) => !this.template.fields.some((f) => f.submitter_uuid === submitter.uuid))
@@ -1482,7 +1432,7 @@ export default {
if (submitterWithoutFields) { if (submitterWithoutFields) {
e.preventDefault() e.preventDefault()
alert(this.t('please_add_fields_for_the_submitter_name_or_remove_the_submitter_name_if_not_needed').replaceAll('{submitter_name}', submitterWithoutFields.name)) alert(`Please add fields for the ${submitterWithoutFields.name}. Or, remove the ${submitterWithoutFields.name} if not needed.`)
} }
} }
}, },
@@ -1498,13 +1448,13 @@ export default {
} }
if (!this.template.fields.length) { if (!this.template.fields.length) {
alert(this.t('please_draw_fields_to_prepare_the_document')) alert('Please draw fields to prepare the document.')
} else { } else {
const submitterWithoutFields = const submitterWithoutFields =
this.template.submitters.find((submitter) => !this.template.fields.some((f) => f.submitter_uuid === submitter.uuid)) this.template.submitters.find((submitter) => !this.template.fields.some((f) => f.submitter_uuid === submitter.uuid))
if (submitterWithoutFields) { if (submitterWithoutFields) {
alert(this.t('please_add_fields_for_the_submitter_name_or_remove_the_submitter_name_if_not_needed').replaceAll('{submitter_name}', submitterWithoutFields.name)) alert(`Please add fields for the ${submitterWithoutFields.name}. Or, remove the ${submitterWithoutFields.name} if not needed.`)
} else { } else {
this.isSaving = true this.isSaving = true
@@ -23,10 +23,10 @@
class="bg-base-300 rounded-xl py-2 px-3 text-center" class="bg-base-300 rounded-xl py-2 px-3 text-center"
> >
<a <a
href="https://www.docuseal.com/pricing" href="https://www.docuseal.co/pricing"
target="_blank" target="_blank"
class="link" class="link"
>{{ t('available_in_pro') }}</a> >Available in Pro</a>
</div> </div>
<form @submit.prevent="validateSaveAndClose"> <form @submit.prevent="validateSaveAndClose">
<div class="my-4 space-y-5"> <div class="my-4 space-y-5">
@@ -50,7 +50,6 @@
</div> </div>
<select <select
class="select select-bordered select-sm w-full bg-white h-11 pl-4 text-base font-normal" class="select select-bordered select-sm w-full bg-white h-11 pl-4 text-base font-normal"
:class="{ 'text-gray-300': !condition.field_uuid }"
required required
@change="[ @change="[
condition.field_uuid = $event.target.value, condition.field_uuid = $event.target.value,
@@ -69,7 +68,6 @@
v-for="f in fields" v-for="f in fields"
:key="f.uuid" :key="f.uuid"
:value="f.uuid" :value="f.uuid"
class="text-base-content"
:selected="condition.field_uuid === f.uuid" :selected="condition.field_uuid === f.uuid"
> >
{{ f.name || buildDefaultName(f, template.fields) }} {{ f.name || buildDefaultName(f, template.fields) }}
@@ -90,9 +88,8 @@
</option> </option>
</select> </select>
<select <select
v-if="['radio', 'select', 'multiple'].includes(conditionField(condition)?.type) && conditionField(condition)?.options" v-if="conditionField(condition)?.options?.length"
class="select select-bordered select-sm w-full bg-white h-11 pl-4 text-base font-normal" class="select select-bordered select-sm w-full bg-white h-11 pl-4 text-base font-normal"
:class="{ 'text-gray-300': !condition.value }"
required required
@change="condition.value = $event.target.value" @change="condition.value = $event.target.value"
> >
@@ -108,7 +105,6 @@
:key="option.uuid" :key="option.uuid"
:value="option.uuid" :value="option.uuid"
:selected="condition.value === option.uuid" :selected="condition.value === option.uuid"
class="text-base-content"
> >
{{ option.value || `${t('option')} ${index + 1}` }} {{ option.value || `${t('option')} ${index + 1}` }}
</option> </option>
@@ -204,7 +200,7 @@ export default {
}, },
validateSaveAndClose () { validateSaveAndClose () {
if (!this.withConditions) { if (!this.withConditions) {
return alert(this.t('available_only_in_pro')) return alert('Available only in Pro')
} }
if (this.conditions.find((f) => f.field_uuid)) { if (this.conditions.find((f) => f.field_uuid)) {
+1 -1
View File
@@ -105,7 +105,7 @@ export default {
this.upload() this.upload()
} else { } else {
alert(this.t('only_pdf_and_images_are_supported')) alert('Only PDF and images are supported.')
} }
} }
} }
+2 -2
View File
@@ -312,7 +312,6 @@ export default {
}, },
computed: { computed: {
fieldNames: FieldType.computed.fieldNames, fieldNames: FieldType.computed.fieldNames,
fieldLabels: FieldType.computed.fieldLabels,
dropdownBgColor () { dropdownBgColor () {
return ['', null, 'transparent'].includes(this.backgroundColor) ? 'white' : this.backgroundColor return ['', null, 'transparent'].includes(this.backgroundColor) ? 'white' : this.backgroundColor
}, },
@@ -368,7 +367,8 @@ export default {
if (this.field.type === 'heading') { if (this.field.type === 'heading') {
return `${this.fieldNames[field.type]} ${typeIndex + 1}` return `${this.fieldNames[field.type]} ${typeIndex + 1}`
} else { } else {
return `${this.fieldLabels[field.type]} ${typeIndex + 1}` const suffix = { multiple: this.t('select'), radio: this.t('group') }[field.type] || this.t('field')
return `${this.fieldNames[field.type]} ${suffix} ${typeIndex + 1}`
} }
} }
}, },
@@ -330,7 +330,7 @@ export default {
this.$emit('update:model-value', submitter.uuid) this.$emit('update:model-value', submitter.uuid)
}, },
remove (submitter) { remove (submitter) {
if (window.confirm(this.t('are_you_sure_'))) { if (window.confirm(this.t('are_you_sure'))) {
this.$emit('remove', submitter) this.$emit('remove', submitter)
} }
}, },
@@ -114,25 +114,6 @@ export default {
phone: this.t('phone') phone: this.t('phone')
} }
}, },
fieldLabels () {
return {
text: this.t('text_field'),
signature: this.t('signature_field'),
initials: this.t('initials_field'),
date: this.t('date_field'),
number: this.t('number_field'),
image: this.t('image_field'),
file: this.t('file_field'),
select: this.t('select_field'),
checkbox: this.t('checkbox_field'),
multiple: this.t('multiple_field'),
radio: this.t('radio_field'),
cells: this.t('cells_field'),
stamp: this.t('stamp_field'),
payment: this.t('payment_field'),
phone: this.t('phone_field')
}
},
fieldIcons () { fieldIcons () {
return { return {
heading: IconHeading, heading: IconHeading,
+5 -5
View File
@@ -137,10 +137,10 @@
v-else-if="type == 'phone' && (fieldTypes.length === 0 || fieldTypes.includes(type))" v-else-if="type == 'phone' && (fieldTypes.length === 0 || fieldTypes.includes(type))"
class="tooltip tooltip-bottom flex" class="tooltip tooltip-bottom flex"
:class="{'tooltip-bottom-end': withPayment, 'tooltip-bottom': !withPayment }" :class="{'tooltip-bottom-end': withPayment, 'tooltip-bottom': !withPayment }"
:data-tip="t('unlock_sms_verified_phone_number_field_with_paid_plan_use_text_field_for_phone_numbers_without_verification')" data-tip="Unlock SMS-verified phone number field with paid plan. Use text field for phone numbers without verification."
> >
<a <a
href="https://www.docuseal.com/pricing" href="https://www.docuseal.co/pricing"
target="_blank" target="_blank"
class="opacity-50 flex items-center justify-center border border-dashed border-base-300 w-full rounded relative" class="opacity-50 flex items-center justify-center border border-dashed border-base-300 w-full rounded relative"
:style="{ backgroundColor }" :style="{ backgroundColor }"
@@ -168,13 +168,13 @@
> >
<ul class="list-disc list-outside ml-3"> <ul class="list-disc list-outside ml-3">
<li> <li>
{{ t('draw_a_text_field_on_the_page_with_a_mouse') }} Draw a text field on the page with a mouse
</li> </li>
<li> <li>
{{ t('drag_and_drop_any_other_field_type_on_the_page') }} Drag &amp; drop any other field type on the page
</li> </li>
<li> <li>
{{ t('click_on_the_field_type_above_to_start_drawing_it') }} Click on the field type above to start drawing it
</li> </li>
</ul> </ul>
</div> </div>
@@ -23,10 +23,10 @@
class="bg-base-300 rounded-xl py-2 px-3 text-center" class="bg-base-300 rounded-xl py-2 px-3 text-center"
> >
<a <a
href="https://www.docuseal.com/pricing" href="https://www.docuseal.co/pricing"
target="_blank" target="_blank"
class="link" class="link"
>{{ t('available_in_pro') }}</a> >Available in Pro</a>
</div> </div>
<div class="flex-inline mb-2 gap-2 space-y-1"> <div class="flex-inline mb-2 gap-2 space-y-1">
<button <button
@@ -195,13 +195,13 @@ export default {
}, },
validateSaveAndClose () { validateSaveAndClose () {
if (!this.withFormula) { if (!this.withFormula) {
return alert(this.t('available_only_in_pro')) return alert('Available only in Pro')
} }
const normalizedFormula = this.normalizeFormula(this.formula) const normalizedFormula = this.normalizeFormula(this.formula)
if (normalizedFormula.includes('FIELD NOT FOUND')) { if (normalizedFormula.includes('FIELD NOT FOUND')) {
alert(this.t('some_fields_are_missing_in_the_formula')) alert('Some fields are missing in the formula.')
} else { } else {
this.field.preferences.formula = normalizedFormula this.field.preferences.formula = normalizedFormula
Loaded 100 of 344 files, more files were not shown because too many files have changed in this diff. Show more