mirror of
https://github.com/docusealco/docuseal.git
synced 2026-08-07 07:14:43 +00:00
Compare commits
190 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 50acff2e81 | |||
| cb6d24dfca | |||
| ab4a22d8ba | |||
| bc40a15de1 | |||
| 2542f26d96 | |||
| 84edb6dbda | |||
| 88776e7316 | |||
| f669045362 | |||
| 68d3efbdba | |||
| 22d4833496 | |||
| ff14caa9f7 | |||
| 05647135bc | |||
| ae76c2a4a8 | |||
| fb2b3d44fe | |||
| 3868557efd | |||
| 9bef28c8ba | |||
| f25fd3b03d | |||
| d1d7571ee4 | |||
| 6a298a068c | |||
| d4aef79b68 | |||
| 05d578d88f | |||
| 8773b30b10 | |||
| 1b636c5878 | |||
| 46e948ab99 | |||
| 889103aa5e | |||
| 558a14a5f2 | |||
| 72bbdb38bf | |||
| 3975a03cb4 | |||
| e398327fb7 | |||
| 9266694d65 | |||
| 350c4b1c2a | |||
| c3a933d87d | |||
| 970e53a5a3 | |||
| 520bac2ec7 | |||
| e1eddd31c4 | |||
| 3c616a824c | |||
| 90d86b58c2 | |||
| 4332b4ebb8 | |||
| e2f930f6f7 | |||
| c303159c63 | |||
| c1ce2bec00 | |||
| 3d2046f227 | |||
| babaf1aa48 | |||
| 3f03d343ca | |||
| 110cdb5123 | |||
| 440d1114be | |||
| 9032517d4e | |||
| 7c71fefc47 | |||
| e8bd8ff76f | |||
| fed420cae1 | |||
| 910490528d | |||
| ab810be250 | |||
| 178ee47093 | |||
| 6f90147a11 | |||
| 3ebaacbb7a | |||
| 64b39b53a2 | |||
| 2948910c25 | |||
| 1d20c79cf4 | |||
| 48f3b13f3e | |||
| 1fc1e9af16 | |||
| ce70016603 | |||
| e0e79e343d | |||
| 6568f81ee9 | |||
| 386f130d7a | |||
| 3ee41975fd | |||
| d9f314898b | |||
| 4fc7412cac | |||
| 9adbfc52c2 | |||
| 1ad8d9196a | |||
| e9172392b5 | |||
| d6d94de328 | |||
| 77261f457c | |||
| c258915031 | |||
| c1db8bdd3b | |||
| c700484ce8 | |||
| b62bd200ed | |||
| a164fe8dd8 | |||
| bb40e872b3 | |||
| bab80e9488 | |||
| d090d79ebf | |||
| 2299ba4e29 | |||
| d6474f0e6e | |||
| e0a297cf81 | |||
| 40f5fa4217 | |||
| 94e552d170 | |||
| cf1b0a4c16 | |||
| 9aa0a8ecbf | |||
| fd4bef1706 | |||
| d32b0e6d20 | |||
| 4a846be214 | |||
| 3dd3877ea9 | |||
| b4dba0d809 | |||
| dea80a82d8 | |||
| eefa994724 | |||
| 35cce24539 | |||
| e6c8203959 | |||
| 9479771fc8 | |||
| 711427521c | |||
| 5dae742361 | |||
| d8ccd4beee | |||
| c532a0115d | |||
| c9c3364764 | |||
| 1ff23d7c40 | |||
| 95e9b34033 | |||
| 6b42dcf91b | |||
| 28962c949b | |||
| 4ecbbf8410 | |||
| 990b35be18 | |||
| 8002764855 | |||
| b80982658c | |||
| 443ac23c29 | |||
| 37bb987e3e | |||
| c7b08e8d30 | |||
| dd502e8cef | |||
| a554182e57 | |||
| 0c039ee64f | |||
| c6b004f107 | |||
| 2fcb08d27d | |||
| 75fe5ce381 | |||
| fd7d0eaf53 | |||
| 7e537e4232 | |||
| a892975010 | |||
| 05ec92186b | |||
| 8b035f80b3 | |||
| c717293306 | |||
| b3f3cf2aa6 | |||
| 7406334f77 | |||
| 3e1350a221 | |||
| 961e0a1c89 | |||
| 2ce9bbecb9 | |||
| 704da3a2ec | |||
| 2f530ccfca | |||
| e6c20f5ff6 | |||
| d267978437 | |||
| e9d728fa4a | |||
| 3c60fb4ee6 | |||
| f1e494b669 | |||
| f7a8e4102b | |||
| 311c2b0e34 | |||
| 5dd59f4f7c | |||
| 571f5d8221 | |||
| 136b0eb5ed | |||
| 2d813e184b | |||
| fd23674e87 | |||
| 3b9d35acf1 | |||
| 17fc6e2f1c | |||
| 2e132d8b5a | |||
| 53bef85a63 | |||
| 99ad6e36ac | |||
| bfa41ac35f | |||
| bc4414a368 | |||
| 26ae5f1d80 | |||
| be4962a02a | |||
| e72090c2e0 | |||
| ad8bca8bf4 | |||
| 22be3b37cf | |||
| cfc32ee522 | |||
| 10fbdf6612 | |||
| 3604fb6461 | |||
| 24392d0258 | |||
| 4ab1065773 | |||
| 50b0f7e5ed | |||
| be4a0505bc | |||
| bd853c6265 | |||
| 5a1efd0e27 | |||
| 71b8cc7172 | |||
| 126a36dbf3 | |||
| 90db64cc03 | |||
| 227a067c77 | |||
| dc24df0d4c | |||
| 2598962c5b | |||
| b4884e94f6 | |||
| 4741e64178 | |||
| 25fe35e212 | |||
| 92909cea09 | |||
| b16322dd5b | |||
| a04c04141c | |||
| 90f21d3ed9 | |||
| f31225e8c2 | |||
| eead1b2bc4 | |||
| f90a4e5576 | |||
| 50987ce38b | |||
| 26113ac3a6 | |||
| 0b7b72ce16 | |||
| 95d48b2808 | |||
| 8fecf94a28 | |||
| 66dd87b290 | |||
| d20e8cc37d | |||
| c189e2ed20 | |||
| 57f8f45320 |
@@ -11,7 +11,7 @@ jobs:
|
||||
- name: Install Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: 3.3.3
|
||||
ruby-version: 3.3.4
|
||||
- name: Cache gems
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
- name: Install Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: 3.3.3
|
||||
ruby-version: 3.3.4
|
||||
- name: Cache gems
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
@@ -100,7 +100,7 @@ jobs:
|
||||
- name: Install Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: 3.3.3
|
||||
ruby-version: 3.3.4
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
|
||||
+7
-1
@@ -34,6 +34,7 @@ Metrics/MethodLength:
|
||||
Max: 30
|
||||
Exclude:
|
||||
- 'db/migrate/**'
|
||||
- 'spec/**/*'
|
||||
|
||||
Metrics/CyclomaticComplexity:
|
||||
Max: 15
|
||||
@@ -46,6 +47,8 @@ Layout/LineLength:
|
||||
|
||||
Metrics/AbcSize:
|
||||
Max: 45
|
||||
Exclude:
|
||||
- spec/**/*
|
||||
|
||||
Metrics/ModuleLength:
|
||||
Max: 500
|
||||
@@ -63,7 +66,10 @@ RSpec/ExampleLength:
|
||||
Max: 40
|
||||
|
||||
RSpec/MultipleMemoizedHelpers:
|
||||
Max: 6
|
||||
Max: 9
|
||||
|
||||
Metrics/BlockNesting:
|
||||
Max: 4
|
||||
|
||||
Rails/I18nLocaleTexts:
|
||||
Enabled: false
|
||||
|
||||
+5
-5
@@ -1,4 +1,4 @@
|
||||
FROM ruby:3.3.3-alpine as fonts
|
||||
FROM ruby:3.3.4-alpine as 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")'
|
||||
|
||||
FROM ruby:3.3.3-alpine as webpack
|
||||
FROM ruby:3.3.4-alpine as webpack
|
||||
|
||||
ENV RAILS_ENV=production
|
||||
ENV NODE_ENV=production
|
||||
@@ -32,7 +32,7 @@ COPY ./app/views ./app/views
|
||||
|
||||
RUN echo "gem 'shakapacker'" > Gemfile && ./bin/shakapacker
|
||||
|
||||
FROM ruby:3.3.3-alpine as app
|
||||
FROM ruby:3.3.4-alpine as app
|
||||
|
||||
ENV RAILS_ENV=production
|
||||
ENV BUNDLE_WITHOUT="development:test"
|
||||
@@ -41,7 +41,7 @@ ENV OPENSSL_CONF=/app/openssl_legacy.cnf
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apk add --no-cache sqlite-dev libpq-dev mariadb-dev vips-dev vips-poppler poppler-utils redis vips-heif gcompat ttf-freefont && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf
|
||||
RUN echo '@edge https://dl-cdn.alpinelinux.org/alpine/edge/community' >> /etc/apk/repositories && apk add --no-cache sqlite-dev libpq-dev mariadb-dev vips-dev vips-poppler poppler-utils redis libheif@edge vips-heif gcompat ttf-freefont && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf
|
||||
|
||||
RUN echo $'.include = /etc/ssl/openssl.cnf\n\
|
||||
\n\
|
||||
@@ -57,7 +57,7 @@ activate = 1' >> /app/openssl_legacy.cnf
|
||||
|
||||
COPY ./Gemfile ./Gemfile.lock ./
|
||||
|
||||
RUN apk add --no-cache build-base && bundle install && apk del build-base && rm -rf ~/.bundle /usr/local/bundle/cache && ruby -e "puts Dir['/usr/local/bundle/**/{spec,rdoc,resources/shared,resources/collation,resources/locales}']" | xargs rm -rf
|
||||
RUN apk add --no-cache build-base && bundle install && apk del --no-cache build-base && rm -rf ~/.bundle /usr/local/bundle/cache && ruby -e "puts Dir['/usr/local/bundle/**/{spec,rdoc,resources/shared,resources/collation,resources/locales}']" | xargs rm -rf
|
||||
|
||||
COPY ./bin ./bin
|
||||
COPY ./app ./app
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
source 'https://rubygems.org'
|
||||
|
||||
ruby '3.3.3'
|
||||
ruby '3.3.4'
|
||||
|
||||
gem 'arabic-letter-connector', require: 'arabic-letter-connector/logic'
|
||||
gem 'aws-sdk-s3', require: false
|
||||
@@ -23,7 +23,6 @@ gem 'image_processing'
|
||||
gem 'jwt'
|
||||
gem 'lograge'
|
||||
gem 'mysql2', require: false
|
||||
gem 'net-smtp', '0.4.0'
|
||||
gem 'oj'
|
||||
gem 'pagy'
|
||||
gem 'pg', require: false
|
||||
@@ -40,7 +39,7 @@ gem 'ruby-vips'
|
||||
gem 'rubyXL'
|
||||
gem 'shakapacker'
|
||||
gem 'sidekiq'
|
||||
gem 'sqlite3', require: false, force_ruby_platform: true
|
||||
gem 'sqlite3', require: false
|
||||
gem 'strip_attributes'
|
||||
gem 'turbo-rails'
|
||||
gem 'twitter_cldr', require: false
|
||||
|
||||
+187
-183
@@ -1,105 +1,101 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
actioncable (7.1.3.4)
|
||||
actionpack (= 7.1.3.4)
|
||||
activesupport (= 7.1.3.4)
|
||||
actioncable (7.2.1.1)
|
||||
actionpack (= 7.2.1.1)
|
||||
activesupport (= 7.2.1.1)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
zeitwerk (~> 2.6)
|
||||
actionmailbox (7.1.3.4)
|
||||
actionpack (= 7.1.3.4)
|
||||
activejob (= 7.1.3.4)
|
||||
activerecord (= 7.1.3.4)
|
||||
activestorage (= 7.1.3.4)
|
||||
activesupport (= 7.1.3.4)
|
||||
mail (>= 2.7.1)
|
||||
net-imap
|
||||
net-pop
|
||||
net-smtp
|
||||
actionmailer (7.1.3.4)
|
||||
actionpack (= 7.1.3.4)
|
||||
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
|
||||
actionmailbox (7.2.1.1)
|
||||
actionpack (= 7.2.1.1)
|
||||
activejob (= 7.2.1.1)
|
||||
activerecord (= 7.2.1.1)
|
||||
activestorage (= 7.2.1.1)
|
||||
activesupport (= 7.2.1.1)
|
||||
mail (>= 2.8.0)
|
||||
actionmailer (7.2.1.1)
|
||||
actionpack (= 7.2.1.1)
|
||||
actionview (= 7.2.1.1)
|
||||
activejob (= 7.2.1.1)
|
||||
activesupport (= 7.2.1.1)
|
||||
mail (>= 2.8.0)
|
||||
rails-dom-testing (~> 2.2)
|
||||
actionpack (7.1.3.4)
|
||||
actionview (= 7.1.3.4)
|
||||
activesupport (= 7.1.3.4)
|
||||
actionpack (7.2.1.1)
|
||||
actionview (= 7.2.1.1)
|
||||
activesupport (= 7.2.1.1)
|
||||
nokogiri (>= 1.8.5)
|
||||
racc
|
||||
rack (>= 2.2.4)
|
||||
rack (>= 2.2.4, < 3.2)
|
||||
rack-session (>= 1.0.1)
|
||||
rack-test (>= 0.6.3)
|
||||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
actiontext (7.1.3.4)
|
||||
actionpack (= 7.1.3.4)
|
||||
activerecord (= 7.1.3.4)
|
||||
activestorage (= 7.1.3.4)
|
||||
activesupport (= 7.1.3.4)
|
||||
useragent (~> 0.16)
|
||||
actiontext (7.2.1.1)
|
||||
actionpack (= 7.2.1.1)
|
||||
activerecord (= 7.2.1.1)
|
||||
activestorage (= 7.2.1.1)
|
||||
activesupport (= 7.2.1.1)
|
||||
globalid (>= 0.6.0)
|
||||
nokogiri (>= 1.8.5)
|
||||
actionview (7.1.3.4)
|
||||
activesupport (= 7.1.3.4)
|
||||
actionview (7.2.1.1)
|
||||
activesupport (= 7.2.1.1)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.11)
|
||||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
activejob (7.1.3.4)
|
||||
activesupport (= 7.1.3.4)
|
||||
activejob (7.2.1.1)
|
||||
activesupport (= 7.2.1.1)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (7.1.3.4)
|
||||
activesupport (= 7.1.3.4)
|
||||
activerecord (7.1.3.4)
|
||||
activemodel (= 7.1.3.4)
|
||||
activesupport (= 7.1.3.4)
|
||||
activemodel (7.2.1.1)
|
||||
activesupport (= 7.2.1.1)
|
||||
activerecord (7.2.1.1)
|
||||
activemodel (= 7.2.1.1)
|
||||
activesupport (= 7.2.1.1)
|
||||
timeout (>= 0.4.0)
|
||||
activestorage (7.1.3.4)
|
||||
actionpack (= 7.1.3.4)
|
||||
activejob (= 7.1.3.4)
|
||||
activerecord (= 7.1.3.4)
|
||||
activesupport (= 7.1.3.4)
|
||||
activestorage (7.2.1.1)
|
||||
actionpack (= 7.2.1.1)
|
||||
activejob (= 7.2.1.1)
|
||||
activerecord (= 7.2.1.1)
|
||||
activesupport (= 7.2.1.1)
|
||||
marcel (~> 1.0)
|
||||
activesupport (7.1.3.4)
|
||||
activesupport (7.2.1.1)
|
||||
base64
|
||||
bigdecimal
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
concurrent-ruby (~> 1.0, >= 1.3.1)
|
||||
connection_pool (>= 2.2.5)
|
||||
drb
|
||||
i18n (>= 1.6, < 2)
|
||||
logger (>= 1.4.2)
|
||||
minitest (>= 5.1)
|
||||
mutex_m
|
||||
tzinfo (~> 2.0)
|
||||
addressable (2.8.6)
|
||||
public_suffix (>= 2.0.2, < 6.0)
|
||||
securerandom (>= 0.3)
|
||||
tzinfo (~> 2.0, >= 2.0.5)
|
||||
addressable (2.8.7)
|
||||
public_suffix (>= 2.0.2, < 7.0)
|
||||
annotate (3.2.0)
|
||||
activerecord (>= 3.2, < 8.0)
|
||||
rake (>= 10.4, < 14.0)
|
||||
arabic-letter-connector (0.1.1)
|
||||
ast (2.4.2)
|
||||
aws-eventstream (1.3.0)
|
||||
aws-partitions (1.913.0)
|
||||
aws-sdk-core (3.191.6)
|
||||
aws-partitions (1.985.0)
|
||||
aws-sdk-core (3.209.1)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.651.0)
|
||||
aws-sigv4 (~> 1.8)
|
||||
aws-sigv4 (~> 1.9)
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
aws-sdk-kms (1.79.0)
|
||||
aws-sdk-core (~> 3, >= 3.191.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-s3 (1.146.1)
|
||||
aws-sdk-core (~> 3, >= 3.191.0)
|
||||
aws-sdk-kms (1.94.0)
|
||||
aws-sdk-core (~> 3, >= 3.207.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.167.0)
|
||||
aws-sdk-core (~> 3, >= 3.207.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.8)
|
||||
aws-sdk-secretsmanager (1.91.0)
|
||||
aws-sdk-core (~> 3, >= 3.191.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sigv4 (1.8.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-secretsmanager (1.108.0)
|
||||
aws-sdk-core (~> 3, >= 3.207.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sigv4 (1.10.0)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
azure-storage-blob (2.0.3)
|
||||
azure-storage-common (~> 2.0)
|
||||
@@ -120,14 +116,14 @@ GEM
|
||||
smart_properties
|
||||
bigdecimal (3.1.8)
|
||||
bindex (0.8.1)
|
||||
bootsnap (1.18.3)
|
||||
bootsnap (1.18.4)
|
||||
msgpack (~> 1.2)
|
||||
builder (3.3.0)
|
||||
bullet (7.1.6)
|
||||
bullet (7.2.0)
|
||||
activesupport (>= 3.0.0)
|
||||
uniform_notifier (~> 1.11)
|
||||
camertron-eprun (1.1.1)
|
||||
cancancan (3.5.0)
|
||||
cancancan (3.6.1)
|
||||
capybara (3.40.0)
|
||||
addressable
|
||||
matrix
|
||||
@@ -137,7 +133,8 @@ GEM
|
||||
rack-test (>= 0.6.3)
|
||||
regexp_parser (>= 1.5, < 3.0)
|
||||
xpath (~> 3.2)
|
||||
childprocess (5.0.0)
|
||||
childprocess (5.1.0)
|
||||
logger (~> 1.5)
|
||||
chunky_png (1.4.0)
|
||||
cldr-plurals-runtime-rb (1.1.0)
|
||||
cmdparse (3.0.7)
|
||||
@@ -148,12 +145,12 @@ GEM
|
||||
bigdecimal
|
||||
rexml
|
||||
crass (1.0.6)
|
||||
css_parser (1.17.1)
|
||||
css_parser (1.19.0)
|
||||
addressable
|
||||
csv (3.3.0)
|
||||
cuprite (0.15)
|
||||
cuprite (0.15.1)
|
||||
capybara (~> 3.0)
|
||||
ferrum (~> 0.14.0)
|
||||
ferrum (~> 0.15.0)
|
||||
date (3.3.4)
|
||||
debug (1.9.2)
|
||||
irb (~> 1.10)
|
||||
@@ -165,7 +162,7 @@ GEM
|
||||
railties (>= 4.1.0)
|
||||
responders
|
||||
warden (~> 1.2.3)
|
||||
devise-two-factor (5.0.0)
|
||||
devise-two-factor (6.0.0)
|
||||
activesupport (~> 7.0)
|
||||
devise (~> 4.0)
|
||||
railties (~> 7.0)
|
||||
@@ -173,26 +170,26 @@ GEM
|
||||
diff-lcs (1.5.1)
|
||||
digest-crc (0.6.5)
|
||||
rake (>= 12.0.0, < 14.0.0)
|
||||
docile (1.4.0)
|
||||
dotenv (3.1.0)
|
||||
docile (1.4.1)
|
||||
dotenv (3.1.4)
|
||||
drb (2.2.1)
|
||||
email_typo (0.2.3)
|
||||
erb_lint (0.5.0)
|
||||
erb_lint (0.6.0)
|
||||
activesupport
|
||||
better_html (>= 2.0.1)
|
||||
parser (>= 2.7.1.4)
|
||||
rainbow
|
||||
rubocop
|
||||
rubocop (>= 1)
|
||||
smart_properties
|
||||
erubi (1.13.0)
|
||||
factory_bot (6.4.6)
|
||||
factory_bot (6.5.0)
|
||||
activesupport (>= 5.0.0)
|
||||
factory_bot_rails (6.4.3)
|
||||
factory_bot (~> 6.4)
|
||||
railties (>= 5.0.0)
|
||||
faker (3.3.1)
|
||||
faker (3.4.2)
|
||||
i18n (>= 1.8.11, < 2)
|
||||
faraday (1.10.3)
|
||||
faraday (1.10.4)
|
||||
faraday-em_http (~> 1.0)
|
||||
faraday-em_synchrony (~> 1.0)
|
||||
faraday-excon (~> 1.1)
|
||||
@@ -212,87 +209,90 @@ GEM
|
||||
faraday-httpclient (1.0.1)
|
||||
faraday-multipart (1.0.4)
|
||||
multipart-post (~> 2)
|
||||
faraday-net_http (1.0.1)
|
||||
faraday-net_http (1.0.2)
|
||||
faraday-net_http_persistent (1.2.0)
|
||||
faraday-patron (1.0.0)
|
||||
faraday-rack (1.0.0)
|
||||
faraday-retry (1.0.3)
|
||||
faraday_middleware (1.2.0)
|
||||
faraday_middleware (1.2.1)
|
||||
faraday (~> 1.0)
|
||||
ferrum (0.14)
|
||||
ferrum (0.15)
|
||||
addressable (~> 2.5)
|
||||
concurrent-ruby (~> 1.1)
|
||||
webrick (~> 1.7)
|
||||
websocket-driver (>= 0.6, < 0.8)
|
||||
ffi (1.16.3)
|
||||
websocket-driver (~> 0.7)
|
||||
ffi (1.17.0)
|
||||
ffi (1.17.0-aarch64-linux-gnu)
|
||||
ffi (1.17.0-x86_64-linux-gnu)
|
||||
geom2d (0.4.1)
|
||||
globalid (1.2.1)
|
||||
activesupport (>= 6.1)
|
||||
google-apis-core (0.14.1)
|
||||
google-apis-core (0.15.1)
|
||||
addressable (~> 2.5, >= 2.5.1)
|
||||
googleauth (~> 1.9)
|
||||
httpclient (>= 2.8.1, < 3.a)
|
||||
httpclient (>= 2.8.3, < 3.a)
|
||||
mini_mime (~> 1.0)
|
||||
mutex_m
|
||||
representable (~> 3.0)
|
||||
retriable (>= 2.0, < 4.a)
|
||||
rexml
|
||||
google-apis-iamcredentials_v1 (0.20.0)
|
||||
google-apis-core (>= 0.14.0, < 2.a)
|
||||
google-apis-storage_v1 (0.37.0)
|
||||
google-apis-core (>= 0.14.0, < 2.a)
|
||||
google-cloud-core (1.7.0)
|
||||
google-apis-iamcredentials_v1 (0.21.0)
|
||||
google-apis-core (>= 0.15.0, < 2.a)
|
||||
google-apis-storage_v1 (0.46.0)
|
||||
google-apis-core (>= 0.15.0, < 2.a)
|
||||
google-cloud-core (1.7.1)
|
||||
google-cloud-env (>= 1.0, < 3.a)
|
||||
google-cloud-errors (~> 1.0)
|
||||
google-cloud-env (2.1.1)
|
||||
google-cloud-env (2.2.1)
|
||||
faraday (>= 1.0, < 3.a)
|
||||
google-cloud-errors (1.4.0)
|
||||
google-cloud-storage (1.49.0)
|
||||
google-cloud-storage (1.52.0)
|
||||
addressable (~> 2.8)
|
||||
digest-crc (~> 0.4)
|
||||
google-apis-core (~> 0.13)
|
||||
google-apis-iamcredentials_v1 (~> 0.18)
|
||||
google-apis-storage_v1 (~> 0.33)
|
||||
google-apis-storage_v1 (~> 0.38)
|
||||
google-cloud-core (~> 1.6)
|
||||
googleauth (~> 1.9)
|
||||
mini_mime (~> 1.0)
|
||||
googleauth (1.11.0)
|
||||
googleauth (1.11.1)
|
||||
faraday (>= 1.0, < 3.a)
|
||||
google-cloud-env (~> 2.1)
|
||||
jwt (>= 1.4, < 3.0)
|
||||
multi_json (~> 1.11)
|
||||
os (>= 0.9, < 2.0)
|
||||
signet (>= 0.16, < 2.a)
|
||||
hashdiff (1.1.0)
|
||||
hexapdf (0.42.0)
|
||||
hashdiff (1.1.1)
|
||||
hexapdf (0.47.0)
|
||||
cmdparse (~> 3.0, >= 3.0.3)
|
||||
geom2d (~> 0.4, >= 0.4.1)
|
||||
openssl (>= 2.2.1)
|
||||
htmlentities (4.3.4)
|
||||
httpclient (2.8.3)
|
||||
i18n (1.14.5)
|
||||
i18n (1.14.6)
|
||||
concurrent-ruby (~> 1.0)
|
||||
image_processing (1.12.2)
|
||||
image_processing (1.13.0)
|
||||
mini_magick (>= 4.9.5, < 5)
|
||||
ruby-vips (>= 2.0.17, < 3)
|
||||
io-console (0.7.2)
|
||||
irb (1.13.1)
|
||||
irb (1.14.1)
|
||||
rdoc (>= 4.0.0)
|
||||
reline (>= 0.4.2)
|
||||
jmespath (1.6.2)
|
||||
json (2.7.2)
|
||||
jwt (2.8.1)
|
||||
jwt (2.9.3)
|
||||
base64
|
||||
language_server-protocol (3.17.0.3)
|
||||
launchy (3.0.0)
|
||||
launchy (3.0.1)
|
||||
addressable (~> 2.8)
|
||||
childprocess (~> 5.0)
|
||||
letter_opener (1.10.0)
|
||||
launchy (>= 2.2, < 4)
|
||||
letter_opener_web (2.0.0)
|
||||
actionmailer (>= 5.2)
|
||||
letter_opener (~> 1.7)
|
||||
railties (>= 5.2)
|
||||
letter_opener_web (3.0.0)
|
||||
actionmailer (>= 6.1)
|
||||
letter_opener (~> 1.9)
|
||||
railties (>= 6.1)
|
||||
rexml
|
||||
logger (1.6.1)
|
||||
lograge (0.14.0)
|
||||
actionpack (>= 4)
|
||||
activesupport (>= 4)
|
||||
@@ -309,26 +309,27 @@ GEM
|
||||
marcel (1.0.4)
|
||||
matrix (0.4.2)
|
||||
method_source (1.1.0)
|
||||
mini_magick (4.12.0)
|
||||
mini_magick (4.13.2)
|
||||
mini_mime (1.1.5)
|
||||
mini_portile2 (2.8.7)
|
||||
minitest (5.24.1)
|
||||
msgpack (1.7.2)
|
||||
minitest (5.25.1)
|
||||
msgpack (1.7.3)
|
||||
multi_json (1.15.0)
|
||||
multipart-post (2.4.0)
|
||||
multipart-post (2.4.1)
|
||||
mutex_m (0.2.0)
|
||||
mysql2 (0.5.6)
|
||||
net-http-persistent (4.0.2)
|
||||
net-http-persistent (4.0.4)
|
||||
connection_pool (~> 2.2)
|
||||
net-imap (0.4.14)
|
||||
net-imap (0.4.17)
|
||||
date
|
||||
net-protocol
|
||||
net-pop (0.1.2)
|
||||
net-protocol
|
||||
net-protocol (0.2.2)
|
||||
timeout
|
||||
net-smtp (0.4.0)
|
||||
net-smtp (0.5.0)
|
||||
net-protocol
|
||||
nio4r (2.7.1)
|
||||
nio4r (2.7.3)
|
||||
nokogiri (1.16.7)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
racc (~> 1.4)
|
||||
@@ -336,21 +337,23 @@ GEM
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.16.7-x86_64-linux)
|
||||
racc (~> 1.4)
|
||||
oj (3.16.3)
|
||||
oj (3.16.6)
|
||||
bigdecimal (>= 3.0)
|
||||
ostruct (>= 0.2)
|
||||
openssl (3.2.0)
|
||||
orm_adapter (0.5.0)
|
||||
os (1.1.4)
|
||||
ostruct (0.6.0)
|
||||
package_json (0.1.0)
|
||||
pagy (8.1.2)
|
||||
parallel (1.25.1)
|
||||
parser (3.3.4.0)
|
||||
pagy (9.1.0)
|
||||
parallel (1.26.3)
|
||||
parser (3.3.5.0)
|
||||
ast (~> 2.4.1)
|
||||
racc
|
||||
pg (1.5.6)
|
||||
premailer (1.23.0)
|
||||
pg (1.5.8)
|
||||
premailer (1.27.0)
|
||||
addressable
|
||||
css_parser (>= 1.12.0)
|
||||
css_parser (>= 1.19.0)
|
||||
htmlentities (>= 4.0.0)
|
||||
premailer-rails (1.12.0)
|
||||
actionmailer (>= 3)
|
||||
@@ -361,15 +364,15 @@ GEM
|
||||
pry (0.14.2)
|
||||
coderay (~> 1.1)
|
||||
method_source (~> 1.0)
|
||||
pry-rails (0.3.9)
|
||||
pry (>= 0.10.4)
|
||||
pry-rails (0.3.11)
|
||||
pry (>= 0.13.0)
|
||||
psych (5.1.2)
|
||||
stringio
|
||||
public_suffix (5.0.5)
|
||||
puma (6.4.2)
|
||||
public_suffix (6.0.1)
|
||||
puma (6.4.3)
|
||||
nio4r (~> 2.0)
|
||||
racc (1.8.1)
|
||||
rack (3.1.7)
|
||||
rack (3.1.8)
|
||||
rack-proxy (0.7.7)
|
||||
rack
|
||||
rack-session (2.0.0)
|
||||
@@ -379,20 +382,20 @@ GEM
|
||||
rackup (2.1.0)
|
||||
rack (>= 3)
|
||||
webrick (~> 1.8)
|
||||
rails (7.1.3.4)
|
||||
actioncable (= 7.1.3.4)
|
||||
actionmailbox (= 7.1.3.4)
|
||||
actionmailer (= 7.1.3.4)
|
||||
actionpack (= 7.1.3.4)
|
||||
actiontext (= 7.1.3.4)
|
||||
actionview (= 7.1.3.4)
|
||||
activejob (= 7.1.3.4)
|
||||
activemodel (= 7.1.3.4)
|
||||
activerecord (= 7.1.3.4)
|
||||
activestorage (= 7.1.3.4)
|
||||
activesupport (= 7.1.3.4)
|
||||
rails (7.2.1.1)
|
||||
actioncable (= 7.2.1.1)
|
||||
actionmailbox (= 7.2.1.1)
|
||||
actionmailer (= 7.2.1.1)
|
||||
actionpack (= 7.2.1.1)
|
||||
actiontext (= 7.2.1.1)
|
||||
actionview (= 7.2.1.1)
|
||||
activejob (= 7.2.1.1)
|
||||
activemodel (= 7.2.1.1)
|
||||
activerecord (= 7.2.1.1)
|
||||
activestorage (= 7.2.1.1)
|
||||
activesupport (= 7.2.1.1)
|
||||
bundler (>= 1.15.0)
|
||||
railties (= 7.1.3.4)
|
||||
railties (= 7.2.1.1)
|
||||
rails-dom-testing (2.2.0)
|
||||
activesupport (>= 5.0.0)
|
||||
minitest
|
||||
@@ -407,100 +410,101 @@ GEM
|
||||
actionview (> 3.1)
|
||||
activesupport (> 3.1)
|
||||
railties (> 3.1)
|
||||
railties (7.1.3.4)
|
||||
actionpack (= 7.1.3.4)
|
||||
activesupport (= 7.1.3.4)
|
||||
irb
|
||||
railties (7.2.1.1)
|
||||
actionpack (= 7.2.1.1)
|
||||
activesupport (= 7.2.1.1)
|
||||
irb (~> 1.13)
|
||||
rackup (>= 1.0.0)
|
||||
rake (>= 12.2)
|
||||
thor (~> 1.0, >= 1.2.2)
|
||||
zeitwerk (~> 2.6)
|
||||
rainbow (3.1.1)
|
||||
rake (13.2.1)
|
||||
rdoc (6.6.3.1)
|
||||
rdoc (6.7.0)
|
||||
psych (>= 4.0.0)
|
||||
redis-client (0.22.2)
|
||||
connection_pool
|
||||
regexp_parser (2.9.2)
|
||||
reline (0.5.7)
|
||||
reline (0.5.10)
|
||||
io-console (~> 0.5)
|
||||
representable (3.2.0)
|
||||
declarative (< 0.1.0)
|
||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||
uber (< 0.2.0)
|
||||
request_store (1.6.0)
|
||||
request_store (1.7.0)
|
||||
rack (>= 1.4)
|
||||
responders (3.1.1)
|
||||
actionpack (>= 5.2)
|
||||
railties (>= 5.2)
|
||||
retriable (3.1.2)
|
||||
rexml (3.3.6)
|
||||
strscan
|
||||
rexml (3.3.9)
|
||||
rotp (6.3.0)
|
||||
rqrcode (2.2.0)
|
||||
chunky_png (~> 1.0)
|
||||
rqrcode_core (~> 1.0)
|
||||
rqrcode_core (1.2.0)
|
||||
rspec-core (3.13.0)
|
||||
rspec-core (3.13.1)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-expectations (3.13.0)
|
||||
rspec-expectations (3.13.3)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-mocks (3.13.0)
|
||||
rspec-mocks (3.13.2)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.13.0)
|
||||
rspec-rails (6.1.2)
|
||||
actionpack (>= 6.1)
|
||||
activesupport (>= 6.1)
|
||||
railties (>= 6.1)
|
||||
rspec-rails (7.0.1)
|
||||
actionpack (>= 7.0)
|
||||
activesupport (>= 7.0)
|
||||
railties (>= 7.0)
|
||||
rspec-core (~> 3.13)
|
||||
rspec-expectations (~> 3.13)
|
||||
rspec-mocks (~> 3.13)
|
||||
rspec-support (~> 3.13)
|
||||
rspec-support (3.13.1)
|
||||
rubocop (1.65.0)
|
||||
rubocop (1.66.1)
|
||||
json (~> 2.3)
|
||||
language_server-protocol (>= 3.17.0)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 3.3.0.2)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 2.4, < 3.0)
|
||||
rexml (>= 3.2.5, < 4.0)
|
||||
rubocop-ast (>= 1.31.1, < 2.0)
|
||||
rubocop-ast (>= 1.32.2, < 2.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 2.4.0, < 3.0)
|
||||
rubocop-ast (1.31.3)
|
||||
rubocop-ast (1.32.3)
|
||||
parser (>= 3.3.1.0)
|
||||
rubocop-performance (1.21.0)
|
||||
rubocop-performance (1.22.1)
|
||||
rubocop (>= 1.48.1, < 2.0)
|
||||
rubocop-ast (>= 1.31.1, < 2.0)
|
||||
rubocop-rails (2.24.1)
|
||||
rubocop-rails (2.26.2)
|
||||
activesupport (>= 4.2.0)
|
||||
rack (>= 1.1)
|
||||
rubocop (>= 1.33.0, < 2.0)
|
||||
rubocop (>= 1.52.0, < 2.0)
|
||||
rubocop-ast (>= 1.31.1, < 2.0)
|
||||
rubocop-rspec (3.0.3)
|
||||
rubocop-rspec (3.1.0)
|
||||
rubocop (~> 1.61)
|
||||
ruby-progressbar (1.13.0)
|
||||
ruby-vips (2.2.1)
|
||||
ruby-vips (2.2.2)
|
||||
ffi (~> 1.12)
|
||||
logger
|
||||
ruby2_keywords (0.0.5)
|
||||
rubyXL (3.4.27)
|
||||
nokogiri (>= 1.10.8)
|
||||
rubyzip (>= 1.3.0)
|
||||
rubyzip (2.3.2)
|
||||
securerandom (0.3.1)
|
||||
semantic_range (3.0.0)
|
||||
shakapacker (8.0.0)
|
||||
shakapacker (8.0.2)
|
||||
activesupport (>= 5.2)
|
||||
package_json
|
||||
rack-proxy (>= 0.6.1)
|
||||
railties (>= 5.2)
|
||||
semantic_range (>= 2.3.0)
|
||||
sidekiq (7.2.4)
|
||||
sidekiq (7.3.2)
|
||||
concurrent-ruby (< 2)
|
||||
connection_pool (>= 2.3.0)
|
||||
logger
|
||||
rack (>= 2.2.4)
|
||||
redis-client (>= 0.19.0)
|
||||
redis-client (>= 0.22.2)
|
||||
signet (0.19.0)
|
||||
addressable (~> 2.8)
|
||||
faraday (>= 0.17.5, < 3.a)
|
||||
@@ -510,33 +514,34 @@ GEM
|
||||
docile (~> 1.1)
|
||||
simplecov-html (~> 0.11)
|
||||
simplecov_json_formatter (~> 0.1)
|
||||
simplecov-html (0.12.3)
|
||||
simplecov-html (0.13.1)
|
||||
simplecov_json_formatter (0.1.4)
|
||||
smart_properties (1.17.0)
|
||||
sqlite3 (1.7.3)
|
||||
sqlite3 (2.1.0)
|
||||
mini_portile2 (~> 2.8.0)
|
||||
stringio (3.1.0)
|
||||
sqlite3 (2.1.0-aarch64-linux-gnu)
|
||||
sqlite3 (2.1.0-x86_64-linux-gnu)
|
||||
stringio (3.1.1)
|
||||
strip_attributes (1.13.0)
|
||||
activemodel (>= 3.0, < 8.0)
|
||||
strscan (3.1.0)
|
||||
thor (1.3.1)
|
||||
thor (1.3.2)
|
||||
timeout (0.4.1)
|
||||
trailblazer-option (0.1.2)
|
||||
turbo-rails (2.0.5)
|
||||
turbo-rails (2.0.10)
|
||||
actionpack (>= 6.0.0)
|
||||
activejob (>= 6.0.0)
|
||||
railties (>= 6.0.0)
|
||||
twitter_cldr (6.12.0)
|
||||
twitter_cldr (6.12.1)
|
||||
camertron-eprun
|
||||
cldr-plurals-runtime-rb (~> 1.1)
|
||||
tzinfo
|
||||
tzinfo (2.0.6)
|
||||
concurrent-ruby (~> 1.0)
|
||||
tzinfo-data (1.2024.1)
|
||||
tzinfo-data (1.2024.2)
|
||||
tzinfo (>= 1.0.0)
|
||||
uber (0.1.0)
|
||||
unicode-display_width (2.5.0)
|
||||
unicode-display_width (2.6.0)
|
||||
uniform_notifier (1.16.0)
|
||||
useragent (0.16.10)
|
||||
warden (1.2.9)
|
||||
rack (>= 2.0.9)
|
||||
web-console (4.2.1)
|
||||
@@ -544,17 +549,17 @@ GEM
|
||||
activemodel (>= 6.0.0)
|
||||
bindex (>= 0.4.0)
|
||||
railties (>= 6.0.0)
|
||||
webmock (3.23.0)
|
||||
webmock (3.24.0)
|
||||
addressable (>= 2.8.0)
|
||||
crack (>= 0.3.2)
|
||||
hashdiff (>= 0.4.0, < 2.0.0)
|
||||
webrick (1.8.1)
|
||||
webrick (1.8.2)
|
||||
websocket-driver (0.7.6)
|
||||
websocket-extensions (>= 0.1.0)
|
||||
websocket-extensions (0.1.5)
|
||||
xpath (3.2.0)
|
||||
nokogiri (~> 1.8)
|
||||
zeitwerk (2.6.14)
|
||||
zeitwerk (2.7.0)
|
||||
|
||||
PLATFORMS
|
||||
aarch64-linux
|
||||
@@ -591,7 +596,6 @@ DEPENDENCIES
|
||||
letter_opener_web
|
||||
lograge
|
||||
mysql2
|
||||
net-smtp (= 0.4.0)
|
||||
oj
|
||||
pagy
|
||||
pg
|
||||
@@ -624,7 +628,7 @@ DEPENDENCIES
|
||||
webmock
|
||||
|
||||
RUBY VERSION
|
||||
ruby 3.3.3p89
|
||||
ruby 3.3.4p94
|
||||
|
||||
BUNDLED WITH
|
||||
2.5.3
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<h1 align="center" style="border-bottom: none">
|
||||
<div>
|
||||
<a href="https://www.docuseal.co">
|
||||
<a href="https://www.docuseal.com">
|
||||
<img alt="DocuSeal" src="https://github.com/docusealco/docuseal/assets/5418788/c12cd051-81cd-4402-bc3a-92f2cfdc1b06" width="80" />
|
||||
<br>
|
||||
</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.
|
||||
</p>
|
||||
<h2 align="center">
|
||||
<a href="https://demo.docuseal.co">✨ Live Demo</a>
|
||||
<a href="https://demo.docuseal.tech">✨ Live Demo</a>
|
||||
<span>|</span>
|
||||
<a href="https://docuseal.co/sign_up">☁️ Try in Cloud</a>
|
||||
<a href="https://docuseal.com/sign_up">☁️ Try in Cloud</a>
|
||||
</h2>
|
||||
|
||||
[](https://demo.docuseal.co)
|
||||
[](https://demo.docuseal.tech)
|
||||
|
||||
## Features
|
||||
- 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
|
||||
- Users management
|
||||
- Mobile-optimized
|
||||
- Signing available in 13 languages
|
||||
- 6 UI languages with signing available in 13 languages
|
||||
- API and Webhooks for integrations
|
||||
- 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
|
||||
- Bulk send with CSV, XLSX spreadsheet import
|
||||
- SSO / SAML
|
||||
- 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.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.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.co/docs/embedded))
|
||||
- [Learn more](https://www.docuseal.co/pricing)
|
||||
- Template creation with HTML API ([Guide](https://www.docuseal.com/guides/create-pdf-document-fillable-form-with-html-api))
|
||||
- Template creation with PDF or DOCX and field tags API ([Guide](https://www.docuseal.com/guides/use-embedded-text-field-tags-in-the-pdf-to-create-a-fillable-form))
|
||||
- Embedded signing form ([React](https://github.com/docusealco/docuseal-react), [Vue](https://github.com/docusealco/docuseal-vue), [Angular](https://github.com/docusealco/docuseal-angular) or [JavaScript](https://www.docuseal.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.com/docs/embedded))
|
||||
- [Learn more](https://www.docuseal.com/pricing)
|
||||
|
||||
## 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.
|
||||
|
||||
[Book a Meeting](https://www.docuseal.co/contact)
|
||||
[Book a Meeting](https://www.docuseal.com/contact)
|
||||
|
||||
## License
|
||||
|
||||
@@ -102,6 +102,6 @@ Unless otherwise noted, all files © 2023 DocuSeal LLC.
|
||||
|
||||
## Tools
|
||||
|
||||
- [Signature Maker](https://www.docuseal.co/online-signature)
|
||||
- [Sign Document Online](https://www.docuseal.co/sign-documents-online)
|
||||
- [Fill PDF Online](https://www.docuseal.co/fill-pdf)
|
||||
- [Signature Maker](https://www.docuseal.com/online-signature)
|
||||
- [Sign Document Online](https://www.docuseal.com/sign-documents-online)
|
||||
- [Fill PDF Online](https://www.docuseal.com/fill-pdf)
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
# Reporting a Vulnerability
|
||||
|
||||
If you come across any security concern or vulnarability, please report the information via email to security@docuseal.co instead of opening a GitHub issue. We will promptly respond and will collaborate with you to validate the issue, and resolve it ASAP.
|
||||
If you come across any security concern or vulnarability, please report the information via email to security@docuseal.com instead of opening a GitHub issue. We will promptly respond and will collaborate with you to validate the issue, and resolve it ASAP.
|
||||
|
||||
**We have a bug bounty program to reward security researchers.**
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ class AccountConfigsController < ApplicationController
|
||||
AccountConfig::ALLOW_TYPED_SIGNATURE,
|
||||
AccountConfig::FORCE_MFA,
|
||||
AccountConfig::ALLOW_TO_RESUBMIT,
|
||||
AccountConfig::ALLOW_TO_DECLINE_KEY,
|
||||
AccountConfig::FORM_PREFILL_SIGNATURE_KEY,
|
||||
AccountConfig::ESIGNING_PREFERENCE_KEY,
|
||||
AccountConfig::FORM_WITH_CONFETTI_KEY,
|
||||
|
||||
@@ -4,11 +4,11 @@ class AccountsController < ApplicationController
|
||||
LOCALE_OPTIONS = {
|
||||
'en-US' => 'English (United States)',
|
||||
'en-GB' => 'English (United Kingdom)',
|
||||
'fr-FR' => 'French (France)',
|
||||
'es-ES' => 'Spanish (Spain)',
|
||||
'pt-PT' => 'Portuguese (Portugal)',
|
||||
'de-DE' => 'German (Germany)',
|
||||
'it-IT' => 'Italian (Italy)'
|
||||
'fr-FR' => 'Français',
|
||||
'es-ES' => 'Español',
|
||||
'pt-PT' => 'Português',
|
||||
'de-DE' => 'Deutsch',
|
||||
'it-IT' => 'Italiano'
|
||||
}.freeze
|
||||
|
||||
before_action :load_account
|
||||
@@ -19,21 +19,25 @@ class AccountsController < ApplicationController
|
||||
def update
|
||||
current_account.update!(account_params)
|
||||
|
||||
@encrypted_config = EncryptedConfig.find_or_initialize_by(account: current_account,
|
||||
key: EncryptedConfig::APP_URL_KEY)
|
||||
@encrypted_config.assign_attributes(app_url_params)
|
||||
unless Docuseal.multitenant?
|
||||
@encrypted_config = EncryptedConfig.find_or_initialize_by(account: current_account,
|
||||
key: EncryptedConfig::APP_URL_KEY)
|
||||
@encrypted_config.assign_attributes(app_url_params)
|
||||
|
||||
unless URI.parse(@encrypted_config.value.to_s).class.in?([URI::HTTP, URI::HTTPS])
|
||||
@encrypted_config.errors.add(:value, 'should be a valid URL')
|
||||
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'))
|
||||
|
||||
return render :show, status: :unprocessable_entity
|
||||
return render :show, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
@encrypted_config.save!
|
||||
|
||||
Docuseal.refresh_default_url_options!
|
||||
end
|
||||
|
||||
@encrypted_config.save!
|
||||
|
||||
Docuseal.refresh_default_url_options!
|
||||
|
||||
redirect_to settings_account_path, notice: 'Account information has been updated'
|
||||
with_locale do
|
||||
redirect_to settings_account_path, notice: I18n.t('account_information_has_been_updated')
|
||||
end
|
||||
rescue ActiveRecord::RecordInvalid
|
||||
render :show, status: :unprocessable_entity
|
||||
end
|
||||
@@ -43,11 +47,12 @@ class AccountsController < ApplicationController
|
||||
|
||||
true_user.update!(locked_at: Time.current)
|
||||
|
||||
# rubocop:disable Layout/LineLength
|
||||
render turbo_stream: turbo_stream.replace(
|
||||
:account_delete_button,
|
||||
html: helpers.tag.p('Your account removal request will be processed within 2 weeks. ' \
|
||||
'Please contact us if you want to keep your account.')
|
||||
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'))
|
||||
)
|
||||
# rubocop:enable Layout/LineLength
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -38,12 +38,12 @@ module Api
|
||||
private
|
||||
|
||||
def authorization_check!(blob)
|
||||
is_authorized =
|
||||
blob.attachments.all? do |a|
|
||||
a.name.in?(%w[logo preview_images]) ||
|
||||
(current_user && a.record.account.id == current_user.account_id) ||
|
||||
!a.record.account.account_configs.find_or_initialize_by(key: AccountConfig::DOWNLOAD_LINKS_AUTH_KEY).value
|
||||
end
|
||||
attachment = blob.attachments.take
|
||||
|
||||
is_authorized = attachment.name.in?(%w[logo preview_images]) ||
|
||||
(current_user && attachment.record.account.id == current_user.account_id) ||
|
||||
!attachment.record.account.account_configs
|
||||
.find_or_initialize_by(key: AccountConfig::DOWNLOAD_LINKS_AUTH_KEY).value
|
||||
|
||||
return if is_authorized
|
||||
|
||||
|
||||
@@ -25,9 +25,9 @@ module Api
|
||||
render json: { error: 'Too many requests' }, status: :too_many_requests
|
||||
end
|
||||
|
||||
if Rails.env.production?
|
||||
unless Rails.env.development?
|
||||
rescue_from CanCan::AccessDenied do |e|
|
||||
render json: { error: e.message }, status: :forbidden
|
||||
render json: { error: access_denied_error_message(e) }, status: :forbidden
|
||||
end
|
||||
|
||||
rescue_from JSON::ParserError do |e|
|
||||
@@ -39,6 +39,33 @@ module Api
|
||||
|
||||
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)
|
||||
result = relation.order(field => :desc)
|
||||
.limit([params.fetch(:limit, DEFAULT_LIMIT).to_i, MAX_LIMIT].min)
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Api
|
||||
class SubmissionDocumentsController < ApiBaseController
|
||||
load_and_authorize_resource :submission
|
||||
|
||||
def index
|
||||
documents =
|
||||
if @submission.submitters.all?(&:completed_at?)
|
||||
last_submitter = @submission.submitters.max_by(&:completed_at)
|
||||
|
||||
if last_submitter.documents_attachments.blank?
|
||||
last_submitter.documents_attachments = Submissions::EnsureResultGenerated.call(submitter)
|
||||
end
|
||||
|
||||
last_submitter.documents_attachments
|
||||
else
|
||||
values_hash = Submissions::GeneratePreviewAttachments.build_values_hash(@submission)
|
||||
|
||||
if @submission.preview_documents.present? &&
|
||||
@submission.preview_documents.all? { |s| s.metadata['values_hash'] == values_hash }
|
||||
@submission.preview_documents
|
||||
else
|
||||
ApplicationRecord.no_touching do
|
||||
@submission.preview_documents.each(&:destroy)
|
||||
end
|
||||
|
||||
Submissions::GeneratePreviewAttachments.call(@submission, values_hash:)
|
||||
end
|
||||
end
|
||||
|
||||
ActiveRecord::Associations::Preloader.new(
|
||||
records: documents,
|
||||
associations: [:blob]
|
||||
).call
|
||||
|
||||
render json: {
|
||||
id: @submission.id,
|
||||
documents: documents.map do |attachment|
|
||||
{ name: attachment.filename.base, url: ActiveStorage::Blob.proxy_url(attachment.blob) }
|
||||
end
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -23,7 +23,10 @@ module Api
|
||||
audit_trail_attachment: :blob))
|
||||
|
||||
render json: {
|
||||
data: submissions.as_json(Submissions::SerializeForApi::SERIALIZE_PARAMS),
|
||||
data: submissions.map do |s|
|
||||
Submissions::SerializeForApi.call(s, s.submitters, params,
|
||||
with_events: false, with_documents: false, with_values: false)
|
||||
end,
|
||||
pagination: {
|
||||
count: submissions.size,
|
||||
next: submissions.last&.id,
|
||||
@@ -64,20 +67,24 @@ module Api
|
||||
|
||||
submissions = create_submissions(@template, params)
|
||||
|
||||
submissions.each do |submission|
|
||||
SendSubmissionCreatedWebhookRequestJob.perform_async({ 'submission_id' => submission.id })
|
||||
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
|
||||
|
||||
Submissions.send_signature_requests(submissions)
|
||||
|
||||
submissions.each do |submission|
|
||||
if submission.submitters.all?(&:completed_at?) && submission.submitters.last
|
||||
ProcessSubmitterCompletionJob.perform_async({ 'submitter_id' => submission.submitters.last.id })
|
||||
submission.submitters.each do |submitter|
|
||||
ProcessSubmitterCompletionJob.perform_async('submitter_id' => submitter.id) if submitter.completed_at?
|
||||
end
|
||||
end
|
||||
|
||||
render json: build_create_json(submissions)
|
||||
rescue Submitters::NormalizeValues::BaseError => e
|
||||
rescue Submitters::NormalizeValues::BaseError, Submissions::CreateFromSubmitters::BaseError,
|
||||
DownloadUtils::UnableToDownload => e
|
||||
Rollbar.warning(e) if defined?(Rollbar)
|
||||
|
||||
render json: { error: e.message }, status: :unprocessable_entity
|
||||
@@ -89,7 +96,10 @@ module Api
|
||||
else
|
||||
@submission.update!(archived_at: Time.current)
|
||||
|
||||
SendSubmissionArchivedWebhookRequestJob.perform_async('submission_id' => @submission.id)
|
||||
WebhookUrls.for_account_id(@submission.account_id, 'submission.archived').each do |webhook_url|
|
||||
SendSubmissionArchivedWebhookRequestJob.perform_async('submission_id' => @submission.id,
|
||||
'webhook_url_id' => webhook_url.id)
|
||||
end
|
||||
end
|
||||
|
||||
render json: @submission.as_json(only: %i[id archived_at])
|
||||
@@ -155,7 +165,7 @@ module Api
|
||||
:completed, :phone, :application_key, :external_id, :reply_to, :go_to_last,
|
||||
{ metadata: {}, values: {}, readonly_fields: [], message: %i[subject body],
|
||||
fields: [:name, :uuid, :default_value, :value, :title, :description,
|
||||
:readonly, :redacted, :validation_pattern, :invalid_message,
|
||||
:readonly, :validation_pattern, :invalid_message,
|
||||
{ default_value: [], value: [], preferences: {} }] }]]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -13,7 +13,10 @@ module Api
|
||||
|
||||
SubmissionEvents.create_with_tracking_data(submitter, 'view_form', request)
|
||||
|
||||
SendFormViewedWebhookRequestJob.perform_async({ 'submitter_id' => submitter.id })
|
||||
WebhookUrls.for_account_id(submitter.account_id, 'form.viewed').each do |webhook_url|
|
||||
SendFormViewedWebhookRequestJob.perform_async('submitter_id' => submitter.id,
|
||||
'webhook_url_id' => webhook_url.id)
|
||||
end
|
||||
|
||||
render json: {}
|
||||
end
|
||||
|
||||
@@ -68,7 +68,7 @@ module Api
|
||||
end
|
||||
|
||||
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]
|
||||
Submitters.send_signature_requests([@submitter])
|
||||
end
|
||||
@@ -77,7 +77,7 @@ module Api
|
||||
with_urls: true,
|
||||
with_events: false,
|
||||
params:)
|
||||
rescue Submitters::NormalizeValues::BaseError => e
|
||||
rescue Submitters::NormalizeValues::BaseError, DownloadUtils::UnableToDownload => e
|
||||
Rollbar.warning(e) if defined?(Rollbar)
|
||||
|
||||
render json: { error: e.message }, status: :unprocessable_entity
|
||||
@@ -91,7 +91,7 @@ module Api
|
||||
:completed, :phone, :application_key, :external_id, :go_to_last,
|
||||
{ metadata: {}, values: {}, readonly_fields: [], message: %i[subject body],
|
||||
fields: [[:name, :uuid, :default_value, :value,
|
||||
:readonly, :redacted, :validation_pattern, :invalid_message,
|
||||
:readonly, :validation_pattern, :invalid_message,
|
||||
{ default_value: [], value: [], preferences: {} }]] }
|
||||
)
|
||||
end
|
||||
@@ -120,6 +120,7 @@ module Api
|
||||
end&.dig('uuid')
|
||||
|
||||
submitter.email = Submissions.normalize_email(attrs[:email]) if attrs.key?(:email)
|
||||
submitter.name = attrs[:name] if attrs.key?(:name)
|
||||
|
||||
if attrs.key?(:phone)
|
||||
submitter.phone = attrs[:phone].to_s.gsub(/[^0-9+]/, '')
|
||||
|
||||
@@ -25,7 +25,10 @@ module Api
|
||||
|
||||
schema_documents = Templates::CloneAttachments.call(template: cloned_template, original_template: @template)
|
||||
|
||||
SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => cloned_template.id)
|
||||
WebhookUrls.for_account_id(cloned_template.account_id, 'template.created').each do |webhook_url|
|
||||
SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => cloned_template.id,
|
||||
'webhook_url_id' => webhook_url.id)
|
||||
end
|
||||
|
||||
render json: Templates::SerializeForApi.call(cloned_template, schema_documents)
|
||||
end
|
||||
|
||||
@@ -18,7 +18,7 @@ module Api
|
||||
|
||||
preview_image_attachments =
|
||||
ActiveStorage::Attachment.joins(:blob)
|
||||
.where(blob: { filename: '0.jpg' })
|
||||
.where(blob: { filename: ['0.png', '0.jpg'] })
|
||||
.where(record_id: schema_documents.map(&:id),
|
||||
record_type: 'ActiveStorage::Attachment',
|
||||
name: :preview_images)
|
||||
@@ -65,7 +65,10 @@ module Api
|
||||
|
||||
@template.update!(template_params)
|
||||
|
||||
SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => @template.id)
|
||||
WebhookUrls.for_account_id(@template.account_id, 'template.updated').each do |webhook_url|
|
||||
SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => @template.id,
|
||||
'webhook_url_id' => webhook_url.id)
|
||||
end
|
||||
|
||||
render json: @template.as_json(only: %i[id updated_at])
|
||||
end
|
||||
@@ -97,7 +100,7 @@ module Api
|
||||
:name,
|
||||
:external_id,
|
||||
{
|
||||
submitters: [%i[name uuid]],
|
||||
submitters: [%i[name uuid is_requester invite_by_uuid linked_to_uuid email]],
|
||||
fields: [[:uuid, :submitter_uuid, :name, :type,
|
||||
:required, :readonly, :default_value,
|
||||
:title, :description,
|
||||
|
||||
@@ -20,10 +20,7 @@ module Api
|
||||
pdf = HexaPDF::Document.new(io: StringIO.new(file))
|
||||
|
||||
trusted_certs = Accounts.load_trusted_certs(current_account)
|
||||
|
||||
is_checksum_found = ActiveStorage::Attachment.joins(:blob)
|
||||
.where(name: 'documents', record_type: 'Submitter')
|
||||
.exists?(blob: { checksum: Digest::MD5.base64digest(file) })
|
||||
is_checksum_found = CompletedDocument.exists?(sha256: Base64.urlsafe_encode64(Digest::SHA256.digest(file)))
|
||||
|
||||
render json: {
|
||||
checksum_status: is_checksum_found ? 'verified' : 'not_found',
|
||||
|
||||
@@ -12,6 +12,6 @@ class ApiSettingsController < ApplicationController
|
||||
|
||||
current_user.access_token.save!
|
||||
|
||||
redirect_back(fallback_location: settings_api_index_path, notice: 'API token as been updated.')
|
||||
redirect_back(fallback_location: settings_api_index_path, notice: I18n.t('api_token_has_been_updated'))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -8,6 +8,7 @@ class ApplicationController < ActionController::Base
|
||||
|
||||
check_authorization unless: :devise_controller?
|
||||
|
||||
around_action :with_locale
|
||||
before_action :sign_in_for_demo, if: -> { Docuseal.demo? }
|
||||
before_action :maybe_redirect_to_setup, unless: :signed_in?
|
||||
before_action :authenticate_user!, unless: :devise_controller?
|
||||
@@ -37,6 +38,10 @@ class ApplicationController < ActionController::Base
|
||||
end
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
@@ -51,7 +56,18 @@ class ApplicationController < ActionController::Base
|
||||
|
||||
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(&)
|
||||
return yield if I18n.locale != :'en-US' && I18n.locale != :en
|
||||
|
||||
locale = params[:lang].presence
|
||||
locale ||= request.env['HTTP_ACCEPT_LANGUAGE'].to_s[BROWSER_LOCALE_REGEXP].to_s
|
||||
|
||||
@@ -79,7 +95,8 @@ class ApplicationController < ActionController::Base
|
||||
redirect_to setup_index_path unless User.exists?
|
||||
end
|
||||
|
||||
def button_title(title: 'Submit', disabled_with: 'Submitting', title_class: '', icon: nil, icon_disabled: nil)
|
||||
def button_title(title: I18n.t('submit'), disabled_with: I18n.t('submitting'), title_class: '', icon: nil,
|
||||
icon_disabled: nil)
|
||||
render_to_string(partial: 'shared/button_title',
|
||||
locals: { title:, disabled_with:, title_class:, icon:, icon_disabled: })
|
||||
end
|
||||
@@ -87,4 +104,10 @@ class ApplicationController < ActionController::Base
|
||||
def svg_icon(icon_name, class: '')
|
||||
render_to_string(partial: "icons/#{icon_name}", locals: { class: })
|
||||
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
|
||||
|
||||
@@ -33,7 +33,7 @@ class DashboardController < ApplicationController
|
||||
account_id: current_user.account_id,
|
||||
key: AccountConfig::FORCE_MFA)
|
||||
|
||||
redirect_to mfa_setup_path, notice: 'Setup 2FA to continue'
|
||||
redirect_to mfa_setup_path, notice: I18n.t('setup_2fa_to_continue')
|
||||
end
|
||||
|
||||
def maybe_render_landing
|
||||
|
||||
@@ -9,9 +9,9 @@ class EmailSmtpSettingsController < ApplicationController
|
||||
|
||||
def create
|
||||
if @encrypted_config.update(email_configs)
|
||||
SettingsMailer.smtp_successful_setup(@encrypted_config.value['from_email']).deliver_now!
|
||||
SettingsMailer.smtp_successful_setup(@encrypted_config.value['from_email'] || current_user.email).deliver_now!
|
||||
|
||||
redirect_to settings_email_index_path, notice: 'Changes have been saved'
|
||||
redirect_to settings_email_index_path, notice: I18n.t('changes_have_been_saved')
|
||||
else
|
||||
render :index, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
class ErrorsController < ActionController::Base
|
||||
ENTERPRISE_FEATURE_MESSAGE =
|
||||
'This feature is available in Pro Edition: https://www.docuseal.co/pricing'
|
||||
'This feature is available in Pro Edition: https://www.docuseal.com/pricing'
|
||||
|
||||
ENTERPRISE_PATHS = [
|
||||
'/templates/html',
|
||||
|
||||
@@ -21,9 +21,9 @@ class EsignSettingsController < ApplicationController
|
||||
default_pkcs = GenerateCertificate.load_pkcs(cert_data) if cert_data['cert'].present?
|
||||
|
||||
custom_pkcs_list = (cert_data['custom'] || []).map do |e|
|
||||
{ 'pkcs' => OpenSSL::PKCS12.new(Base64.urlsafe_decode64(e['data']), e['password'].to_s),
|
||||
'name' => e['name'],
|
||||
'status' => e['status'] }
|
||||
pkcs = e['data'].present? ? OpenSSL::PKCS12.new(Base64.urlsafe_decode64(e['data']), e['password'].to_s) : nil
|
||||
|
||||
{ 'pkcs' => pkcs, 'name' => e['name'], 'status' => e['status'] }
|
||||
end
|
||||
|
||||
@pkcs_list = [
|
||||
@@ -48,7 +48,7 @@ class EsignSettingsController < ApplicationController
|
||||
if (@encrypted_config.value && @encrypted_config.value['custom']&.any? { |e| e['name'] == @cert_record.name }) ||
|
||||
@cert_record.name == DEFAULT_CERT_NAME
|
||||
|
||||
@cert_record.errors.add(:name, 'already exists')
|
||||
@cert_record.errors.add(:name, I18n.t('already_exists'))
|
||||
|
||||
return render turbo_stream: turbo_stream.replace(:modal, template: 'esign_settings/new'),
|
||||
status: :unprocessable_entity
|
||||
@@ -56,7 +56,7 @@ class EsignSettingsController < ApplicationController
|
||||
|
||||
save_new_cert!(@encrypted_config, @cert_record)
|
||||
|
||||
redirect_to settings_esign_path, notice: 'Certificate has been successfully added!'
|
||||
redirect_to settings_esign_path, notice: I18n.t('certificate_has_been_successfully_added')
|
||||
rescue OpenSSL::PKCS12::PKCS12Error => e
|
||||
Rollbar.error(e) if defined?(Rollbar)
|
||||
|
||||
@@ -66,14 +66,20 @@ class EsignSettingsController < ApplicationController
|
||||
end
|
||||
|
||||
def update
|
||||
@encrypted_config.value['custom'].each { |e| e['status'] = 'validate' }
|
||||
@encrypted_config.value['custom'].to_a.each { |e| e['status'] = 'validate' }
|
||||
|
||||
custom_cert_data = @encrypted_config.value['custom'].find { |e| e['name'] == params[:name] }
|
||||
custom_cert_data['status'] = 'default' if custom_cert_data
|
||||
custom_cert_data = @encrypted_config.value['custom'].to_a.find { |e| e['name'] == params[:name] }
|
||||
|
||||
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!
|
||||
|
||||
redirect_to settings_esign_path, notice: 'Default certificate has been selected'
|
||||
redirect_to settings_esign_path, notice: I18n.t('default_certificate_has_been_selected')
|
||||
end
|
||||
|
||||
def destroy
|
||||
@@ -81,7 +87,7 @@ class EsignSettingsController < ApplicationController
|
||||
|
||||
@encrypted_config.save!
|
||||
|
||||
redirect_to settings_esign_path, notice: 'Certificate has been removed'
|
||||
redirect_to settings_esign_path, notice: I18n.t('certificate_has_been_removed')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -18,11 +18,11 @@ class MfaSetupController < ApplicationController
|
||||
current_user.otp_required_for_login = true
|
||||
current_user.save!
|
||||
|
||||
redirect_to settings_profile_index_path, notice: '2FA has been configured'
|
||||
redirect_to settings_profile_index_path, notice: I18n.t('2fa_has_been_configured')
|
||||
else
|
||||
@provision_url = current_user.otp_provisioning_uri(current_user.email, issuer: Docuseal.product_name)
|
||||
|
||||
@error_message = 'Code is invalid'
|
||||
@error_message = I18n.t('code_is_invalid')
|
||||
|
||||
render turbo_stream: turbo_stream.replace(:mfa_form, partial: 'mfa_setup/form'), status: :unprocessable_entity
|
||||
end
|
||||
@@ -32,9 +32,9 @@ class MfaSetupController < ApplicationController
|
||||
if current_user.validate_and_consume_otp!(params[:otp_attempt])
|
||||
current_user.update!(otp_required_for_login: false, otp_secret: nil)
|
||||
|
||||
redirect_to settings_profile_index_path, notice: '2FA has been removed'
|
||||
redirect_to settings_profile_index_path, notice: I18n.t('2fa_has_been_removed')
|
||||
else
|
||||
@error_message = 'Code is invalid'
|
||||
@error_message = I18n.t('code_is_invalid')
|
||||
|
||||
render turbo_stream: turbo_stream.replace(:modal, template: 'mfa_setup/edit'), status: :unprocessable_entity
|
||||
end
|
||||
@@ -43,7 +43,7 @@ class MfaSetupController < ApplicationController
|
||||
private
|
||||
|
||||
def set_provision_url
|
||||
return redirect_to root_path, alert: '2FA has been set up already' if current_user.otp_required_for_login
|
||||
return redirect_to root_path, alert: I18n.t('2fa_has_been_set_up_already') if current_user.otp_required_for_login
|
||||
|
||||
current_user.otp_secret ||= User.generate_otp_secret
|
||||
|
||||
|
||||
@@ -13,9 +13,9 @@ class NotificationsSettingsController < ApplicationController
|
||||
|
||||
def create
|
||||
if @account_config.value.present? ? @account_config.save : @account_config.delete
|
||||
redirect_back fallback_location: settings_notifications_path, notice: 'Changes have been saved'
|
||||
redirect_back fallback_location: settings_notifications_path, notice: I18n.t('changes_have_been_saved')
|
||||
else
|
||||
redirect_back fallback_location: settings_notifications_path, alert: 'Unable to save'
|
||||
redirect_back fallback_location: settings_notifications_path, alert: I18n.t('unable_to_save')
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ class PersonalizationSettingsController < ApplicationController
|
||||
@account_config.save!
|
||||
end
|
||||
|
||||
redirect_back(fallback_location: settings_personalization_path, notice: 'Settings have been saved.')
|
||||
redirect_back(fallback_location: settings_personalization_path, notice: I18n.t('settings_have_been_saved'))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -12,7 +12,8 @@ class PreviewDocumentPageController < ActionController::API
|
||||
|
||||
return head :not_found unless attachment
|
||||
|
||||
preview_image = attachment.preview_images.joins(:blob).find_by(blob: { filename: "#{params[:id]}#{FORMAT}" })
|
||||
preview_image = attachment.preview_images.joins(:blob)
|
||||
.find_by(blob: { filename: ["#{params[:id]}.png", "#{params[:id]}.jpg"] })
|
||||
|
||||
return redirect_to preview_image.url, allow_other_host: true if preview_image
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ class ProfileController < ApplicationController
|
||||
|
||||
def update_contact
|
||||
if current_user.update(contact_params)
|
||||
redirect_to settings_profile_index_path, notice: 'Contact information has been updated'
|
||||
redirect_to settings_profile_index_path, notice: I18n.t('contact_information_has_been_update')
|
||||
else
|
||||
render :index, status: :unprocessable_entity
|
||||
end
|
||||
@@ -18,7 +18,7 @@ class ProfileController < ApplicationController
|
||||
def update_password
|
||||
if current_user.update(password_params)
|
||||
bypass_sign_in(current_user)
|
||||
redirect_to settings_profile_index_path, notice: 'Password has been changed'
|
||||
redirect_to settings_profile_index_path, notice: I18n.t('password_has_been_changed')
|
||||
else
|
||||
render :index, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
@@ -7,6 +7,8 @@ class SendSubmissionEmailController < ApplicationController
|
||||
skip_before_action :verify_authenticity_token
|
||||
skip_authorization_check
|
||||
|
||||
SEND_DURATION = 30.minutes
|
||||
|
||||
def success; end
|
||||
|
||||
def create
|
||||
@@ -23,7 +25,10 @@ class SendSubmissionEmailController < ApplicationController
|
||||
|
||||
RateLimit.call("send-email-#{@submitter.id}", limit: 2, ttl: 5.minutes)
|
||||
|
||||
SubmitterMailer.documents_copy_email(@submitter, sig: true).deliver_later!
|
||||
unless EmailEvent.exists?(tag: :submitter_documents_copy, email: @submitter.email, emailable: @submitter,
|
||||
event_type: :send, created_at: SEND_DURATION.ago..Time.current)
|
||||
SubmitterMailer.documents_copy_email(@submitter, sig: true).deliver_later!
|
||||
end
|
||||
|
||||
respond_to do |f|
|
||||
f.html { redirect_to success_send_submission_email_index_path }
|
||||
|
||||
@@ -12,7 +12,7 @@ class SessionsController < Devise::SessionsController
|
||||
Rollbar.warning('Sign in new user') if defined?(Rollbar)
|
||||
|
||||
return redirect_to new_registration_path(sign_up: true, user: sign_in_params.slice(:email)),
|
||||
notice: 'Create a new account'
|
||||
notice: I18n.t('create_a_new_account')
|
||||
end
|
||||
|
||||
if User.exists?(email:, otp_required_for_login: true) && sign_in_params[:otp_attempt].blank?
|
||||
|
||||
@@ -21,7 +21,7 @@ class SetupController < ApplicationController
|
||||
@encrypted_config = EncryptedConfig.new(encrypted_config_params)
|
||||
|
||||
unless URI.parse(encrypted_config_params[:value].to_s).class.in?([URI::HTTP, URI::HTTPS])
|
||||
@encrypted_config.errors.add(:value, 'should be a valid URL')
|
||||
@encrypted_config.errors.add(:value, I18n.t('should_be_a_valid_url'))
|
||||
|
||||
return render :index, status: :unprocessable_entity
|
||||
end
|
||||
@@ -56,7 +56,7 @@ class SetupController < ApplicationController
|
||||
def account_params
|
||||
return {} unless params[:account]
|
||||
|
||||
params.require(:account).permit(:name, :timezone)
|
||||
params.require(:account).permit(:name, :timezone, :locale)
|
||||
end
|
||||
|
||||
def encrypted_config_params
|
||||
@@ -66,10 +66,10 @@ class SetupController < ApplicationController
|
||||
end
|
||||
|
||||
def redirect_to_root_if_signed
|
||||
redirect_to root_path, notice: 'You are already signed in'
|
||||
redirect_to root_path, notice: I18n.t('you_are_already_signed_in')
|
||||
end
|
||||
|
||||
def ensure_first_user_not_created!
|
||||
redirect_to new_user_session_path, notice: 'Please sign in.' if User.exists?
|
||||
redirect_to new_user_session_path, notice: I18n.t('please_sign_in') if User.exists?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -7,11 +7,13 @@ class StartFormController < ApplicationController
|
||||
skip_authorization_check
|
||||
|
||||
around_action :with_browser_locale, only: %i[show completed]
|
||||
before_action :maybe_redirect_com, only: %i[show completed]
|
||||
before_action :load_template
|
||||
|
||||
def show
|
||||
@submitter = @template.submissions.new(account_id: @template.account_id)
|
||||
.submitters.new(uuid: @template.submitters.first['uuid'])
|
||||
.submitters.new(uuid: (filter_undefined_submitters(@template).first ||
|
||||
@template.submitters.first)['uuid'])
|
||||
end
|
||||
|
||||
def update
|
||||
@@ -22,19 +24,24 @@ class StartFormController < ApplicationController
|
||||
if @submitter.completed_at?
|
||||
redirect_to start_form_completed_path(@template.slug, email: submitter_params[:email])
|
||||
else
|
||||
if @template.submitters.to_a.size > 1 && @submitter.new_record?
|
||||
@error_message = 'Not found'
|
||||
if filter_undefined_submitters(@template).size > 1 && @submitter.new_record?
|
||||
@error_message = I18n.t('not_found')
|
||||
|
||||
return render :show
|
||||
end
|
||||
|
||||
assign_submission_attributes(@submitter, @template) if @submitter.new_record?
|
||||
if (is_new_record = @submitter.new_record?)
|
||||
assign_submission_attributes(@submitter, @template)
|
||||
|
||||
is_new_record = @submitter.new_record?
|
||||
Submissions::AssignDefinedSubmitters.call(@submitter.submission)
|
||||
end
|
||||
|
||||
if @submitter.save
|
||||
if is_new_record
|
||||
SendSubmissionCreatedWebhookRequestJob.perform_async({ 'submission_id' => @submitter.submission.id })
|
||||
WebhookUrls.for_account_id(@submitter.account_id, 'submission.created').each do |webhook_url|
|
||||
SendSubmissionCreatedWebhookRequestJob.perform_async('submission_id' => @submitter.submission_id,
|
||||
'webhook_url_id' => webhook_url.id)
|
||||
end
|
||||
end
|
||||
|
||||
redirect_to submit_form_path(@submitter.slug)
|
||||
@@ -63,12 +70,10 @@ class StartFormController < ApplicationController
|
||||
|
||||
def assign_submission_attributes(submitter, template)
|
||||
resubmit_submitter =
|
||||
if params[:resubmit].present?
|
||||
Submitter.where(submission: @template.submissions).find_by(slug: params[:resubmit])
|
||||
end
|
||||
(Submitter.where(submission: template.submissions).find_by(slug: params[:resubmit]) if params[:resubmit].present?)
|
||||
|
||||
submitter.assign_attributes(
|
||||
uuid: template.submitters.first['uuid'],
|
||||
uuid: (filter_undefined_submitters(template).first || @template.submitters.first)['uuid'],
|
||||
ip: request.remote_ip,
|
||||
ua: request.user_agent,
|
||||
values: resubmit_submitter&.preferences&.fetch('default_values', nil) || {},
|
||||
@@ -85,6 +90,7 @@ class StartFormController < ApplicationController
|
||||
submitter.submission ||= Submission.new(template:,
|
||||
account_id: template.account_id,
|
||||
template_submitters: template.submitters,
|
||||
submitters: [submitter],
|
||||
source: :link)
|
||||
|
||||
submitter.account_id = submitter.submission.account_id
|
||||
@@ -92,6 +98,10 @@ class StartFormController < ApplicationController
|
||||
submitter
|
||||
end
|
||||
|
||||
def filter_undefined_submitters(template)
|
||||
Templates.filter_undefined_submitters(template)
|
||||
end
|
||||
|
||||
def submitter_params
|
||||
params.require(:submitter).permit(:email, :phone, :name).tap do |attrs|
|
||||
attrs[:email] = Submissions.normalize_email(attrs[:email])
|
||||
|
||||
@@ -11,7 +11,7 @@ class StorageSettingsController < ApplicationController
|
||||
if @encrypted_config.update(storage_configs)
|
||||
LoadActiveStorageConfigs.reload
|
||||
|
||||
redirect_to settings_storage_index_path, notice: 'Changes have been saved'
|
||||
redirect_to settings_storage_index_path, notice: I18n.t('changes_have_been_saved')
|
||||
else
|
||||
render :index, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
@@ -9,6 +9,13 @@ class SubmissionsController < ApplicationController
|
||||
def show
|
||||
@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'
|
||||
end
|
||||
|
||||
@@ -43,13 +50,11 @@ class SubmissionsController < ApplicationController
|
||||
params: params.merge('send_completed_email' => true))
|
||||
end
|
||||
|
||||
submissions.each do |submission|
|
||||
SendSubmissionCreatedWebhookRequestJob.perform_async({ 'submission_id' => submission.id })
|
||||
end
|
||||
enqueue_submission_created_webhooks(@template, submissions)
|
||||
|
||||
Submissions.send_signature_requests(submissions)
|
||||
|
||||
redirect_to template_path(@template), notice: 'New recipients have been added'
|
||||
redirect_to template_path(@template), notice: I18n.t('new_recipients_have_been_added')
|
||||
end
|
||||
|
||||
def destroy
|
||||
@@ -57,13 +62,16 @@ class SubmissionsController < ApplicationController
|
||||
if params[:permanently].present?
|
||||
@submission.destroy!
|
||||
|
||||
'Submission has been removed'
|
||||
I18n.t('submission_has_been_removed')
|
||||
else
|
||||
@submission.update!(archived_at: Time.current)
|
||||
|
||||
SendSubmissionArchivedWebhookRequestJob.perform_async('submission_id' => @submission.id)
|
||||
WebhookUrls.for_account_id(@submission.account_id, 'submission.archived').each do |webhook_url|
|
||||
SendSubmissionArchivedWebhookRequestJob.perform_async('submission_id' => @submission.id,
|
||||
'webhook_url_id' => webhook_url.id)
|
||||
end
|
||||
|
||||
'Submission has been archived'
|
||||
I18n.t('submission_has_been_archived')
|
||||
end
|
||||
|
||||
redirect_back(fallback_location: template_path(@submission.template), notice:)
|
||||
@@ -78,6 +86,15 @@ class SubmissionsController < ApplicationController
|
||||
template.save!
|
||||
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
|
||||
params.permit(submission: { submitters: [%i[uuid email phone name]] })
|
||||
end
|
||||
|
||||
@@ -15,6 +15,6 @@ class SubmissionsDashboardController < ApplicationController
|
||||
@submissions = @submissions.pending if params[:status] == 'pending'
|
||||
@submissions = @submissions.completed if params[:status] == 'completed'
|
||||
|
||||
@pagy, @submissions = pagy(@submissions.preload(:submitters).order(id: :desc))
|
||||
@pagy, @submissions = pagy(@submissions.preload(submitters: :start_form_submission_events).order(id: :desc))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4,6 +4,8 @@ class SubmissionsPreviewController < ApplicationController
|
||||
skip_before_action :authenticate_user!
|
||||
skip_authorization_check
|
||||
|
||||
before_action :maybe_redirect_com, only: %i[show completed]
|
||||
|
||||
TTL = 40.minutes
|
||||
|
||||
def show
|
||||
@@ -19,7 +21,7 @@ class SubmissionsPreviewController < ApplicationController
|
||||
@submission ||= Submission.find_by!(slug: params[:slug])
|
||||
|
||||
if !@submission.submitters.all?(&:completed_at?) && current_user.blank?
|
||||
raise ActionController::RoutingError, 'Not Found'
|
||||
raise ActionController::RoutingError, I18n.t('not_found')
|
||||
end
|
||||
|
||||
if !submission_valid_ttl?(@submission) && !signature_valid
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SubmissionsUnarchiveController < ApplicationController
|
||||
load_and_authorize_resource :submission
|
||||
|
||||
def create
|
||||
@submission.update!(archived_at: nil)
|
||||
|
||||
redirect_to submission_path(@submission), notice: I18n.t('submission_has_been_unarchived')
|
||||
end
|
||||
end
|
||||
@@ -7,21 +7,27 @@ class SubmitFormController < ApplicationController
|
||||
skip_before_action :authenticate_user!
|
||||
skip_authorization_check
|
||||
|
||||
before_action :maybe_redirect_com, only: %i[show completed]
|
||||
|
||||
CONFIG_KEYS = [].freeze
|
||||
|
||||
def show
|
||||
@submitter = Submitter.find_by!(slug: params[:slug])
|
||||
|
||||
submission = @submitter.submission
|
||||
|
||||
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
|
||||
return render :archived if @submitter.submission.template.archived_at? || @submitter.submission.archived_at?
|
||||
return render :expired if @submitter.submission.expired?
|
||||
return render :archived if submission.template.archived_at? || submission.archived_at?
|
||||
return render :expired if submission.expired?
|
||||
return render :declined if @submitter.declined_at?
|
||||
return render :awaiting if submission.template.preferences['submitters_order'] == 'preserved' &&
|
||||
!Submitters.current_submitter_order?(@submitter)
|
||||
|
||||
Submitters.preload_with_pages(@submitter)
|
||||
|
||||
Submitters::MaybeUpdateDefaultValues.call(@submitter, current_user)
|
||||
|
||||
@attachments_index = ActiveStorage::Attachment.where(record: @submitter.submission.submitters, name: :attachments)
|
||||
@attachments_index = ActiveStorage::Attachment.where(record: submission.submitters, name: :attachments)
|
||||
.preload(:blob).index_by(&:uuid)
|
||||
|
||||
@form_configs = Submitters::FormConfigs.call(@submitter, CONFIG_KEYS)
|
||||
@@ -46,18 +52,21 @@ class SubmitFormController < ApplicationController
|
||||
submitter = Submitter.find_by!(slug: params[:slug])
|
||||
|
||||
if submitter.completed_at?
|
||||
return render json: { error: 'Form has been completed already.' }, status: :unprocessable_entity
|
||||
return render json: { error: I18n.t('form_has_been_completed_already') }, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
if submitter.template.archived_at? || submitter.submission.archived_at?
|
||||
return render json: { error: 'Form has been archived.' }, status: :unprocessable_entity
|
||||
return render json: { error: I18n.t('form_has_been_archived') }, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
if submitter.submission.expired?
|
||||
return render json: { error: 'Form has been expired.' }, status: :unprocessable_entity
|
||||
return render json: { error: I18n.t('form_has_been_expired') }, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
return render json: { error: 'Form has been declined.' }, status: :unprocessable_entity if submitter.declined_at?
|
||||
if submitter.declined_at?
|
||||
return render json: { error: I18n.t('form_has_been_declined') },
|
||||
status: :unprocessable_entity
|
||||
end
|
||||
|
||||
Submitters::SubmitValues.call(submitter, params, request)
|
||||
|
||||
|
||||
@@ -21,8 +21,14 @@ class SubmitFormDeclineController < ApplicationController
|
||||
|
||||
user = submitter.submission.created_by_user || submitter.template.author
|
||||
|
||||
SubmitterMailer.declined_email(submitter, user).deliver_later!
|
||||
SendFormDeclinedWebhookRequestJob.perform_async('submitter_id' => submitter.id)
|
||||
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
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
# 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,8 +7,10 @@ class SubmitFormValuesController < ApplicationController
|
||||
def index
|
||||
submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||
|
||||
return render json: {} if submitter.completed_at?
|
||||
return render json: {} if submitter.submission.template.archived_at? || submitter.submission.archived_at?
|
||||
return render json: {} if submitter.completed_at? || submitter.declined_at?
|
||||
return render json: {} if submitter.submission.template.archived_at? ||
|
||||
submitter.submission.archived_at? ||
|
||||
submitter.submission.expired?
|
||||
|
||||
value = submitter.values[params['field_uuid']]
|
||||
attachment = submitter.attachments.where(created_at: params[:after]..).find_by(uuid: value) if value.present?
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SubmittersController < ApplicationController
|
||||
load_and_authorize_resource :submitter, only: %i[edit update]
|
||||
|
||||
def edit
|
||||
@submitter_email_message =
|
||||
if @submitter.preferences['email_message_uuid'].present?
|
||||
@submitter.account.email_messages.find_by(uuid: @submitter.preferences['email_message_uuid'])
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
submission = @submitter.submission
|
||||
|
||||
if @submitter.submission_events.exists?(event_type: 'start_form') || submission.archived_at? || submission.expired?
|
||||
return redirect_back fallback_location: submission_path(submission), alert: I18n.t('submitter_cannot_be_updated')
|
||||
end
|
||||
|
||||
if submitter_params.values.all?(&:blank?)
|
||||
return redirect_back fallback_location: submission_path(submission),
|
||||
alert: I18n.t('at_least_one_field_must_be_filled')
|
||||
end
|
||||
|
||||
if params[:is_custom_message] != '1'
|
||||
params.delete(:subject)
|
||||
params.delete(:body)
|
||||
end
|
||||
|
||||
submitter_preferences = Submitters.normalize_preferences(@submitter.account, current_user, params)
|
||||
|
||||
if submitter_preferences.key?('email_message_uuid')
|
||||
@submitter.preferences['email_message_uuid'] = submitter_preferences['email_message_uuid']
|
||||
end
|
||||
|
||||
assign_submitter_attrs(@submitter, submitter_params)
|
||||
|
||||
if @submitter.save
|
||||
maybe_resend_email_sms(@submitter, params)
|
||||
|
||||
redirect_back fallback_location: submission_path(submission), notice: I18n.t('changes_have_been_saved')
|
||||
else
|
||||
redirect_back fallback_location: submission_path(submission), alert: I18n.t('unable_to_save')
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def maybe_resend_email_sms(submitter, params)
|
||||
if params[:send_email] == '1' && submitter.email.present?
|
||||
is_sent_recently = Docuseal.multitenant? &&
|
||||
EmailEvent.exists?(email: submitter.email,
|
||||
tag: 'submitter_invitation',
|
||||
emailable: submitter,
|
||||
event_type: 'send',
|
||||
created_at: 4.hours.ago..Time.current)
|
||||
|
||||
SendSubmitterInvitationEmailJob.perform_async('submitter_id' => submitter.id) unless is_sent_recently
|
||||
end
|
||||
|
||||
return if submitter.phone.blank?
|
||||
return unless params[:send_sms] == '1'
|
||||
|
||||
SendSubmitterInvitationSmsJob.perform_async('submitter_id' => submitter.id)
|
||||
end
|
||||
|
||||
def assign_submitter_attrs(submitter, attrs)
|
||||
submitter.phone = attrs[:phone].to_s.gsub(/[^0-9+]/, '') if attrs.key?(:phone)
|
||||
|
||||
submitter.email = Submissions.normalize_email(attrs[:email]) if attrs.key?(:email)
|
||||
|
||||
submitter.name = attrs[:name] if attrs.key?(:name)
|
||||
|
||||
submitter
|
||||
end
|
||||
|
||||
def submitter_params
|
||||
params.require(:submitter).permit(:email, :name, :phone).transform_values(&:strip)
|
||||
end
|
||||
end
|
||||
@@ -10,7 +10,7 @@ class SubmittersSendEmailController < ApplicationController
|
||||
Rollbar.warning("Already sent: #{@submitter.id}") if defined?(Rollbar)
|
||||
|
||||
return redirect_back(fallback_location: submission_path(@submitter.submission),
|
||||
alert: 'Email has been sent already.')
|
||||
alert: I18n.t('email_has_been_sent_already'))
|
||||
end
|
||||
|
||||
SendSubmitterInvitationEmailJob.perform_async('submitter_id' => @submitter.id)
|
||||
@@ -18,6 +18,6 @@ class SubmittersSendEmailController < ApplicationController
|
||||
@submitter.sent_at ||= Time.current
|
||||
@submitter.save!
|
||||
|
||||
redirect_back(fallback_location: submission_path(@submitter.submission), notice: 'Email has been sent')
|
||||
redirect_back(fallback_location: submission_path(@submitter.submission), notice: I18n.t('email_has_been_sent'))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5,7 +5,7 @@ class TemplateDocumentsController < ApplicationController
|
||||
|
||||
def create
|
||||
if params[:blobs].blank? && params[:files].blank?
|
||||
return render json: { error: 'File is missing' }, status: :unprocessable_entity
|
||||
return render json: { error: I18n.t('file_is_missing') }, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
old_fields_hash = @template.fields.hash
|
||||
@@ -28,6 +28,6 @@ class TemplateDocumentsController < ApplicationController
|
||||
)
|
||||
}
|
||||
rescue Templates::CreateAttachments::PdfEncrypted
|
||||
render json: { error: 'PDF encrypted' }, status: :unprocessable_entity
|
||||
render json: { error: 'PDF encrypted', status: 'pdf_encrypted' }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
@@ -7,7 +7,7 @@ class TemplateFoldersController < ApplicationController
|
||||
@templates = @template_folder.templates.active.preload(:author).order(id: :desc)
|
||||
@templates = Templates.search(@templates, params[:q])
|
||||
|
||||
@pagy, @templates = pagy(@templates, items: 12)
|
||||
@pagy, @templates = pagy(@templates, limit: 12)
|
||||
end
|
||||
|
||||
def edit; end
|
||||
@@ -15,9 +15,9 @@ class TemplateFoldersController < ApplicationController
|
||||
def update
|
||||
if @template_folder != current_account.default_template_folder &&
|
||||
@template_folder.update(template_folder_params)
|
||||
redirect_to folder_path(@template_folder), notice: 'Folder name has been updated'
|
||||
redirect_to folder_path(@template_folder), notice: I18n.t('folder_name_has_been_updated')
|
||||
else
|
||||
redirect_to folder_path(@template_folder), alert: 'Unable to rename folder'
|
||||
redirect_to folder_path(@template_folder), alert: I18n.t('unable_to_rename_folder')
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -7,6 +7,6 @@ class TemplatesArchivedController < ApplicationController
|
||||
@templates = @templates.where.not(archived_at: nil).preload(:author, :folder).order(id: :desc)
|
||||
@templates = Templates.search(@templates, params[:q])
|
||||
|
||||
@pagy, @templates = pagy(@templates, items: 12)
|
||||
@pagy, @templates = pagy(@templates, limit: 12)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -15,13 +15,13 @@ class TemplatesController < ApplicationController
|
||||
submissions = submissions.pending if params[:status] == 'pending'
|
||||
submissions = submissions.completed if params[:status] == 'completed'
|
||||
|
||||
@pagy, @submissions = pagy(submissions.preload(:submitters).order(id: :desc))
|
||||
@pagy, @submissions = pagy(submissions.preload(submitters: :start_form_submission_events).order(id: :desc))
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
redirect_to root_path
|
||||
end
|
||||
|
||||
def new
|
||||
@template.name = "#{@base_template.name} (Clone)" if @base_template
|
||||
@template.name = "#{@base_template.name} (#{I18n.t('clone')})" if @base_template
|
||||
end
|
||||
|
||||
def edit
|
||||
@@ -66,7 +66,7 @@ class TemplatesController < ApplicationController
|
||||
if @template.save
|
||||
Templates::CloneAttachments.call(template: @template, original_template: @base_template) if @base_template
|
||||
|
||||
SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => @template.id)
|
||||
enqueue_template_created_webhooks(@template)
|
||||
|
||||
maybe_redirect_to_template(@template)
|
||||
else
|
||||
@@ -77,7 +77,7 @@ class TemplatesController < ApplicationController
|
||||
def update
|
||||
@template.update!(template_params)
|
||||
|
||||
SendTemplateUpdatedWebhookRequestJob.perform_async('template_id' => @template.id)
|
||||
enqueue_template_updated_webhooks(@template)
|
||||
|
||||
head :ok
|
||||
end
|
||||
@@ -87,11 +87,11 @@ class TemplatesController < ApplicationController
|
||||
if params[:permanently].present?
|
||||
@template.destroy!
|
||||
|
||||
'Template has been removed.'
|
||||
I18n.t('template_has_been_removed')
|
||||
else
|
||||
@template.update!(archived_at: Time.current)
|
||||
|
||||
'Template has been archived.'
|
||||
I18n.t('template_has_been_archived')
|
||||
end
|
||||
|
||||
redirect_back(fallback_location: root_path, notice:)
|
||||
@@ -103,7 +103,7 @@ class TemplatesController < ApplicationController
|
||||
params.require(:template).permit(
|
||||
:name,
|
||||
{ schema: [%i[attachment_uuid name]],
|
||||
submitters: [%i[name uuid]],
|
||||
submitters: [%i[name uuid is_requester linked_to_uuid invite_by_uuid email]],
|
||||
fields: [[:uuid, :submitter_uuid, :name, :type,
|
||||
:required, :readonly, :default_value,
|
||||
:title, :description,
|
||||
@@ -116,14 +116,29 @@ class TemplatesController < ApplicationController
|
||||
end
|
||||
|
||||
def authorized_clone_account_id?(account_id)
|
||||
true_user.account_id.to_s == account_id.to_s || true_user.account.linked_accounts.exists?(id: account_id)
|
||||
true_user.account_id.to_s == account_id.to_s ||
|
||||
true_user.account.linked_accounts.accessible_by(current_ability).exists?(id: account_id)
|
||||
end
|
||||
|
||||
def maybe_redirect_to_template(template)
|
||||
if template.account == current_account
|
||||
redirect_to(edit_template_path(@template))
|
||||
else
|
||||
redirect_back(fallback_location: root_path, notice: 'Template has been clonned')
|
||||
redirect_back(fallback_location: root_path, notice: I18n.t('template_has_been_cloned'))
|
||||
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
|
||||
|
||||
|
||||
@@ -23,14 +23,14 @@ class TemplatesDashboardController < ApplicationController
|
||||
@template_folders = @template_folders.reject { |e| e.name == TemplateFolder::DEFAULT_NAME }
|
||||
@templates = filter_templates(@templates)
|
||||
|
||||
items =
|
||||
limit =
|
||||
if @template_folders.size < 4
|
||||
TEMPLATES_PER_PAGE
|
||||
else
|
||||
(@template_folders.size < 7 ? 9 : 6)
|
||||
end
|
||||
|
||||
@pagy, @templates = pagy(@templates, items:)
|
||||
@pagy, @templates = pagy(@templates, limit:)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -9,9 +9,9 @@ class TemplatesFoldersController < ApplicationController
|
||||
@template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:name])
|
||||
|
||||
if @template.save
|
||||
redirect_back(fallback_location: template_path(@template), notice: 'Document template has been moved')
|
||||
redirect_back(fallback_location: template_path(@template), notice: I18n.t('document_template_has_been_moved'))
|
||||
else
|
||||
redirect_back(fallback_location: template_path(@template), notice: 'Unable to move template into folder')
|
||||
redirect_back(fallback_location: template_path(@template), notice: I18n.t('unable_to_move_template_into_folder'))
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ class TemplatesPreferencesController < ApplicationController
|
||||
authorize!(:update, @template)
|
||||
|
||||
@template.preferences = @template.preferences.merge(template_params[:preferences])
|
||||
@template.preferences = @template.preferences.reject { |_, v| v.is_a?(String) && v.blank? }
|
||||
@template.preferences = @template.preferences.reject { |_, v| (v.is_a?(String) || v.is_a?(Hash)) && v.blank? }
|
||||
@template.save!
|
||||
|
||||
head :ok
|
||||
@@ -23,12 +23,17 @@ class TemplatesPreferencesController < ApplicationController
|
||||
documents_copy_email_subject documents_copy_email_body
|
||||
documents_copy_email_enabled documents_copy_email_attach_audit
|
||||
completed_notification_email_attach_documents
|
||||
completed_redirect_url
|
||||
submitters_order
|
||||
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|
|
||||
attrs[:preferences] = attrs[:preferences].transform_values do |value|
|
||||
if %w[true false].include?(value)
|
||||
value == 'true'
|
||||
elsif value.respond_to?(:compact_blank)
|
||||
value.compact_blank
|
||||
else
|
||||
value
|
||||
end
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
# 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
|
||||
@template.update!(archived_at: nil)
|
||||
|
||||
redirect_to template_path(@template), notice: 'Template has been unarchived'
|
||||
redirect_to template_path(@template), notice: I18n.t('template_has_been_unarchived')
|
||||
end
|
||||
end
|
||||
|
||||
@@ -23,7 +23,7 @@ class TemplatesUploadsController < ApplicationController
|
||||
|
||||
@template.update!(schema:)
|
||||
|
||||
SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => @template.id)
|
||||
enqueue_template_created_webhooks(@template)
|
||||
|
||||
redirect_to edit_template_path(@template)
|
||||
rescue Templates::CreateAttachments::PdfEncrypted
|
||||
@@ -33,7 +33,7 @@ class TemplatesUploadsController < ApplicationController
|
||||
|
||||
raise if Rails.env.local?
|
||||
|
||||
redirect_to root_path, alert: 'Unable to upload file'
|
||||
redirect_to root_path, alert: I18n.t('unable_to_update_file')
|
||||
end
|
||||
|
||||
private
|
||||
@@ -52,7 +52,7 @@ class TemplatesUploadsController < ApplicationController
|
||||
def create_file_params_from_url
|
||||
tempfile = Tempfile.new
|
||||
tempfile.binmode
|
||||
tempfile.write(conn.get(Addressable::URI.parse(params[:url]).display_uri.to_s).body)
|
||||
tempfile.write(DownloadUtils.call(params[:url]).body)
|
||||
tempfile.rewind
|
||||
|
||||
file = ActionDispatch::Http::UploadedFile.new(
|
||||
@@ -66,9 +66,10 @@ class TemplatesUploadsController < ApplicationController
|
||||
{ files: [file] }
|
||||
end
|
||||
|
||||
def conn
|
||||
Faraday.new do |faraday|
|
||||
faraday.response :follow_redirects
|
||||
def enqueue_template_created_webhooks(template)
|
||||
WebhookUrls.for_account_id(template.account_id, 'template.created').each do |webhook_url|
|
||||
SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => template.id,
|
||||
'webhook_url_id' => webhook_url.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4,9 +4,8 @@ class TestingApiSettingsController < ApplicationController
|
||||
def index
|
||||
authorize!(:manage, current_user.access_token)
|
||||
|
||||
@webhook_config =
|
||||
current_account.encrypted_configs.find_or_initialize_by(key: EncryptedConfig::WEBHOOK_URL_KEY)
|
||||
@webhook_url = current_account.webhook_urls.first_or_initialize
|
||||
|
||||
authorize!(:manage, @webhook_config)
|
||||
authorize!(:manage, @webhook_url)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -14,12 +14,12 @@ class TimestampServerController < ApplicationController
|
||||
test_timeserver_url(@encrypted_config.value) if @encrypted_config.value.present?
|
||||
|
||||
if @encrypted_config.value.present? ? @encrypted_config.save : @encrypted_config.delete
|
||||
redirect_back fallback_location: settings_notifications_path, notice: 'Changes have been saved'
|
||||
redirect_back fallback_location: settings_notifications_path, notice: I18n.t('changes_have_been_saved')
|
||||
else
|
||||
redirect_back fallback_location: settings_notifications_path, alert: 'Unable to save'
|
||||
redirect_back fallback_location: settings_notifications_path, alert: I18n.t('unable_to_save')
|
||||
end
|
||||
rescue SocketError, TimestampError, OpenSSL::Timestamp::TimestampError
|
||||
redirect_back fallback_location: settings_notifications_path, alert: 'Invalid Timeserver'
|
||||
redirect_back fallback_location: settings_notifications_path, alert: t('invalid_timeserver')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -9,7 +9,7 @@ class UserInitialsController < ApplicationController
|
||||
def update
|
||||
file = params[:file]
|
||||
|
||||
return redirect_to settings_profile_index_path, notice: 'Unable to save initials' if file.blank?
|
||||
return redirect_to settings_profile_index_path, notice: I18n.t('unable_to_save_initials') if file.blank?
|
||||
|
||||
blob = ActiveStorage::Blob.create_and_upload!(io: file.open,
|
||||
filename: file.original_filename,
|
||||
@@ -22,16 +22,16 @@ class UserInitialsController < ApplicationController
|
||||
)
|
||||
|
||||
if @user_config.update(value: attachment.uuid)
|
||||
redirect_to settings_profile_index_path, notice: 'Initials has been saved'
|
||||
redirect_to settings_profile_index_path, notice: I18n.t('initials_has_been_saved')
|
||||
else
|
||||
redirect_to settings_profile_index_path, notice: 'Unable to save initials'
|
||||
redirect_to settings_profile_index_path, notice: I18n.t('unable_to_save_initials')
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@user_config.destroy
|
||||
|
||||
redirect_to settings_profile_index_path, notice: 'Initials has been removed'
|
||||
redirect_to settings_profile_index_path, notice: I18n.t('initials_has_been_removed')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -9,7 +9,7 @@ class UserSignaturesController < ApplicationController
|
||||
def update
|
||||
file = params[:file]
|
||||
|
||||
return redirect_to settings_profile_index_path, notice: 'Unable to save signature' if file.blank?
|
||||
return redirect_to settings_profile_index_path, notice: I18n.t('unable_to_save_signature') if file.blank?
|
||||
|
||||
blob = ActiveStorage::Blob.create_and_upload!(io: file.open,
|
||||
filename: file.original_filename,
|
||||
@@ -22,16 +22,16 @@ class UserSignaturesController < ApplicationController
|
||||
)
|
||||
|
||||
if @user_config.update(value: attachment.uuid)
|
||||
redirect_to settings_profile_index_path, notice: 'Signature has been saved'
|
||||
redirect_to settings_profile_index_path, notice: I18n.t('signature_has_been_saved')
|
||||
else
|
||||
redirect_to settings_profile_index_path, notice: 'Unable to save signature'
|
||||
redirect_to settings_profile_index_path, notice: I18n.t('unable_to_save_signature')
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@user_config.destroy
|
||||
|
||||
redirect_to settings_profile_index_path, notice: 'Signature has been removed'
|
||||
redirect_to settings_profile_index_path, notice: I18n.t('signature_has_been_removed')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -9,9 +9,11 @@ class UsersController < ApplicationController
|
||||
def index
|
||||
@users =
|
||||
if params[:status] == 'archived'
|
||||
@users.archived
|
||||
@users.archived.where.not(role: 'integration')
|
||||
elsif params[:status] == 'integration'
|
||||
@users.active.where(role: 'integration')
|
||||
else
|
||||
@users.active
|
||||
@users.active.where.not(role: 'integration')
|
||||
end
|
||||
|
||||
@pagy, @users = pagy(@users.where(account: current_account).order(id: :desc))
|
||||
@@ -35,14 +37,14 @@ class UsersController < ApplicationController
|
||||
if @user.save
|
||||
UserMailer.invitation_email(@user).deliver_later!
|
||||
|
||||
redirect_back fallback_location: settings_users_path, notice: 'User has been invited'
|
||||
redirect_back fallback_location: settings_users_path, notice: I18n.t('user_has_been_invited')
|
||||
else
|
||||
render turbo_stream: turbo_stream.replace(:modal, template: 'users/new'), status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
return redirect_to settings_users_path, notice: 'Unable to update user.' if Docuseal.demo?
|
||||
return redirect_to settings_users_path, notice: I18n.t('unable_to_update_user') if Docuseal.demo?
|
||||
|
||||
attrs = user_params.compact_blank.merge(user_params.slice(:archived_at))
|
||||
attrs.delete(:role) if !role_valid?(attrs[:role]) || current_user == @user
|
||||
@@ -56,7 +58,7 @@ class UsersController < ApplicationController
|
||||
end
|
||||
|
||||
if @user.update(attrs)
|
||||
redirect_back fallback_location: settings_users_path, notice: 'User has been updated'
|
||||
redirect_back fallback_location: settings_users_path, notice: I18n.t('user_has_been_updated')
|
||||
else
|
||||
render turbo_stream: turbo_stream.replace(:modal, template: 'users/edit'), status: :unprocessable_entity
|
||||
end
|
||||
@@ -64,12 +66,12 @@ class UsersController < ApplicationController
|
||||
|
||||
def destroy
|
||||
if Docuseal.demo? || @user.id == current_user.id
|
||||
return redirect_to settings_users_path, notice: 'Unable to remove user'
|
||||
return redirect_to settings_users_path, notice: I18n.t('unable_to_remove_user')
|
||||
end
|
||||
|
||||
@user.update!(archived_at: Time.current)
|
||||
|
||||
redirect_back fallback_location: settings_users_path, notice: 'User has been removed'
|
||||
redirect_back fallback_location: settings_users_path, notice: I18n.t('user_has_been_removed')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -14,6 +14,6 @@ class VerifyPdfSignatureController < ApplicationController
|
||||
render turbo_stream: turbo_stream.replace('result', partial: 'result',
|
||||
locals: { pdfs:, files: params[:files], trusted_certs: })
|
||||
rescue HexaPDF::MalformedPDFError
|
||||
render turbo_stream: turbo_stream.replace('result', html: helpers.tag.div('Invalid PDF', id: 'result'))
|
||||
render turbo_stream: turbo_stream.replace('result', html: helpers.tag.div(I18n.t('invalid_pdf'), id: 'result'))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,37 +1,22 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class WebhookPreferencesController < ApplicationController
|
||||
EVENTS = %w[
|
||||
form.viewed
|
||||
form.started
|
||||
form.completed
|
||||
form.declined
|
||||
template.created
|
||||
template.updated
|
||||
submission.created
|
||||
submission.archived
|
||||
].freeze
|
||||
load_and_authorize_resource :webhook_url, parent: false
|
||||
|
||||
before_action :load_account_config
|
||||
authorize_resource :account_config, parent: false
|
||||
def update
|
||||
webhook_preferences_params[:events].each do |event, val|
|
||||
@webhook_url.events.delete(event) if val == '0'
|
||||
@webhook_url.events.push(event) if val == '1' && @webhook_url.events.exclude?(event)
|
||||
end
|
||||
|
||||
def create
|
||||
@account_config.value[account_config_params[:event]] = account_config_params[:value] == '1'
|
||||
|
||||
@account_config.save!
|
||||
@webhook_url.save!
|
||||
|
||||
head :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_account_config
|
||||
@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)
|
||||
def webhook_preferences_params
|
||||
params.require(:webhook_url).permit(events: {})
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,29 +1,21 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class WebhookSecretController < ApplicationController
|
||||
before_action :load_encrypted_config
|
||||
authorize_resource :encrypted_config, parent: false
|
||||
load_and_authorize_resource :webhook_url, parent: false
|
||||
|
||||
def index; end
|
||||
def show; end
|
||||
|
||||
def create
|
||||
@encrypted_config.assign_attributes(value: {
|
||||
encrypted_config_params[:key] => encrypted_config_params[:value]
|
||||
def update
|
||||
@webhook_url.update!(secret: {
|
||||
webhook_secret_params[:key] => webhook_secret_params[:value]
|
||||
}.compact_blank)
|
||||
|
||||
@encrypted_config.value.present? ? @encrypted_config.save! : @encrypted_config.delete
|
||||
|
||||
redirect_back(fallback_location: settings_webhooks_path, notice: 'Webhook Secret has been saved.')
|
||||
redirect_back(fallback_location: settings_webhooks_path, notice: I18n.t('webhook_secret_has_been_saved'))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_encrypted_config
|
||||
@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, {})
|
||||
def webhook_secret_params
|
||||
params.require(:webhook_url).permit(secret: %i[key value]).fetch(:secret, {})
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,36 +1,35 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class WebhookSettingsController < ApplicationController
|
||||
before_action :load_encrypted_config
|
||||
authorize_resource :encrypted_config, parent: false
|
||||
before_action :load_webhook_url
|
||||
authorize_resource :webhook_url, parent: false
|
||||
|
||||
def show; end
|
||||
|
||||
def create
|
||||
@encrypted_config.assign_attributes(encrypted_config_params)
|
||||
@webhook_url.assign_attributes(webhook_params)
|
||||
|
||||
@encrypted_config.value.present? ? @encrypted_config.save! : @encrypted_config.delete
|
||||
@webhook_url.url.present? ? @webhook_url.save! : @webhook_url.delete
|
||||
|
||||
redirect_back(fallback_location: settings_webhooks_path, notice: 'Webhook URL has been saved.')
|
||||
redirect_back(fallback_location: settings_webhooks_path, notice: I18n.t('webhook_url_has_been_saved'))
|
||||
end
|
||||
|
||||
def update
|
||||
submitter = current_account.submitters.where.not(completed_at: nil).order(:id).last
|
||||
|
||||
SendFormCompletedWebhookRequestJob.perform_async({ 'submitter_id' => submitter.id,
|
||||
'encrypted_config_id' => @encrypted_config.id })
|
||||
SendFormCompletedWebhookRequestJob.perform_async('submitter_id' => submitter.id,
|
||||
'webhook_url_id' => @webhook_url.id)
|
||||
|
||||
redirect_back(fallback_location: settings_webhooks_path, notice: 'Webhook request has been sent.')
|
||||
redirect_back(fallback_location: settings_webhooks_path, notice: I18n.t('webhook_request_has_been_sent'))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_encrypted_config
|
||||
@encrypted_config =
|
||||
current_account.encrypted_configs.find_or_initialize_by(key: EncryptedConfig::WEBHOOK_URL_KEY)
|
||||
def load_webhook_url
|
||||
@webhook_url = current_account.webhook_urls.first_or_initialize
|
||||
end
|
||||
|
||||
def encrypted_config_params
|
||||
params.require(:encrypted_config).permit(:value)
|
||||
def webhook_params
|
||||
params.require(:webhook_url).permit(:url)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -26,6 +26,10 @@ import EmailsTextarea from './elements/emails_textarea'
|
||||
import ToggleOnSubmit from './elements/toggle_on_submit'
|
||||
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'
|
||||
|
||||
@@ -89,9 +93,15 @@ safeRegisterElement('toggle-cookies', ToggleCookies)
|
||||
safeRegisterElement('toggle-on-submit', ToggleOnSubmit)
|
||||
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 {
|
||||
connectedCallback () {
|
||||
document.addEventListener('turbo:submit-end', this.onSubmit)
|
||||
|
||||
this.appElem = document.createElement('div')
|
||||
|
||||
this.appElem.classList.add('md:h-screen')
|
||||
@@ -107,17 +117,29 @@ safeRegisterElement('template-builder', class extends HTMLElement {
|
||||
withPayment: this.dataset.withPayment === 'true',
|
||||
isPaymentConnected: this.dataset.isPaymentConnected === 'true',
|
||||
withFormula: this.dataset.withFormula === 'true',
|
||||
withSendButton: this.dataset.withSendButton !== 'false',
|
||||
withSignYourselfButton: this.dataset.withSignYourselfButton !== 'false',
|
||||
withConditions: this.dataset.withConditions === 'true',
|
||||
currencies: (this.dataset.currencies || '').split(',').filter(Boolean),
|
||||
acceptFileTypes: this.dataset.acceptFileTypes
|
||||
})
|
||||
|
||||
this.app.mount(this.appElem)
|
||||
this.component = this.app.mount(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 () {
|
||||
document.removeEventListener('turbo:submit-end', this.onSubmit)
|
||||
|
||||
this.app?.unmount()
|
||||
this.appElem?.remove()
|
||||
}
|
||||
@@ -130,7 +152,8 @@ safeRegisterElement('import-list', class extends HTMLElement {
|
||||
this.app = createApp(ImportList, {
|
||||
template: JSON.parse(this.dataset.template),
|
||||
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)
|
||||
|
||||
@@ -100,7 +100,7 @@ button[disabled] .enabled {
|
||||
.autocomplete {
|
||||
background: white;
|
||||
z-index: 1000;
|
||||
font: 14px/22px "-apple-system", BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
font: 16px/25px "-apple-system", BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
overflow: auto;
|
||||
box-sizing: border-box;
|
||||
@apply border border-base-300 mt-1 rounded-md;
|
||||
@@ -111,7 +111,7 @@ button[disabled] .enabled {
|
||||
}
|
||||
|
||||
.autocomplete > div {
|
||||
@apply px-2 py-1 font-normal text-sm;
|
||||
@apply px-2 py-1.5 font-normal;
|
||||
}
|
||||
|
||||
.autocomplete .group {
|
||||
@@ -130,7 +130,3 @@ button[disabled] .enabled {
|
||||
outline-offset: 3px;
|
||||
outline-color: hsl(var(--bc) / 0.2);
|
||||
}
|
||||
|
||||
select:required:invalid {
|
||||
color: gray !important;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
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"]')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
export default class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
const maskedToken = this.input.value
|
||||
|
||||
this.input.addEventListener('focus', () => {
|
||||
this.input.value = this.dataset.token
|
||||
this.input.select()
|
||||
})
|
||||
|
||||
this.input.addEventListener('focusout', () => {
|
||||
this.input.value = maskedToken
|
||||
})
|
||||
}
|
||||
|
||||
get input () {
|
||||
return this.querySelector('input')
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ export default class extends HTMLElement {
|
||||
|
||||
if (input && item[field]) {
|
||||
input.value = item[field]
|
||||
input.dispatchEvent(new CustomEvent('linked-input.update', { bubbles: true }))
|
||||
}
|
||||
|
||||
if (textarea && item[field]) {
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ export default actionable(class extends HTMLElement {
|
||||
}
|
||||
|
||||
onSubmit = (e) => {
|
||||
if (e.detail.success) {
|
||||
if (e.detail.success && e.detail?.formSubmission?.formElement?.dataset?.closeOnSubmit !== 'false') {
|
||||
this.close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,18 +14,21 @@ safeRegisterElement('submission-form', class extends HTMLElement {
|
||||
|
||||
this.app = createApp(Form, {
|
||||
submitter: JSON.parse(this.dataset.submitter),
|
||||
inviteSubmitters: JSON.parse(this.dataset.inviteSubmitters),
|
||||
canSendEmail: this.dataset.canSendEmail === 'true',
|
||||
previousSignatureValue: this.dataset.previousSignatureValue,
|
||||
goToLast: this.dataset.goToLast === 'true',
|
||||
isDemo: this.dataset.isDemo === 'true',
|
||||
attribution: this.dataset.attribution !== 'false',
|
||||
scrollPadding: this.dataset.scrollPadding || '-80px',
|
||||
language: this.dataset.language,
|
||||
dryRun: this.dataset.dryRun === 'true',
|
||||
expand: ['true', 'false'].includes(this.dataset.expand) ? this.dataset.expand === 'true' : null,
|
||||
withSignatureId: this.dataset.withSignatureId === 'true',
|
||||
requireSigningReason: this.dataset.requireSigningReason === 'true',
|
||||
withConfetti: this.dataset.withConfetti !== 'false',
|
||||
withDisclosure: this.dataset.withDisclosure === 'true',
|
||||
reuseSignature: this.dataset.reuseSignature !== 'false',
|
||||
withTypedSignature: this.dataset.withTypedSignature !== 'false',
|
||||
authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content,
|
||||
rememberSignature: this.dataset.rememberSignature === 'true',
|
||||
|
||||
@@ -31,10 +31,6 @@ button[disabled] .enabled {
|
||||
display: none;
|
||||
}
|
||||
|
||||
select:required:invalid {
|
||||
@apply text-gray-300;
|
||||
}
|
||||
|
||||
.input-bordered {
|
||||
@apply border-base-content/20;
|
||||
}
|
||||
|
||||
@@ -18,9 +18,15 @@ if (token) {
|
||||
transform (payload) {
|
||||
payload.body.telemetry = []
|
||||
|
||||
const string = JSON.stringify(payload)
|
||||
if (payload.request.query_string) {
|
||||
payload.request.query_string = ''
|
||||
}
|
||||
|
||||
return JSON.parse(string.replace(/(\/[des]\/)\w{6}/g, (_, m) => m))
|
||||
if (payload.request.url) {
|
||||
payload.request.url = payload.request.url.replace(/(\/[sdep]\/)(\w{5})[^/]+/, '$1$2')
|
||||
}
|
||||
|
||||
return payload
|
||||
},
|
||||
payload: {
|
||||
client: {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
class="field-area flex absolute lg:text-base -outline-offset-1"
|
||||
dir="auto"
|
||||
:style="computedStyle"
|
||||
: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 }"
|
||||
: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 }"
|
||||
>
|
||||
<div
|
||||
v-if="(!withFieldPlaceholder || !field.name || field.type === 'cells') && !isActive && !isValueSet && field.type !== 'checkbox' && submittable && !area.option_uuid"
|
||||
@@ -29,12 +29,12 @@
|
||||
{{ optionValue(option) }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ field.name || fieldNames[field.type] }}
|
||||
{{ field.title || field.name || fieldNames[field.type] }}
|
||||
<template v-if="field.type === 'checkbox' && !field.name">
|
||||
{{ fieldIndex + 1 }}
|
||||
</template>
|
||||
<template v-else-if="!field.required && field.type !== 'checkbox'">
|
||||
(optional)
|
||||
({{ t('optional') }})
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
@@ -226,7 +226,8 @@ export default {
|
||||
},
|
||||
submitter: {
|
||||
type: Object,
|
||||
required: true
|
||||
required: false,
|
||||
default: () => ({})
|
||||
},
|
||||
withSignatureId: {
|
||||
type: Boolean,
|
||||
@@ -404,7 +405,8 @@ export default {
|
||||
}
|
||||
|
||||
if (this.field.preferences?.font_size) {
|
||||
style.fontSize = this.field.preferences.font_size + 'pt'
|
||||
style.fontSize = `clamp(4pt, 1.6vw, ${this.field.preferences.font_size}pt)`
|
||||
style.lineHeight = `clamp(6pt, 2.0vw, ${parseInt(this.field.preferences.font_size) + 3}pt)`
|
||||
}
|
||||
|
||||
return style
|
||||
|
||||
@@ -21,14 +21,14 @@
|
||||
:values="values"
|
||||
:field="field"
|
||||
:area="area"
|
||||
:submittable="true"
|
||||
:submittable="submittable"
|
||||
:field-index="fieldIndex"
|
||||
:scroll-padding="scrollPadding"
|
||||
:submitter="submitter"
|
||||
:with-field-placeholder="withFieldPlaceholder"
|
||||
:with-signature-id="withSignatureId"
|
||||
:is-active="currentStep === step"
|
||||
:with-label="withLabel && !withFieldPlaceholder"
|
||||
:with-label="withLabel && !withFieldPlaceholder && step.length < 2"
|
||||
:is-value-set="step.some((f) => f.uuid in values)"
|
||||
:attachments-index="attachmentsIndex"
|
||||
@click="$emit('focus-step', stepIndex)"
|
||||
@@ -58,6 +58,11 @@ export default {
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
submittable: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
},
|
||||
submitter: {
|
||||
type: Object,
|
||||
required: true
|
||||
@@ -127,10 +132,18 @@ export default {
|
||||
this.scrollInContainer(areaRef.$el)
|
||||
} else {
|
||||
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
|
||||
|
||||
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' })
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
id="form_completed"
|
||||
class="mx-auto max-w-md flex flex-col"
|
||||
dir="auto"
|
||||
>
|
||||
@@ -10,7 +11,7 @@
|
||||
:height="30"
|
||||
/>
|
||||
<span>
|
||||
{{ completedMessage.title || t('form_has_been_completed') }}
|
||||
{{ completedMessage.title || (hasSignatureFields ? (hasMultipleDocuments ? t('documents_have_been_signed') : t('document_has_been_signed')) : t('form_has_been_completed')) }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
@@ -75,7 +76,7 @@
|
||||
</a>
|
||||
<a
|
||||
v-if="isDemo"
|
||||
href="https://docuseal.co/sign_up"
|
||||
href="https://docuseal.com/sign_up"
|
||||
class="white-button flex items-center space-x-1 w-full"
|
||||
>
|
||||
<IconLogin />
|
||||
@@ -88,9 +89,9 @@
|
||||
v-if="attribution"
|
||||
class="text-center mt-4"
|
||||
>
|
||||
{{ t('signed_with') }}
|
||||
{{ t('powered_by') }}
|
||||
<a
|
||||
href="https://www.docuseal.co/start"
|
||||
href="https://www.docuseal.com/start"
|
||||
target="_blank"
|
||||
class="underline"
|
||||
>DocuSeal</a> - {{ t('open_source_documents_software') }}
|
||||
@@ -129,6 +130,16 @@ export default {
|
||||
required: false,
|
||||
default: true
|
||||
},
|
||||
hasSignatureFields: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
hasMultipleDocuments: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
withDownloadButton: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
@@ -212,7 +223,7 @@ export default {
|
||||
this.downloadUrls(urls)
|
||||
}
|
||||
} else {
|
||||
alert('Failed to download files')
|
||||
alert(this.t('failed_to_download_files'))
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
:name="`values[${field.uuid}]`"
|
||||
@keydown.enter="onEnter"
|
||||
@focus="$emit('focus')"
|
||||
@paste="onPaste"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
@@ -98,6 +99,25 @@ export default {
|
||||
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 () {
|
||||
const inputEl = this.$refs.input
|
||||
|
||||
|
||||
@@ -13,6 +13,12 @@
|
||||
:scroll-padding="scrollPadding"
|
||||
@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
|
||||
v-if="formulaFields.length"
|
||||
:fields="formulaFields"
|
||||
@@ -32,8 +38,11 @@
|
||||
<template v-else-if="alwaysMinimize">
|
||||
{{ t('next') }}
|
||||
</template>
|
||||
<template v-else-if="isShowContinue">
|
||||
{{ t('continue') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ t('submit_form') }}
|
||||
{{ t('start_now') }}
|
||||
</template>
|
||||
<IconArrowsDiagonal
|
||||
class="absolute right-0 mr-4"
|
||||
@@ -53,7 +62,7 @@
|
||||
id="minimize_form_button"
|
||||
class="absolute right-0 mr-2 mt-2 top-0 hidden md:block"
|
||||
:title="t('minimize')"
|
||||
@click.prevent="isFormVisible = false"
|
||||
@click.prevent="minimizeForm"
|
||||
>
|
||||
<IconArrowsDiagonalMinimize2
|
||||
:width="20"
|
||||
@@ -64,7 +73,7 @@
|
||||
:class="{ 'md:px-4': isBreakpointMd }"
|
||||
>
|
||||
<form
|
||||
v-if="!isCompleted"
|
||||
v-if="!isCompleted && !isInvite"
|
||||
ref="form"
|
||||
:action="submitPath"
|
||||
method="post"
|
||||
@@ -106,7 +115,7 @@
|
||||
v-model="values[currentField.uuid]"
|
||||
:show-field-names="showFieldNames"
|
||||
:field="currentField"
|
||||
@submit="submitStep"
|
||||
@submit="!isSubmitting && submitStep()"
|
||||
@focus="scrollIntoField(currentField)"
|
||||
/>
|
||||
<div v-else-if="currentField.type === 'select'">
|
||||
@@ -143,6 +152,7 @@
|
||||
dir="auto"
|
||||
:required="currentField.required"
|
||||
class="select base-input !text-2xl w-full text-center font-normal"
|
||||
:class="{ 'text-gray-300': !values[currentField.uuid] }"
|
||||
:name="`values[${currentField.uuid}]`"
|
||||
@change="values[currentField.uuid] = $event.target.value"
|
||||
@focus="scrollIntoField(currentField)"
|
||||
@@ -150,6 +160,7 @@
|
||||
<option
|
||||
value=""
|
||||
:selected="!values[currentField.uuid]"
|
||||
class="text-gray-300"
|
||||
>
|
||||
{{ t('select_your_option') }}
|
||||
</option>
|
||||
@@ -158,6 +169,7 @@
|
||||
:key="option.uuid"
|
||||
:selected="values[currentField.uuid] == option.value"
|
||||
:value="option.value"
|
||||
class="text-base-content"
|
||||
>
|
||||
{{ option.value }}
|
||||
</option>
|
||||
@@ -195,7 +207,7 @@
|
||||
<span
|
||||
@click="scrollIntoField(currentField)"
|
||||
>
|
||||
{{ t('complete_hightlighted_checkboxes_and_click') }} <span class="font-semibold">{{ stepFields.length === currentStep + 1 ? t('submit') : t('next') }}</span>.
|
||||
{{ t('complete_hightlighted_checkboxes_and_click') }} <span class="font-semibold">{{ submitButtonText }}</span>.
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
@@ -259,7 +271,7 @@
|
||||
>
|
||||
<template v-if="isAnonymousChecboxes || !showFieldNames">
|
||||
<span class="text-xl">
|
||||
{{ t('complete_hightlighted_checkboxes_and_click') }} <span class="font-semibold">{{ stepFields.length === currentStep + 1 ? t('submit') : t('next') }}</span>.
|
||||
{{ t('complete_hightlighted_checkboxes_and_click') }} <span class="font-semibold">{{ submitButtonText }}</span>.
|
||||
</span>
|
||||
<input
|
||||
v-for="field in currentStepFields"
|
||||
@@ -334,7 +346,7 @@
|
||||
:remember-signature="rememberSignature"
|
||||
:attachments-index="attachmentsIndex"
|
||||
:require-signing-reason="requireSigningReason"
|
||||
:button-text="buttonText"
|
||||
:button-text="submitButtonText"
|
||||
:dry-run="dryRun"
|
||||
:with-disclosure="withDisclosure"
|
||||
:with-qr-button="withQrButton"
|
||||
@@ -343,7 +355,7 @@
|
||||
@update:reason="values[currentField.preferences?.reason_field_uuid] = $event"
|
||||
@attached="attachments.push($event)"
|
||||
@start="scrollIntoField(currentField)"
|
||||
@minimize="isFormVisible = false"
|
||||
@minimize="minimizeForm"
|
||||
/>
|
||||
<InitialsStep
|
||||
v-else-if="currentField.type === 'initials'"
|
||||
@@ -359,7 +371,7 @@
|
||||
@attached="attachments.push($event)"
|
||||
@start="scrollIntoField(currentField)"
|
||||
@focus="scrollIntoField(currentField)"
|
||||
@minimize="isFormVisible = false"
|
||||
@minimize="minimizeForm"
|
||||
/>
|
||||
<AttachmentStep
|
||||
v-else-if="currentField.type === 'file'"
|
||||
@@ -383,7 +395,7 @@
|
||||
:default-value="submitter.phone"
|
||||
:submitter-slug="submitterSlug"
|
||||
@focus="scrollIntoField(currentField)"
|
||||
@submit="submitStep"
|
||||
@submit="!isSubmitting && submitStep()"
|
||||
/>
|
||||
<PaymentStep
|
||||
v-else-if="currentField.type === 'payment'"
|
||||
@@ -395,7 +407,7 @@
|
||||
:values="values"
|
||||
@attached="attachments.push($event)"
|
||||
@focus="scrollIntoField(currentField)"
|
||||
@submit="submitStep"
|
||||
@submit="!isSubmitting && submitStep()"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
@@ -415,7 +427,7 @@
|
||||
class="mr-1 animate-spin"
|
||||
/>
|
||||
<span>
|
||||
{{ buttonText }}
|
||||
{{ submitButtonText }}
|
||||
</span><span
|
||||
v-if="isSubmitting"
|
||||
class="w-6 flex justify-start mr-1"
|
||||
@@ -430,10 +442,21 @@
|
||||
</div>
|
||||
</div>
|
||||
</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
|
||||
v-else
|
||||
:is-demo="isDemo"
|
||||
: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-message="completedRedirectUrl ? {} : completedMessage"
|
||||
:with-send-copy-button="withSendCopyButton && !completedRedirectUrl"
|
||||
@@ -479,6 +502,7 @@ import TextStep from './text_step'
|
||||
import NumberStep from './number_step'
|
||||
import DateStep from './date_step'
|
||||
import MarkdownContent from './markdown_content'
|
||||
import InviteForm from './invite_form'
|
||||
import FormCompleted from './completed'
|
||||
import { IconInnerShadowTop, IconArrowsDiagonal, IconWritingSign, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
|
||||
import AppearsOn from './appears_on'
|
||||
@@ -516,6 +540,7 @@ export default {
|
||||
IconWritingSign,
|
||||
AttachmentStep,
|
||||
InitialsStep,
|
||||
InviteForm,
|
||||
MultiSelectStep,
|
||||
IconInnerShadowTop,
|
||||
DateStep,
|
||||
@@ -542,6 +567,11 @@ export default {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
inviteSubmitters: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => []
|
||||
},
|
||||
withSignatureId: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
@@ -733,9 +763,11 @@ export default {
|
||||
data () {
|
||||
return {
|
||||
isCompleted: false,
|
||||
isInvite: false,
|
||||
isFormVisible: this.expand !== false,
|
||||
showFillAllRequiredFields: false,
|
||||
currentStep: 0,
|
||||
isShowContinue: false,
|
||||
enableScrollIntoField: true,
|
||||
phoneVerifiedValues: {},
|
||||
orientation: screen?.orientation?.type,
|
||||
@@ -748,9 +780,15 @@ export default {
|
||||
isMobile () {
|
||||
return /android|iphone|ipad/i.test(navigator.userAgent)
|
||||
},
|
||||
buttonText () {
|
||||
if (this.alwaysMinimize || this.stepFields.length === this.currentStep + 1) {
|
||||
submitButtonText () {
|
||||
if (this.alwaysMinimize) {
|
||||
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 {
|
||||
return this.t('next')
|
||||
}
|
||||
@@ -758,6 +796,18 @@ export default {
|
||||
alwaysMinimize () {
|
||||
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 () {
|
||||
return this.stepFields[this.currentStep] || []
|
||||
},
|
||||
@@ -802,6 +852,9 @@ export default {
|
||||
currentField () {
|
||||
return this.currentStepFields[0]
|
||||
},
|
||||
readonlyConditionalFields () {
|
||||
return this.fields.filter((f) => f.readonly && f.conditions?.length && this.checkFieldConditions(f))
|
||||
},
|
||||
stepFields () {
|
||||
return this.fields.filter((f) => !f.readonly).reduce((acc, f) => {
|
||||
const prevStep = acc[acc.length - 1]
|
||||
@@ -847,7 +900,7 @@ export default {
|
||||
mounted () {
|
||||
this.submittedValues = JSON.parse(JSON.stringify(this.values))
|
||||
|
||||
screen?.orientation.addEventListener('change', this.onOrientationChange)
|
||||
screen?.orientation?.addEventListener('change', this.onOrientationChange)
|
||||
|
||||
this.fields.forEach((field) => {
|
||||
if (field.default_value && !field.readonly) {
|
||||
@@ -879,11 +932,11 @@ export default {
|
||||
}
|
||||
|
||||
if (document.body?.clientWidth >= 768 && this.expand !== true && ['signature', 'initials', 'file', 'image'].includes(this.currentField?.type)) {
|
||||
this.isFormVisible = false
|
||||
this.minimizeForm()
|
||||
}
|
||||
|
||||
if (this.alwaysMinimize) {
|
||||
this.isFormVisible = false
|
||||
this.minimizeForm()
|
||||
}
|
||||
|
||||
if (/iPhone|iPad|iPod/i.test(navigator.userAgent)) {
|
||||
@@ -933,12 +986,12 @@ export default {
|
||||
return acc && isEmpty(this.values[c.field_uuid])
|
||||
} else if (['not_empty', 'checked'].includes(c.action)) {
|
||||
return acc && !isEmpty(this.values[c.field_uuid])
|
||||
} else if (['equal', 'contains'].includes(c.action)) {
|
||||
} else if (['equal', 'contains'].includes(c.action) && field) {
|
||||
const option = field.options.find((o) => o.uuid === c.value)
|
||||
const values = [this.values[c.field_uuid]].flat()
|
||||
|
||||
return acc && values.includes(this.optionValue(option, field.options.indexOf(option)))
|
||||
} else if (['not_equal', 'does_not_contain'].includes(c.action)) {
|
||||
} else if (['not_equal', 'does_not_contain'].includes(c.action) && field) {
|
||||
const option = field.options.find((o) => o.uuid === c.value)
|
||||
const values = [this.values[c.field_uuid]].flat()
|
||||
|
||||
@@ -1054,6 +1107,10 @@ export default {
|
||||
const currentFieldUuids = this.currentStepFields.map((f) => f.uuid)
|
||||
const currentFieldType = this.currentField.type
|
||||
|
||||
if (!formData && !this.$refs.form.checkValidity()) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.dryRun) {
|
||||
currentFieldUuids.forEach((fieldUuid) => {
|
||||
this.submittedValues[fieldUuid] = this.values[fieldUuid]
|
||||
@@ -1092,13 +1149,15 @@ export default {
|
||||
async submitStep () {
|
||||
this.isSubmitting = true
|
||||
|
||||
const submitStep = this.currentStep
|
||||
|
||||
const stepPromise = ['signature', 'phone', 'initials', 'payment'].includes(this.currentField.type)
|
||||
? this.$refs.currentStep.submit
|
||||
: () => Promise.resolve({})
|
||||
|
||||
stepPromise().then(async () => {
|
||||
const emptyRequiredField = this.stepFields.find((fields, index) => {
|
||||
if (index >= this.currentStep) {
|
||||
if (index >= submitStep) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1108,9 +1167,9 @@ export default {
|
||||
})
|
||||
|
||||
const formData = new FormData(this.$refs.form)
|
||||
const isLastStep = this.currentStep === this.stepFields.length - 1
|
||||
const isLastStep = submitStep === this.stepFields.length - 1
|
||||
|
||||
if (isLastStep && !emptyRequiredField) {
|
||||
if (isLastStep && !emptyRequiredField && !this.inviteSubmitters.length) {
|
||||
formData.append('completed', 'true')
|
||||
}
|
||||
|
||||
@@ -1133,15 +1192,11 @@ export default {
|
||||
return Promise.reject(new Error(data.error))
|
||||
}
|
||||
|
||||
if (isLastStep) {
|
||||
this.isSecondWalkthrough = true
|
||||
}
|
||||
|
||||
const nextStep = (isLastStep && emptyRequiredField) || this.stepFields[this.currentStep + 1]
|
||||
const nextStep = (isLastStep && emptyRequiredField) || this.stepFields[submitStep + 1]
|
||||
|
||||
if (nextStep) {
|
||||
if (this.alwaysMinimize) {
|
||||
this.isFormVisible = false
|
||||
this.minimizeForm()
|
||||
}
|
||||
|
||||
this.goToStep(this.stepFields.indexOf(nextStep), this.autoscrollFields)
|
||||
@@ -1149,18 +1204,10 @@ export default {
|
||||
if (emptyRequiredField === nextStep) {
|
||||
this.showFillAllRequiredFields = true
|
||||
}
|
||||
} else if (this.inviteSubmitters.length) {
|
||||
this.isInvite = true
|
||||
} else {
|
||||
this.isCompleted = true
|
||||
|
||||
const respData = await response.text()
|
||||
|
||||
if (respData) {
|
||||
this.onComplete(JSON.parse(respData))
|
||||
}
|
||||
|
||||
if (this.completedRedirectUrl) {
|
||||
window.location.href = this.completedRedirectUrl
|
||||
}
|
||||
this.performComplete(response)
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error(error)
|
||||
@@ -1169,13 +1216,32 @@ export default {
|
||||
})
|
||||
}).catch(error => {
|
||||
if (error?.message === 'Image too small') {
|
||||
alert('Signature is too small - please redraw.')
|
||||
alert(this.t('signature_is_too_small_please_redraw'))
|
||||
} else {
|
||||
console.log(error)
|
||||
}
|
||||
}).finally(() => {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
const en = {
|
||||
complete: 'Complete',
|
||||
sign_and_complete: 'Sign and Complete',
|
||||
text: 'Text',
|
||||
by_clicking_you_agree_to_the: 'By clicking "{button}", you agree to the',
|
||||
electronic_signature_disclosure: 'Electronic Signature Disclosure',
|
||||
@@ -10,6 +12,8 @@ const en = {
|
||||
reviewed: 'Reviewed',
|
||||
other: 'Other',
|
||||
authored_by_me: 'Authored by me',
|
||||
invite: 'Invite',
|
||||
email: 'Email',
|
||||
approved_by: 'Approved by',
|
||||
reviewed_by: 'Reviewed by',
|
||||
authored_by: 'Authored by',
|
||||
@@ -36,7 +40,8 @@ const en = {
|
||||
minimize: 'Minimize',
|
||||
payment: 'Payment',
|
||||
phone: 'Phone',
|
||||
submit_form: 'Submit Form',
|
||||
start_now: 'Start Now',
|
||||
continue: 'Continue',
|
||||
sign_now: 'Sign Now',
|
||||
type_here_: 'Type here...',
|
||||
optional: 'optional',
|
||||
@@ -57,8 +62,10 @@ const en = {
|
||||
type_signature_here: 'Type signature here',
|
||||
type_initial_here: 'Type initials here',
|
||||
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',
|
||||
signed_with: 'Signed with',
|
||||
powered_by: 'Powered by',
|
||||
please_check_the_box_to_continue: 'Please check the box to continue',
|
||||
open_source_documents_software: 'open source documents software',
|
||||
verified_phone_number: 'Verify Phone Number',
|
||||
@@ -85,6 +92,10 @@ const en = {
|
||||
}
|
||||
|
||||
const es = {
|
||||
complete: 'Completar',
|
||||
sign_and_complete: 'Firmar y Completar',
|
||||
invite: 'Invitar',
|
||||
email: 'Correo electrónico',
|
||||
approved: 'Aprobado',
|
||||
reviewed: 'Revisado',
|
||||
other: 'Otro',
|
||||
@@ -122,7 +133,8 @@ const es = {
|
||||
payment: 'Pago',
|
||||
phone: 'Teléfono',
|
||||
take_photo: 'Tomar foto',
|
||||
submit_form: 'Enviar Formulario',
|
||||
start_now: 'Comenzar ahora',
|
||||
continue: 'Continuar',
|
||||
sign_now: 'Firmar ahora',
|
||||
type_here_: 'Escribe aquí...',
|
||||
optional: 'opcional',
|
||||
@@ -142,8 +154,10 @@ const es = {
|
||||
type_signature_here: 'Escribe la firma aquí',
|
||||
type_initial_here: 'Escribe las iniciales aquí',
|
||||
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',
|
||||
signed_with: 'Firmado con',
|
||||
powered_by: 'Desarrollado por',
|
||||
please_check_the_box_to_continue: 'Por favor marque la casilla para continuar',
|
||||
open_source_documents_software: 'software de documentos de código abierto',
|
||||
verified_phone_number: 'Verificar número de teléfono',
|
||||
@@ -170,6 +184,10 @@ const es = {
|
||||
}
|
||||
|
||||
const it = {
|
||||
complete: 'Completa',
|
||||
sign_and_complete: 'Firma e Completa',
|
||||
invite: 'Invita',
|
||||
email: 'Email',
|
||||
approved: 'Approvato',
|
||||
reviewed: 'Revisionato',
|
||||
other: 'Altro',
|
||||
@@ -198,7 +216,7 @@ const it = {
|
||||
pay: 'Pagamento',
|
||||
file: 'File',
|
||||
select: 'Seleziona',
|
||||
checkbox: 'Casella di controllo',
|
||||
checkbox: 'Checkbox',
|
||||
multiple: 'Multiplo',
|
||||
radio: 'Radio',
|
||||
cells: 'Celle',
|
||||
@@ -206,7 +224,8 @@ const it = {
|
||||
payment: 'Pagamento',
|
||||
number_phone_is_invalid: '{number} telefono non è valido',
|
||||
phone: 'Telefono',
|
||||
submit_form: 'Invia Modulo',
|
||||
start_now: 'Inizia ora',
|
||||
continue: 'Continua',
|
||||
sign_now: 'Firma ora',
|
||||
type_here_: 'Digita qui...',
|
||||
optional: 'opzionale',
|
||||
@@ -227,8 +246,10 @@ const it = {
|
||||
type_signature_here: 'Scrivi la firma qui',
|
||||
type_initial_here: 'Scrivi le iniziali qui',
|
||||
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',
|
||||
signed_with: 'Firmato con',
|
||||
powered_by: 'Desarrollado por',
|
||||
please_check_the_box_to_continue: 'Si prega di spuntare la casella per continuare',
|
||||
open_source_documents_software: 'software di documenti open source',
|
||||
verified_phone_number: 'Verifica numero di telefono',
|
||||
@@ -255,6 +276,10 @@ const it = {
|
||||
}
|
||||
|
||||
const de = {
|
||||
complete: 'Abschließen',
|
||||
sign_and_complete: 'Signieren und Abschließen',
|
||||
invite: 'Einladen',
|
||||
email: 'E-Mail',
|
||||
approved: 'Genehmigt',
|
||||
reviewed: 'Überprüft',
|
||||
other: 'Andere',
|
||||
@@ -291,7 +316,8 @@ const de = {
|
||||
payment: 'Zahlung',
|
||||
number_phone_is_invalid: '{number} Telefonnummer ist ungültig',
|
||||
phone: 'Telefon',
|
||||
submit_form: 'Formular absenden',
|
||||
start_now: 'Jetzt starten',
|
||||
continue: 'Fortsetzen',
|
||||
sign_now: 'Jetzt unterschreiben',
|
||||
type_here_: 'Hier eingeben...',
|
||||
optional: 'optional',
|
||||
@@ -312,8 +338,10 @@ const de = {
|
||||
type_signature_here: 'Unterschrift hier eingeben',
|
||||
type_initial_here: 'Initialen hier eingeben',
|
||||
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',
|
||||
signed_with: 'Unterschrieben mit',
|
||||
powered_by: 'Bereitgestellt von',
|
||||
please_check_the_box_to_continue: 'Bitte setzen Sie das Häkchen, um fortzufahren',
|
||||
open_source_documents_software: 'Open-Source-Dokumentensoftware',
|
||||
verified_phone_number: 'Telefonnummer überprüfen',
|
||||
@@ -340,6 +368,10 @@ const de = {
|
||||
}
|
||||
|
||||
const fr = {
|
||||
complete: 'Terminer',
|
||||
sign_and_complete: 'Signer et Terminer',
|
||||
invite: 'Inviter',
|
||||
email: 'Courriel',
|
||||
approved: 'Approuvé',
|
||||
reviewed: 'Révisé',
|
||||
other: 'Autre',
|
||||
@@ -376,7 +408,8 @@ const fr = {
|
||||
payment: 'Paiement',
|
||||
number_phone_is_invalid: '{number} téléphone est invalide',
|
||||
phone: 'Téléphone',
|
||||
submit_form: 'Envoyer le Formulaire',
|
||||
start_now: 'Commencer maintenant',
|
||||
continue: 'Continuer',
|
||||
sign_now: 'Signer maintenant',
|
||||
type_here_: 'Tapez ici...',
|
||||
optional: 'facultatif',
|
||||
@@ -397,8 +430,10 @@ const fr = {
|
||||
type_signature_here: 'Tapez la signature ici',
|
||||
type_initial_here: 'Tapez les initiales ici',
|
||||
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',
|
||||
signed_with: 'Signé avec',
|
||||
powered_by: 'Propulsé par',
|
||||
please_check_the_box_to_continue: 'Veuillez cocher la case pour continuer',
|
||||
open_source_documents_software: 'logiciel de documents open source',
|
||||
verified_phone_number: 'Vérifier le numéro de téléphone',
|
||||
@@ -425,6 +460,10 @@ const fr = {
|
||||
}
|
||||
|
||||
const pl = {
|
||||
complete: 'Zakończ',
|
||||
sign_and_complete: 'Podpisz i zakończ',
|
||||
invite: 'Zaproś',
|
||||
email: 'E-mail',
|
||||
approved: 'Zaakceptowany',
|
||||
reviewed: 'Przejrzany',
|
||||
other: 'Inny',
|
||||
@@ -461,7 +500,8 @@ const pl = {
|
||||
payment: 'Płatność',
|
||||
number_phone_is_invalid: '{number} telefon jest nieprawidłowy',
|
||||
phone: 'Telefon',
|
||||
submit_form: 'Wyślij Formularz',
|
||||
start_now: 'Zacznij teraz',
|
||||
continue: 'Kontynuuj',
|
||||
sign_now: 'Podpisz teraz',
|
||||
type_here_: 'Wpisz tutaj...',
|
||||
optional: 'opcjonalny',
|
||||
@@ -482,8 +522,10 @@ const pl = {
|
||||
type_signature_here: 'Wpisz podpis tutaj',
|
||||
type_initial_here: 'Wpisz inicjały tutaj',
|
||||
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',
|
||||
signed_with: 'Podpisane za pomocą',
|
||||
powered_by: 'Napędzany przez',
|
||||
please_check_the_box_to_continue: 'Proszę zaznaczyć pole, aby kontynuować',
|
||||
open_source_documents_software: 'oprogramowanie do dokumentów open source',
|
||||
verified_phone_number: 'Zweryfikuj numer telefonu',
|
||||
@@ -510,6 +552,10 @@ const pl = {
|
||||
}
|
||||
|
||||
const uk = {
|
||||
complete: 'Завершити',
|
||||
sign_and_complete: 'Підписати і завершити',
|
||||
invite: 'Запросити',
|
||||
email: 'Електронна пошта',
|
||||
approved: 'Затверджено',
|
||||
reviewed: 'Переглянуто',
|
||||
other: 'Інше',
|
||||
@@ -546,7 +592,8 @@ const uk = {
|
||||
payment: 'Платіж',
|
||||
number_phone_is_invalid: '{number} телефон недійсний',
|
||||
phone: 'Телефон',
|
||||
submit_form: 'Надіслати Форму',
|
||||
start_now: 'Почати зараз',
|
||||
continue: 'Продовжити',
|
||||
sign_now: 'Підписати зараз',
|
||||
type_here_: 'Введіть тут',
|
||||
optional: 'необов’язково',
|
||||
@@ -567,8 +614,10 @@ const uk = {
|
||||
type_signature_here: 'Введіть підпис тут',
|
||||
type_initial_here: 'Введіть ініціали тут',
|
||||
form_has_been_completed: 'Форму заповнено!',
|
||||
document_has_been_signed: 'Документ був підписаний!',
|
||||
documents_have_been_signed: 'Документи були підписані!',
|
||||
create_a_free_account: 'Створити безкоштовний обліковий запис',
|
||||
signed_with: 'Підписано за допомогою',
|
||||
powered_by: 'Працює на базі',
|
||||
please_check_the_box_to_continue: 'Будь ласка, позначте прапорець, щоб продовжити',
|
||||
open_source_documents_software: 'відкритий програмний засіб для документів',
|
||||
verified_phone_number: 'Підтвердіть номер телефону',
|
||||
@@ -595,6 +644,10 @@ const uk = {
|
||||
}
|
||||
|
||||
const cs = {
|
||||
complete: 'Dokončit',
|
||||
sign_and_complete: 'Podepsat a dokončit',
|
||||
invite: 'Pozvat',
|
||||
email: 'E-mail',
|
||||
approved: 'Schváleno',
|
||||
reviewed: 'Zkontrolováno',
|
||||
other: 'Jiné',
|
||||
@@ -631,7 +684,8 @@ const cs = {
|
||||
payment: 'Platba',
|
||||
phone: 'Telefon',
|
||||
number_phone_is_invalid: '{number} telefon je neplatný',
|
||||
submit_form: 'Odeslat formulář',
|
||||
start_now: 'Začít nyní',
|
||||
continue: 'Pokračovat',
|
||||
sign_now: 'Podepsat nyní',
|
||||
type_here_: 'Zadejte zde',
|
||||
optional: 'volitelné',
|
||||
@@ -652,8 +706,10 @@ const cs = {
|
||||
type_signature_here: 'Sem zadejte podpis',
|
||||
type_initial_here: 'Sem zadejte iniciály',
|
||||
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',
|
||||
signed_with: 'Podepsáno pomocí',
|
||||
powered_by: 'Poháněno',
|
||||
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',
|
||||
verified_phone_number: 'Ověřte telefonní číslo',
|
||||
@@ -680,6 +736,10 @@ const cs = {
|
||||
}
|
||||
|
||||
const pt = {
|
||||
complete: 'Completar',
|
||||
sign_and_complete: 'Assinar e Completar',
|
||||
invite: 'Convidar',
|
||||
email: 'E-mail',
|
||||
approved: 'Aprovado',
|
||||
reviewed: 'Revisado',
|
||||
other: 'Outro',
|
||||
@@ -716,7 +776,8 @@ const pt = {
|
||||
payment: 'Pagamento',
|
||||
number_phone_is_invalid: '{number} telefone é inválido',
|
||||
phone: 'Telefone',
|
||||
submit_form: 'Enviar Formulário',
|
||||
start_now: 'Começar agora',
|
||||
continue: 'Continuar',
|
||||
sign_now: 'Assinar agora',
|
||||
type_here_: 'Digite aqui',
|
||||
optional: 'opcional',
|
||||
@@ -737,8 +798,10 @@ const pt = {
|
||||
type_signature_here: 'Digite a assinatura aqui',
|
||||
type_initial_here: 'Digite as iniciais aqui',
|
||||
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',
|
||||
signed_with: 'Assinado com',
|
||||
powered_by: 'Desenvolvido por',
|
||||
please_check_the_box_to_continue: 'Por favor, marque a caixa para continuar',
|
||||
open_source_documents_software: 'software de documentos de código aberto',
|
||||
verified_phone_number: 'Verificar Número de Telefone',
|
||||
@@ -765,6 +828,10 @@ const pt = {
|
||||
}
|
||||
|
||||
const he = {
|
||||
complete: 'השלם',
|
||||
sign_and_complete: 'חתום והשלם',
|
||||
invite: 'הזמן',
|
||||
email: 'דוא"ל',
|
||||
approved: 'מאושר',
|
||||
reviewed: 'נסקר',
|
||||
other: 'אחר',
|
||||
@@ -801,7 +868,8 @@ const he = {
|
||||
payment: 'תשלום',
|
||||
phone: 'טלפון',
|
||||
number_phone_is_invalid: '{number} טלפון אינו חוקי',
|
||||
submit_form: 'שלח טופס',
|
||||
start_now: 'התחל עכשיו',
|
||||
continue: 'המשך',
|
||||
sign_now: 'חתום כעת',
|
||||
type_here_: 'הקלד כאן',
|
||||
optional: 'אופציונלי',
|
||||
@@ -823,8 +891,10 @@ const he = {
|
||||
type_signature_here: 'הקלד חתימה כאן',
|
||||
type_initial_here: 'הקלד ציוני ראשי תיבות כאן',
|
||||
form_has_been_completed: 'הטופס הושלם',
|
||||
document_has_been_signed: 'המסמך נחתם!',
|
||||
documents_have_been_signed: 'המסמכים נחתמו!',
|
||||
create_a_free_account: 'צור חשבון חינם',
|
||||
signed_with: 'חתום באמצעות',
|
||||
powered_by: 'מופעל על ידי',
|
||||
please_check_the_box_to_continue: 'אנא סמן את התיבה כדי להמשיך',
|
||||
open_source_documents_software: 'תוכנה פתוחה למסמכים',
|
||||
verified_phone_number: 'אימות מספר טלפון',
|
||||
@@ -851,6 +921,10 @@ const he = {
|
||||
}
|
||||
|
||||
const nl = {
|
||||
complete: 'Voltooien',
|
||||
sign_and_complete: 'Ondertekenen en voltooien',
|
||||
invite: 'Uitnodigen',
|
||||
email: 'E-mail',
|
||||
approved: 'Goedgekeurd',
|
||||
reviewed: 'Beoordeeld',
|
||||
other: 'Anders',
|
||||
@@ -888,7 +962,8 @@ const nl = {
|
||||
payment: 'Betaling',
|
||||
phone: 'Telefoon',
|
||||
number_phone_is_invalid: '{number} telefoon is ongeldig',
|
||||
submit_form: 'Formulier verzenden',
|
||||
start_now: 'Nu beginnen',
|
||||
continue: 'Doorgaan',
|
||||
sign_now: 'Nu ondertekenen',
|
||||
type_here_: 'Typ hier...',
|
||||
optional: 'Optioneel',
|
||||
@@ -909,8 +984,10 @@ const nl = {
|
||||
type_signature_here: 'Typ hier uw handtekening',
|
||||
type_initial_here: 'Typ hier uw initialen',
|
||||
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',
|
||||
signed_with: 'Ondertekend met',
|
||||
powered_by: 'Aangedreven door',
|
||||
please_check_the_box_to_continue: 'Vink het vakje aan om door te gaan',
|
||||
open_source_documents_software: 'Open source documenten software',
|
||||
verified_phone_number: 'Geverifieerd telefoonnummer',
|
||||
@@ -937,6 +1014,10 @@ const nl = {
|
||||
}
|
||||
|
||||
const ar = {
|
||||
complete: 'اكتمال',
|
||||
sign_and_complete: 'التوقيع والاكتمال',
|
||||
invite: 'دعوة',
|
||||
email: 'البريد الإلكتروني',
|
||||
approved: 'موافق عليه',
|
||||
reviewed: 'تمت مراجعته',
|
||||
other: 'آخر',
|
||||
@@ -973,7 +1054,8 @@ const ar = {
|
||||
payment: 'الدفع',
|
||||
phone: 'هاتف',
|
||||
number_phone_is_invalid: '{number} هاتف غير صالح',
|
||||
submit_form: 'إرسال النموذج',
|
||||
start_now: 'ابدأ الآن',
|
||||
continue: 'متابعة',
|
||||
sign_now: 'وقع الآن',
|
||||
type_here_: 'اكتب هنا',
|
||||
optional: 'اختياري',
|
||||
@@ -994,8 +1076,10 @@ const ar = {
|
||||
type_signature_here: 'اكتب التوقيع هنا',
|
||||
type_initial_here: 'اكتب الاختصارات هنا',
|
||||
form_has_been_completed: 'تم إكمال النموذج!',
|
||||
document_has_been_signed: 'تم توقيع الوثيقة!',
|
||||
documents_have_been_signed: 'تم توقيع الوثائق!',
|
||||
create_a_free_account: 'إنشاء حساب مجاني',
|
||||
signed_with: 'تم التوقيع بواسطة',
|
||||
powered_by: 'مدعوم من',
|
||||
please_check_the_box_to_continue: 'الرجاء التحقق من الخانة للمتابعة',
|
||||
open_source_documents_software: 'برنامج وثائق مفتوح المصدر',
|
||||
verified_phone_number: 'تحقق من رقم الهاتف',
|
||||
@@ -1022,6 +1106,10 @@ const ar = {
|
||||
}
|
||||
|
||||
const ko = {
|
||||
complete: '완료',
|
||||
sign_and_complete: '서명하고 완료하기',
|
||||
invite: '초대하기',
|
||||
email: '이메일',
|
||||
approved: '승인됨',
|
||||
reviewed: '검토됨',
|
||||
other: '기타',
|
||||
@@ -1057,7 +1145,8 @@ const ko = {
|
||||
payment: '결제',
|
||||
phone: '전화',
|
||||
number_phone_is_invalid: '{number} 전화번호가 유효하지 않습니다',
|
||||
submit_form: '양식 제출',
|
||||
start_now: '지금 시작하기',
|
||||
continue: '계속하다',
|
||||
sign_now: '지금 서명',
|
||||
type_here_: '여기에 입력...',
|
||||
optional: '선택 사항',
|
||||
@@ -1078,8 +1167,10 @@ const ko = {
|
||||
type_signature_here: '여기에 서명 입력',
|
||||
type_initial_here: '여기에 이니셜 입력',
|
||||
form_has_been_completed: '양식이 완료되었습니다!',
|
||||
document_has_been_signed: '문서가 서명되었습니다!',
|
||||
documents_have_been_signed: '문서가 서명되었습니다!',
|
||||
create_a_free_account: '무료 계정 생성',
|
||||
signed_with: '다음으로 서명:',
|
||||
powered_by: '구동',
|
||||
please_check_the_box_to_continue: '계속하려면 확인란을 선택하십시오',
|
||||
open_source_documents_software: '오픈 소스 문서 소프트웨어',
|
||||
verified_phone_number: '전화번호 확인됨',
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
<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
@@ -61,30 +61,55 @@
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
<div
|
||||
v-show="!isCodeSent"
|
||||
: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')"
|
||||
class="flex w-full rounded-full outline-neutral-content outline-2 outline-offset-2 focus-within:outline"
|
||||
>
|
||||
<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>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MarkdownContent from './markdown_content'
|
||||
import phoneData from './phone_data'
|
||||
|
||||
function throttle (func, delay) {
|
||||
let lastCallTime = 0
|
||||
@@ -144,13 +169,73 @@ export default {
|
||||
data () {
|
||||
return {
|
||||
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: {
|
||||
emitSubmit: throttle(function (e) {
|
||||
this.$emit('submit')
|
||||
}, 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) {
|
||||
if (e.target.value.length === 6) {
|
||||
this.emitSubmit()
|
||||
@@ -171,14 +256,14 @@ export default {
|
||||
body: JSON.stringify({
|
||||
submitter_slug: this.submitterSlug,
|
||||
locale: this.locale,
|
||||
phone: this.$refs.phone.value
|
||||
phone: this.fullInternationalPhoneValue
|
||||
}),
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
}).then(async (resp) => {
|
||||
if (resp.status === 422) {
|
||||
const data = await resp.json()
|
||||
|
||||
alert(this.t('number_phone_is_invalid').replace('{number}', this.$refs.phone.value))
|
||||
alert(this.t('number_phone_is_invalid').replace('{number}', this.fullInternationalPhoneValue))
|
||||
|
||||
return Promise.reject(new Error(data.error))
|
||||
}
|
||||
@@ -189,13 +274,13 @@ export default {
|
||||
return Promise.resolve({})
|
||||
}
|
||||
|
||||
if (!this.$refs.phone.value.toString().startsWith('+')) {
|
||||
if (!this.fullInternationalPhoneValue.toString().startsWith('+')) {
|
||||
alert(this.t('use_international_format'))
|
||||
|
||||
return Promise.reject(new Error('phone invalid'))
|
||||
} else if (!this.isCodeSent) {
|
||||
return this.sendVerificationCode().then(() => {
|
||||
this.$emit('update:model-value', this.$refs.phone.value)
|
||||
this.$emit('update:model-value', this.fullInternationalPhoneValue)
|
||||
|
||||
this.isCodeSent = true
|
||||
|
||||
|
||||
@@ -145,6 +145,10 @@
|
||||
{{ t('clear') }}
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
v-if="isTextSignature"
|
||||
class="absolute top-0 right-0 left-0 bottom-0"
|
||||
/>
|
||||
<canvas
|
||||
v-show="!modelValue && !computedPreviousValue"
|
||||
ref="canvas"
|
||||
@@ -197,6 +201,7 @@
|
||||
<select
|
||||
v-if="requireSigningReason && !isOtherReason"
|
||||
class="select base-input !text-2xl w-full mt-6 text-center"
|
||||
:class="{ 'text-gray-300': !reason }"
|
||||
required
|
||||
:name="`values[${field.preferences.reason_field_uuid}]`"
|
||||
@change="$event.target.value === 'other' ? [reason = '', isOtherReason = true] : $emit('update:reason', $event.target.value)"
|
||||
@@ -205,6 +210,7 @@
|
||||
value=""
|
||||
disabled
|
||||
:selected="!reason"
|
||||
class="text-gray-300"
|
||||
>
|
||||
{{ t('select_a_reason') }}
|
||||
</option>
|
||||
@@ -213,10 +219,14 @@
|
||||
:key="option"
|
||||
:value="option"
|
||||
:selected="reason === option"
|
||||
class="text-base-content"
|
||||
>
|
||||
{{ label }}
|
||||
</option>
|
||||
<option value="other">
|
||||
<option
|
||||
value="other"
|
||||
class="text-base-content"
|
||||
>
|
||||
{{ t('other') }}
|
||||
</option>
|
||||
</select>
|
||||
@@ -249,7 +259,7 @@
|
||||
class="text-base-content/60 text-xs text-center w-full mt-1"
|
||||
>
|
||||
{{ t('by_clicking_you_agree_to_the').replace('{button}', buttonText.charAt(0).toUpperCase() + buttonText.slice(1)) }} <a
|
||||
href="https://www.docuseal.co/esign-disclosure"
|
||||
href="https://www.docuseal.com/esign-disclosure"
|
||||
target="_blank"
|
||||
>
|
||||
<span class="inline md:hidden">
|
||||
|
||||
@@ -147,7 +147,7 @@ export default {
|
||||
const textarea = this.$refs.textarea
|
||||
|
||||
textarea.style.height = 'auto'
|
||||
textarea.style.height = textarea.scrollHeight + 'px'
|
||||
textarea.style.height = Math.min(250, textarea.scrollHeight) + 'px'
|
||||
},
|
||||
toggleTextArea () {
|
||||
this.isTextArea = true
|
||||
|
||||
@@ -360,6 +360,7 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
fieldNames: FieldType.computed.fieldNames,
|
||||
fieldLabels: FieldType.computed.fieldLabels,
|
||||
fieldIcons: FieldType.computed.fieldIcons,
|
||||
isDefaultValuePresent () {
|
||||
if (this.field?.type === 'radio' && this.field?.areas?.length > 1) {
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
/>
|
||||
<template v-else>
|
||||
<a
|
||||
v-if="withSignYourselfButton"
|
||||
: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"
|
||||
:target="template.submitters.length > 1 ? '' : '_blank'"
|
||||
@@ -73,6 +74,7 @@
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
v-if="withSendButton"
|
||||
:href="`/templates/${template.id}/submissions/new?with_link=true`"
|
||||
data-turbo-frame="modal"
|
||||
class="white-button md:!px-6"
|
||||
@@ -129,7 +131,18 @@
|
||||
class="flex items-center justify-center space-x-2"
|
||||
>
|
||||
<IconEye class="w-6 h-6 flex-shrink-0" />
|
||||
<span class="whitespace-nowrap">Save and Preview</span>
|
||||
<span class="whitespace-nowrap">{{ t('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>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -319,7 +332,7 @@
|
||||
>
|
||||
<div class="bg-base-200 rounded-lg p-5 text-center space-y-4">
|
||||
<p>
|
||||
{{ t('draw_field_on_the_document').replace('{field}', drawField?.name || '') }}
|
||||
{{ t('draw_field_on_the_document') }}
|
||||
</p>
|
||||
<div>
|
||||
<button
|
||||
@@ -402,10 +415,11 @@ import Contenteditable from './contenteditable'
|
||||
import DocumentPreview from './preview'
|
||||
import DocumentControls from './controls'
|
||||
import MobileFields from './mobile_fields'
|
||||
import { IconPlus, IconUsersPlus, IconDeviceFloppy, IconChevronDown, IconEye, IconWritingSign, IconInnerShadowTop, IconInfoCircle } from '@tabler/icons-vue'
|
||||
import FieldSubmitter from './field_submitter'
|
||||
import { IconPlus, IconUsersPlus, IconDeviceFloppy, IconChevronDown, IconEye, IconWritingSign, IconInnerShadowTop, IconInfoCircle, IconAdjustments } from '@tabler/icons-vue'
|
||||
import { v4 } from 'uuid'
|
||||
import { ref, computed } from 'vue'
|
||||
import { en as i18nEn } from './i18n'
|
||||
import * as i18n from './i18n'
|
||||
|
||||
export default {
|
||||
name: 'TemplateBuilder',
|
||||
@@ -426,6 +440,7 @@ export default {
|
||||
Contenteditable,
|
||||
IconUsersPlus,
|
||||
IconChevronDown,
|
||||
IconAdjustments,
|
||||
IconEye,
|
||||
IconDeviceFloppy
|
||||
},
|
||||
@@ -479,6 +494,16 @@ export default {
|
||||
required: false,
|
||||
default: true
|
||||
},
|
||||
withSendButton: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
},
|
||||
withSignYourselfButton: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
},
|
||||
inputMode: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
@@ -534,6 +559,11 @@ export default {
|
||||
required: false,
|
||||
default: () => []
|
||||
},
|
||||
defineSubmitters: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => []
|
||||
},
|
||||
acceptFileTypes: {
|
||||
type: String,
|
||||
required: false,
|
||||
@@ -653,8 +683,12 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
submitterDefaultNames: FieldSubmitter.computed.names,
|
||||
selectedAreaRef: () => ref(),
|
||||
fieldsDragFieldRef: () => ref(),
|
||||
language () {
|
||||
return this.locale.split('-')[0].toLowerCase()
|
||||
},
|
||||
defaultDateFormat () {
|
||||
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)$/)
|
||||
@@ -666,6 +700,7 @@ export default {
|
||||
'de-DE': 'DD.MM.YYYY',
|
||||
'fr-FR': 'DD/MM/YYYY',
|
||||
'it-IT': 'DD/MM/YYYY',
|
||||
'en-GB': 'DD/MM/YYYY',
|
||||
'es-ES': 'DD/MM/YYYY'
|
||||
}
|
||||
},
|
||||
@@ -721,6 +756,18 @@ export default {
|
||||
}
|
||||
})
|
||||
|
||||
this.defineSubmitters.forEach((name, index) => {
|
||||
const submitter = (this.template.submitters[index] ||= {})
|
||||
|
||||
submitter.name = name || this.submitterDefaultNames[index]
|
||||
|
||||
if (existingSubmittersUuids.filter(Boolean).length) {
|
||||
submitter.uuid = existingSubmittersUuids[index] || submitter.uuid || v4()
|
||||
} else {
|
||||
submitter.uuid ||= v4()
|
||||
}
|
||||
})
|
||||
|
||||
this.selectedSubmitter = this.template.submitters[0]
|
||||
},
|
||||
mounted () {
|
||||
@@ -758,8 +805,11 @@ export default {
|
||||
this.documentRefs = []
|
||||
},
|
||||
methods: {
|
||||
closeDropdown () {
|
||||
document.activeElement.blur()
|
||||
},
|
||||
t (key) {
|
||||
return this.i18n[key] || i18nEn[key] || key
|
||||
return this.i18n[key] || i18n[this.language]?.[key] || i18n.en[key] || key
|
||||
},
|
||||
removePendingFields () {
|
||||
this.template.fields = this.template.fields.filter((f) => {
|
||||
@@ -1325,7 +1375,7 @@ export default {
|
||||
this.save()
|
||||
},
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -1424,7 +1474,7 @@ export default {
|
||||
if (!this.template.fields.length) {
|
||||
e.preventDefault()
|
||||
|
||||
alert('Please draw fields to prepare the document.')
|
||||
alert(this.t('please_draw_fields_to_prepare_the_document'))
|
||||
} else {
|
||||
const submitterWithoutFields =
|
||||
this.template.submitters.find((submitter) => !this.template.fields.some((f) => f.submitter_uuid === submitter.uuid))
|
||||
@@ -1432,7 +1482,7 @@ export default {
|
||||
if (submitterWithoutFields) {
|
||||
e.preventDefault()
|
||||
|
||||
alert(`Please add fields for the ${submitterWithoutFields.name}. Or, remove the ${submitterWithoutFields.name} if not needed.`)
|
||||
alert(this.t('please_add_fields_for_the_submitter_name_or_remove_the_submitter_name_if_not_needed').replaceAll('{submitter_name}', submitterWithoutFields.name))
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1448,13 +1498,13 @@ export default {
|
||||
}
|
||||
|
||||
if (!this.template.fields.length) {
|
||||
alert('Please draw fields to prepare the document.')
|
||||
alert(this.t('please_draw_fields_to_prepare_the_document'))
|
||||
} else {
|
||||
const submitterWithoutFields =
|
||||
this.template.submitters.find((submitter) => !this.template.fields.some((f) => f.submitter_uuid === submitter.uuid))
|
||||
|
||||
if (submitterWithoutFields) {
|
||||
alert(`Please add fields for the ${submitterWithoutFields.name}. Or, remove the ${submitterWithoutFields.name} if not needed.`)
|
||||
alert(this.t('please_add_fields_for_the_submitter_name_or_remove_the_submitter_name_if_not_needed').replaceAll('{submitter_name}', submitterWithoutFields.name))
|
||||
} else {
|
||||
this.isSaving = true
|
||||
|
||||
|
||||
@@ -23,10 +23,10 @@
|
||||
class="bg-base-300 rounded-xl py-2 px-3 text-center"
|
||||
>
|
||||
<a
|
||||
href="https://www.docuseal.co/pricing"
|
||||
href="https://www.docuseal.com/pricing"
|
||||
target="_blank"
|
||||
class="link"
|
||||
>Available in Pro</a>
|
||||
>{{ t('available_in_pro') }}</a>
|
||||
</div>
|
||||
<form @submit.prevent="validateSaveAndClose">
|
||||
<div class="my-4 space-y-5">
|
||||
@@ -50,6 +50,7 @@
|
||||
</div>
|
||||
<select
|
||||
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
|
||||
@change="[
|
||||
condition.field_uuid = $event.target.value,
|
||||
@@ -68,6 +69,7 @@
|
||||
v-for="f in fields"
|
||||
:key="f.uuid"
|
||||
:value="f.uuid"
|
||||
class="text-base-content"
|
||||
:selected="condition.field_uuid === f.uuid"
|
||||
>
|
||||
{{ f.name || buildDefaultName(f, template.fields) }}
|
||||
@@ -88,8 +90,9 @@
|
||||
</option>
|
||||
</select>
|
||||
<select
|
||||
v-if="conditionField(condition)?.options?.length"
|
||||
v-if="['radio', 'select', 'multiple'].includes(conditionField(condition)?.type) && conditionField(condition)?.options"
|
||||
class="select select-bordered select-sm w-full bg-white h-11 pl-4 text-base font-normal"
|
||||
:class="{ 'text-gray-300': !condition.value }"
|
||||
required
|
||||
@change="condition.value = $event.target.value"
|
||||
>
|
||||
@@ -105,6 +108,7 @@
|
||||
:key="option.uuid"
|
||||
:value="option.uuid"
|
||||
:selected="condition.value === option.uuid"
|
||||
class="text-base-content"
|
||||
>
|
||||
{{ option.value || `${t('option')} ${index + 1}` }}
|
||||
</option>
|
||||
@@ -200,7 +204,7 @@ export default {
|
||||
},
|
||||
validateSaveAndClose () {
|
||||
if (!this.withConditions) {
|
||||
return alert('Available only in Pro')
|
||||
return alert(this.t('available_only_in_pro'))
|
||||
}
|
||||
|
||||
if (this.conditions.find((f) => f.field_uuid)) {
|
||||
|
||||
@@ -105,7 +105,7 @@ export default {
|
||||
|
||||
this.upload()
|
||||
} else {
|
||||
alert('Only PDF and images are supported.')
|
||||
alert(this.t('only_pdf_and_images_are_supported'))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -312,6 +312,7 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
fieldNames: FieldType.computed.fieldNames,
|
||||
fieldLabels: FieldType.computed.fieldLabels,
|
||||
dropdownBgColor () {
|
||||
return ['', null, 'transparent'].includes(this.backgroundColor) ? 'white' : this.backgroundColor
|
||||
},
|
||||
@@ -367,8 +368,7 @@ export default {
|
||||
if (this.field.type === 'heading') {
|
||||
return `${this.fieldNames[field.type]} ${typeIndex + 1}`
|
||||
} else {
|
||||
const suffix = { multiple: this.t('select'), radio: this.t('group') }[field.type] || this.t('field')
|
||||
return `${this.fieldNames[field.type]} ${suffix} ${typeIndex + 1}`
|
||||
return `${this.fieldLabels[field.type]} ${typeIndex + 1}`
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -330,7 +330,7 @@ export default {
|
||||
this.$emit('update:model-value', submitter.uuid)
|
||||
},
|
||||
remove (submitter) {
|
||||
if (window.confirm(this.t('are_you_sure'))) {
|
||||
if (window.confirm(this.t('are_you_sure_'))) {
|
||||
this.$emit('remove', submitter)
|
||||
}
|
||||
},
|
||||
|
||||
@@ -114,6 +114,25 @@ export default {
|
||||
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 () {
|
||||
return {
|
||||
heading: IconHeading,
|
||||
|
||||
@@ -137,10 +137,10 @@
|
||||
v-else-if="type == 'phone' && (fieldTypes.length === 0 || fieldTypes.includes(type))"
|
||||
class="tooltip tooltip-bottom flex"
|
||||
:class="{'tooltip-bottom-end': withPayment, 'tooltip-bottom': !withPayment }"
|
||||
data-tip="Unlock SMS-verified phone number field with paid plan. Use text field for phone numbers without verification."
|
||||
:data-tip="t('unlock_sms_verified_phone_number_field_with_paid_plan_use_text_field_for_phone_numbers_without_verification')"
|
||||
>
|
||||
<a
|
||||
href="https://www.docuseal.co/pricing"
|
||||
href="https://www.docuseal.com/pricing"
|
||||
target="_blank"
|
||||
class="opacity-50 flex items-center justify-center border border-dashed border-base-300 w-full rounded relative"
|
||||
:style="{ backgroundColor }"
|
||||
@@ -168,13 +168,13 @@
|
||||
>
|
||||
<ul class="list-disc list-outside ml-3">
|
||||
<li>
|
||||
Draw a text field on the page with a mouse
|
||||
{{ t('draw_a_text_field_on_the_page_with_a_mouse') }}
|
||||
</li>
|
||||
<li>
|
||||
Drag & drop any other field type on the page
|
||||
{{ t('drag_and_drop_any_other_field_type_on_the_page') }}
|
||||
</li>
|
||||
<li>
|
||||
Click on the field type above to start drawing it
|
||||
{{ t('click_on_the_field_type_above_to_start_drawing_it') }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -23,10 +23,10 @@
|
||||
class="bg-base-300 rounded-xl py-2 px-3 text-center"
|
||||
>
|
||||
<a
|
||||
href="https://www.docuseal.co/pricing"
|
||||
href="https://www.docuseal.com/pricing"
|
||||
target="_blank"
|
||||
class="link"
|
||||
>Available in Pro</a>
|
||||
>{{ t('available_in_pro') }}</a>
|
||||
</div>
|
||||
<div class="flex-inline mb-2 gap-2 space-y-1">
|
||||
<button
|
||||
@@ -195,13 +195,13 @@ export default {
|
||||
},
|
||||
validateSaveAndClose () {
|
||||
if (!this.withFormula) {
|
||||
return alert('Available only in Pro')
|
||||
return alert(this.t('available_only_in_pro'))
|
||||
}
|
||||
|
||||
const normalizedFormula = this.normalizeFormula(this.formula)
|
||||
|
||||
if (normalizedFormula.includes('FIELD NOT FOUND')) {
|
||||
alert('Some fields are missing in the formula.')
|
||||
alert(this.t('some_fields_are_missing_in_the_formula'))
|
||||
} else {
|
||||
this.field.preferences.formula = normalizedFormula
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
const en = {
|
||||
editable: 'Editable',
|
||||
recurrent: 'Recurrent',
|
||||
one_off: 'One-off',
|
||||
search_field: 'Search field',
|
||||
field_not_found: 'Field not found',
|
||||
clear: 'Clear',
|
||||
@@ -29,7 +31,7 @@ const en = {
|
||||
select_value_: 'Select value...',
|
||||
remove_condition: 'Remove condition',
|
||||
add_condition: 'Add condition',
|
||||
are_you_sure: 'Are you sure?',
|
||||
are_you_sure_: 'Are you sure?',
|
||||
sign_yourself: 'Sign Yourself',
|
||||
set_signing_date: 'Set signing date',
|
||||
signing_date: 'Signing Date',
|
||||
@@ -46,7 +48,7 @@ const en = {
|
||||
drawn: 'Drawn',
|
||||
formula: 'Formula',
|
||||
typed: 'Typed',
|
||||
draw_field_on_the_document: 'Draw {field} field on the document',
|
||||
draw_field_on_the_document: 'Draw a field on the document',
|
||||
click_to_upload: 'Click to upload',
|
||||
or_drag_and_drop_files: 'or drag and drop files',
|
||||
uploading: 'Uploading',
|
||||
@@ -101,9 +103,31 @@ const en = {
|
||||
stamp: 'Stamp',
|
||||
payment: 'Payment',
|
||||
phone: 'Phone',
|
||||
field: 'Field',
|
||||
group: 'Group',
|
||||
text_field: 'Text Field',
|
||||
signature_field: 'Signature Field',
|
||||
initials_field: 'Initials Field',
|
||||
date_field: 'Date Field',
|
||||
number_field: 'Number Field',
|
||||
image_field: 'Image Field',
|
||||
file_field: 'File Field',
|
||||
select_field: 'Select Field',
|
||||
checkbox_field: 'Checkbox Field',
|
||||
multiple_field: 'Multiple Select Field',
|
||||
radio_field: 'Radio Group Field',
|
||||
cells_field: 'Cells Field',
|
||||
stamp_field: 'Stamp Field',
|
||||
payment_field: 'Payment Field',
|
||||
phone_field: 'Phone Field',
|
||||
draw_a_text_field_on_the_page_with_a_mouse: 'Draw a text field on the page with a mouse',
|
||||
drag_and_drop_any_other_field_type_on_the_page: 'Drag & drop any other field type on the page',
|
||||
click_on_the_field_type_above_to_start_drawing_it: 'Click on the field type above to start drawing it',
|
||||
please_draw_fields_to_prepare_the_document: 'Please draw fields to prepare the document.',
|
||||
only_pdf_and_images_are_supported: 'Only PDF and images are supported.',
|
||||
unlock_sms_verified_phone_number_field_with_paid_plan_use_text_field_for_phone_numbers_without_verification: 'Unlock SMS-verified phone number field with paid plan. Use text field for phone numbers without verification.',
|
||||
available_only_in_pro: 'Available only in Pro',
|
||||
failed_to_download_files: 'Failed to download files',
|
||||
signature_is_too_small_please_redraw: 'Signature is too small - please redraw.',
|
||||
please_add_fields_for_the_submitter_name_or_remove_the_submitter_name_if_not_needed: 'Please add fields for the {submitter_name}. Or, remove the {submitter_name} if not needed.',
|
||||
draw_field: 'Draw {field} Field',
|
||||
replace: 'Replace',
|
||||
uploading_: 'Uploading...',
|
||||
@@ -117,7 +141,785 @@ const en = {
|
||||
custom: 'Custom',
|
||||
numbers_only: 'Numbers only',
|
||||
letters_only: 'Letters only',
|
||||
regexp_validation: 'Regexp validation'
|
||||
regexp_validation: 'Regexp validation',
|
||||
enter_pdf_password: 'Enter PDF password',
|
||||
wrong_password: 'Wrong password',
|
||||
currency: 'Currency',
|
||||
save_and_preview: 'Save and Preview',
|
||||
preferences: 'Preferences',
|
||||
available_in_pro: 'Available in Pro',
|
||||
some_fields_are_missing_in_the_formula: 'Some fields are missing in the formula.',
|
||||
learn_more: 'Learn more'
|
||||
}
|
||||
|
||||
export { en }
|
||||
const es = {
|
||||
recurrent: 'Recurrente',
|
||||
one_off: 'Único',
|
||||
editable: 'Editable',
|
||||
search_field: 'Campo de búsqueda',
|
||||
field_not_found: 'Campo no encontrado',
|
||||
clear: 'Borrar',
|
||||
type_value: 'Escriba valor',
|
||||
align: 'Alinear',
|
||||
add_all_required_fields_to_continue: 'Agregar todos los campos requeridos para continuar',
|
||||
uploaded_pdf_contains_form_fields_keep_or_remove_them: 'El PDF cargado tiene campos. ¿Mantenerlos o eliminarlos?',
|
||||
keep: 'Mantener',
|
||||
left: 'Izquierda',
|
||||
heading: 'Encabezado',
|
||||
validation: 'Validación',
|
||||
add_blank_page: 'Agregar página en blanco',
|
||||
right: 'Derecha',
|
||||
center: 'Centro',
|
||||
with_logo: 'Con logotipo',
|
||||
description: 'Descripción',
|
||||
signing_date: 'Fecha de Firma',
|
||||
display_title: 'Título de visualización',
|
||||
unchecked: 'No marcado',
|
||||
price: 'Precio',
|
||||
equal: 'Igual',
|
||||
not_equal: 'No es igual',
|
||||
contains: 'Contiene',
|
||||
does_not_contain: 'No contiene',
|
||||
not_empty: 'No vacío',
|
||||
empty: 'Vacío',
|
||||
select_field_: 'Seleccionar campo...',
|
||||
select_value_: 'Seleccionar valor...',
|
||||
remove_condition: 'Eliminar condición',
|
||||
add_condition: 'Agregar condición',
|
||||
condition: 'Condición',
|
||||
formula: 'Fórmula',
|
||||
edit: 'Editar',
|
||||
settings: 'Configuración',
|
||||
up: 'Arriba',
|
||||
down: 'Abajo',
|
||||
set_signing_date: 'Establecer fecha de firma',
|
||||
are_you_sure_: '¿Estás seguro?',
|
||||
sign_yourself: 'Firma tú mismo',
|
||||
checked: 'Seleccionado',
|
||||
send: 'Enviar',
|
||||
remove: 'Eliminar',
|
||||
save: 'Guardar',
|
||||
cancel: 'Cancelar',
|
||||
or_add_field_without_drawing: 'O añadir campo sin dibujar',
|
||||
draw_field_on_the_document: 'Dibujar un campo en el documento',
|
||||
click_to_upload: 'Haz clic para cargar',
|
||||
or_drag_and_drop_files: 'o arrastra y suelta archivos',
|
||||
uploading: 'Subiendo',
|
||||
processing_: 'Procesando...',
|
||||
add_pdf_documents_or_images: 'Agregar documentos PDF o imágenes',
|
||||
add_documents_or_images: 'Agregar documentos o imágenes',
|
||||
required: 'Requerido',
|
||||
default_value: 'Valor predeterminado',
|
||||
format: 'Formato',
|
||||
read_only: 'Solo lectura',
|
||||
page: 'Página',
|
||||
draw_new_area: 'Dibujar nueva área',
|
||||
copy_to_all_pages: 'Copiar a todas las páginas',
|
||||
add_option: 'Agregar opción',
|
||||
option: 'Opción',
|
||||
first_party: 'Primera Parte',
|
||||
second_party: 'Segunda Parte',
|
||||
third_party: 'Tercera Parte',
|
||||
fourth_party: 'Cuarta Parte',
|
||||
fifth_party: 'Quinta Parte',
|
||||
sixth_party: 'Sexta Parte',
|
||||
seventh_party: 'Séptima Parte',
|
||||
eighth_party: 'Octava Parte',
|
||||
ninth_party: 'Novena Parte',
|
||||
tenth_party: 'Décima Parte',
|
||||
eleventh_party: 'Undécimo Partido',
|
||||
twelfth_party: 'Duodécimo Partido',
|
||||
thirteenth_party: 'Decimotercer Partido',
|
||||
fourteenth_party: 'Catorceavo Partido',
|
||||
fifteenth_party: 'Quinceavo Partido',
|
||||
sixteenth_party: 'Dieciséisavo Partido',
|
||||
seventeenth_party: 'Diecisieteavo Partido',
|
||||
eighteenth_party: 'Dieciochoavo Partido',
|
||||
nineteenth_party: 'Decimonovena Fiesta',
|
||||
twentieth_party: 'Vigésima Fiesta',
|
||||
draw: 'Dibujar',
|
||||
add: 'Agregar',
|
||||
text: 'Texto',
|
||||
signature: 'Firma',
|
||||
initials: 'Iniciales',
|
||||
date: 'Fecha',
|
||||
number: 'Número',
|
||||
image: 'Imagen',
|
||||
file: 'Archivo',
|
||||
select: 'Seleccionar',
|
||||
checkbox: 'Casilla',
|
||||
multiple: 'Múltiple',
|
||||
radio: 'Radio',
|
||||
cells: 'Celdas',
|
||||
stamp: 'Sello',
|
||||
payment: 'Pago',
|
||||
phone: 'Teléfono',
|
||||
text_field: 'Campo de Texto',
|
||||
signature_field: 'Campo de Firma',
|
||||
initials_field: 'Campo de Iniciales',
|
||||
date_field: 'Campo de Fecha',
|
||||
number_field: 'Campo de Número',
|
||||
image_field: 'Campo de Imagen',
|
||||
file_field: 'Campo de Archivo',
|
||||
select_field: 'Campo de Selección',
|
||||
checkbox_field: 'Campo de Casilla',
|
||||
multiple_field: 'Campo Múltiple',
|
||||
radio_field: 'Campo de Grupo Radio',
|
||||
cells_field: 'Campo de Celdas',
|
||||
stamp_field: 'Campo de Sello',
|
||||
payment_field: 'Campo de Pago',
|
||||
phone_field: 'Campo de Teléfono',
|
||||
draw_a_text_field_on_the_page_with_a_mouse: 'Dibujar un campo de texto en la página con el mouse',
|
||||
drag_and_drop_any_other_field_type_on_the_page: 'Arrastra y suelta cualquier otro tipo de campo en la página',
|
||||
click_on_the_field_type_above_to_start_drawing_it: 'Haz clic en el tipo de campo de arriba para comenzar a dibujarlo',
|
||||
please_draw_fields_to_prepare_the_document: 'Por favor, dibuja los campos para preparar el documento.',
|
||||
only_pdf_and_images_are_supported: 'Solo se admiten PDF e imágenes.',
|
||||
unlock_sms_verified_phone_number_field_with_paid_plan_use_text_field_for_phone_numbers_without_verification: 'Desbloquea el campo de número de teléfono verificado por SMS con un plan pago. Usa el campo de texto para números de teléfono sin verificación.',
|
||||
available_only_in_pro: 'Disponible solo en Pro',
|
||||
failed_to_download_files: 'Error al descargar los archivos',
|
||||
signature_is_too_small_please_redraw: 'La firma es demasiado pequeña. Por favor, dibújala de nuevo.',
|
||||
please_add_fields_for_the_submitter_name_or_remove_the_submitter_name_if_not_needed: 'Por favor, añade campos para {submitter_name} o elimina {submitter_name} si no es necesario.',
|
||||
draw_field: 'Dibujar campo {field}',
|
||||
replace: 'Reemplazar',
|
||||
uploading_: 'Subiendo...',
|
||||
add_document: 'Subir',
|
||||
any: 'Cualquier',
|
||||
drawn: 'Dibujado',
|
||||
typed: 'Escrito',
|
||||
none: 'Ninguno',
|
||||
ssn: 'SSN',
|
||||
ein: 'EIN',
|
||||
email: 'Correo electrónico',
|
||||
url: 'URL',
|
||||
zip: 'ZIP',
|
||||
custom: 'Personalizado',
|
||||
numbers_only: 'Solo números',
|
||||
letters_only: 'Solo letras',
|
||||
regexp_validation: 'Validación de expresión regular',
|
||||
enter_pdf_password: 'Ingrese la contraseña del PDF',
|
||||
wrong_password: 'Contraseña incorrecta',
|
||||
currency: 'Moneda',
|
||||
save_and_preview: 'Guardar y previsualizar',
|
||||
preferences: 'Preferencias',
|
||||
available_in_pro: 'Disponible en Pro',
|
||||
some_fields_are_missing_in_the_formula: 'Faltan algunos campos en la fórmula.',
|
||||
learn_more: 'Aprende más'
|
||||
}
|
||||
|
||||
const it = {
|
||||
ricorrente: 'Ricorrente',
|
||||
una_volta: 'Una volta',
|
||||
editable: 'Modificabile',
|
||||
search_field: 'Campo di ricerca',
|
||||
field_not_found: 'Campo non trovato',
|
||||
clear: 'Cancella',
|
||||
align: 'Allinea',
|
||||
add_all_required_fields_to_continue: 'Aggiungi tutti i campi obbligatori per continuare',
|
||||
uploaded_pdf_contains_form_fields_keep_or_remove_them: 'Il PDF caricato contiene campi del modulo. Mantenerli o rimuoverli?',
|
||||
keep: 'Mantieni',
|
||||
left: 'Sinistra',
|
||||
heading: 'Intestazione',
|
||||
validation: 'Validazione',
|
||||
add_blank_page: 'Aggiungi pagina vuota',
|
||||
right: 'Destra',
|
||||
center: 'Centro',
|
||||
description: 'Descrizione',
|
||||
display_title: 'Mostra titolo',
|
||||
with_logo: 'Con logo',
|
||||
unchecked: 'Non selezionato',
|
||||
price: 'Prezzo',
|
||||
type_value: 'Inserisci valore',
|
||||
equal: 'Uguale',
|
||||
not_equal: 'Non uguale',
|
||||
contains: 'Contiene',
|
||||
does_not_contain: 'Non contiene',
|
||||
not_empty: 'Non vuoto',
|
||||
empty: 'Vuoto',
|
||||
select_field_: 'Seleziona campo...',
|
||||
select_value_: 'Seleziona valore...',
|
||||
remove_condition: 'Rimuovi condizione',
|
||||
add_condition: 'Aggiungi condizione',
|
||||
are_you_sure_: 'Sei sicuro?',
|
||||
sign_yourself: 'Firma te stesso',
|
||||
set_signing_date: 'Imposta data di firma',
|
||||
signing_date: 'Data di firma',
|
||||
send: 'Invia',
|
||||
remove: 'Rimuovi',
|
||||
edit: 'Modifica',
|
||||
settings: 'Impostazioni',
|
||||
up: 'Su',
|
||||
down: 'Giù',
|
||||
checked: 'Selezionato',
|
||||
save: 'Salva',
|
||||
cancel: 'Annulla',
|
||||
any: 'Qualsiasi',
|
||||
drawn: 'Disegnato',
|
||||
formula: 'Formula',
|
||||
typed: 'Digitato',
|
||||
draw_field_on_the_document: 'Disegnare un campo sul documento',
|
||||
click_to_upload: 'Clicca per caricare',
|
||||
or_drag_and_drop_files: 'o trascina e rilascia i file',
|
||||
uploading: 'Caricamento in corso',
|
||||
processing_: 'Elaborazione...',
|
||||
add_pdf_documents_or_images: 'Aggiungi documenti PDF o immagini',
|
||||
add_documents_or_images: 'Aggiungi documenti o immagini',
|
||||
required: 'Obbligatorio',
|
||||
default_value: 'Valore predefinito',
|
||||
format: 'Formato',
|
||||
read_only: 'Sola lettura',
|
||||
page: 'Pagina',
|
||||
draw_new_area: 'Disegna nuova area',
|
||||
copy_to_all_pages: 'Copia in tutte le pagine',
|
||||
add_option: 'Aggiungi opzione',
|
||||
option: 'Opzione',
|
||||
condition: 'Condizione',
|
||||
first_party: 'Prima parte',
|
||||
second_party: 'Seconda parte',
|
||||
third_party: 'Terza parte',
|
||||
fourth_party: 'Quarta parte',
|
||||
fifth_party: 'Quinta parte',
|
||||
sixth_party: 'Sesta parte',
|
||||
seventh_party: 'Settima parte',
|
||||
eighth_party: 'Ottava parte',
|
||||
ninth_party: 'Nona parte',
|
||||
tenth_party: 'Decima parte',
|
||||
eleventh_party: 'Undicesima parte',
|
||||
twelfth_party: 'Dodicesima parte',
|
||||
thirteenth_party: 'Tredicesima parte',
|
||||
fourteenth_party: 'Quattordicesima parte',
|
||||
fifteenth_party: 'Quindicesima parte',
|
||||
sixteenth_party: 'Sedicesima parte',
|
||||
seventeenth_party: 'Diciassettesima parte',
|
||||
eighteenth_party: 'Diciottesima parte',
|
||||
nineteenth_party: 'Diciannovesima parte',
|
||||
twentieth_party: 'Ventesima parte',
|
||||
draw: 'Disegna',
|
||||
add: 'Aggiungi',
|
||||
or_add_field_without_drawing: 'Oppure aggiungi campo senza disegno',
|
||||
text: 'Testo',
|
||||
number: 'Numero',
|
||||
signature: 'Firma',
|
||||
initials: 'Iniziali',
|
||||
date: 'Data',
|
||||
image: 'Immagine',
|
||||
file: 'File',
|
||||
select: 'Seleziona',
|
||||
checkbox: 'Checkbox',
|
||||
multiple: 'Multiplo',
|
||||
radio: 'Radio',
|
||||
cells: 'Celle',
|
||||
stamp: 'Timbro',
|
||||
payment: 'Pagamento',
|
||||
phone: 'Telefono',
|
||||
text_field: 'Campo di Testo',
|
||||
signature_field: 'Campo di Firma',
|
||||
initials_field: 'Campo di Iniziali',
|
||||
date_field: 'Campo Data',
|
||||
number_field: 'Campo Numero',
|
||||
image_field: 'Campo Immagine',
|
||||
file_field: 'Campo File',
|
||||
select_field: 'Campo di Selezione',
|
||||
checkbox_field: 'Campo di Checkbox',
|
||||
multiple_field: 'Campo Selezione Multipla',
|
||||
radio_field: 'Campo di Gruppo Radio',
|
||||
cells_field: 'Campo Celle',
|
||||
stamp_field: 'Campo Timbro',
|
||||
payment_field: 'Campo Pagamento',
|
||||
phone_field: 'Campo Telefono',
|
||||
draw_a_text_field_on_the_page_with_a_mouse: 'Disegna un campo di testo sulla pagina con il mouse',
|
||||
drag_and_drop_any_other_field_type_on_the_page: 'Trascina e rilascia qualsiasi altro tipo di campo sulla pagina',
|
||||
click_on_the_field_type_above_to_start_drawing_it: 'Clicca sul tipo di campo sopra per iniziare a disegnarlo',
|
||||
please_draw_fields_to_prepare_the_document: 'Per favore, disegna i campi per preparare il documento.',
|
||||
only_pdf_and_images_are_supported: 'Sono supportati solo PDF e immagini.',
|
||||
unlock_sms_verified_phone_number_field_with_paid_plan_use_text_field_for_phone_numbers_without_verification: 'Sblocca il campo numero di telefono verificato tramite SMS con un piano a pagamento. Usa il campo di testo per numeri di telefono senza verifica.',
|
||||
available_only_in_pro: 'Disponibile solo in Pro',
|
||||
failed_to_download_files: 'Impossibile scaricare i file',
|
||||
signature_is_too_small_please_redraw: 'La firma è troppo piccola. Ridisegnala per favore.',
|
||||
please_add_fields_for_the_submitter_name_or_remove_the_submitter_name_if_not_needed: 'Aggiungi campi per {submitter_name} o rimuovi {submitter_name} se non necessario.',
|
||||
draw_field: 'Disegna il campo {field}',
|
||||
replace: 'Sostituisci',
|
||||
uploading_: 'Caricamento in corso...',
|
||||
add_document: 'Aggiungi',
|
||||
none: 'Nessuno',
|
||||
ssn: 'SSN',
|
||||
ein: 'EIN',
|
||||
email: 'Email',
|
||||
url: 'URL',
|
||||
zip: 'CAP',
|
||||
custom: 'Personalizzato',
|
||||
numbers_only: 'Solo numeri',
|
||||
letters_only: 'Solo lettere',
|
||||
regexp_validation: 'Validazione regexp',
|
||||
enter_pdf_password: 'Inserisci password PDF',
|
||||
wrong_password: 'Password errata',
|
||||
currency: 'Valuta',
|
||||
save_and_preview: 'Salva e Anteprima',
|
||||
preferences: 'Preferenze',
|
||||
available_in_pro: 'Disponibile in Pro',
|
||||
some_fields_are_missing_in_the_formula: 'Alcuni campi mancano nella formula.',
|
||||
learn_more: 'Scopri di più'
|
||||
}
|
||||
|
||||
const pt = {
|
||||
recurrent: 'Recurrente',
|
||||
one_off: 'Único',
|
||||
editable: 'Editável',
|
||||
search_field: 'Campo de busca',
|
||||
field_not_found: 'Campo não encontrado',
|
||||
clear: 'Limpar',
|
||||
type_value: 'Digite valor',
|
||||
add_all_required_fields_to_continue: 'Adicione todos os campos obrigatórios para continuar',
|
||||
uploaded_pdf_contains_form_fields_keep_or_remove_them: 'O PDF carregado contém campos. Manter ou removê-los?',
|
||||
keep: 'Manter',
|
||||
align: 'Alinhar',
|
||||
left: 'Esquerda',
|
||||
heading: 'Cabeçalho',
|
||||
validation: 'Validação',
|
||||
add_blank_page: 'Adicionar página em branco',
|
||||
right: 'Direita',
|
||||
center: 'Centro',
|
||||
with_logo: 'Com logotipo',
|
||||
description: 'Descrição',
|
||||
display_title: 'Título de exibição',
|
||||
signing_date: 'Data da Assinatura',
|
||||
unchecked: 'Não marcado',
|
||||
price: 'Preço',
|
||||
equal: 'Igual',
|
||||
not_equal: 'Não é igual',
|
||||
contains: 'Contém',
|
||||
does_not_contain: 'Não contém',
|
||||
not_empty: 'Não vazio',
|
||||
empty: 'Vazio',
|
||||
add_condition: 'Adicionar condição',
|
||||
select_field_: 'Selecionar campo...',
|
||||
select_value_: 'Selecionar valor...',
|
||||
remove_condition: 'Remover condição',
|
||||
condition: 'Condição',
|
||||
formula: 'Fórmula',
|
||||
edit: 'Editar',
|
||||
settings: 'Configurações',
|
||||
up: 'Para cima',
|
||||
down: 'Para baixo',
|
||||
set_signing_date: 'Definir data de assinatura',
|
||||
are_you_sure_: 'Tem certeza?',
|
||||
sign_yourself: 'Assine você mesmo',
|
||||
checked: 'Marcado',
|
||||
send: 'Enviar',
|
||||
remove: 'Remover',
|
||||
save: 'Salvar',
|
||||
cancel: 'Cancelar',
|
||||
or_add_field_without_drawing: 'Ou adicione campo sem desenhar',
|
||||
draw_field_on_the_document: 'Desenhar um campo no documento',
|
||||
click_to_upload: 'Clique para enviar',
|
||||
or_drag_and_drop_files: 'ou arraste e solte arquivos',
|
||||
uploading: 'Carregando',
|
||||
processing_: 'Processando...',
|
||||
add_pdf_documents_or_images: 'Adicionar documentos PDF ou imagens',
|
||||
add_documents_or_images: 'Adicionar documentos ou imagens',
|
||||
required: 'Obrigatório',
|
||||
default_value: 'Valor padrão',
|
||||
format: 'Formato',
|
||||
read_only: 'Somente leitura',
|
||||
page: 'Página',
|
||||
draw_new_area: 'Desenhar nova área',
|
||||
copy_to_all_pages: 'Copiar para todas as páginas',
|
||||
add_option: 'Adicionar opção',
|
||||
option: 'Opção',
|
||||
first_party: 'Primeira Parte',
|
||||
second_party: 'Segunda Parte',
|
||||
third_party: 'Terceira Parte',
|
||||
fourth_party: 'Quarta Parte',
|
||||
fifth_party: 'Quinta Parte',
|
||||
sixth_party: 'Sexta Parte',
|
||||
seventh_party: 'Sétima Parte',
|
||||
eighth_party: 'Oitava Parte',
|
||||
ninth_party: 'Nona Parte',
|
||||
tenth_party: 'Décima Parte',
|
||||
eleventh_party: 'Décima Primeira Parte',
|
||||
twelfth_party: 'Décima Segunda Parte',
|
||||
thirteenth_party: 'Décima Terceira Parte',
|
||||
fourteenth_party: 'Décima Quarta Parte',
|
||||
fifteenth_party: 'Décima Quinta Parte',
|
||||
sixteenth_party: 'Décima Sexta Parte',
|
||||
seventeenth_party: 'Décima Sétima Parte',
|
||||
eighteenth_party: 'Décima Oitava Parte',
|
||||
nineteenth_party: 'Décima Nona Parte',
|
||||
twentieth_party: 'Vigésima Parte',
|
||||
draw: 'Desenhar',
|
||||
add: 'Adicionar',
|
||||
text: 'Texto',
|
||||
signature: 'Assinatura',
|
||||
initials: 'Rúbrica',
|
||||
date: 'Data',
|
||||
number: 'Número',
|
||||
image: 'Imagem',
|
||||
file: 'Arquivo',
|
||||
select: 'Selecionar',
|
||||
checkbox: 'Caixa',
|
||||
multiple: 'Múltiplo',
|
||||
radio: 'Rádio',
|
||||
cells: 'Células',
|
||||
stamp: 'Carimbo',
|
||||
payment: 'Pagamento',
|
||||
phone: 'Telefone',
|
||||
text_field: 'Campo de Texto',
|
||||
signature_field: 'Campo de Assinatura',
|
||||
initials_field: 'Campo de Rúbrica',
|
||||
date_field: 'Campo de Data',
|
||||
number_field: 'Campo de Número',
|
||||
image_field: 'Campo de Imagem',
|
||||
file_field: 'Campo de Arquivo',
|
||||
select_field: 'Campo de Seleção',
|
||||
checkbox_field: 'Campo de Caixa',
|
||||
multiple_field: 'Campo de Opção Múltipla',
|
||||
radio_field: 'Campo de Grupo Rádio',
|
||||
cells_field: 'Campo de Células',
|
||||
stamp_field: 'Campo de Carimbo',
|
||||
payment_field: 'Campo de Pagamento',
|
||||
phone_field: 'Campo de Telefone',
|
||||
draw_a_text_field_on_the_page_with_a_mouse: 'Desenhar um campo de texto na página com o mouse',
|
||||
drag_and_drop_any_other_field_type_on_the_page: 'Arraste e solte qualquer outro tipo de campo na página',
|
||||
click_on_the_field_type_above_to_start_drawing_it: 'Clique no tipo de campo acima para começar a desenhá-lo',
|
||||
please_draw_fields_to_prepare_the_document: 'Por favor, desenhe os campos para preparar o documento.',
|
||||
only_pdf_and_images_are_supported: 'Apenas PDFs e imagens são suportados.',
|
||||
unlock_sms_verified_phone_number_field_with_paid_plan_use_text_field_for_phone_numbers_without_verification: 'Desbloqueie o campo de número de telefone verificado por SMS com um plano pago. Use o campo de texto para números de telefone sem verificação.',
|
||||
available_only_in_pro: 'Disponível apenas no Pro',
|
||||
failed_to_download_files: 'Falha ao baixar arquivos',
|
||||
signature_is_too_small_please_redraw: 'A assinatura é muito pequena. Por favor, redesenhe.',
|
||||
please_add_fields_for_the_submitter_name_or_remove_the_submitter_name_if_not_needed: 'Adicione campos para {submitter_name} ou remova {submitter_name} se não for necessário.',
|
||||
draw_field: 'Desenhar campo {field}',
|
||||
replace: 'Substituir',
|
||||
uploading_: 'Carregando...',
|
||||
add_document: 'Enviar',
|
||||
any: 'Qualquer',
|
||||
drawn: 'Desenhado',
|
||||
typed: 'Digitado',
|
||||
none: 'Nenhum',
|
||||
ssn: 'SSN',
|
||||
ein: 'EIN',
|
||||
email: 'Email',
|
||||
url: 'URL',
|
||||
zip: 'ZIP',
|
||||
custom: 'Personalizado',
|
||||
numbers_only: 'Somente números',
|
||||
letters_only: 'Somente letras',
|
||||
regexp_validation: 'Validação de expressão regular',
|
||||
enter_pdf_password: 'Digite a senha do PDF',
|
||||
wrong_password: 'Senha incorreta',
|
||||
currency: 'Moeda',
|
||||
save_and_preview: 'Salvar e Pré-visualizar',
|
||||
preferences: 'Preferências',
|
||||
available_in_pro: 'Disponível no Pro',
|
||||
some_fields_are_missing_in_the_formula: 'Faltam alguns campos na fórmula.',
|
||||
learn_more: 'Saiba mais'
|
||||
}
|
||||
|
||||
const fr = {
|
||||
recurrent: 'Récurrent',
|
||||
one_off: 'Ponctuel',
|
||||
editable: 'Éditable',
|
||||
search_field: 'Champ de recherche',
|
||||
field_not_found: 'Champ non trouvé',
|
||||
clear: 'Effacer',
|
||||
type_value: 'Tapez la valeur',
|
||||
add_all_required_fields_to_continue: 'Ajoutez tous les champs obligatoires pour continuer',
|
||||
uploaded_pdf_contains_form_fields_keep_or_remove_them: 'Le PDF téléchargé contient des champs. Les conserver ou les supprimer?',
|
||||
keep: 'Conserver',
|
||||
align: 'Aligner',
|
||||
left: 'Gauche',
|
||||
heading: 'En-tête',
|
||||
validation: 'Validation',
|
||||
add_blank_page: 'Ajouter une page blanche',
|
||||
right: 'Droite',
|
||||
add_condition: 'Ajouter une condition',
|
||||
center: 'Centre',
|
||||
with_logo: 'Avec logo',
|
||||
description: 'Description',
|
||||
display_title: "Titre d'affichage",
|
||||
unchecked: 'Non coché',
|
||||
price: 'Prix',
|
||||
equal: 'Égal',
|
||||
not_equal: 'Non égal',
|
||||
contains: 'Contient',
|
||||
signing_date: 'Date de signature',
|
||||
does_not_contain: 'Ne contient pas',
|
||||
not_empty: 'Non vide',
|
||||
empty: 'Vide',
|
||||
select_field_: 'Sélectionner un champ...',
|
||||
select_value_: 'Sélectionner une valeur...',
|
||||
remove_condition: 'Supprimer la condition',
|
||||
condition: 'Condition',
|
||||
formula: 'Formule',
|
||||
edit: 'Éditer',
|
||||
settings: 'Paramètres',
|
||||
up: 'Haut',
|
||||
down: 'Bas',
|
||||
set_signing_date: 'Définir la date de signature',
|
||||
are_you_sure_: 'Êtes-vous sûr?',
|
||||
sign_yourself: 'Signez-vous',
|
||||
checked: 'Coché',
|
||||
send: 'Envoyer',
|
||||
remove: 'Supprimer',
|
||||
save: 'Enregistrer',
|
||||
cancel: 'Annuler',
|
||||
draw_field_on_the_document: 'Dessiner un champ sur le document',
|
||||
click_to_upload: 'Cliquez pour télécharger',
|
||||
or_drag_and_drop_files: 'ou faites glisser-déposer des fichiers',
|
||||
uploading: 'Téléchargement en cours',
|
||||
processing_: 'Traitement en cours...',
|
||||
add_pdf_documents_or_images: 'Ajoutez des documents PDF ou des images',
|
||||
add_documents_or_images: 'Ajoutez des documents ou des images',
|
||||
required: 'Requis',
|
||||
default_value: 'Valeur par défaut',
|
||||
format: 'Format',
|
||||
read_only: 'Lecture seule',
|
||||
page: 'Page',
|
||||
draw_new_area: 'Dessiner une nouvelle zone',
|
||||
copy_to_all_pages: 'Copier sur toutes les pages',
|
||||
add_option: 'Ajouter une option',
|
||||
option: 'Option',
|
||||
first_party: 'Première partie',
|
||||
second_party: 'Deuxième partie',
|
||||
third_party: 'Troisième partie',
|
||||
fourth_party: 'Quatrième partie',
|
||||
fifth_party: 'Cinquième partie',
|
||||
sixth_party: 'Sixième partie',
|
||||
seventh_party: 'Septième partie',
|
||||
eighth_party: 'Huitième partie',
|
||||
ninth_party: 'Neuvième partie',
|
||||
tenth_party: 'Dixième partie',
|
||||
eleventh_party: 'Onzième Parti',
|
||||
twelfth_party: 'Douzième Parti',
|
||||
thirteenth_party: 'Treizième Parti',
|
||||
fourteenth_party: 'Quatorzième Parti',
|
||||
fifteenth_party: 'Quinzième Parti',
|
||||
sixteenth_party: 'Seizième Parti',
|
||||
seventeenth_party: 'Dix-septième Parti',
|
||||
eighteenth_party: 'Dix-huitième Parti',
|
||||
nineteenth_party: 'Dix-Neuvième Fête',
|
||||
twentieth_party: 'Vingtième Fête',
|
||||
draw: 'Dessiner',
|
||||
add: 'Ajouter',
|
||||
or_add_field_without_drawing: 'Ou ajoutez un champ sans dessiner',
|
||||
text: 'Texte',
|
||||
signature: 'Signature',
|
||||
initials: 'Initiales',
|
||||
date: 'Date',
|
||||
number: 'Numéro',
|
||||
image: 'Image',
|
||||
file: 'Fichier',
|
||||
select: 'Choisir',
|
||||
checkbox: 'Coche',
|
||||
multiple: 'Multiple',
|
||||
radio: 'Radio',
|
||||
cells: 'Cellules',
|
||||
stamp: 'Tampon',
|
||||
payment: 'Paiement',
|
||||
phone: 'Téléphone',
|
||||
text_field: 'Champ de Texte',
|
||||
signature_field: 'Champ de Signature',
|
||||
initials_field: "Champ d'Initiales",
|
||||
date_field: 'Champ de Date',
|
||||
number_field: 'Champ de Numéro',
|
||||
image_field: "Champ d'Image",
|
||||
file_field: 'Champ de Fichier',
|
||||
select_field: 'Champ de Choix',
|
||||
checkbox_field: 'Champ de Coche',
|
||||
multiple_field: 'Champ de Choix Multiple',
|
||||
radio_field: 'Champ de Groupe Radio',
|
||||
cells_field: 'Champ de Cellules',
|
||||
stamp_field: 'Champ de Tampon',
|
||||
payment_field: 'Champ de Paiement',
|
||||
phone_field: 'Champ de Téléphone',
|
||||
draw_a_text_field_on_the_page_with_a_mouse: 'Dessinez un champ texte sur la page avec une souris',
|
||||
drag_and_drop_any_other_field_type_on_the_page: 'Glissez et déposez tout autre type de champ sur la page',
|
||||
click_on_the_field_type_above_to_start_drawing_it: 'Cliquez sur le type de champ ci-dessus pour commencer à le dessiner',
|
||||
please_draw_fields_to_prepare_the_document: 'Veuillez dessiner les champs pour préparer le document.',
|
||||
only_pdf_and_images_are_supported: 'Seuls les PDF et les images sont pris en charge.',
|
||||
unlock_sms_verified_phone_number_field_with_paid_plan_use_text_field_for_phone_numbers_without_verification: 'Débloquez le champ de numéro de téléphone vérifié par SMS avec un plan payant. Utilisez un champ texte pour les numéros de téléphone sans vérification.',
|
||||
available_only_in_pro: 'Disponible uniquement en Pro',
|
||||
failed_to_download_files: 'Échec du téléchargement des fichiers',
|
||||
signature_is_too_small_please_redraw: 'La signature est trop petite. Veuillez la redessiner.',
|
||||
please_add_fields_for_the_submitter_name_or_remove_the_submitter_name_if_not_needed: 'Veuillez ajouter des champs pour {submitter_name} ou retirer {submitter_name} si ce n\'est pas nécessaire.',
|
||||
draw_field: 'Dessiner le champ {field}',
|
||||
replace: 'Remplacer',
|
||||
uploading_: 'Téléchargement en cours...',
|
||||
add_document: 'Télécharger',
|
||||
any: 'Tout',
|
||||
drawn: 'Dessiné',
|
||||
typed: 'Tapé',
|
||||
none: 'Aucun',
|
||||
ssn: 'SSN',
|
||||
ein: 'EIN',
|
||||
email: 'E-mail',
|
||||
url: 'URL',
|
||||
zip: 'ZIP',
|
||||
custom: 'Personnalisé',
|
||||
numbers_only: 'Chiffres uniquement',
|
||||
letters_only: 'Lettres uniquement',
|
||||
regexp_validation: 'Validation par expression régulière',
|
||||
enter_pdf_password: 'Entrez le mot de passe PDF',
|
||||
wrong_password: 'Mot de passe incorrect',
|
||||
currency: 'Devise',
|
||||
save_and_preview: 'Enregistrer et prévisualiser',
|
||||
preferences: 'Préférences',
|
||||
available_in_pro: 'Disponible en version Pro',
|
||||
some_fields_are_missing_in_the_formula: 'Certains champs manquent dans la formule.',
|
||||
learn_more: 'En savoir plus'
|
||||
}
|
||||
|
||||
const de = {
|
||||
wiederkehrend: 'Wiederkehrend',
|
||||
einmalig: 'Einmalig',
|
||||
editable: 'Bearbeitbar',
|
||||
search_field: 'Suchfeld',
|
||||
field_not_found: 'Feld nicht gefunden',
|
||||
clear: 'Löschen',
|
||||
type_value: 'Wert eingeben',
|
||||
add_all_required_fields_to_continue: 'Fügen Sie alle erforderlichen Felder hinzu, um fortzufahren',
|
||||
uploaded_pdf_contains_form_fields_keep_or_remove_them: 'Das hochgeladene PDF enthält Formularfelder. Behalten oder entfernen?',
|
||||
keep: 'Behalten',
|
||||
align: 'Ausrichten',
|
||||
left: 'Links',
|
||||
heading: 'Überschrift',
|
||||
validation: 'Validierung',
|
||||
add_blank_page: 'Leere Seite hinzufügen',
|
||||
right: 'Rechts',
|
||||
add_condition: 'Bedingung hinzufügen',
|
||||
center: 'Zentriert',
|
||||
with_logo: 'Mit Logo',
|
||||
description: 'Beschreibung',
|
||||
display_title: 'Anzeigetitel',
|
||||
unchecked: 'Nicht überprüft',
|
||||
price: 'Preis',
|
||||
equal: 'Gleich',
|
||||
not_equal: 'Ungleich',
|
||||
contains: 'Enthält',
|
||||
does_not_contain: 'Enthält nicht',
|
||||
not_empty: 'Nicht leer',
|
||||
empty: 'Leer',
|
||||
signing_date: 'Signaturdatum',
|
||||
select_field_: 'Feld auswählen...',
|
||||
select_value_: 'Wert auswählen...',
|
||||
remove_condition: 'Bedingung entfernen',
|
||||
condition: 'Bedingung',
|
||||
formula: 'Formel',
|
||||
edit: 'Bearbeiten',
|
||||
settings: 'Einstellungen',
|
||||
up: 'Nach oben',
|
||||
down: 'Nach unten',
|
||||
set_signing_date: 'Unterschriftdatum festlegen',
|
||||
are_you_sure_: 'Bist du sicher?',
|
||||
sign_yourself: 'Unterschreiben Sie selbst',
|
||||
checked: 'Ausgewählt',
|
||||
send: 'Senden',
|
||||
remove: 'Entfernen',
|
||||
save: 'Speichern',
|
||||
cancel: 'Abbrechen',
|
||||
draw_field_on_the_document: 'Ein Feld auf dem Dokument zeichnen',
|
||||
click_to_upload: 'Klicken Sie zum Hochladen',
|
||||
or_drag_and_drop_files: 'oder Dateien hierher ziehen',
|
||||
uploading: 'Hochladen',
|
||||
processing_: 'Verarbeitung...',
|
||||
add_pdf_documents_or_images: 'PDF-Dokumente oder Bilder hinzufügen',
|
||||
add_documents_or_images: 'Dokumente oder Bilder hinzufügen',
|
||||
required: 'Erforderlich',
|
||||
default_value: 'Standardwert',
|
||||
format: 'Format',
|
||||
read_only: 'Nur lesen',
|
||||
page: 'Seite',
|
||||
draw_new_area: 'Neuen Bereich zeichnen',
|
||||
copy_to_all_pages: 'Auf alle Seiten kopieren',
|
||||
add_option: 'Option hinzufügen',
|
||||
option: 'Option',
|
||||
first_party: 'Erste Partei',
|
||||
second_party: 'Zweite Partei',
|
||||
third_party: 'Dritte Partei',
|
||||
fourth_party: 'Vierte Partei',
|
||||
fifth_party: 'Fünfte Partei',
|
||||
sixth_party: 'Sechste Partei',
|
||||
seventh_party: 'Siebte Partei',
|
||||
eighth_party: 'Achte Partei',
|
||||
ninth_party: 'Neunte Partei',
|
||||
tenth_party: 'Zehnte Partei',
|
||||
eleventh_party: 'Elfte Partei',
|
||||
twelfth_party: 'Zwölfte Partei',
|
||||
thirteenth_party: 'Dreizehnte Partei',
|
||||
fourteenth_party: 'Vierzehnte Partei',
|
||||
fifteenth_party: 'Fünfzehnte Partei',
|
||||
sixteenth_party: 'Sechzehnte Partei',
|
||||
seventeenth_party: 'Siebzehnte Partei',
|
||||
eighteenth_party: 'Achtzehnte Partei',
|
||||
nineteenth_party: 'Neunzehnte Party',
|
||||
twentieth_party: 'Zwanzigste Party',
|
||||
draw: 'Zeichnen',
|
||||
add: 'Hinzufügen',
|
||||
or_add_field_without_drawing: 'Oder Feld ohne Zeichnung hinzufügen',
|
||||
text: 'Text',
|
||||
signature: 'Unterschrift',
|
||||
initials: 'Initialen',
|
||||
date: 'Datum',
|
||||
number: 'Nummer',
|
||||
image: 'Bild',
|
||||
file: 'Datei',
|
||||
select: 'Auswählen',
|
||||
checkbox: 'Checkbox',
|
||||
multiple: 'Mehrere',
|
||||
radio: 'Radio',
|
||||
cells: 'Zellen',
|
||||
stamp: 'Stempel',
|
||||
payment: 'Zahlung',
|
||||
phone: 'Telefon',
|
||||
text_field: 'Textfeld',
|
||||
signature_field: 'Unterschriftsfeld',
|
||||
initials_field: 'Initialenfeld',
|
||||
date_field: 'Datumsfeld',
|
||||
number_field: 'Zahlenfeld',
|
||||
image_field: 'Bildfeld',
|
||||
file_field: 'Dateifeld',
|
||||
select_field: 'Auswahlfeld',
|
||||
checkbox_field: 'Checkbox-Feld',
|
||||
multiple_field: 'Mehrfach-Auswahlfeld',
|
||||
radio_field: 'Radio-Gruppenfeld',
|
||||
cells_field: 'Zellenfeld',
|
||||
stamp_field: 'Stempelfeld',
|
||||
payment_field: 'Zahlungsfeld',
|
||||
phone_field: 'Telefonfeld',
|
||||
draw_a_text_field_on_the_page_with_a_mouse: 'Textfeld auf der Seite mit der Maus zeichnen',
|
||||
drag_and_drop_any_other_field_type_on_the_page: 'Ziehe und lasse einen anderen Feldtyp auf die Seite fallen',
|
||||
click_on_the_field_type_above_to_start_drawing_it: 'Klicke auf den Feldtyp oben, um mit dem Zeichnen zu beginnen',
|
||||
please_draw_fields_to_prepare_the_document: 'Bitte zeichne Felder, um das Dokument vorzubereiten.',
|
||||
only_pdf_and_images_are_supported: 'Nur PDFs und Bilder werden unterstützt.',
|
||||
unlock_sms_verified_phone_number_field_with_paid_plan_use_text_field_for_phone_numbers_without_verification: 'Schalte das SMS-verifizierte Telefonnummernfeld mit einem kostenpflichtigen Plan frei. Verwende das Textfeld für Telefonnummern ohne Verifizierung.',
|
||||
available_only_in_pro: 'Nur in Pro verfügbar',
|
||||
failed_to_download_files: 'Fehler beim Herunterladen der Dateien',
|
||||
signature_is_too_small_please_redraw: 'Die Unterschrift ist zu klein. Bitte erneut zeichnen.',
|
||||
please_add_fields_for_the_submitter_name_or_remove_the_submitter_name_if_not_needed: 'Bitte füge Felder für {submitter_name} hinzu oder entferne {submitter_name}, falls nicht erforderlich.',
|
||||
draw_field: 'Feld {field} zeichnen',
|
||||
replace: 'Ersetzen',
|
||||
uploading_: 'Hochladen...',
|
||||
add_document: 'Hochladen',
|
||||
any: 'Jede',
|
||||
drawn: 'Gezeichnet',
|
||||
typed: 'Getippt',
|
||||
none: 'Keine',
|
||||
ssn: 'SSN',
|
||||
ein: 'EIN',
|
||||
email: 'E-Mail',
|
||||
url: 'URL',
|
||||
zip: 'ZIP',
|
||||
custom: 'Benutzerdefiniert',
|
||||
numbers_only: 'Nur Zahlen',
|
||||
letters_only: 'Nur Buchstaben',
|
||||
regexp_validation: 'Regulärer Ausdruck Überprüfung',
|
||||
enter_pdf_password: 'Geben Sie das PDF-Passwort ein',
|
||||
wrong_password: 'Falsches Passwort',
|
||||
currency: 'Währung',
|
||||
save_and_preview: 'Speichern und Vorschau',
|
||||
preferences: 'Einstellungen',
|
||||
available_in_pro: 'In Pro verfügbar',
|
||||
some_fields_are_missing_in_the_formula: 'Einige Felder fehlen in der Formel.',
|
||||
learn_more: 'Erfahren Sie mehr'
|
||||
}
|
||||
|
||||
export { en, es, it, pt, fr, de }
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div v-if="selectedSheetIndex === null && spreadsheet">
|
||||
<form @submit.prevent="[selectedSheetIndex = $refs.selectWorksheet.value, buildDefaultMappings()]">
|
||||
<label class="label">
|
||||
Select Worksheet
|
||||
{{ t('select_worksheet') }}
|
||||
</label>
|
||||
<select
|
||||
ref="selectWorksheet"
|
||||
@@ -18,7 +18,7 @@
|
||||
</option>
|
||||
</select>
|
||||
<button class="base-button mt-4 w-full">
|
||||
Open
|
||||
{{ t('open') }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -36,10 +36,10 @@
|
||||
</div>
|
||||
<div class="flex">
|
||||
<div class="relative w-full py-2 px-2 text-sm">
|
||||
Recipient field
|
||||
{{ t('recipient_field') }}
|
||||
</div>
|
||||
<div class="relative w-full py-2 pl-4 text-sm">
|
||||
Spreadsheet column
|
||||
{{ t('spreadsheet_column') }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@@ -50,6 +50,7 @@
|
||||
<div class="flex">
|
||||
<select
|
||||
class="base-select !select-sm !h-10"
|
||||
:class="{ '!text-gray-300': !mapping.field_name }"
|
||||
required
|
||||
@change="mapping.field_name = $event.target.value"
|
||||
>
|
||||
@@ -58,13 +59,14 @@
|
||||
value=""
|
||||
:selected="!mapping.field_name"
|
||||
>
|
||||
Select Field
|
||||
{{ t('select_field') }}
|
||||
</option>
|
||||
<option
|
||||
v-for="(field, index) in selectFieldsForSubmitter(submitter)"
|
||||
:key="index"
|
||||
:value="field.name"
|
||||
:selected="mapping.field_name === field.name"
|
||||
class="text-base-content"
|
||||
>
|
||||
{{ field.name }}
|
||||
</option>
|
||||
@@ -75,6 +77,7 @@
|
||||
<div class="w-full relative">
|
||||
<select
|
||||
class="base-select !select-sm !h-10"
|
||||
:class="{ '!text-gray-300': !mapping.column_index && mapping.column_index != 0 }"
|
||||
required
|
||||
@change="mapping.column_index = parseInt($event.target.value)"
|
||||
>
|
||||
@@ -83,7 +86,7 @@
|
||||
value=""
|
||||
:selected="mapping.column_index == null"
|
||||
>
|
||||
Select Column
|
||||
{{ t('select_column') }}
|
||||
</option>
|
||||
<template
|
||||
v-for="(column, index) in columns"
|
||||
@@ -93,6 +96,7 @@
|
||||
v-if="column"
|
||||
:value="index"
|
||||
:selected="index === mapping.column_index"
|
||||
class="text-base-content"
|
||||
>
|
||||
{{ column }}
|
||||
</option>
|
||||
@@ -118,8 +122,8 @@
|
||||
</div>
|
||||
<div class="flex items-center pl-1">
|
||||
<span
|
||||
class="tooltip tooltip-top"
|
||||
data-tip="Remove"
|
||||
class="tooltip tooltip-left"
|
||||
:data-tip="t('remove')"
|
||||
>
|
||||
<button
|
||||
:disabled="mappings.filter((m) => m.submitter_uuid === submitter.uuid).length < 2"
|
||||
@@ -138,7 +142,7 @@
|
||||
@click.prevent="addMapping(submitter)"
|
||||
>
|
||||
<IconPlus class="w-4 h-4" />
|
||||
New Field Mapping
|
||||
{{ t('new_field_mapping') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -152,7 +156,7 @@
|
||||
<div
|
||||
class="px-3 border-y py-2 border-base-300 text-center w-full text-sm font-semibold"
|
||||
>
|
||||
Total entries: {{ submissionsData.length }}
|
||||
{{ t('total_entries') }}: {{ submissionsData.length }}
|
||||
<template v-if="multitenant && submissionsData.length >= 1000">
|
||||
/ 1000
|
||||
</template>
|
||||
@@ -187,10 +191,10 @@
|
||||
<div
|
||||
class="font-medium text-lg mb-1"
|
||||
>
|
||||
Upload CSV or XLSX Spreadsheet
|
||||
{{ t('upload_csv_or_xlsx_spreadsheet') }}
|
||||
</div>
|
||||
<div class="text-sm">
|
||||
<span class="font-medium">Click to Upload</span> or drag and drop files.
|
||||
<span class="font-medium">{{ t('click_to_upload') }}</span> {{ t('or_drag_and_drop_files') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -210,11 +214,11 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="text-center mt-2">
|
||||
Or <a
|
||||
{{ t('or') }} <a
|
||||
:download="`${template.name}.csv`"
|
||||
:href="`data:text/csv;base64,${csvBase64}`"
|
||||
class="link font-medium"
|
||||
>download</a> a spreadsheet to fill and import
|
||||
>{{ t('download') }}</a> {{ t('a_sample_spreadsheet_to_fill_and_import') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -248,6 +252,11 @@ export default {
|
||||
type: String,
|
||||
required: false,
|
||||
default: ''
|
||||
},
|
||||
i18n: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
data () {
|
||||
@@ -276,7 +285,7 @@ export default {
|
||||
submittersIndex[mapping.submitter_uuid][mapping.field_name.toLowerCase()] = row[mapping.column_index]
|
||||
}
|
||||
|
||||
const fieldType = this.fieldTypesIndex[mapping.submitter_uuid][mapping.field_name]
|
||||
const fieldType = this.fieldTypesIndex[mapping.submitter_uuid]?.[mapping.field_name]
|
||||
|
||||
if (fieldType && fieldType !== 'phone') {
|
||||
submittersIndex[mapping.submitter_uuid].fields.push({
|
||||
@@ -354,6 +363,9 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
t (key) {
|
||||
return this.i18n[key] || key
|
||||
},
|
||||
onDropFiles (e) {
|
||||
this.uploadFile(e.dataTransfer.files[0])
|
||||
},
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
@click="closeDropdown"
|
||||
>
|
||||
<div
|
||||
v-if="!('price_id' in field.preferences)"
|
||||
class="py-1.5 px-1 relative"
|
||||
@click.stop
|
||||
>
|
||||
@@ -43,7 +44,7 @@
|
||||
class="absolute -top-1 left-2.5 px-1 h-4"
|
||||
style="font-size: 8px"
|
||||
>
|
||||
Currency
|
||||
{{ t('currency') }}
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
@@ -58,6 +59,13 @@
|
||||
class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0"
|
||||
@blur="save"
|
||||
>
|
||||
<input
|
||||
v-else-if="'price_id' in field.preferences"
|
||||
v-model="field.preferences.price_id"
|
||||
placeholder="Price ID: price_XXXXX"
|
||||
class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0"
|
||||
@blur="save"
|
||||
>
|
||||
<input
|
||||
v-else
|
||||
v-model="field.preferences.price"
|
||||
@@ -72,8 +80,25 @@
|
||||
class="absolute -top-1 left-2.5 px-1 h-4"
|
||||
style="font-size: 8px"
|
||||
>
|
||||
Price
|
||||
{{ t('price') }}
|
||||
</label>
|
||||
<div class="flex items-center justify-center">
|
||||
<a
|
||||
href="#"
|
||||
class="hover:underline"
|
||||
style="font-size: 11px"
|
||||
:class="{'underline': !('price_id' in field.preferences)}"
|
||||
@click="delete field.preferences.price_id"
|
||||
>{{ t('one_off') }}</a>
|
||||
<span class="h-2.5 border-l border-base-content mx-1" />
|
||||
<a
|
||||
href="#"
|
||||
class="hover:underline"
|
||||
style="font-size: 11px"
|
||||
:class="{'underline': ('price_id' in field.preferences)}"
|
||||
@click="field.preferences.price_id ??= ''"
|
||||
>{{ t('recurrent') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="!isConnected || isOauthSuccess"
|
||||
@@ -153,12 +178,15 @@
|
||||
<a
|
||||
v-if="!isConnected"
|
||||
class="block link text-center mt-1"
|
||||
href="https://www.docuseal.co/blog/accept-payments-and-request-signatures-with-ease"
|
||||
href="https://www.docuseal.com/blog/accept-payments-and-request-signatures-with-ease"
|
||||
target="_blank"
|
||||
data-turbo="false"
|
||||
>Learn more</a>
|
||||
>{{ t('learn_more') }}</a>
|
||||
</div>
|
||||
<li class="mb-1">
|
||||
<li
|
||||
v-if="!('price_id' in field.preferences)"
|
||||
class="mb-1"
|
||||
>
|
||||
<label
|
||||
class="label-text cursor-pointer text-center w-full flex items-center"
|
||||
@click="$emit('click-formula')"
|
||||
|
||||
@@ -90,10 +90,10 @@ export default {
|
||||
})
|
||||
} else if (resp.status === 422) {
|
||||
resp.json().then((data) => {
|
||||
if (data.error === 'PDF encrypted') {
|
||||
if (data.status === 'pdf_encrypted') {
|
||||
const formData = new FormData(this.$refs.form)
|
||||
|
||||
formData.append('password', prompt('Enter PDF password'))
|
||||
formData.append('password', prompt(this.t('enter_pdf_password')))
|
||||
|
||||
this.baseFetch(`/templates/${this.templateId}/documents`, {
|
||||
method: 'POST',
|
||||
@@ -103,7 +103,7 @@ export default {
|
||||
this.$emit('success', await resp.json())
|
||||
this.$refs.input.value = ''
|
||||
} else {
|
||||
alert('Wrong password')
|
||||
alert(this.t('wrong_password'))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class GeneratePreviewImagesJob
|
||||
include Sidekiq::Job
|
||||
|
||||
sidekiq_options queue: :images
|
||||
|
||||
def perform(params = {})
|
||||
attachment = ActiveStorage::Attachment.find(params['attachment_id'])
|
||||
|
||||
max_page = [attachment.metadata['pdf']['number_of_pages'].to_i - 1,
|
||||
Templates::ProcessDocument::MAX_NUMBER_OF_PAGES_PROCESSED].min
|
||||
|
||||
Templates::ProcessDocument.generate_document_preview_images(attachment, attachment.download, (1..max_page),
|
||||
concurrency: 1)
|
||||
end
|
||||
end
|
||||
@@ -6,6 +6,8 @@ class ProcessSubmitterCompletionJob
|
||||
def perform(params = {})
|
||||
submitter = Submitter.find(params['submitter_id'])
|
||||
|
||||
create_completed_submitter!(submitter)
|
||||
|
||||
is_all_completed = !submitter.submission.submitters.exists?(completed_at: nil)
|
||||
|
||||
if !is_all_completed && submitter.submission.submitters_order_preserved?
|
||||
@@ -24,36 +26,52 @@ class ProcessSubmitterCompletionJob
|
||||
enqueue_completed_emails(submitter)
|
||||
end
|
||||
|
||||
create_completed_documents!(submitter)
|
||||
|
||||
enqueue_completed_webhooks(submitter, is_all_completed:)
|
||||
end
|
||||
|
||||
def enqueue_completed_webhooks(submitter, is_all_completed: false)
|
||||
webhook_config = Accounts.load_webhook_config(submitter.account)
|
||||
def create_completed_submitter!(submitter)
|
||||
completed_submitter = CompletedSubmitter.find_or_initialize_by(submitter_id: submitter.id)
|
||||
|
||||
if webhook_config
|
||||
SendFormCompletedWebhookRequestJob.perform_async({ 'submitter_id' => submitter.id,
|
||||
'encrypted_config_id' => webhook_config.id })
|
||||
end
|
||||
return completed_submitter if completed_submitter.persisted?
|
||||
|
||||
webhook_urls = submitter.account.webhook_urls
|
||||
submission = submitter.submission
|
||||
|
||||
webhook_urls = webhook_urls.where(
|
||||
Arel::Table.new(:webhook_urls)[:events].matches('%"form.completed"%')
|
||||
).or(
|
||||
webhook_urls.where(
|
||||
Arel::Table.new(:webhook_urls)[:events].matches('%"submission.completed"%')
|
||||
)
|
||||
completed_submitter.assign_attributes(
|
||||
submission_id: submitter.submission_id,
|
||||
account_id: submission.account_id,
|
||||
template_id: submission.template_id,
|
||||
source: submission.source,
|
||||
sms_count: submitter.submission_events.where(event_type: %w[send_sms send_2fa_sms]).count,
|
||||
completed_at: submitter.completed_at
|
||||
)
|
||||
|
||||
webhook_urls.each do |webhook|
|
||||
completed_submitter.save!
|
||||
|
||||
completed_submitter
|
||||
rescue ActiveRecord::RecordNotUnique
|
||||
retry
|
||||
end
|
||||
|
||||
def create_completed_documents!(submitter)
|
||||
submitter.documents.filter_map do |attachment|
|
||||
next if attachment.metadata['sha256'].blank?
|
||||
|
||||
CompletedDocument.find_or_create_by!(sha256: attachment.metadata['sha256'], submitter_id: submitter.id)
|
||||
end
|
||||
end
|
||||
|
||||
def enqueue_completed_webhooks(submitter, is_all_completed: false)
|
||||
WebhookUrls.for_account_id(submitter.account_id, %w[form.completed submission.completed]).each do |webhook|
|
||||
if webhook.events.include?('form.completed')
|
||||
SendFormCompletedWebhookRequestJob.perform_async({ 'submitter_id' => submitter.id,
|
||||
'webhook_url_id' => webhook.id })
|
||||
SendFormCompletedWebhookRequestJob.perform_async('submitter_id' => submitter.id,
|
||||
'webhook_url_id' => webhook.id)
|
||||
end
|
||||
|
||||
if webhook.events.include?('submission.completed') && is_all_completed
|
||||
SendSubmissionCompletedWebhookRequestJob.perform_async({ 'submission_id' => submitter.submission_id,
|
||||
'webhook_url_id' => webhook.id })
|
||||
SendSubmissionCompletedWebhookRequestJob.perform_async('submission_id' => submitter.submission_id,
|
||||
'webhook_url_id' => webhook.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user