Compare commits

...
84 Commits
Author SHA1 Message Date
Alex Turchyn 1f89accac3 Merge from docusealco/wip 2026-06-01 14:58:06 +03:00
Pete Matsyburka 9fcaef4cf7 use new_from_memory_copy 2026-05-31 14:28:35 +03:00
Pete Matsyburka d5738a0631 disable variant_processor 2026-05-31 12:51:52 +03:00
Pete Matsyburka 8bf7a1f95a adjust image detect fields 2026-05-31 09:20:51 +03:00
Pete Matsyburka 04cf36891e fix complete button press enter 2026-05-31 09:09:57 +03:00
Pete Matsyburka b2d9948c30 remove active storage analyzers 2026-05-31 08:06:52 +03:00
Pete Matsyburka a2c9ac1707 remove image_processing 2026-05-30 21:37:00 +03:00
Pete Matsyburka 46a6bd0108 fix stamp 2026-05-30 12:04:23 +03:00
Pete Matsyburka 5f069e7a40 use load vips 2026-05-30 07:49:40 +03:00
Pete Matsyburka ff57e5c6ae fix pdfa 2026-05-29 15:31:32 +03:00
Alex Turchyn f65b6e2d76 add confirm prompt for template upload via URL 2026-05-29 09:29:21 +03:00
Pete Matsyburka 58fd180ae0 update gem 2026-05-29 08:04:53 +03:00
Pete Matsyburka cd6503c4c3 adjust image size 2026-05-28 15:37:20 +03:00
Pete Matsyburka e8b36c2b6d update gh stars 2026-05-28 14:38:34 +03:00
Pete Matsyburka 51743f1359 fix spec 2026-05-28 10:18:45 +03:00
Pete Matsyburka 504c42646b refactor template builder data 2026-05-28 10:06:47 +03:00
Pete Matsyburka c61e84d1b4 adjust archive 2026-05-28 08:40:05 +03:00
Pete Matsyburka b8ab01c46c add dynamic documents to template response 2026-05-28 08:29:10 +03:00
Pete Matsyburka 9558060bde check resubmit config 2026-05-28 08:29:09 +03:00
Pete Matsyburka 0741c879f1 use expires_at with file links 2026-05-27 22:19:18 +03:00
Pete Matsyburka 2e6dd2867e file url ttl env var 2026-05-26 21:54:59 +03:00
Pete Matsyburka 9f3fcccb61 optimize build 2026-05-26 16:50:10 +03:00
Pete Matsyburka ae08d7abad adjust opacity_layer 2026-05-26 13:11:15 +03:00
Pete Matsyburka 16808a7ec6 use pngload_buffer 2026-05-26 13:11:15 +03:00
Pete Matsyburka b1dfcd6283 fix email typo 2026-05-26 13:11:15 +03:00
Pete Matsyburka 369f4c0f08 sync prefillable field changes 2026-05-26 13:11:15 +03:00
Pete Matsyburka 827de03579 fix result generatrion 2026-05-26 08:57:30 +03:00
Alex Turchyn 0c7f4e5b43 fix sidekiq embedded redis connection 2026-05-25 21:52:34 +03:00
Alex Turchyn 9c700a3fb6 Merge from docusealco/wip 2026-05-25 12:28:05 +03:00
Pete Matsyburka 89bf83febb adjust autorot 2026-05-23 11:43:28 +03:00
Pete Matsyburka d44b35cf0a convert images on upload 2026-05-23 08:25:04 +03:00
Pete Matsyburka 891833e273 refactor process doc 2026-05-22 14:06:04 +03:00
Pete Matsyburka 29d3a80d3f fix rubocop 2026-05-21 21:55:30 +03:00
Pete Matsyburka 90fef58684 ruby 4.0.5 2026-05-21 21:46:27 +03:00
Pete Matsyburka 07841722ac adjust image load 2026-05-21 21:44:13 +03:00
Pete Matsyburka 9e4da5948b convert images on upload 2026-05-21 14:50:30 +03:00
Pete Matsyburka dc6e4313a1 fix ci 2026-05-20 16:15:15 +03:00
Pete Matsyburka a68cc0b689 convert images on upload 2026-05-20 15:23:42 +03:00
Pete Matsyburka d057fb0f67 content type priority 2026-05-19 13:27:16 +03:00
Pete Matsyburka 7e8f045a29 cleanup 2026-05-19 11:42:14 +03:00
Pete Matsyburka 5249d8d18d update gem 2026-05-19 11:02:15 +03:00
Pete Matsyburka f0fafbadd4 isolate editor nodes 2026-05-19 10:52:48 +03:00
Pete Matsyburka 8b51079dcc update gem 2026-05-18 18:42:04 +03:00
Pete Matsyburka 354bccd6e8 handle dangerous extensions 2026-05-18 18:05:16 +03:00
Pete Matsyburka 6806772346 add security headers 2026-05-18 16:47:19 +03:00
Alex Turchyn 60082655d4 Merge from docusealco/wip 2026-05-18 13:45:07 +03:00
Pete Matsyburka 7fe56941fd optimize pdf images 2026-05-17 19:55:03 +03:00
Pete Matsyburka abd498dd33 adjust validation message 2026-05-16 08:15:02 +03:00
Pete Matsyburka 99ca0136ed add percent format 2026-05-16 08:15:02 +03:00
Pete Matsyburka e52830c9b4 add authorization checks 2026-05-16 08:15:01 +03:00
Pete Matsyburka 755decca27 adjust capture revision 2026-05-15 17:50:18 +03:00
Pete Matsyburka 41604008d1 clamp area box 2026-05-15 17:20:29 +03:00
Pete Matsyburka 5bddce8969 fix i18n 2026-05-14 12:16:36 +03:00
Pete Matsyburka b25503f141 fix i18n 2026-05-13 10:08:13 +03:00
Pete Matsyburka bdf1850448 resend emails 2026-05-13 09:59:06 +03:00
Pete Matsyburka 230d3ccf69 fix with download config 2026-05-13 08:52:41 +03:00
Pete Matsyburka e378025a2d fix refactor complete button 2026-05-12 22:24:09 +03:00
Alex Turchyn 04129ded90 add complete button to signing form header 2026-05-12 19:19:49 +03:00
Pete Matsyburka a7891f89f8 do not override custom webhook header 2026-05-11 17:13:26 +03:00
Pete Matsyburka 37d4a8e834 update gem 2026-05-11 14:06:17 +03:00
Alex Turchyn 528a1216f8 Merge from docusealco/wip 2026-05-11 13:52:58 +03:00
Pete Matsyburka f2479bd259 adjust open revisions 2026-05-11 11:11:16 +03:00
Pete Matsyburka 135f0826d2 fix style 2026-05-09 21:17:18 +03:00
Pete Matsyburka 5710f0177b update gh stars 2026-05-09 19:25:42 +03:00
Pete Matsyburka 1a3a0528ba fix i18n 2026-05-09 10:34:20 +03:00
Pete Matsyburka 59793ff374 adjust builder 2026-05-08 19:35:46 +03:00
Pete Matsyburka 3c4ed42419 fix i18n 2026-05-08 18:08:35 +03:00
Pete Matsyburka 6a17b61550 update gem 2026-05-08 16:25:11 +03:00
Pete Matsyburka 76659497b7 typos 2026-05-08 16:12:09 +03:00
Pete Matsyburka ee50c957cb update gem 2026-05-08 15:43:34 +03:00
Pete Matsyburka a9dd200919 add pdfium rotate 2026-05-08 15:43:34 +03:00
Pete Matsyburka 1fa953104a update gh stars 2026-05-08 15:43:34 +03:00
Pete Matsyburka 50eb5b070e fix typo 2026-05-08 15:43:34 +03:00
Pete Matsyburka 10fd624bec add revisions 2026-05-08 15:43:34 +03:00
Alex Turchyn 04ec2f8260 allow permanent delete submissions in archived templates 2026-05-08 15:43:34 +03:00
Pete Matsyburka d828d79574 improve log 2026-05-08 15:43:34 +03:00
Pete Matsyburka 01dd3fefe5 retry webhooks in test mode 2026-05-08 15:43:34 +03:00
Pete Matsyburka 45ae954c0c add hmac webhook secret 2026-05-08 15:43:34 +03:00
Pete Matsyburka 1304849b55 update gh stars 2026-05-08 15:43:34 +03:00
Pete Matsyburka 0875faa079 fix date preview 2026-05-08 15:43:34 +03:00
Pete Matsyburka 3aa15d6ea6 update gem 2026-05-08 15:43:34 +03:00
Pete Matsyburka 6557329e97 gh 13k 2026-05-08 15:43:34 +03:00
Pete Matsyburka be27ce4161 Merge pull request #666 from aqilaziz/docs-fix-readme-typos
docs: fix README typos
2026-05-08 14:02:33 +03:00
aqilaziz a3391a970e docs: fix README typos 2026-05-08 05:23:28 +07:00
132 changed files with 2411 additions and 786 deletions

No files matched your search

+5 -5
View File
@@ -12,7 +12,7 @@ jobs:
- name: Install Ruby - name: Install Ruby
uses: ruby/setup-ruby@v1 uses: ruby/setup-ruby@v1
with: with:
ruby-version: 4.0.1 ruby-version: 4.0.5
- name: Cache gems - name: Cache gems
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
@@ -37,7 +37,7 @@ jobs:
- name: Install Ruby - name: Install Ruby
uses: ruby/setup-ruby@v1 uses: ruby/setup-ruby@v1
with: with:
ruby-version: 4.0.1 ruby-version: 4.0.5
- name: Cache gems - name: Cache gems
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
@@ -89,7 +89,7 @@ jobs:
- name: Install Ruby - name: Install Ruby
uses: ruby/setup-ruby@v1 uses: ruby/setup-ruby@v1
with: with:
ruby-version: 4.0.1 ruby-version: 4.0.5
- name: Cache gems - name: Cache gems
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
@@ -132,7 +132,7 @@ jobs:
- name: Install Ruby - name: Install Ruby
uses: ruby/setup-ruby@v1 uses: ruby/setup-ruby@v1
with: with:
ruby-version: 4.0.1 ruby-version: 4.0.5
- name: Set up Node - name: Set up Node
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
@@ -163,7 +163,7 @@ jobs:
yarn install yarn install
sudo apt-get update sudo apt-get update
sudo apt-get install -y libvips sudo apt-get install -y libvips
wget -O pdfium-linux.tgz "https://github.com/docusealco/pdfium-binaries/releases/latest/download/pdfium-linux-$(uname -m | sed 's/x86_64/x64/;s/aarch64/arm64/').tgz" wget -O pdfium-linux.tgz "https://github.com/bblanchon/pdfium-binaries/releases/latest/download/pdfium-linux-$(uname -m | sed 's/x86_64/x64/;s/aarch64/arm64/').tgz"
sudo tar -xzf pdfium-linux.tgz --strip-components=1 -C /usr/lib lib/libpdfium.so sudo tar -xzf pdfium-linux.tgz --strip-components=1 -C /usr/lib lib/libpdfium.so
rm -f pdfium-linux.tgz rm -f pdfium-linux.tgz
- name: Run - name: Run
+6 -4
View File
@@ -1,4 +1,4 @@
FROM ruby:4.0.1-alpine AS download FROM ruby:4.0.5-alpine AS download
WORKDIR /fonts WORKDIR /fonts
@@ -13,7 +13,7 @@ RUN apk --no-cache add wget && \
mkdir -p /pdfium-linux && \ mkdir -p /pdfium-linux && \
tar -xzf pdfium-linux.tgz -C /pdfium-linux tar -xzf pdfium-linux.tgz -C /pdfium-linux
FROM ruby:4.0.1-alpine AS webpack FROM ruby:4.0.5-alpine AS webpack
ENV RAILS_ENV=production ENV RAILS_ENV=production
ENV NODE_ENV=production ENV NODE_ENV=production
@@ -40,7 +40,7 @@ COPY ./app/views ./app/views
RUN echo "gem 'shakapacker'" > Gemfile && ./bin/shakapacker RUN echo "gem 'shakapacker'" > Gemfile && ./bin/shakapacker
FROM ruby:4.0.1-alpine AS app FROM ruby:4.0.5-alpine AS app
ENV RAILS_ENV=production ENV RAILS_ENV=production
ENV BUNDLE_WITHOUT="development:test" ENV BUNDLE_WITHOUT="development:test"
@@ -48,7 +48,8 @@ ENV OPENSSL_CONF=/etc/openssl_legacy.cnf
WORKDIR /app WORKDIR /app
RUN apk add --no-cache libpq vips redis vips-heif onnxruntime RUN apk add --no-cache libpq vips redis onnxruntime && \
rm -f /usr/bin/onnx_test_runner /usr/bin/onnxruntime_test
RUN addgroup -g 2000 docuseal && adduser -u 2000 -G docuseal -s /bin/sh -D -h /home/docuseal docuseal RUN addgroup -g 2000 docuseal && adduser -u 2000 -G docuseal -s /bin/sh -D -h /home/docuseal docuseal
@@ -94,6 +95,7 @@ WORKDIR /data/docuseal
ENV HOME=/home/docuseal ENV HOME=/home/docuseal
ENV WORKDIR=/data/docuseal ENV WORKDIR=/data/docuseal
ENV VIPS_MAX_COORD=17000 ENV VIPS_MAX_COORD=17000
ENV VIPS_BLOCK_UNTRUSTED=1
EXPOSE 3000 EXPOSE 3000
CMD ["/app/bin/bundle", "exec", "puma", "-C", "/app/config/puma.rb", "--dir", "/app"] CMD ["/app/bin/bundle", "exec", "puma", "-C", "/app/config/puma.rb", "--dir", "/app"]
+1 -2
View File
@@ -2,7 +2,7 @@
source 'https://rubygems.org' source 'https://rubygems.org'
ruby '4.0.1' ruby '4.0.5'
gem 'addressable' gem 'addressable'
gem 'arabic-letter-connector', require: false gem 'arabic-letter-connector', require: false
@@ -21,7 +21,6 @@ gem 'faraday'
gem 'faraday-follow_redirects' gem 'faraday-follow_redirects'
gem 'google-cloud-storage', require: false gem 'google-cloud-storage', require: false
gem 'hexapdf' gem 'hexapdf'
gem 'image_processing'
gem 'jwt', require: false gem 'jwt', require: false
gem 'lograge' gem 'lograge'
gem 'numo-narray-alt', require: false gem 'numo-narray-alt', require: false
+19 -25
View File
@@ -116,7 +116,7 @@ GEM
erubi (~> 1.4) erubi (~> 1.4)
parser (>= 2.4) parser (>= 2.4)
smart_properties smart_properties
bigdecimal (4.1.0) bigdecimal (4.1.2)
bindex (0.8.1) bindex (0.8.1)
bootsnap (1.23.0) bootsnap (1.23.0)
msgpack (~> 1.2) msgpack (~> 1.2)
@@ -161,7 +161,7 @@ GEM
irb (~> 1.10) irb (~> 1.10)
reline (>= 0.3.8) reline (>= 0.3.8)
declarative (0.0.20) declarative (0.0.20)
devise (5.0.3) devise (5.0.4)
bcrypt (~> 3.0) bcrypt (~> 3.0)
orm_adapter (~> 0.1) orm_adapter (~> 0.1)
railties (>= 7.0) railties (>= 7.0)
@@ -195,7 +195,7 @@ GEM
railties (>= 6.1.0) railties (>= 6.1.0)
faker (3.6.1) faker (3.6.1)
i18n (>= 1.8.11, < 2) i18n (>= 1.8.11, < 2)
faraday (2.14.1) faraday (2.14.2)
faraday-net_http (>= 2.0, < 3.5) faraday-net_http (>= 2.0, < 3.5)
json json
logger logger
@@ -257,25 +257,22 @@ GEM
os (>= 0.9, < 2.0) os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a) signet (>= 0.16, < 2.a)
hashdiff (1.2.1) hashdiff (1.2.1)
hexapdf (1.6.0) hexapdf (1.7.0)
cmdparse (~> 3.0, >= 3.0.3) cmdparse (~> 3.0, >= 3.0.3)
geom2d (~> 0.4, >= 0.4.1) geom2d (~> 0.4, >= 0.4.1)
openssl (>= 2.2.1) openssl (>= 2.2.1)
strscan (>= 3.1.2) strscan (>= 3.1.2)
i18n (1.14.8) i18n (1.14.8)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
image_processing (1.14.0)
mini_magick (>= 4.9.5, < 6)
ruby-vips (>= 2.0.17, < 3)
io-console (0.8.2) io-console (0.8.2)
irb (1.17.0) irb (1.18.0)
pp (>= 0.6.0) pp (>= 0.6.0)
prism (>= 1.3.0) prism (>= 1.3.0)
rdoc (>= 4.0.0) rdoc (>= 4.0.0)
reline (>= 0.4.2) reline (>= 0.4.2)
jmespath (1.6.2) jmespath (1.6.2)
json (2.19.3) json (2.19.7)
jwt (3.1.2) jwt (3.2.0)
base64 base64
language_server-protocol (3.17.0.5) language_server-protocol (3.17.0.5)
launchy (3.1.1) launchy (3.1.1)
@@ -308,17 +305,15 @@ GEM
marcel (1.1.0) marcel (1.1.0)
matrix (0.4.3) matrix (0.4.3)
method_source (1.1.0) method_source (1.1.0)
mini_magick (5.3.1)
logger
mini_mime (1.1.5) mini_mime (1.1.5)
minitest (6.0.3) minitest (6.0.6)
drb (~> 2.0) drb (~> 2.0)
prism (~> 1.5) prism (~> 1.5)
msgpack (1.8.0) msgpack (1.8.0)
multi_json (1.19.1) multi_json (1.19.1)
net-http (0.9.1) net-http (0.9.1)
uri (>= 0.11.1) uri (>= 0.11.1)
net-imap (0.6.3) net-imap (0.6.4)
date date
net-protocol net-protocol
net-pop (0.1.2) net-pop (0.1.2)
@@ -328,15 +323,15 @@ GEM
net-smtp (0.5.1) net-smtp (0.5.1)
net-protocol net-protocol
nio4r (2.7.5) nio4r (2.7.5)
nokogiri (1.19.2-aarch64-linux-gnu) nokogiri (1.19.3-aarch64-linux-gnu)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.19.2-aarch64-linux-musl) nokogiri (1.19.3-aarch64-linux-musl)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.19.2-arm64-darwin) nokogiri (1.19.3-arm64-darwin)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.19.2-x86_64-linux-gnu) nokogiri (1.19.3-x86_64-linux-gnu)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.19.2-x86_64-linux-musl) nokogiri (1.19.3-x86_64-linux-musl)
racc (~> 1.4) racc (~> 1.4)
numo-narray-alt (0.10.3) numo-narray-alt (0.10.3)
oj (3.16.16) oj (3.16.16)
@@ -429,12 +424,12 @@ GEM
tsort (>= 0.2) tsort (>= 0.2)
zeitwerk (~> 2.6) zeitwerk (~> 2.6)
rainbow (3.1.1) rainbow (3.1.1)
rake (13.3.1) rake (13.4.2)
rdoc (7.2.0) rdoc (7.2.0)
erb erb
psych (>= 4.0.0) psych (>= 4.0.0)
tsort tsort
redis-client (0.28.0) redis-client (0.29.0)
connection_pool connection_pool
regexp_parser (2.11.3) regexp_parser (2.11.3)
reline (0.6.3) reline (0.6.3)
@@ -516,12 +511,12 @@ GEM
rack-proxy (>= 0.6.1) rack-proxy (>= 0.6.1)
railties (>= 5.2) railties (>= 5.2)
semantic_range (>= 2.3.0) semantic_range (>= 2.3.0)
sidekiq (8.1.2) sidekiq (8.1.6)
connection_pool (>= 3.0.0) connection_pool (>= 3.0.0)
json (>= 2.16.0) json (>= 2.16.0)
logger (>= 1.7.0) logger (>= 1.7.0)
rack (>= 3.2.0) rack (>= 3.2.0)
redis-client (>= 0.26.0) redis-client (>= 0.29.0)
signet (0.21.0) signet (0.21.0)
addressable (~> 2.8) addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a) faraday (>= 0.17.5, < 3.a)
@@ -624,7 +619,6 @@ DEPENDENCIES
foreman foreman
google-cloud-storage google-cloud-storage
hexapdf hexapdf
image_processing
jwt jwt
letter_opener_web letter_opener_web
lograge lograge
@@ -662,7 +656,7 @@ DEPENDENCIES
webmock webmock
RUBY VERSION RUBY VERSION
ruby 4.0.1 ruby 4.0.5
BUNDLED WITH BUNDLED WITH
4.0.3 4.0.3
+2 -2
View File
@@ -50,7 +50,7 @@ DocuSeal is an open source platform that provides secure and efficient digital d
- Company logo and white-label - Company logo and white-label
- User roles - User roles
- Automated reminders - Automated reminders
- Invitation and identify verification via SMS - Invitation and identity verification via SMS
- Conditional fields and formulas - Conditional fields and formulas
- Bulk send with CSV, XLSX spreadsheet import - Bulk send with CSV, XLSX spreadsheet import
- SSO / SAML - SSO / SAML
@@ -74,7 +74,7 @@ DocuSeal is an open source platform that provides secure and efficient digital d
docker run --name docuseal -p 3000:3000 -v.:/data docuseal/docuseal docker run --name docuseal -p 3000:3000 -v.:/data docuseal/docuseal
``` ```
By default DocuSeal docker container uses an SQLite database to store data and configurations. Alternatively, it is possible use PostgreSQL or MySQL databases by specifying the `DATABASE_URL` env variable. By default DocuSeal docker container uses an SQLite database to store data and configurations. Alternatively, it is possible to use PostgreSQL or MySQL databases by specifying the `DATABASE_URL` env variable.
#### Docker Compose #### Docker Compose
@@ -9,7 +9,9 @@ module Api
before_action :set_cors_headers before_action :set_cors_headers
before_action :set_noindex_headers before_action :set_noindex_headers
before_action :set_security_headers
# rubocop:disable Metrics
def show def show
blob_uuid, purp, exp = ApplicationRecord.signed_id_verifier.verified(params[:signed_uuid]) blob_uuid, purp, exp = ApplicationRecord.signed_id_verifier.verified(params[:signed_uuid])
@@ -21,6 +23,12 @@ module Api
blob = ActiveStorage::Blob.find_by!(uuid: blob_uuid) blob = ActiveStorage::Blob.find_by!(uuid: blob_uuid)
if Submitters::DANGEROUS_EXTENSIONS.include?(blob.filename.extension.to_s.downcase)
Rollbar.error('Dangerous extension') if defined?(Rollbar)
return head :unprocessable_content
end
attachment = blob.attachments.take attachment = blob.attachments.take
@record = attachment.record @record = attachment.record
@@ -45,6 +53,7 @@ module Api
end end
end end
end end
# rubocop:enable Metrics
private private
@@ -9,6 +9,7 @@ module Api
before_action :set_cors_headers before_action :set_cors_headers
before_action :set_noindex_headers before_action :set_noindex_headers
before_action :set_security_headers
# rubocop:disable Metrics # rubocop:disable Metrics
def show def show
@@ -18,6 +19,12 @@ module Api
return head :not_found unless blob return head :not_found unless blob
if Submitters::DANGEROUS_EXTENSIONS.include?(blob.filename.extension.to_s.downcase)
Rollbar.error('Dangerous extension') if defined?(Rollbar)
return head :unprocessable_content
end
is_permitted = blob.attachments.any? do |a| is_permitted = blob.attachments.any? do |a|
(current_user && a.record.account.id == current_user.account_id) || (current_user && a.record.account.id == current_user.account_id) ||
a.record.account.account_configs.any? { |e| e.key == 'legacy_blob_proxy' } || a.record.account.account_configs.any? { |e| e.key == 'legacy_blob_proxy' } ||
@@ -102,6 +102,10 @@ module Api
headers['X-Robots-Tag'] = 'noindex' headers['X-Robots-Tag'] = 'noindex'
end end
def set_security_headers
response.headers['X-Content-Type-Options'] = 'nosniff'
end
def set_cors_headers def set_cors_headers
headers['Access-Control-Allow-Origin'] = '*' headers['Access-Control-Allow-Origin'] = '*'
headers['Access-Control-Allow-Methods'] = 'POST, GET, PUT, PATCH, DELETE, OPTIONS' headers['Access-Control-Allow-Methods'] = 'POST, GET, PUT, PATCH, DELETE, OPTIONS'
+12 -10
View File
@@ -8,35 +8,37 @@ module Api
COOKIE_STORE_LIMIT = 10 COOKIE_STORE_LIMIT = 10
def create def create
submitter = Submitter.find_by!(slug: params[:submitter_slug]) @submitter = Submitter.find_by!(slug: params[:submitter_slug])
unless can_upload?(submitter)
Rollbar.error("Can't upload: #{submitter.id}") if defined?(Rollbar)
unless can_upload?(@submitter)
return render json: { error: I18n.t('form_has_been_archived') }, status: :unprocessable_content return render json: { error: I18n.t('form_has_been_archived') }, status: :unprocessable_content
end end
file = params[:file]
if params[:type].in?(%w[initials signature]) if params[:type].in?(%w[initials signature])
image = Vips::Image.new_from_file(params[:file].path) image = ImageUtils.load_vips(file.read, content_type: file.content_type)
if ImageUtils.blank?(image) if ImageUtils.blank?(image)
Rollbar.error("Empty signature: #{submitter.id}") if defined?(Rollbar) Rollbar.error("Empty signature: #{@submitter.id}") if defined?(Rollbar)
return render json: { error: "#{params[:type]} is empty" }, status: :unprocessable_content return render json: { error: "#{params[:type]} is empty" }, status: :unprocessable_content
end end
if ImageUtils.error?(image) if ImageUtils.error?(image)
Rollbar.error("Error signature: #{submitter.id}") if defined?(Rollbar) Rollbar.error("Error signature: #{@submitter.id}") if defined?(Rollbar)
return render json: { error: "#{params[:type]} error, try to sign on another device" }, return render json: { error: "#{params[:type]} error, try to sign on another device" },
status: :unprocessable_content status: :unprocessable_content
end end
metadata = { analyzed: true, identified: true, width: image.width, height: image.height }
end end
attachment = Submitters.create_attachment!(submitter, params) attachment = Submitters.create_attachment!(@submitter, file, metadata:)
if params[:remember_signature] == 'true' && submitter.email.present? if params[:remember_signature] == 'true' && @submitter.email.present?
cookies.encrypted[:signature_uuids] = build_new_cookie_signatures_json(submitter, attachment) cookies.encrypted[:signature_uuids] = build_new_cookie_signatures_json(@submitter, attachment)
end end
render json: attachment.as_json(only: %i[uuid created_at], methods: %i[url filename content_type]) render json: attachment.as_json(only: %i[uuid created_at], methods: %i[url filename content_type])
+37 -14
View File
@@ -9,20 +9,7 @@ module Api
templates = paginate(templates.preload(:author, folder: :parent_folder)) templates = paginate(templates.preload(:author, folder: :parent_folder))
schema_documents = schema_documents, dynamic_documents, preview_image_attachments = preload_relations(templates)
ActiveStorage::Attachment.where(record_id: templates.map(&:id),
record_type: 'Template',
name: :documents,
uuid: templates.flat_map { |t| t.schema.pluck('attachment_uuid') })
.preload(:blob)
preview_image_attachments =
ActiveStorage::Attachment.joins(:blob)
.where(blob: { filename: ['0.png', '0.jpg'] })
.where(record_id: schema_documents.map(&:id),
record_type: 'ActiveStorage::Attachment',
name: :preview_images)
.preload(:blob)
expires_at = Accounts.link_expires_at(current_account) expires_at = Accounts.link_expires_at(current_account)
@@ -30,6 +17,7 @@ module Api
data: templates.map do |t| data: templates.map do |t|
Templates::SerializeForApi.call(t, Templates::SerializeForApi.call(t,
schema_documents: schema_documents.select { |e| e.record_id == t.id }, schema_documents: schema_documents.select { |e| e.record_id == t.id },
dynamic_documents:,
preview_image_attachments:, preview_image_attachments:,
expires_at:) expires_at:)
end, end,
@@ -88,6 +76,41 @@ module Api
private private
def preload_relations(templates)
schema_documents =
ActiveStorage::Attachment.where(record_id: templates.map(&:id),
record_type: 'Template',
name: :documents,
uuid: templates.flat_map { |t| t.schema.pluck('attachment_uuid') })
.preload(:blob)
dynamic_document_uuids =
templates.flat_map { |t| t.schema.select { |item| item['dynamic'] }.pluck('attachment_uuid') }
dynamic_documents =
if dynamic_document_uuids.present?
DynamicDocument.where(template: templates.map(&:id))
.where(uuid: dynamic_document_uuids)
.preload(current_version: { document_attachment: :blob })
.select(:id, :uuid, :template_id, :sha1, :created_at, :updated_at)
else
DynamicDocument.none
end
preview_attachment_ids =
schema_documents.map(&:id) + dynamic_documents.filter_map { |d| d.current_version&.document_attachment&.id }
preview_image_attachments =
ActiveStorage::Attachment.joins(:blob)
.where(blob: { filename: ['0.png', '0.jpg'] })
.where(record_id: preview_attachment_ids,
record_type: 'ActiveStorage::Attachment',
name: :preview_images)
.preload(:blob)
[schema_documents, dynamic_documents, preview_image_attachments]
end
def filter_templates(templates, params) def filter_templates(templates, params)
templates = Templates.search(current_user, templates, params[:q]) templates = Templates.search(current_user, templates, params[:q])
templates = params[:archived].in?(['true', true]) ? templates.archived : templates.active templates = params[:archived].in?(['true', true]) ? templates.archived : templates.active
+8 -1
View File
@@ -101,11 +101,18 @@ class StartFormController < ApplicationController
def load_resubmit_submitter def load_resubmit_submitter
@resubmit_submitter = @resubmit_submitter =
if params[:resubmit].present? && !params[:resubmit].in?([true, 'true']) if params[:resubmit].present? && !params[:resubmit].in?([true, 'true'])
Submitter.find_by(slug: params[:resubmit]) submitter = Submitter.find_by(slug: params[:resubmit])
submitter if submitter && can_resubmit?(submitter)
end end
end end
def can_resubmit?(submitter)
submitter.account.account_configs.find_or_initialize_by(key: AccountConfig::ALLOW_TO_RESUBMIT).value != false
end
def authorize_start! def authorize_start!
return redirect_to submit_form_path(@resubmit_submitter.slug) if @resubmit_submitter && @template.archived_at?
return redirect_to start_form_path(@template.slug) if @template.archived_at? return redirect_to start_form_path(@template.slug) if @template.archived_at?
return if @resubmit_submitter return if @resubmit_submitter
@@ -0,0 +1,39 @@
# frozen_string_literal: true
class SubmissionsResendEmailController < ApplicationController
load_and_authorize_resource :submission
before_action do
authorize!(:manage, :resend_all)
authorize!(:update, @submission)
end
def create
submitters = @submission.submitters.reject(&:completed_at?).select { |s| s.email.present? && !s.declined_at? }
if Docuseal.multitenant?
recent_submitter_ids =
SubmissionEvent.where(submitter_id: submitters.map(&:id),
event_type: 'send_email',
created_at: 10.hours.ago..Time.current).pluck(:submitter_id).to_set
submitters = submitters.reject { |s| recent_submitter_ids.include?(s.id) }
end
submitters.each do |submitter|
SendSubmitterInvitationEmailJob.perform_async('submitter_id' => submitter.id)
submitter.sent_at ||= Time.current
submitter.save!
end
notice =
if submitters.empty?
I18n.t('email_has_been_sent_already')
else
I18n.t('emails_have_been_sent_to_n_recipients', count: submitters.size)
end
redirect_back(fallback_location: submission_path(@submission), notice:)
end
end
@@ -4,6 +4,8 @@ class SubmissionsUnarchiveController < ApplicationController
load_and_authorize_resource :submission load_and_authorize_resource :submission
def create def create
authorize!(:update, @submission)
@submission.update!(archived_at: nil) @submission.update!(archived_at: nil)
redirect_to submission_path(@submission), notice: I18n.t('submission_has_been_unarchived') redirect_to submission_path(@submission), notice: I18n.t('submission_has_been_unarchived')
@@ -5,12 +5,12 @@ class SubmitFormInviteController < ApplicationController
skip_authorization_check skip_authorization_check
def create def create
submitter = Submitter.find_by!(slug: params[:submit_form_slug]) @submitter = Submitter.find_by!(slug: params[:submit_form_slug])
return head :unprocessable_content unless can_invite?(submitter) return head :unprocessable_content unless can_invite?(@submitter)
invite_submitters = filter_invite_submitters(submitter, 'invite_by_uuid') invite_submitters = filter_invite_submitters(@submitter, 'invite_by_uuid')
optional_invite_submitters = filter_invite_submitters(submitter, 'optional_invite_by_uuid') optional_invite_submitters = filter_invite_submitters(@submitter, 'optional_invite_by_uuid')
ApplicationRecord.transaction do ApplicationRecord.transaction do
(invite_submitters + optional_invite_submitters).each do |item| (invite_submitters + optional_invite_submitters).each do |item|
@@ -21,18 +21,18 @@ class SubmitFormInviteController < ApplicationController
email = Submissions.normalize_email(attrs[:email]) email = Submissions.normalize_email(attrs[:email])
submitter.submission.submitters.create!(uuid: attrs[:uuid], email:, account_id: submitter.account_id) @submitter.submission.submitters.create!(uuid: attrs[:uuid], email:, account_id: @submitter.account_id)
SubmissionEvents.create_with_tracking_data(submitter, 'invite_party', request, { uuid: submitter.uuid }) SubmissionEvents.create_with_tracking_data(@submitter, 'invite_party', request, { uuid: @submitter.uuid })
end end
submitter.submission.update!(submitters_order: :preserved) @submitter.submission.update!(submitters_order: :preserved)
end end
submitter.submission.submitters.reload @submitter.submission.submitters.reload
if invite_submitters.all? { |s| submitter.submission.submitters.any? { |e| e.uuid == s['uuid'] } } if invite_submitters.all? { |s| @submitter.submission.submitters.any? { |e| e.uuid == s['uuid'] } }
Submitters::SubmitValues.call(submitter, ActionController::Parameters.new(completed: 'true'), request) Submitters::SubmitValues.call(@submitter, ActionController::Parameters.new(completed: 'true'), request)
head :ok head :ok
else else
@@ -5,19 +5,19 @@ class SubmitFormMetadataController < ApplicationController
skip_authorization_check skip_authorization_check
def index def index
submitter = Submitter.find_by!(slug: params[:submit_form_slug]) @submitter = Submitter.find_by!(slug: params[:submit_form_slug])
return head :not_found if submitter.declined_at? || return head :not_found if @submitter.declined_at? ||
submitter.completed_at? || @submitter.completed_at? ||
submitter.submission.archived_at? || @submitter.submission.archived_at? ||
submitter.submission.expired? || @submitter.submission.expired? ||
submitter.submission.template&.archived_at? || @submitter.submission.template&.archived_at? ||
submitter.account.archived_at? || @submitter.account.archived_at? ||
!Submitters::AuthorizedForForm.call(submitter, current_user, request) !Submitters::AuthorizedForForm.call(@submitter, current_user, request)
submission = submitter.submission submission = @submitter.submission
values = submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) } values = submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) }
schema = Submissions.filtered_conditions_schema(submission, values:, include_submitter_uuid: submitter.uuid) schema = Submissions.filtered_conditions_schema(submission, values:, include_submitter_uuid: @submitter.uuid)
documents = schema.filter_map do |item| documents = schema.filter_map do |item|
submission.schema_documents.find { |a| a.uuid == item['attachment_uuid'] } submission.schema_documents.find { |a| a.uuid == item['attachment_uuid'] }
@@ -5,21 +5,21 @@ class SubmitFormValuesController < ApplicationController
skip_authorization_check skip_authorization_check
def index def index
submitter = Submitter.find_by!(slug: params[:submit_form_slug]) @submitter = Submitter.find_by!(slug: params[:submit_form_slug])
return render json: {} if submitter.completed_at? || return render json: {} if @submitter.completed_at? ||
submitter.declined_at? || @submitter.declined_at? ||
submitter.submission.template&.archived_at? || @submitter.submission.template&.archived_at? ||
submitter.submission.archived_at? || @submitter.submission.archived_at? ||
submitter.submission.expired? || @submitter.submission.expired? ||
!Submitters::AuthorizedForForm.call(submitter, current_user, request) !Submitters::AuthorizedForForm.call(@submitter, current_user, request)
value = submitter.values[params['field_uuid']] value = @submitter.values[params['field_uuid']]
attachment = submitter.attachments.where(created_at: params[:after]..).find_by(uuid: value) if value.present? attachment = @submitter.attachments.where(created_at: params[:after]..).find_by(uuid: value) if value.present?
render json: { render json: {
value:, value:,
attachment: attachment&.as_json(only: %i[uuid created_at], methods: %i[url filename content_type]) attachment: attachment&.as_json(only: %i[uuid created_at], methods: %i[url filename content_type])
}, head: :ok }
end end
end end
@@ -4,6 +4,8 @@ class SubmittersSendEmailController < ApplicationController
load_and_authorize_resource :submitter load_and_authorize_resource :submitter
def create def create
authorize!(:update, @submitter)
if Docuseal.multitenant? && SubmissionEvent.exists?(submitter: @submitter, if Docuseal.multitenant? && SubmissionEvent.exists?(submitter: @submitter,
event_type: 'send_email', event_type: 'send_email',
created_at: 10.hours.ago..Time.current) created_at: 10.hours.ago..Time.current)
@@ -10,6 +10,8 @@ class TemplateDocumentsController < ApplicationController
end end
def create def create
authorize!(:update, @template)
if params[:blobs].blank? && params[:files].blank? if params[:blobs].blank? && params[:files].blank?
return render json: { error: I18n.t('file_is_missing') }, status: :unprocessable_content return render json: { error: I18n.t('file_is_missing') }, status: :unprocessable_content
end end
@@ -13,6 +13,9 @@ class TemplatesCloneAndReplaceController < ApplicationController
cloned_template = Templates::Clone.call(@template, author: current_user) cloned_template = Templates::Clone.call(@template, author: current_user)
cloned_template.name = File.basename(params[:files].first.original_filename, '.*') cloned_template.name = File.basename(params[:files].first.original_filename, '.*')
authorize!(:create, cloned_template)
cloned_template.save! cloned_template.save!
documents = Templates::ReplaceAttachments.call(cloned_template, params, extract_fields: true) documents = Templates::ReplaceAttachments.call(cloned_template, params, extract_fields: true)
+1 -17
View File
@@ -1,10 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
class TemplatesController < ApplicationController class TemplatesController < ApplicationController
TEMPLATE_FIELDS = %i[id author_id folder_id external_id name slug
schema fields submitters variables_schema preferences
shared_link source archived_at created_at updated_at].freeze
load_and_authorize_resource :template load_and_authorize_resource :template
def show def show
@@ -31,19 +27,7 @@ class TemplatesController < ApplicationController
def new; end def new; end
def edit def edit
ActiveRecord::Associations::Preloader.new( @template_data = Templates.serialize_for_builder(@template)
records: [@template],
associations: [{ schema_documents: [:blob, { preview_images_attachments: :blob }] }]
).call
@template_data =
@template.as_json(only: TEMPLATE_FIELDS).merge(
documents: @template.schema_documents.as_json(
only: %i[id uuid],
methods: %i[metadata signed_key],
include: { preview_images: { only: %i[id], methods: %i[url metadata filename] } }
)
).to_json
render :edit, layout: 'plain' render :edit, layout: 'plain'
end end
@@ -16,7 +16,12 @@ class TemplatesDetectFieldsController < ApplicationController
page_number = params[:page].presence&.to_i page_number = params[:page].presence&.to_i
documents.each do |document| documents.each do |document|
io = StringIO.new(document.download) io =
if document.image?
StringIO.new(document.preview_images.joins(:blob).find_by(blob: { filename: ['0.png', '0.jpg'] }).download)
else
StringIO.new(document.download)
end
Templates::DetectFields.call(io, attachment: document, page_number:) do |(attachment_uuid, page, fields)| Templates::DetectFields.call(io, attachment: document, page_number:) do |(attachment_uuid, page, fields)|
sse.write({ attachment_uuid:, page:, fields: }) sse.write({ attachment_uuid:, page:, fields: })
@@ -6,6 +6,8 @@ class TemplatesFoldersController < ApplicationController
def edit; end def edit; end
def update def update
authorize!(:update, @template)
name = [params[:parent_name], params[:name]].compact_blank.join(' / ') name = [params[:parent_name], params[:name]].compact_blank.join(' / ')
@template.folder = TemplateFolders.find_or_create_by_name(current_user, name) @template.folder = TemplateFolders.find_or_create_by_name(current_user, name)
@@ -4,18 +4,7 @@ class TemplatesPreviewController < ApplicationController
load_and_authorize_resource :template load_and_authorize_resource :template
def show def show
ActiveRecord::Associations::Preloader.new( @template_data = Templates.serialize_for_builder(@template)
records: [@template],
associations: [{ schema_documents: { preview_images_attachments: :blob } }]
).call
@template_data =
@template.as_json.merge(
documents: @template.schema_documents.as_json(
methods: %i[metadata signed_key],
include: { preview_images: { methods: %i[url metadata filename] } }
)
).to_json
render :show, layout: 'plain' render :show, layout: 'plain'
end end
@@ -4,6 +4,8 @@ class TemplatesRestoreController < ApplicationController
load_and_authorize_resource :template load_and_authorize_resource :template
def create def create
authorize!(:update, @template)
@template.update!(archived_at: nil) @template.update!(archived_at: nil)
WebhookUrls.enqueue_events(@template, 'template.updated') WebhookUrls.enqueue_events(@template, 'template.updated')
@@ -5,7 +5,9 @@ class TemplatesUploadsController < ApplicationController
layout 'plain' layout 'plain'
def show; end def show
redirect_to root_path if params[:url].blank?
end
def create def create
url_params = create_file_params_from_url if params[:url].present? url_params = create_file_params_from_url if params[:url].present?
@@ -0,0 +1,25 @@
# frozen_string_literal: true
class TemplatesVersionsController < ApplicationController
load_and_authorize_resource :template
def index
versions = @template.template_versions.order(id: :desc).preload(:author)
render json: versions.as_json(TemplateVersions::SERIALIZE_PARAMS)
end
def show
version = @template.template_versions.find(params[:id])
render json: TemplateVersions.serialize(version)
end
def create
authorize!(:update, @template)
TemplateVersions.find_or_create_for(@template, author: current_user)
head :ok
end
end
@@ -11,6 +11,12 @@ class UserInitialsController < ApplicationController
return redirect_to settings_profile_index_path, notice: I18n.t('unable_to_save_initials') if file.blank? return redirect_to settings_profile_index_path, notice: I18n.t('unable_to_save_initials') if file.blank?
extension = File.extname(file.original_filename).delete_prefix('.').downcase
if Submitters::DANGEROUS_EXTENSIONS.include?(extension)
raise Submitters::MaliciousFileExtension, "File type '.#{extension}' is not allowed."
end
blob = ActiveStorage::Blob.create_and_upload!(io: file.open, blob = ActiveStorage::Blob.create_and_upload!(io: file.open,
filename: file.original_filename, filename: file.original_filename,
content_type: file.content_type) content_type: file.content_type)
@@ -11,6 +11,12 @@ class UserSignaturesController < ApplicationController
return redirect_to settings_profile_index_path, notice: I18n.t('unable_to_save_signature') if file.blank? return redirect_to settings_profile_index_path, notice: I18n.t('unable_to_save_signature') if file.blank?
extension = File.extname(file.original_filename).delete_prefix('.').downcase
if Submitters::DANGEROUS_EXTENSIONS.include?(extension)
raise Submitters::MaliciousFileExtension, "File type '.#{extension}' is not allowed."
end
blob = ActiveStorage::Blob.create_and_upload!(io: file.open, blob = ActiveStorage::Blob.create_and_upload!(io: file.open,
filename: file.original_filename, filename: file.original_filename,
content_type: file.content_type) content_type: file.content_type)
@@ -0,0 +1,7 @@
# frozen_string_literal: true
class WebhookHmacController < ApplicationController
load_and_authorize_resource :webhook_url, parent: false
def show; end
end
+32 -3
View File
@@ -21,6 +21,7 @@ import SubmittersAutocomplete from './elements/submitter_autocomplete'
import FolderAutocomplete from './elements/folder_autocomplete' import FolderAutocomplete from './elements/folder_autocomplete'
import SignatureForm from './elements/signature_form' import SignatureForm from './elements/signature_form'
import SubmitForm from './elements/submit_form' import SubmitForm from './elements/submit_form'
import ConvertUpload from './elements/convert_upload'
import PromptPassword from './elements/prompt_password' import PromptPassword from './elements/prompt_password'
import EmailsTextarea from './elements/emails_textarea' import EmailsTextarea from './elements/emails_textarea'
import ToggleSubmit from './elements/toggle_submit' import ToggleSubmit from './elements/toggle_submit'
@@ -47,13 +48,13 @@ import ScrollTo from './elements/scroll_to'
import SetValue from './elements/set_value' import SetValue from './elements/set_value'
import ReviewForm from './elements/review_form' import ReviewForm from './elements/review_form'
import ShowOnValue from './elements/show_on_value' import ShowOnValue from './elements/show_on_value'
import CustomValidation from './elements/custom_validation'
import ToggleClasses from './elements/toggle_classes' import ToggleClasses from './elements/toggle_classes'
import AutosizeField from './elements/autosize_field' import AutosizeField from './elements/autosize_field'
import GoogleDriveFilePicker from './elements/google_drive_file_picker' import GoogleDriveFilePicker from './elements/google_drive_file_picker'
import OpenModal from './elements/open_modal' import OpenModal from './elements/open_modal'
import BarChart from './elements/bar_chart' import BarChart from './elements/bar_chart'
import FieldCondition from './elements/field_condition' import FieldCondition from './elements/field_condition'
import ConfirmUpload from './elements/confirm_upload'
import * as TurboInstantClick from './lib/turbo_instant_click' import * as TurboInstantClick from './lib/turbo_instant_click'
@@ -112,6 +113,7 @@ safeRegisterElement('submitters-autocomplete', SubmittersAutocomplete)
safeRegisterElement('folder-autocomplete', FolderAutocomplete) safeRegisterElement('folder-autocomplete', FolderAutocomplete)
safeRegisterElement('signature-form', SignatureForm) safeRegisterElement('signature-form', SignatureForm)
safeRegisterElement('submit-form', SubmitForm) safeRegisterElement('submit-form', SubmitForm)
safeRegisterElement('convert-upload', ConvertUpload)
safeRegisterElement('prompt-password', PromptPassword) safeRegisterElement('prompt-password', PromptPassword)
safeRegisterElement('emails-textarea', EmailsTextarea) safeRegisterElement('emails-textarea', EmailsTextarea)
safeRegisterElement('toggle-cookies', ToggleCookies) safeRegisterElement('toggle-cookies', ToggleCookies)
@@ -139,13 +141,13 @@ safeRegisterElement('scroll-to', ScrollTo)
safeRegisterElement('set-value', SetValue) safeRegisterElement('set-value', SetValue)
safeRegisterElement('review-form', ReviewForm) safeRegisterElement('review-form', ReviewForm)
safeRegisterElement('show-on-value', ShowOnValue) safeRegisterElement('show-on-value', ShowOnValue)
safeRegisterElement('custom-validation', CustomValidation)
safeRegisterElement('toggle-classes', ToggleClasses) safeRegisterElement('toggle-classes', ToggleClasses)
safeRegisterElement('autosize-field', AutosizeField) safeRegisterElement('autosize-field', AutosizeField)
safeRegisterElement('google-drive-file-picker', GoogleDriveFilePicker) safeRegisterElement('google-drive-file-picker', GoogleDriveFilePicker)
safeRegisterElement('open-modal', OpenModal) safeRegisterElement('open-modal', OpenModal)
safeRegisterElement('bar-chart', BarChart) safeRegisterElement('bar-chart', BarChart)
safeRegisterElement('field-condition', FieldCondition) safeRegisterElement('field-condition', FieldCondition)
safeRegisterElement('confirm-upload', ConfirmUpload)
safeRegisterElement('template-builder', class extends HTMLElement { safeRegisterElement('template-builder', class extends HTMLElement {
connectedCallback () { connectedCallback () {
@@ -170,6 +172,8 @@ safeRegisterElement('template-builder', class extends HTMLElement {
withLogo: this.dataset.withLogo !== 'false', withLogo: this.dataset.withLogo !== 'false',
withFieldsDetection: this.dataset.withFieldsDetection === 'true', withFieldsDetection: this.dataset.withFieldsDetection === 'true',
withDetectExistingFields: this.dataset.withDetectExistingFields === 'true', withDetectExistingFields: this.dataset.withDetectExistingFields === 'true',
withRevisions: true,
withRevisionsMenu: this.dataset.withRevisionsMenu === 'true',
editable: this.dataset.editable !== 'false', editable: this.dataset.editable !== 'false',
authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content, authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content,
withCustomFields: true, withCustomFields: true,
@@ -195,11 +199,36 @@ safeRegisterElement('template-builder', class extends HTMLElement {
} }
onSubmit = (e) => { onSubmit = (e) => {
if (e.detail.success && e.detail?.formSubmission?.formElement?.id === 'submitters_form') { if (e.detail.success) {
if (e.detail?.formSubmission?.formElement?.id === 'submitters_form') {
e.detail.fetchResponse.response.json().then((data) => { e.detail.fetchResponse.response.json().then((data) => {
this.component.template.submitters = data.submitters this.component.template.submitters = data.submitters
}) })
} }
if (e.detail?.formSubmission?.formElement?.action?.endsWith('/prefillable_fields')) {
e.detail.fetchResponse.response.text().then((data) => {
const doc = new DOMParser().parseFromString(data, 'text/html')
const fragment = doc.querySelector('turbo-stream template').content
const prefillableUuidsIndex = {}
fragment.querySelectorAll('[name="field_uuid"]').forEach((field) => {
prefillableUuidsIndex[field.value] = true
})
this.component.template.fields.forEach((field) => {
if (prefillableUuidsIndex[field.uuid]) {
field.prefillable = true
field.readonly = true
} else if (field.prefillable) {
delete field.prefillable
delete field.readonly
}
})
})
}
}
} }
disconnectedCallback () { disconnectedCallback () {
+27
View File
@@ -0,0 +1,27 @@
import { target, targetable } from '@github/catalyst/lib/targetable'
export default targetable(class extends HTMLElement {
static [target.static] = [
'prompt',
'processing',
'logo'
]
connectedCallback () {
this.form.addEventListener('submit', this.onSubmit)
}
disconnectedCallback () {
this.form.removeEventListener('submit', this.onSubmit)
}
onSubmit = () => {
this.prompt.classList.add('hidden')
this.processing.classList.remove('hidden')
this.logo.classList.add('animate-bounce')
}
get form () {
return this.querySelector('form')
}
})
+73
View File
@@ -0,0 +1,73 @@
export function convertImage (sourceFile, targetType, quality) {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = function (event) {
const img = new Image()
img.onload = function () {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
canvas.width = img.width
canvas.height = img.height
ctx.drawImage(img, 0, 0)
canvas.toBlob(function (blob) {
const ext = targetType === 'image/jpeg' ? '.jpg' : '.png'
const newFile = new File([blob], sourceFile.name.replace(/\.\w+$/, ext), { type: targetType })
resolve(newFile)
}, targetType, quality)
}
img.onerror = () => reject(new Error(`browser cannot decode ${sourceFile.type || sourceFile.name}`))
img.src = event.target.result
}
reader.onerror = reject
reader.readAsDataURL(sourceFile)
})
}
export async function convertImagesInInput (input) {
if (!input.files || input.files.length === 0) return
const dt = new DataTransfer()
let didConvert = false
for (const file of Array.from(input.files)) {
let converted = file
try {
if (['image/bmp', 'image/vnd.microsoft.icon', 'image/svg+xml', 'image/gif'].includes(file.type)) {
converted = await convertImage(file, 'image/png')
didConvert = true
} else if (['image/heic', 'image/heif', 'image/heic-sequence', 'image/heif-sequence', 'image/avif', 'image/avif-sequence', 'image/webp'].includes(file.type)) {
converted = await convertImage(file, 'image/jpeg', 0.9)
didConvert = true
}
} catch (e) {
alert(e.message)
}
dt.items.add(converted)
}
if (didConvert) {
input.files = dt.files
}
}
export default class extends HTMLElement {
connectedCallback () {
const input = this.querySelector('input[type="file"]')
const form = input.form
input.addEventListener('change', async () => {
await convertImagesInInput(input)
form.querySelector('[type="submit"]')?.setAttribute('disabled', true)
form.requestSubmit()
})
}
}
@@ -1,14 +0,0 @@
export default class extends HTMLElement {
connectedCallback () {
const input = this.querySelector('input')
const invalidMessage = this.dataset.invalidMessage || ''
input.addEventListener('invalid', () => {
input.setCustomValidity(input.value ? invalidMessage : '')
})
input.addEventListener('input', () => {
input.setCustomValidity('')
})
}
}
@@ -1,4 +1,5 @@
import { target, targets, targetable } from '@github/catalyst/lib/targetable' import { target, targets, targetable } from '@github/catalyst/lib/targetable'
import { convertImagesInInput } from './convert_upload'
const loadingIconHtml = `<svg xmlns="http://www.w3.org/2000/svg" class="animate-spin" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> const loadingIconHtml = `<svg xmlns="http://www.w3.org/2000/svg" class="animate-spin" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" /> <path stroke="none" d="M0 0h24v24H0z" fill="none" />
@@ -150,12 +151,16 @@ export default targetable(class extends HTMLElement {
if (!this.isLoading) this.hideDraghover() if (!this.isLoading) this.hideDraghover()
} }
uploadFiles (files, url) { async uploadFiles (files, url) {
this.isLoading = true this.isLoading = true
this.form.action = url this.form.action = url
this.form.querySelector('[type="file"]').files = files const input = this.form.querySelector('[type="file"]')
input.files = files
await convertImagesInInput(input)
this.form.querySelector('[type="submit"]').click() this.form.querySelector('[type="submit"]').click()
} }
+8 -3
View File
@@ -1,5 +1,6 @@
import { actionable } from '@github/catalyst/lib/actionable' import { actionable } from '@github/catalyst/lib/actionable'
import { target, targetable } from '@github/catalyst/lib/targetable' import { target, targetable } from '@github/catalyst/lib/targetable'
import { convertImagesInInput } from './convert_upload'
export default actionable(targetable(class extends HTMLElement { export default actionable(targetable(class extends HTMLElement {
static [target.static] = [ static [target.static] = [
@@ -38,17 +39,21 @@ export default actionable(targetable(class extends HTMLElement {
this.classList.add('border-base-300', 'hover:bg-base-200/30') this.classList.add('border-base-300', 'hover:bg-base-200/30')
} }
onDrop (e) { async onDrop (e) {
e.preventDefault() e.preventDefault()
this.input.files = e.dataTransfer.files this.input.files = e.dataTransfer.files
this.uploadFiles(e.dataTransfer.files) await convertImagesInInput(this.input)
this.uploadFiles(this.input.files)
} }
onSelectFiles (e) { async onSelectFiles (e) {
e.preventDefault() e.preventDefault()
await convertImagesInInput(this.input)
this.uploadFiles(this.input.files) this.uploadFiles(this.input.files)
} }
+3 -1
View File
@@ -52,7 +52,9 @@ safeRegisterElement('submission-form', class extends HTMLElement {
completedMessage: JSON.parse(this.dataset.completedMessage || '{}'), completedMessage: JSON.parse(this.dataset.completedMessage || '{}'),
completedRedirectUrl: this.dataset.completedRedirectUrl, completedRedirectUrl: this.dataset.completedRedirectUrl,
attachments: reactive(JSON.parse(this.dataset.attachments)), attachments: reactive(JSON.parse(this.dataset.attachments)),
fields: JSON.parse(this.dataset.fields) fields: JSON.parse(this.dataset.fields),
completeButtonContainer: document.getElementById('complete_button_container'),
completeButtonScrollContainer: document.getElementById('complete_button_container_scroll')
}) })
this.app.mount(this.appElem) this.app.mount(this.appElem)
+4
View File
@@ -643,6 +643,10 @@ export default {
return new Intl.NumberFormat('de-DE').format(number) return new Intl.NumberFormat('de-DE').format(number)
} else if (format === 'space') { } else if (format === 'space') {
return new Intl.NumberFormat('fr-FR').format(number) return new Intl.NumberFormat('fr-FR').format(number)
} else if (format === 'percent') {
return `${number}%`
} else if (format === 'percent_space') {
return `${String(number).replace('.', ',')} %`
} else { } else {
return number return number
} }
@@ -27,6 +27,7 @@
</label> </label>
<button <button
v-if="withToday" v-if="withToday"
type="button"
class="btn btn-outline btn-sm !normal-case font-normal set-current-date-button" class="btn btn-outline btn-sm !normal-case font-normal set-current-date-button"
@click.prevent="[setCurrentDate(), $emit('focus')]" @click.prevent="[setCurrentDate(), $emit('focus')]"
> >
+38 -28
View File
@@ -51,6 +51,36 @@
<script> <script>
import { IconCloudUpload, IconInnerShadowTop } from '@tabler/icons-vue' import { IconCloudUpload, IconInnerShadowTop } from '@tabler/icons-vue'
function convertImage (sourceFile, targetType, quality) {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = function (event) {
const img = new Image()
img.onload = function () {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
canvas.width = img.width
canvas.height = img.height
ctx.drawImage(img, 0, 0)
canvas.toBlob(function (blob) {
const ext = targetType === 'image/jpeg' ? '.jpg' : '.png'
const newFile = new File([blob], sourceFile.name.replace(/\.\w+$/, ext), { type: targetType })
resolve(newFile)
}, targetType, quality)
}
img.onerror = () => reject(new Error(`browser cannot decode ${sourceFile.type || sourceFile.name}`))
img.src = event.target.result
}
reader.onerror = reject
reader.readAsDataURL(sourceFile)
})
}
export default { export default {
name: 'FileDropzone', name: 'FileDropzone',
components: { components: {
@@ -153,8 +183,14 @@ export default {
} }
}) })
} else { } else {
if (file.type === 'image/bmp' || file.type === 'image/vnd.microsoft.icon') { try {
file = await this.convertBmpToPng(file) if (['image/bmp', 'image/vnd.microsoft.icon', 'image/svg+xml', 'image/gif'].includes(file.type)) {
file = await convertImage(file, 'image/png')
} else if (['image/heic', 'image/heif', 'image/heic-sequence', 'image/heif-sequence', 'image/avif', 'image/avif-sequence', 'image/webp'].includes(file.type)) {
file = await convertImage(file, 'image/jpeg', 0.9)
}
} catch (e) {
alert(e.message)
} }
formData.append('file', file) formData.append('file', file)
@@ -181,32 +217,6 @@ export default {
}).finally(() => { }).finally(() => {
this.isLoading = false this.isLoading = false
}) })
},
convertBmpToPng (bmpFile) {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = function (event) {
const img = new Image()
img.onload = function () {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
canvas.width = img.width
canvas.height = img.height
ctx.drawImage(img, 0, 0)
canvas.toBlob(function (blob) {
const newFile = new File([blob], bmpFile.name.replace(/\.\w+$/, '.png'), { type: 'image/png' })
resolve(newFile)
}, 'image/png')
}
img.src = event.target.result
}
reader.onerror = reject
reader.readAsDataURL(bmpFile)
})
} }
} }
} }
+52 -1
View File
@@ -95,7 +95,7 @@
v-else v-else
id="complete_form_button" id="complete_form_button"
class="btn btn-sm btn-neutral text-white px-4 w-full flex justify-center" class="btn btn-sm btn-neutral text-white px-4 w-full flex justify-center"
form="steps_form" form="complete_form"
type="submit" type="submit"
name="completed" name="completed"
value="true" value="true"
@@ -113,6 +113,39 @@
</span> </span>
</button> </button>
</Teleport> </Teleport>
<Teleport
v-for="ref in (showCompleteButton ? [completeButtonContainer, completeButtonScrollContainer].filter(Boolean) : [])"
:key="ref"
:to="ref"
>
<button
class="complete-button btn btn-sm btn-neutral text-white px-4"
form="complete_form"
type="submit"
name="completed"
value="true"
:disabled="isSubmittingComplete"
>
<span class="flex items-center">
<IconInnerShadowTop
v-if="isSubmittingComplete"
class="mr-1 animate-spin w-5 h-5"
aria-hidden="true"
/>
<span>
{{ t('complete') }}
</span>
</span>
</button>
</Teleport>
<form
v-if="!isCompleted && !isInvite"
id="complete_form"
class="hidden"
:action="submitPath"
method="post"
@submit.prevent="submitStep"
/>
<button <button
v-if="!isFormVisible" v-if="!isFormVisible"
id="expand_form_button" id="expand_form_button"
@@ -791,6 +824,16 @@ export default {
required: false, required: false,
default: null default: null
}, },
completeButtonContainer: {
type: Object,
required: false,
default: null
},
completeButtonScrollContainer: {
type: Object,
required: false,
default: null
},
schema: { schema: {
type: Array, type: Array,
required: false, required: false,
@@ -1008,6 +1051,7 @@ export default {
isSubmitting: false, isSubmitting: false,
isSubmittingComplete: false, isSubmittingComplete: false,
submittedValues: {}, submittedValues: {},
isFormStarted: false,
recalculateButtonDisabledKey: '', recalculateButtonDisabledKey: '',
isAccessibilityMode: false isAccessibilityMode: false
} }
@@ -1056,6 +1100,10 @@ export default {
}) })
}) })
}, },
showCompleteButton () {
return this.completeButtonContainer && !this.isCompleted && !this.isInvite && this.isFormStarted &&
!this.stepFields.find((fields) => fields.some((f) => f.required && isEmpty(this.submittedValues[f.uuid])))
},
submitButtonText () { submitButtonText () {
if (this.alwaysMinimize) { if (this.alwaysMinimize) {
return this.t('submit') return this.t('submit')
@@ -1657,6 +1705,8 @@ export default {
return Promise.reject(new Error(data.error)) return Promise.reject(new Error(data.error))
} }
this.isFormStarted = true
const nextStep = (isLastStep && emptyRequiredField) || (forceComplete ? null : this.findNextStep(submitStepIndex)) const nextStep = (isLastStep && emptyRequiredField) || (forceComplete ? null : this.findNextStep(submitStepIndex))
if (nextStep) { if (nextStep) {
@@ -1693,6 +1743,7 @@ export default {
}, },
async performComplete (resp) { async performComplete (resp) {
this.isCompleted = true this.isCompleted = true
this.isFormVisible = true
if (resp?.text) { if (resp?.text) {
const respData = await resp.text() const respData = await resp.text()
+136 -38
View File
@@ -7,6 +7,13 @@ const en = {
kba: 'KBA', kba: 'KBA',
please_upload_an_image_file: 'Please upload an image file', please_upload_an_image_file: 'Please upload an image file',
must_be_characters_length: 'Must be {number} characters long', must_be_characters_length: 'Must be {number} characters long',
must_be_valid_ssn: 'Must be a valid SSN (XXX-XX-XXXX)',
must_be_valid_ein: 'Must be a valid EIN (XX-XXXXXXX)',
must_be_valid_email: 'Must be a valid email address',
must_be_valid_url: 'Must be a valid URL (starting with http:// or https://)',
must_be_valid_zip: 'Must be a valid ZIP code (XXXXX or XXXXX-XXXX)',
must_contain_numbers_only: 'Must contain numbers only',
must_contain_letters_only: 'Must contain letters only',
complete_all_required_fields_to_proceed_with_identity_verification: 'Complete all required fields to proceed with identity verification.', complete_all_required_fields_to_proceed_with_identity_verification: 'Complete all required fields to proceed with identity verification.',
verify_id: 'Verify ID', verify_id: 'Verify ID',
identity_verification: 'Identity verification', identity_verification: 'Identity verification',
@@ -116,6 +123,13 @@ const es = {
kba: 'KBA', kba: 'KBA',
please_upload_an_image_file: 'Por favor, sube un archivo de imagen', please_upload_an_image_file: 'Por favor, sube un archivo de imagen',
must_be_characters_length: 'Debe tener {number} caracteres de longitud', must_be_characters_length: 'Debe tener {number} caracteres de longitud',
must_be_valid_ssn: 'Debe ser un SSN válido (XXX-XX-XXXX)',
must_be_valid_ein: 'Debe ser un EIN válido (XX-XXXXXXX)',
must_be_valid_email: 'Debe ser una dirección de correo electrónico válida',
must_be_valid_url: 'Debe ser una URL válida (que comience con http:// o https://)',
must_be_valid_zip: 'Debe ser un ZIP válido (XXXXX o XXXXX-XXXX)',
must_contain_numbers_only: 'Debe contener solo números',
must_contain_letters_only: 'Debe contener solo letras',
complete_all_required_fields_to_proceed_with_identity_verification: 'Complete todos los campos requeridos para continuar con la verificación de identidad.', complete_all_required_fields_to_proceed_with_identity_verification: 'Complete todos los campos requeridos para continuar con la verificación de identidad.',
verify_id: 'Verificar ID', verify_id: 'Verificar ID',
identity_verification: 'Verificación de identidad', identity_verification: 'Verificación de identidad',
@@ -136,7 +150,7 @@ const es = {
verification_code_is_invalid: 'El código de verificación no es válido', verification_code_is_invalid: 'El código de verificación no es válido',
sign_on_the_touchscreen: 'Firmar en pantalla táctil', sign_on_the_touchscreen: 'Firmar en pantalla táctil',
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Escanea el código QR con la aplicación de la cámara para abrir el formulario en el móvil y dibujar tu firma', scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Escanea el código QR con la aplicación de la cámara para abrir el formulario en el móvil y dibujar tu firma',
by_clicking_you_agree_to_the: 'Al hacer clic en "{button}", usted acepta el', by_clicking_you_agree_to_the: 'Al hacer clic en "{button}", usted acepta la',
electronic_signature_disclosure: 'Divulgación de Firma Electrónica', electronic_signature_disclosure: 'Divulgación de Firma Electrónica',
esignature_disclosure: 'Divulgación de eFirma', esignature_disclosure: 'Divulgación de eFirma',
already_paid: 'Ya pagado', already_paid: 'Ya pagado',
@@ -225,6 +239,13 @@ const it = {
kba: 'KBA', kba: 'KBA',
please_upload_an_image_file: 'Per favore carica un file immagine', please_upload_an_image_file: 'Per favore carica un file immagine',
must_be_characters_length: 'Deve essere lungo {number} caratteri', must_be_characters_length: 'Deve essere lungo {number} caratteri',
must_be_valid_ssn: 'Deve essere un SSN valido (XXX-XX-XXXX)',
must_be_valid_ein: 'Deve essere un EIN valido (XX-XXXXXXX)',
must_be_valid_email: 'Deve essere un indirizzo email valido',
must_be_valid_url: 'Deve essere un URL valido (che inizia con http:// o https://)',
must_be_valid_zip: 'Deve essere uno ZIP valido (XXXXX o XXXXX-XXXX)',
must_contain_numbers_only: 'Deve contenere solo numeri',
must_contain_letters_only: 'Deve contenere solo lettere',
complete_all_required_fields_to_proceed_with_identity_verification: "Compila tutti i campi obbligatori per procedere con la verifica dell'identità.", complete_all_required_fields_to_proceed_with_identity_verification: "Compila tutti i campi obbligatori per procedere con la verifica dell'identità.",
verify_id: 'Verifica ID', verify_id: 'Verifica ID',
identity_verification: "Verifica dell'identità", identity_verification: "Verifica dell'identità",
@@ -245,7 +266,7 @@ const it = {
verification_code_is_invalid: 'Il codice di verifica non è valido', verification_code_is_invalid: 'Il codice di verifica non è valido',
sign_on_the_touchscreen: 'Firma su schermo tattile', sign_on_the_touchscreen: 'Firma su schermo tattile',
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: "Scansiona il codice QR con l'app della fotocamera per aprire il modulo sul cellulare e disegnare la tua firma", scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: "Scansiona il codice QR con l'app della fotocamera per aprire il modulo sul cellulare e disegnare la tua firma",
by_clicking_you_agree_to_the: 'Cliccando su "{button}", accetti il', by_clicking_you_agree_to_the: 'Cliccando su "{button}", accetti la',
electronic_signature_disclosure: 'Divulgazione della Firma Elettronica', electronic_signature_disclosure: 'Divulgazione della Firma Elettronica',
esignature_disclosure: 'Divulgazione della eFirma', esignature_disclosure: 'Divulgazione della eFirma',
minimize: 'Minimizza', minimize: 'Minimizza',
@@ -258,7 +279,7 @@ const it = {
date: 'Data', date: 'Data',
number: 'Numero', number: 'Numero',
image: 'Immagine', image: 'Immagine',
pay: 'Pagamento', pay: 'Paga',
file: 'File', file: 'File',
select: 'Seleziona', select: 'Seleziona',
checkbox: 'Checkbox', checkbox: 'Checkbox',
@@ -334,6 +355,13 @@ const de = {
kba: 'KBA', kba: 'KBA',
please_upload_an_image_file: 'Bitte laden Sie eine Bilddatei hoch', please_upload_an_image_file: 'Bitte laden Sie eine Bilddatei hoch',
must_be_characters_length: 'Muss {number} Zeichen lang sein', must_be_characters_length: 'Muss {number} Zeichen lang sein',
must_be_valid_ssn: 'Muss eine gültige SSN sein (XXX-XX-XXXX)',
must_be_valid_ein: 'Muss eine gültige EIN sein (XX-XXXXXXX)',
must_be_valid_email: 'Muss eine gültige E-Mail-Adresse sein',
must_be_valid_url: 'Muss eine gültige URL sein (beginnend mit http:// oder https://)',
must_be_valid_zip: 'Muss eine gültige ZIP sein (XXXXX oder XXXXX-XXXX)',
must_contain_numbers_only: 'Darf nur Zahlen enthalten',
must_contain_letters_only: 'Darf nur Buchstaben enthalten',
complete_all_required_fields_to_proceed_with_identity_verification: 'Füllen Sie alle Pflichtfelder aus, um mit der Identitätsprüfung fortzufahren.', complete_all_required_fields_to_proceed_with_identity_verification: 'Füllen Sie alle Pflichtfelder aus, um mit der Identitätsprüfung fortzufahren.',
verify_id: 'ID überprüfen', verify_id: 'ID überprüfen',
identity_verification: 'Identitätsprüfung', identity_verification: 'Identitätsprüfung',
@@ -443,6 +471,13 @@ const fr = {
kba: 'KBA', kba: 'KBA',
please_upload_an_image_file: 'Veuillez téléverser un fichier image', please_upload_an_image_file: 'Veuillez téléverser un fichier image',
must_be_characters_length: 'Doit comporter {number} caractères', must_be_characters_length: 'Doit comporter {number} caractères',
must_be_valid_ssn: 'Doit être un SSN valide (XXX-XX-XXXX)',
must_be_valid_ein: 'Doit être un EIN valide (XX-XXXXXXX)',
must_be_valid_email: 'Doit être une adresse e-mail valide',
must_be_valid_url: 'Doit être une URL valide (commençant par http:// ou https://)',
must_be_valid_zip: 'Doit être un ZIP valide (XXXXX ou XXXXX-XXXX)',
must_contain_numbers_only: 'Doit contenir uniquement des chiffres',
must_contain_letters_only: 'Doit contenir uniquement des lettres',
complete_all_required_fields_to_proceed_with_identity_verification: "Veuillez remplir tous les champs obligatoires pour poursuivre la vérification d'identité.", complete_all_required_fields_to_proceed_with_identity_verification: "Veuillez remplir tous les champs obligatoires pour poursuivre la vérification d'identité.",
verify_id: "Vérifier l'ID", verify_id: "Vérifier l'ID",
identity_verification: "Vérification d'identité", identity_verification: "Vérification d'identité",
@@ -552,6 +587,13 @@ const pl = {
kba: 'KBA', kba: 'KBA',
please_upload_an_image_file: 'Proszę przesłać plik obrazu', please_upload_an_image_file: 'Proszę przesłać plik obrazu',
must_be_characters_length: 'Musi mieć długość {number} znaków', must_be_characters_length: 'Musi mieć długość {number} znaków',
must_be_valid_ssn: 'Musi być prawidłowym numerem SSN (XXX-XX-XXXX)',
must_be_valid_ein: 'Musi być prawidłowym numerem EIN (XX-XXXXXXX)',
must_be_valid_email: 'Musi być prawidłowym adresem e-mail',
must_be_valid_url: 'Musi być prawidłowym adresem URL (zaczynającym się od http:// lub https://)',
must_be_valid_zip: 'Musi być prawidłowym ZIP (XXXXX lub XXXXX-XXXX)',
must_contain_numbers_only: 'Może zawierać tylko cyfry',
must_contain_letters_only: 'Może zawierać tylko litery',
complete_all_required_fields_to_proceed_with_identity_verification: 'Uzupełnij wszystkie wymagane pola, aby kontynuować weryfikację tożsamości.', complete_all_required_fields_to_proceed_with_identity_verification: 'Uzupełnij wszystkie wymagane pola, aby kontynuować weryfikację tożsamości.',
verify_id: 'Zweryfikuj ID', verify_id: 'Zweryfikuj ID',
identity_verification: 'Weryfikacja tożsamości', identity_verification: 'Weryfikacja tożsamości',
@@ -574,7 +616,7 @@ const pl = {
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Zeskanuj kod QR za pomocą aplikacji aparatu, aby otworzyć formularz na telefonie i narysować swój podpis', scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Zeskanuj kod QR za pomocą aplikacji aparatu, aby otworzyć formularz na telefonie i narysować swój podpis',
by_clicking_you_agree_to_the: 'Klikając na "{button}", zgadzasz się na', by_clicking_you_agree_to_the: 'Klikając na "{button}", zgadzasz się na',
electronic_signature_disclosure: 'Ujawnienie Elektronicznej Sygnatury', electronic_signature_disclosure: 'Ujawnienie Elektronicznej Sygnatury',
esignature_disclosure: 'Ujawnienie ePodpisu', esignature_disclosure: 'informację o e-podpisie',
minimize: 'Zminimalizuj', minimize: 'Zminimalizuj',
text: 'Tekst', text: 'Tekst',
already_paid: 'Już zapłacono', already_paid: 'Już zapłacono',
@@ -642,7 +684,7 @@ const pl = {
toggle_multiline_text: 'Przełącz Tekst Wielolinijkowy', toggle_multiline_text: 'Przełącz Tekst Wielolinijkowy',
email_has_been_sent: 'E-mail został wysłany', email_has_been_sent: 'E-mail został wysłany',
processing: 'Przetwarzanie', processing: 'Przetwarzanie',
pay_with_stripe: 'Płatność za pomocą Stripe', pay_with_stripe: 'Zapłać za pomocą Stripe',
reupload: 'Ponowne przesłanie', reupload: 'Ponowne przesłanie',
upload: 'Przesyłanie', upload: 'Przesyłanie',
files: 'Pliki', files: 'Pliki',
@@ -661,6 +703,13 @@ const uk = {
kba: 'KBA', kba: 'KBA',
please_upload_an_image_file: 'Будь ласка, завантажте файл зображення', please_upload_an_image_file: 'Будь ласка, завантажте файл зображення',
must_be_characters_length: 'Має містити {number} символів', must_be_characters_length: 'Має містити {number} символів',
must_be_valid_ssn: 'Має бути дійсним SSN (XXX-XX-XXXX)',
must_be_valid_ein: 'Має бути дійсним EIN (XX-XXXXXXX)',
must_be_valid_email: 'Має бути дійсною адресою електронної пошти',
must_be_valid_url: 'Має бути дійсним URL (починаючи з http:// або https://)',
must_be_valid_zip: 'Має бути дійсним ZIP (XXXXX або XXXXX-XXXX)',
must_contain_numbers_only: 'Має містити лише цифри',
must_contain_letters_only: 'Має містити лише літери',
complete_all_required_fields_to_proceed_with_identity_verification: "Заповніть всі обов'язкові поля, щоб продовжити перевірку особи.", complete_all_required_fields_to_proceed_with_identity_verification: "Заповніть всі обов'язкові поля, щоб продовжити перевірку особи.",
verify_id: 'Підтвердження ідентичності', verify_id: 'Підтвердження ідентичності',
identity_verification: 'Ідентифікація особи', identity_verification: 'Ідентифікація особи',
@@ -678,12 +727,12 @@ const uk = {
authored_by: 'Автор', authored_by: 'Автор',
select_a_reason: 'Виберіть причину', select_a_reason: 'Виберіть причину',
value_is_invalid: 'Значення є неправильним', value_is_invalid: 'Значення є неправильним',
verification_code_is_invalid: 'Код підтвердження є неправильним', verification_code_is_invalid: 'Невірний код підтвердження',
sign_on_the_touchscreen: 'Підписати на сенсорному екрані', sign_on_the_touchscreen: 'Підписати на сенсорному екрані',
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Скануйте QR-код за допомогою програми камери, щоб відкрити форму на мобільному пристрої та намалювати свій підпис', scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Скануйте QR-код за допомогою програми камери, щоб відкрити форму на мобільному пристрої та намалювати свій підпис',
by_clicking_you_agree_to_the: 'Натиснувши на "{button}", ви погоджуєтеся з', by_clicking_you_agree_to_the: 'Натиснувши на "{button}", ви погоджуєтеся з',
electronic_signature_disclosure: 'Розголошення Електронного Підпису', electronic_signature_disclosure: 'інформацією про е-підпис',
esignature_disclosure: 'Розголошення еПідпису', esignature_disclosure: 'інформацією про е-підпис',
minimize: 'Зменшити', minimize: 'Зменшити',
text: 'Текст', text: 'Текст',
already_paid: 'Вже оплачено', already_paid: 'Вже оплачено',
@@ -751,7 +800,7 @@ const uk = {
toggle_multiline_text: 'Перемкнути Багаторядковий Текст', toggle_multiline_text: 'Перемкнути Багаторядковий Текст',
email_has_been_sent: 'Електронний лист був відправлений', email_has_been_sent: 'Електронний лист був відправлений',
processing: 'Обробка', processing: 'Обробка',
pay_with_strip: 'Сплатити за допомогою Stripe', pay_with_stripe: 'Сплатити за допомогою Stripe',
reupload: 'Перезавантажити', reupload: 'Перезавантажити',
upload: 'Завантажити', upload: 'Завантажити',
files: 'Файли', files: 'Файли',
@@ -770,11 +819,18 @@ const cs = {
kba: 'KBA', kba: 'KBA',
please_upload_an_image_file: 'Nahrajte prosím obrázkový soubor', please_upload_an_image_file: 'Nahrajte prosím obrázkový soubor',
must_be_characters_length: 'Musí mít délku {number} znaků', must_be_characters_length: 'Musí mít délku {number} znaků',
must_be_valid_ssn: 'Musí být platné SSN (XXX-XX-XXXX)',
must_be_valid_ein: 'Musí být platné EIN (XX-XXXXXXX)',
must_be_valid_email: 'Musí být platná e-mailová adresa',
must_be_valid_url: 'Musí být platná URL (začínající http:// nebo https://)',
must_be_valid_zip: 'Musí být platný ZIP (XXXXX nebo XXXXX-XXXX)',
must_contain_numbers_only: 'Musí obsahovat pouze čísla',
must_contain_letters_only: 'Musí obsahovat pouze písmena',
complete_all_required_fields_to_proceed_with_identity_verification: 'Vyplňte všechna povinná pole, abyste mohli pokračovat v ověření identity.', complete_all_required_fields_to_proceed_with_identity_verification: 'Vyplňte všechna povinná pole, abyste mohli pokračovat v ověření identity.',
verify_id: 'Ověřit ID', verify_id: 'Ověřit ID',
identity_verification: 'Ověření identity', identity_verification: 'Ověření identity',
complete: 'Dokončit', complete: 'Dokončit',
fill_all_required_fields_to_complete: 'Please complete all mandatory fields', fill_all_required_fields_to_complete: 'Vyplňte povinná pole pro dokončení',
sign_and_complete: 'Podepsat a dokončit', sign_and_complete: 'Podepsat a dokončit',
invite: 'Pozvat', invite: 'Pozvat',
email: 'E-mail', email: 'E-mail',
@@ -782,8 +838,8 @@ const cs = {
reviewed: 'Zkontrolováno', reviewed: 'Zkontrolováno',
other: 'Jiné', other: 'Jiné',
authored_by_me: 'Autorem jsem já', authored_by_me: 'Autorem jsem já',
approved_by: 'Schváleno kým', approved_by: 'Schválil(a)',
reviewed_by: 'Zkontrolováno kým', reviewed_by: 'Zkontroloval(a)',
authored_by: 'Autorem', authored_by: 'Autorem',
select_a_reason: 'Vyberte důvod', select_a_reason: 'Vyberte důvod',
value_is_invalid: 'Hodnota je neplatná', value_is_invalid: 'Hodnota je neplatná',
@@ -792,7 +848,7 @@ const cs = {
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Naskenujte QR kód pomocí aplikace fotoaparátu, abyste otevřeli formulář na mobilním zařízení a nakreslili svůj podpis', scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Naskenujte QR kód pomocí aplikace fotoaparátu, abyste otevřeli formulář na mobilním zařízení a nakreslili svůj podpis',
by_clicking_you_agree_to_the: 'Kliknutím na "{button}" souhlasíte s', by_clicking_you_agree_to_the: 'Kliknutím na "{button}" souhlasíte s',
electronic_signature_disclosure: 'Zveřejněním Elektronického Podpisu', electronic_signature_disclosure: 'Zveřejněním Elektronického Podpisu',
esignature_disclosure: 'Zveřejnění ePodpisu', esignature_disclosure: 'informací o e-podpisu',
already_paid: 'Už zaplaceno', already_paid: 'Už zaplaceno',
minimize: 'Minimalizovat', minimize: 'Minimalizovat',
text: 'Text', text: 'Text',
@@ -860,7 +916,7 @@ const cs = {
toggle_multiline_text: 'Přepnout Víceřádkový Text', toggle_multiline_text: 'Přepnout Víceřádkový Text',
email_has_been_sent: 'E-mail byl odeslán', email_has_been_sent: 'E-mail byl odeslán',
processing: 'Zpracování', processing: 'Zpracování',
pay_with_stripe: 'Zaplacení přes Stripe', pay_with_stripe: 'Zaplatit přes Stripe',
reupload: 'Znovu nahrát', reupload: 'Znovu nahrát',
upload: 'Nahrát', upload: 'Nahrát',
files: 'Soubory', files: 'Soubory',
@@ -879,6 +935,13 @@ const pt = {
kba: 'KBA', kba: 'KBA',
please_upload_an_image_file: 'Por favor, envie um arquivo de imagem', please_upload_an_image_file: 'Por favor, envie um arquivo de imagem',
must_be_characters_length: 'Deve ter {number} caracteres', must_be_characters_length: 'Deve ter {number} caracteres',
must_be_valid_ssn: 'Deve ser um SSN válido (XXX-XX-XXXX)',
must_be_valid_ein: 'Deve ser um EIN válido (XX-XXXXXXX)',
must_be_valid_email: 'Deve ser um endereço de e-mail válido',
must_be_valid_url: 'Deve ser uma URL válida (começando com http:// ou https://)',
must_be_valid_zip: 'Deve ser um ZIP válido (XXXXX ou XXXXX-XXXX)',
must_contain_numbers_only: 'Deve conter apenas números',
must_contain_letters_only: 'Deve conter apenas letras',
complete_all_required_fields_to_proceed_with_identity_verification: 'Preencha todos os campos obrigatórios para prosseguir com a verificação de identidade.', complete_all_required_fields_to_proceed_with_identity_verification: 'Preencha todos os campos obrigatórios para prosseguir com a verificação de identidade.',
verify_id: 'Verificar ID', verify_id: 'Verificar ID',
identity_verification: 'Verificação de identidade', identity_verification: 'Verificação de identidade',
@@ -890,18 +953,18 @@ const pt = {
approved: 'Aprovado', approved: 'Aprovado',
reviewed: 'Revisado', reviewed: 'Revisado',
other: 'Outro', other: 'Outro',
authored_by_me: 'Autorizado por mim', authored_by_me: 'Escrito por mim',
approved_by: 'Aprovado por', approved_by: 'Aprovado por',
reviewed_by: 'Revisado por', reviewed_by: 'Revisado por',
authored_by: 'Autorizado por', authored_by: 'Escrito por',
select_a_reason: 'Selecione um motivo', select_a_reason: 'Selecione um motivo',
value_is_invalid: 'Valor é inválido', value_is_invalid: 'Valor é inválido',
verification_code_is_invalid: 'Código de verificação é inválido', verification_code_is_invalid: 'Código de verificação é inválido',
sign_on_the_touchscreen: 'Assinar na tela sensível', sign_on_the_touchscreen: 'Assinar na tela sensível',
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Escaneie o código QR com o aplicativo da câmera para abrir o formulário no celular e desenhar sua assinatura', scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Escaneie o código QR com o aplicativo da câmera para abrir o formulário no celular e desenhar sua assinatura',
by_clicking_you_agree_to_the: 'Ao clicar em "{button}", você concorda com o', by_clicking_you_agree_to_the: 'Ao clicar em "{button}", você concorda com a',
electronic_signature_disclosure: 'Divulgação de Assinatura Eletrônica', electronic_signature_disclosure: 'Divulgação de Assinatura Eletrônica',
esignature_disclosure: 'Divulgação da eAssinatura', esignature_disclosure: 'Informação sobre eAssinatura',
already_paid: 'Já pago', already_paid: 'Já pago',
minimize: 'Minimizar', minimize: 'Minimizar',
text: 'Texto', text: 'Texto',
@@ -970,7 +1033,7 @@ const pt = {
email_has_been_sent: 'Email enviado', email_has_been_sent: 'Email enviado',
processing: 'Processamento', processing: 'Processamento',
pay_with_stripe: 'Pagar com Stripe', pay_with_stripe: 'Pagar com Stripe',
reupload: 'Reenviar', reupload: 'Recarregar',
upload: 'Carregar', upload: 'Carregar',
files: 'Arquivos', files: 'Arquivos',
signature_is_too_small_or_simple_please_redraw: 'A assinatura é muito pequena ou simples. Por favor, redesenhe.', signature_is_too_small_or_simple_please_redraw: 'A assinatura é muito pequena ou simples. Por favor, redesenhe.',
@@ -988,6 +1051,13 @@ const he = {
kba: 'KBA', kba: 'KBA',
please_upload_an_image_file: 'אנא העלה קובץ תמונה', please_upload_an_image_file: 'אנא העלה קובץ תמונה',
must_be_characters_length: 'חייב להיות באורך של {number} תווים', must_be_characters_length: 'חייב להיות באורך של {number} תווים',
must_be_valid_ssn: 'חייב להיות SSN חוקי (XXX-XX-XXXX)',
must_be_valid_ein: 'חייב להיות EIN חוקי (XX-XXXXXXX)',
must_be_valid_email: 'חייבת להיות כתובת אימייל חוקית',
must_be_valid_url: 'חייבת להיות כתובת URL חוקית (המתחילה ב-http:// או https://)',
must_be_valid_zip: 'חייב להיות ZIP חוקי (XXXXX או XXXXX-XXXX)',
must_contain_numbers_only: 'חייב להכיל מספרים בלבד',
must_contain_letters_only: 'חייב להכיל אותיות בלבד',
complete_all_required_fields_to_proceed_with_identity_verification: 'מלא את כל השדות הנדרשים כדי להמשיך עם אימות זהות.', complete_all_required_fields_to_proceed_with_identity_verification: 'מלא את כל השדות הנדרשים כדי להמשיך עם אימות זהות.',
verify_id: 'אמת מזהה', verify_id: 'אמת מזהה',
identity_verification: 'אימות זהות', identity_verification: 'אימות זהות',
@@ -1010,8 +1080,8 @@ const he = {
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'סרוק את קוד ה-QR באמצעות אפליקציית המצלמה כדי לפתוח את הטופס במובייל ולצייר את החתימה שלך', scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'סרוק את קוד ה-QR באמצעות אפליקציית המצלמה כדי לפתוח את הטופס במובייל ולצייר את החתימה שלך',
by_clicking_you_agree_to_the: 'על ידי לחיצה על "{button}", אתה מסכים ל', by_clicking_you_agree_to_the: 'על ידי לחיצה על "{button}", אתה מסכים ל',
electronic_signature_disclosure: 'חשיפת חתימה אלקטרונית', electronic_signature_disclosure: 'חשיפת חתימה אלקטרונית',
esignature_disclosure: 'חשיפת ה-eחתימה', esignature_disclosure: 'חשיפת חתימה אלקטרונית',
minimize: 'לקטן', minimize: 'מזער',
text: 'טקסט', text: 'טקסט',
already_paid: 'כבר שולם', already_paid: 'כבר שולם',
signature: 'חתימה', signature: 'חתימה',
@@ -1022,9 +1092,9 @@ const he = {
number: 'מספר', number: 'מספר',
image: 'תמונה', image: 'תמונה',
file: 'קובץ', file: 'קובץ',
pay: 'תשלום', pay: 'שלם',
select: 'בחר', select: 'בחר',
checkbox: 'תיק בחירה', checkbox: 'סימון',
multiple: 'רב ערכים', multiple: 'רב ערכים',
radio: 'רדיו', radio: 'רדיו',
cells: 'תאים', cells: 'תאים',
@@ -1051,9 +1121,9 @@ const he = {
download: 'הורדה', download: 'הורדה',
clear: 'נקה', clear: 'נקה',
redraw: 'צייר מחדש', redraw: 'צייר מחדש',
draw_initials: 'צייר ציוני ראשי תיבות', draw_initials: 'צייר ראשי תיבות',
type_signature_here: 'הקלד חתימה כאן', type_signature_here: 'הקלד חתימה כאן',
type_initial_here: 'הקלד ציוני ראשי תיבות כאן', type_initial_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: 'המסמכים נחתמו!',
@@ -1063,7 +1133,7 @@ const he = {
open_source_documents_software: 'תוכנה פתוחה למסמכים', open_source_documents_software: 'תוכנה פתוחה למסמכים',
verified_phone_number: 'אימות מספר טלפון', verified_phone_number: 'אימות מספר טלפון',
use_international_format: 'השתמש בפורמט בינלאומי: +1xxx', use_international_format: 'השתמש בפורמט בינלאומי: +1xxx',
six_digits_code: 'קוד משתמש שש ספרות', six_digits_code: 'קוד שש ספרות',
change_phone_number: 'שינוי מספר טלפון', change_phone_number: 'שינוי מספר טלפון',
sending: 'שולח', sending: 'שולח',
resend_code: 'שלח מחדש קוד', resend_code: 'שלח מחדש קוד',
@@ -1072,7 +1142,7 @@ const he = {
set_today: 'קבע היום', set_today: 'קבע היום',
toggle_multiline_text: 'שנה בין טקסט במספר שורות לטקסט בשורה אחת', toggle_multiline_text: 'שנה בין טקסט במספר שורות לטקסט בשורה אחת',
draw_signature: 'צייר חתימה', draw_signature: 'צייר חתימה',
type_initial: 'הקלד ציוני ראשי תיבות', type_initial: 'הקלד ראשי תיבות',
draw: 'צייר', draw: 'צייר',
type: 'הקלד', type: 'הקלד',
type_text: 'הקלד טקסט', type_text: 'הקלד טקסט',
@@ -1097,8 +1167,15 @@ const nl = {
kba: 'KBA', kba: 'KBA',
please_upload_an_image_file: 'Upload alstublieft een afbeeldingsbestand', please_upload_an_image_file: 'Upload alstublieft een afbeeldingsbestand',
must_be_characters_length: 'Moet {number} tekens lang zijn', must_be_characters_length: 'Moet {number} tekens lang zijn',
must_be_valid_ssn: 'Moet een geldig SSN zijn (XXX-XX-XXXX)',
must_be_valid_ein: 'Moet een geldig EIN zijn (XX-XXXXXXX)',
must_be_valid_email: 'Moet een geldig e-mailadres zijn',
must_be_valid_url: 'Moet een geldige URL zijn (beginnend met http:// of https://)',
must_be_valid_zip: 'Moet een geldige ZIP zijn (XXXXX of XXXXX-XXXX)',
must_contain_numbers_only: 'Mag alleen cijfers bevatten',
must_contain_letters_only: 'Mag alleen letters bevatten',
complete_all_required_fields_to_proceed_with_identity_verification: 'Vul alle verplichte velden in om door te gaan met de identiteitsverificatie.', complete_all_required_fields_to_proceed_with_identity_verification: 'Vul alle verplichte velden in om door te gaan met de identiteitsverificatie.',
verify_id: 'Verifiëren ID', verify_id: 'ID verifiëren',
identity_verification: 'Identiteitsverificatie', identity_verification: 'Identiteitsverificatie',
complete: 'Voltooien', complete: 'Voltooien',
fill_all_required_fields_to_complete: 'Vul alle verplichte velden in om te voltooien', fill_all_required_fields_to_complete: 'Vul alle verplichte velden in om te voltooien',
@@ -1146,7 +1223,7 @@ const nl = {
continue: 'Doorgaan', continue: 'Doorgaan',
sign_now: 'Nu ondertekenen', sign_now: 'Nu ondertekenen',
type_here_: 'Typ hier...', type_here_: 'Typ hier...',
optional: 'Optioneel', optional: 'optioneel',
option: 'Optie', option: 'Optie',
appears_on: 'Verschijnt op', appears_on: 'Verschijnt op',
page: 'Pagina', page: 'Pagina',
@@ -1170,11 +1247,11 @@ const nl = {
powered_by: 'Aangedreven door', powered_by: 'Aangedreven door',
please_check_the_box_to_continue: 'Vink het vakje aan om door te gaan.', please_check_the_box_to_continue: 'Vink het vakje aan om door te gaan.',
open_source_documents_software: 'Open source documenten software', open_source_documents_software: 'Open source documenten software',
verified_phone_number: 'Geverifieerd telefoonnummer', verified_phone_number: 'Telefoonnummer verifiëren',
use_international_format: 'Gebruik internationaal formaat: +1xxx', use_international_format: 'Gebruik internationaal formaat: +1xxx',
six_digits_code: '6-cijferige code', six_digits_code: '6-cijferige code',
change_phone_number: 'Wijzig telefoonnummer', change_phone_number: 'Wijzig telefoonnummer',
sending: 'Voltooien...', sending: 'Verzenden...',
resend_code: 'Code opnieuw verzenden', resend_code: 'Code opnieuw verzenden',
verification_code_has_been_resent: 'Verificatiecode is opnieuw verzonden via SMS', verification_code_has_been_resent: 'Verificatiecode is opnieuw verzonden via SMS',
please_fill_all_required_fields: 'Vul alle verplichte velden in', please_fill_all_required_fields: 'Vul alle verplichte velden in',
@@ -1206,6 +1283,13 @@ const ar = {
kba: 'KBA', kba: 'KBA',
please_upload_an_image_file: 'يرجى تحميل ملف صورة', please_upload_an_image_file: 'يرجى تحميل ملف صورة',
must_be_characters_length: 'يجب أن يكون الطول {number} حرفًا', must_be_characters_length: 'يجب أن يكون الطول {number} حرفًا',
must_be_valid_ssn: 'يجب أن يكون SSN صالحًا (XXX-XX-XXXX)',
must_be_valid_ein: 'يجب أن يكون EIN صالحًا (XX-XXXXXXX)',
must_be_valid_email: 'يجب أن يكون عنوان بريد إلكتروني صالحًا',
must_be_valid_url: 'يجب أن يكون عنوان URL صالحًا (يبدأ بـ http:// أو https://)',
must_be_valid_zip: 'يجب أن يكون ZIP صالحًا (XXXXX أو XXXXX-XXXX)',
must_contain_numbers_only: 'يجب أن يحتوي على أرقام فقط',
must_contain_letters_only: 'يجب أن يحتوي على أحرف فقط',
complete_all_required_fields_to_proceed_with_identity_verification: 'أكمل جميع الحقول المطلوبة للمتابعة في التحقق من الهوية.', complete_all_required_fields_to_proceed_with_identity_verification: 'أكمل جميع الحقول المطلوبة للمتابعة في التحقق من الهوية.',
verify_id: 'تحقق من الهوية', verify_id: 'تحقق من الهوية',
identity_verification: 'التحقق من الهوية', identity_verification: 'التحقق من الهوية',
@@ -1232,7 +1316,7 @@ const ar = {
esignature_disclosure: 'كشف التوقيع الإلكتروني', esignature_disclosure: 'كشف التوقيع الإلكتروني',
text: 'نص', text: 'نص',
signature: 'توقيع', signature: 'توقيع',
initials: 'الاختصارات', initials: 'حروف أولى',
date: 'تاريخ', date: 'تاريخ',
number: 'رقم', number: 'رقم',
digitally_signed_by: 'تم التوقيع رقميًا بواسطة', digitally_signed_by: 'تم التوقيع رقميًا بواسطة',
@@ -1269,9 +1353,9 @@ const ar = {
download: 'تحميل', download: 'تحميل',
clear: 'مسح', clear: 'مسح',
redraw: 'إعادة الرسم', redraw: 'إعادة الرسم',
draw_initials: 'ارسم الاختصارات', draw_initials: 'ارسم حروف أولى',
type_signature_here: 'اكتب التوقيع هنا', type_signature_here: 'اكتب التوقيع هنا',
type_initial_here: 'اكتب الاختصارات هنا', type_initial_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: 'تم توقيع الوثائق!',
@@ -1290,13 +1374,13 @@ const ar = {
set_today: 'تعيين اليوم', set_today: 'تعيين اليوم',
toggle_multiline_text: 'تبديل النصوص متعددة الأسطر', toggle_multiline_text: 'تبديل النصوص متعددة الأسطر',
draw_signature: 'ارسم التوقيع', draw_signature: 'ارسم التوقيع',
type_initial: 'اكتب الاختصارات', type_initial: 'اكتب حروف أولى',
draw: 'ارسم', draw: 'ارسم',
type: 'اكتب', type: 'اكتب',
type_text: 'اكتب نصًا', type_text: 'اكتب نصًا',
email_has_been_sent: 'تم إرسال البريد الإلكتروني', email_has_been_sent: 'تم إرسال البريد الإلكتروني',
processing: 'جارٍ المعالجة', processing: 'جارٍ المعالجة',
pay_with_strip: 'الدفع بواسطة Stripe', pay_with_stripe: 'الدفع بواسطة Stripe',
reupload: 'إعادة التحميل', reupload: 'إعادة التحميل',
upload: 'تحميل', upload: 'تحميل',
files: 'الملفات', files: 'الملفات',
@@ -1315,6 +1399,13 @@ const ko = {
kba: 'KBA', kba: 'KBA',
please_upload_an_image_file: '이미지 파일을 업로드해 주세요', please_upload_an_image_file: '이미지 파일을 업로드해 주세요',
must_be_characters_length: '{number}자여야 합니다', must_be_characters_length: '{number}자여야 합니다',
must_be_valid_ssn: '유효한 SSN이어야 합니다 (XXX-XX-XXXX)',
must_be_valid_ein: '유효한 EIN이어야 합니다 (XX-XXXXXXX)',
must_be_valid_email: '유효한 이메일 주소여야 합니다',
must_be_valid_url: '유효한 URL이어야 합니다 (http:// 또는 https://로 시작)',
must_be_valid_zip: '유효한 ZIP이어야 합니다 (XXXXX 또는 XXXXX-XXXX)',
must_contain_numbers_only: '숫자만 포함해야 합니다',
must_contain_letters_only: '문자만 포함해야 합니다',
complete_all_required_fields_to_proceed_with_identity_verification: '신원 확인을 진행하려면 모든 필수 필드를 작성하십시오.', complete_all_required_fields_to_proceed_with_identity_verification: '신원 확인을 진행하려면 모든 필수 필드를 작성하십시오.',
verify_id: '아이디 확인', verify_id: '아이디 확인',
identity_verification: '신원 확인', identity_verification: '신원 확인',
@@ -1343,10 +1434,10 @@ const ko = {
signature: '서명', signature: '서명',
initials: '이니셜', initials: '이니셜',
date: '날짜', date: '날짜',
digitally_signed_by: '디지털 서명이 완료되었습니다', digitally_signed_by: '디지털 서명자:',
reason: '이유', reason: '이유',
number: '숫자', number: '숫자',
pay: '급여', pay: '결제',
image: '이미지', image: '이미지',
take_photo: '사진 찍기', take_photo: '사진 찍기',
file: '파일', file: '파일',
@@ -1424,6 +1515,13 @@ const ja = {
kba: 'KBA', kba: 'KBA',
please_upload_an_image_file: '画像ファイルをアップロードしてください', please_upload_an_image_file: '画像ファイルをアップロードしてください',
must_be_characters_length: '{number}文字でなければなりません', must_be_characters_length: '{number}文字でなければなりません',
must_be_valid_ssn: '有効なSSNである必要があります (XXX-XX-XXXX)',
must_be_valid_ein: '有効なEINである必要があります (XX-XXXXXXX)',
must_be_valid_email: '有効なメールアドレスである必要があります',
must_be_valid_url: '有効なURLである必要があります (http://またはhttps://で始まる)',
must_be_valid_zip: '有効なZIPである必要があります (XXXXX または XXXXX-XXXX)',
must_contain_numbers_only: '数字のみを含む必要があります',
must_contain_letters_only: '文字のみを含む必要があります',
complete_all_required_fields_to_proceed_with_identity_verification: '本人確認を進めるには、すべての必須項目を入力してください。', complete_all_required_fields_to_proceed_with_identity_verification: '本人確認を進めるには、すべての必須項目を入力してください。',
verify_id: '本人確認', verify_id: '本人確認',
identity_verification: '本人確認', identity_verification: '本人確認',
@@ -15,6 +15,7 @@
</template> </template>
</label> </label>
<button <button
type="button"
class="btn btn-outline btn-sm reupload-button" class="btn btn-outline btn-sm reupload-button"
@click.prevent="remove" @click.prevent="remove"
> >
@@ -42,6 +42,7 @@
{{ error }} {{ error }}
</div> </div>
<button <button
type="button"
class="base-button w-full flex justify-center submit-form-button" class="base-button w-full flex justify-center submit-form-button"
@click="restartKba" @click="restartKba"
> >
@@ -33,6 +33,7 @@
<div v-else> <div v-else>
<button <button
v-if="sessionId" v-if="sessionId"
type="button"
disabled disabled
class="base-button w-full modal-save-button" class="base-button w-full modal-save-button"
> >
@@ -47,6 +48,7 @@
<button <button
v-else v-else
:id="field.uuid" :id="field.uuid"
type="button"
class="btn bg-[#7B73FF] text-white hover:bg-[#0A2540] text-lg w-full" class="btn bg-[#7B73FF] text-white hover:bg-[#0A2540] text-lg w-full"
:class="{ disabled: isCreatingCheckout }" :class="{ disabled: isCreatingCheckout }"
:disabled="isCreatingCheckout" :disabled="isCreatingCheckout"
+16 -2
View File
@@ -41,12 +41,11 @@
:class="{ '!pr-11 -mr-10': !field.validation?.pattern }" :class="{ '!pr-11 -mr-10': !field.validation?.pattern }"
:required="field.required" :required="field.required"
:pattern="field.validation?.pattern" :pattern="field.validation?.pattern"
:title="validationMessage"
:aria-describedby="field.description ? field.uuid + '-desc' : undefined" :aria-describedby="field.description ? field.uuid + '-desc' : undefined"
:placeholder="`${t('type_here_')}${field.required ? '' : ` (${t('optional')})`}`" :placeholder="`${t('type_here_')}${field.required ? '' : ` (${t('optional')})`}`"
type="text" type="text"
:name="`values[${field.uuid}]`" :name="`values[${field.uuid}]`"
@invalid="validationMessage ? $event.target.setCustomValidity(validationMessage) : ''"
@input="validationMessage ? $event.target.setCustomValidity('') : ''"
@focus="$emit('focus')" @focus="$emit('focus')"
> >
<textarea <textarea
@@ -138,6 +137,17 @@ export default {
return null return null
} }
}, },
patternMessageKeys () {
return {
'^[0-9]{3}-[0-9]{2}-[0-9]{4}$': 'must_be_valid_ssn',
'^[0-9]{2}-[0-9]{7}$': 'must_be_valid_ein',
'^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$': 'must_be_valid_email',
'^https?://.*': 'must_be_valid_url',
'^[0-9]{5}(?:-[0-9]{4})?$': 'must_be_valid_zip',
'^[0-9]+$': 'must_contain_numbers_only',
'^[a-zA-Z]+$': 'must_contain_letters_only'
}
},
validationMessage () { validationMessage () {
if (this.field.validation?.message) { if (this.field.validation?.message) {
return this.field.validation.message return this.field.validation.message
@@ -149,6 +159,10 @@ export default {
.join('-') .join('-')
return this.t('must_be_characters_length').replace('{number}', number) return this.t('must_be_characters_length').replace('{number}', number)
} else if (this.field.validation?.pattern) {
const key = this.patternMessageKeys[this.field.validation.pattern]
return key ? this.t(key) : ''
} else { } else {
return '' return ''
} }
+20 -21
View File
@@ -50,6 +50,7 @@
@remove="$emit('remove')" @remove="$emit('remove')"
@scroll-to="$emit('scroll-to', $event)" @scroll-to="$emit('scroll-to', $event)"
@add-custom-field="$emit('add-custom-field')" @add-custom-field="$emit('add-custom-field')"
@click-title="$emit('click-title')"
/> />
<div <div
ref="touchValueTarget" ref="touchValueTarget"
@@ -317,7 +318,7 @@ export default {
default: false default: false
} }
}, },
emits: ['start-resize', 'stop-resize', 'start-drag', 'stop-drag', 'remove', 'scroll-to', 'add-custom-field'], emits: ['start-resize', 'stop-resize', 'start-drag', 'stop-drag', 'remove', 'scroll-to', 'add-custom-field', 'click-title'],
data () { data () {
return { return {
isContenteditable: false, isContenteditable: false,
@@ -619,6 +620,10 @@ export default {
return new Intl.NumberFormat('de-DE').format(number) return new Intl.NumberFormat('de-DE').format(number)
} else if (format === 'space') { } else if (format === 'space') {
return new Intl.NumberFormat('fr-FR').format(number) return new Intl.NumberFormat('fr-FR').format(number)
} else if (format === 'percent') {
return `${number}%`
} else if (format === 'percent_space') {
return `${String(number).replace('.', ',')} %`
} else { } else {
return number return number
} }
@@ -715,14 +720,6 @@ export default {
} }
} }
}, },
drag (e) {
if (e.target.id === 'mask' && this.editable) {
this.isDragged = true
this.area.x = (e.offsetX - this.dragFrom.x) / e.target.clientWidth
this.area.y = (e.offsetY - this.dragFrom.y) / e.target.clientHeight
}
},
startTouchDrag (e) { startTouchDrag (e) {
if (e.target !== this.$refs.touchTarget && e.target !== this.$refs.touchValueTarget) { if (e.target !== this.$refs.touchTarget && e.target !== this.$refs.touchValueTarget) {
return return
@@ -768,6 +765,8 @@ export default {
this.maybeChangeAreaPage(this.area) this.maybeChangeAreaPage(this.area)
this.clampAreaBounds(this.area)
if (this.isDragged) { if (this.isDragged) {
this.save() this.save()
} }
@@ -835,6 +834,8 @@ export default {
this.maybeChangeAreaPage(this.area) this.maybeChangeAreaPage(this.area)
this.clampAreaBounds(this.area)
if (this.isMoved) { if (this.isMoved) {
this.save() this.save()
} }
@@ -859,18 +860,6 @@ export default {
area.y = area.y - 1 - (16.0 / this.$parent.$refs.mask.previousSibling.offsetHeight) area.y = area.y - 1 - (16.0 / this.$parent.$refs.mask.previousSibling.offsetHeight)
} }
}, },
stopDrag () {
this.$el.getRootNode().removeEventListener('mousemove', this.drag)
this.$el.getRootNode().removeEventListener('mouseup', this.stopDrag)
if (this.isDragged) {
this.save()
}
this.isDragged = false
this.$emit('stop-drag')
},
startResize () { startResize () {
if (!this.selectedAreasRef.value.includes(this.area)) { if (!this.selectedAreasRef.value.includes(this.area)) {
this.selectedAreasRef.value = [this.area] this.selectedAreasRef.value = [this.area]
@@ -885,6 +874,8 @@ export default {
this.$el.getRootNode().removeEventListener('mousemove', this.resize) this.$el.getRootNode().removeEventListener('mousemove', this.resize)
this.$el.getRootNode().removeEventListener('mouseup', this.stopResize) this.$el.getRootNode().removeEventListener('mouseup', this.stopResize)
this.clampAreaBounds(this.area)
this.$emit('stop-resize') this.$emit('stop-resize')
this.save() this.save()
@@ -923,9 +914,17 @@ export default {
this.$el.getRootNode().removeEventListener('touchmove', this.touchResize) this.$el.getRootNode().removeEventListener('touchmove', this.touchResize)
this.$el.getRootNode().removeEventListener('touchend', this.stopTouchResize) this.$el.getRootNode().removeEventListener('touchend', this.stopTouchResize)
this.clampAreaBounds(this.area)
this.$emit('stop-resize') this.$emit('stop-resize')
this.save() this.save()
},
clampAreaBounds (area) {
area.x = Math.min(Math.max(area.x, 0), 1)
area.y = Math.min(Math.max(area.y, 0), 1)
area.w = Math.min(Math.max(area.w, 0), 1)
area.h = Math.min(Math.max(area.h, 0), 1)
} }
} }
} }
@@ -3,7 +3,7 @@
v-if="field?.type && (isSelected || isNameFocus) && !isInMultiSelection" v-if="field?.type && (isSelected || isNameFocus) && !isInMultiSelection"
class="absolute bg-white rounded-t border overflow-visible whitespace-nowrap flex z-10 field-area-controls" class="absolute bg-white rounded-t border overflow-visible whitespace-nowrap flex z-10 field-area-controls"
style="top: -25px; height: 25px" style="top: -25px; height: 25px"
@mousedown.stop @mousedown.stop="$emit('click-title')"
@pointerdown.stop @pointerdown.stop
> >
<FieldSubmitter <FieldSubmitter
@@ -279,7 +279,7 @@ export default {
default: false default: false
} }
}, },
emits: ['remove', 'scroll-to', 'add-custom-field', 'change'], emits: ['remove', 'scroll-to', 'add-custom-field', 'change', 'click-title'],
data () { data () {
return { return {
isShowFormulaModal: false, isShowFormulaModal: false,
+152 -5
View File
@@ -51,6 +51,30 @@
</div> </div>
</div> </div>
</div> </div>
<div
v-if="beforeRevisionSnapshot"
class="top-1.5 sticky h-0 z-20 max-w-2xl mx-auto"
>
<div class="alert border-base-content/30 py-2 px-2.5">
<IconInfoCircle class="stroke-info shrink-0 w-6 h-6" />
<span>{{ t('viewing_revision_from').replace('{date}', formatRevisionTime(beforeRevisionSnapshot.revision.created_at)) }}</span>
<div>
<button
class="btn btn-sm"
@click.prevent="cancelRevision"
>
{{ t('cancel') }}
</button>
<button
v-if="editable"
class="btn btn-sm btn-neutral text-white"
@click.prevent="applyRevision"
>
{{ t('apply') }}
</button>
</div>
</div>
</div>
<div <div
v-if="$slots.buttons || withTitle" v-if="$slots.buttons || withTitle"
id="title_container" id="title_container"
@@ -213,6 +237,18 @@
<span class="whitespace-nowrap">{{ t('preferences') }}</span> <span class="whitespace-nowrap">{{ t('preferences') }}</span>
</a> </a>
</li> </li>
<li v-if="withRevisionsMenu">
<button
class="flex space-x-2"
@click.prevent="openRevisionsModal"
@mouseenter="preloadRevisions"
>
<span class="w-6 h-6 flex-shrink-0 flex items-center justify-center">
<IconHistory class="w-5 h-5" />
</span>
<span class="whitespace-nowrap">{{ t('revisions') }}</span>
</button>
</li>
<li v-if="withDownload"> <li v-if="withDownload">
<button <button
class="flex space-x-2" class="flex space-x-2"
@@ -600,8 +636,17 @@
<div <div
id="docuseal_modal_container" id="docuseal_modal_container"
class="modal-container" class="modal-container"
>
<RevisionsModal
v-if="isRevisionsModalOpen"
:template="template"
:revisions="revisions"
:locale="locale"
@close="isRevisionsModalOpen = false"
@apply="onRevisionApply"
/> />
</div> </div>
</div>
</template> </template>
<script> <script>
@@ -618,7 +663,8 @@ import DocumentPreview from './preview'
import DocumentControls from './controls' import DocumentControls from './controls'
import MobileFields from './mobile_fields' import MobileFields from './mobile_fields'
import FieldSubmitter from './field_submitter' import FieldSubmitter from './field_submitter'
import { IconPlus, IconUsersPlus, IconDeviceFloppy, IconChevronDown, IconEye, IconWritingSign, IconInnerShadowTop, IconInfoCircle, IconAdjustments, IconDownload } from '@tabler/icons-vue' import RevisionsModal from './revisions_modal'
import { IconPlus, IconUsersPlus, IconDeviceFloppy, IconChevronDown, IconEye, IconWritingSign, IconInnerShadowTop, IconInfoCircle, IconAdjustments, IconDownload, IconHistory } from '@tabler/icons-vue'
import { v4 } from 'uuid' import { v4 } from 'uuid'
import { ref, computed, toRaw, defineAsyncComponent } from 'vue' import { ref, computed, toRaw, defineAsyncComponent } from 'vue'
import * as i18n from './i18n' import * as i18n from './i18n'
@@ -658,7 +704,9 @@ export default {
IconDownload, IconDownload,
IconAdjustments, IconAdjustments,
IconEye, IconEye,
IconDeviceFloppy IconHistory,
IconDeviceFloppy,
RevisionsModal
}, },
provide () { provide () {
return { return {
@@ -980,6 +1028,16 @@ export default {
type: Boolean, type: Boolean,
required: false, required: false,
default: false default: false
},
withRevisions: {
type: Boolean,
required: false,
default: false
},
withRevisionsMenu: {
type: Boolean,
required: false,
default: false
} }
}, },
data () { data () {
@@ -1002,7 +1060,10 @@ export default {
drawOption: null, drawOption: null,
dragField: null, dragField: null,
isDragFile: false, isDragFile: false,
isMathLoaded: false isMathLoaded: false,
isRevisionsModalOpen: false,
revisions: [],
beforeRevisionSnapshot: null
} }
}, },
computed: { computed: {
@@ -1767,6 +1828,73 @@ export default {
closeDropdown () { closeDropdown () {
document.activeElement.blur() document.activeElement.blur()
}, },
preloadRevisions () {
this.loadRevisionsPromise ||= this.baseFetch(`/templates/${this.template.id}/versions`)
},
openRevisionsModal () {
this.closeDropdown()
this.loadRevisionsPromise ||= this.baseFetch(`/templates/${this.template.id}/versions`)
this.loadRevisionsPromise.then(async (resp) => {
this.revisions = await resp.json()
this.isRevisionsModalOpen = true
}).finally(() => {
this.loadRevisionsPromise = null
})
},
onRevisionApply (revision) {
this.beforeRevisionSnapshot = {
template: JSON.parse(JSON.stringify(this.template)),
dynamicDocuments: JSON.parse(JSON.stringify(this.dynamicDocuments)),
revision
}
const { dynamic_documents: nextDynamicDocs = [], ...nextTemplate } = revision.data
Object.assign(this.template, nextTemplate)
this.dynamicDocuments.splice(0, this.dynamicDocuments.length, ...nextDynamicDocs)
this.$nextTick(() => this.reloadDynamicDocumentContent())
this.isRevisionsModalOpen = false
},
cancelRevision () {
Object.assign(this.template, this.beforeRevisionSnapshot.template)
this.dynamicDocuments.splice(0, this.dynamicDocuments.length, ...this.beforeRevisionSnapshot.dynamicDocuments)
this.beforeRevisionSnapshot = null
this.$nextTick(() => this.reloadDynamicDocumentContent())
},
applyRevision () {
this.beforeRevisionSnapshot = null
const dynamicDocumentRefs = this.documentRefs.filter((ref) => ref.isDynamic)
dynamicDocumentRefs.forEach((ref) => ref.update())
this.rebuildVariablesSchema({ disable: false })
return Promise.all([this.save({ force: true }), ...dynamicDocumentRefs.map((ref) => ref.saveBody())])
},
reloadDynamicDocumentContent () {
this.documentRefs.forEach((ref) => {
if (ref.isDynamic) ref.reloadContent()
})
},
formatRevisionTime (string) {
return new Date(string).toLocaleString(this.locale || undefined, {
year: 'numeric',
month: 'numeric',
day: 'numeric',
hour: 'numeric',
minute: '2-digit'
})
},
t (key) { t (key) {
return this.i18n[key] || i18n[this.language]?.[key] || i18n.en[key] || key return this.i18n[key] || i18n[this.language]?.[key] || i18n.en[key] || key
}, },
@@ -2384,6 +2512,11 @@ export default {
this.selectedAreasRef.value = [area] this.selectedAreasRef.value = [area]
area.x = Math.min(Math.max(area.x, 0), 1)
area.y = Math.min(Math.max(area.y, 0), 1)
area.w = Math.min(Math.max(area.w, 0), 1)
area.h = Math.min(Math.max(area.h, 0), 1)
this.save() this.save()
} else { } else {
const documentRef = this.documentRefs.find((e) => e.document.uuid === area.attachment_uuid) const documentRef = this.documentRefs.find((e) => e.document.uuid === area.attachment_uuid)
@@ -3013,7 +3146,11 @@ export default {
const dynamicDocumentSaves = dynamicDocumentRefs.map((ref) => ref.saveBody()) const dynamicDocumentSaves = dynamicDocumentRefs.map((ref) => ref.saveBody())
Promise.all([this.save(), ...dynamicDocumentSaves]).then(() => { Promise.all([this.save({ force: true }), ...dynamicDocumentSaves]).then(() => {
if (this.withRevisions) {
this.captureRevision()
}
window.Turbo.visit(`/templates/${this.template.id}`) window.Turbo.visit(`/templates/${this.template.id}`)
}).finally(() => { }).finally(() => {
this.isSaving = false this.isSaving = false
@@ -3244,9 +3381,13 @@ export default {
} }
}) })
}, },
save ({ force } = { force: false }) { save ({ force = false } = {}) {
this.pendingFieldAttachmentUuids = [] this.pendingFieldAttachmentUuids = []
if (this.beforeRevisionSnapshot) {
this.beforeRevisionSnapshot = null
}
if (this.onChange) { if (this.onChange) {
this.onChange(this.template) this.onChange(this.template)
} }
@@ -3281,6 +3422,12 @@ export default {
} }
}) })
}, },
captureRevision () {
return this.baseFetch(`/templates/${this.template.id}/versions`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
})
},
onDynamicDocumentUpdate () { onDynamicDocumentUpdate () {
this.rebuildVariablesSchema() this.rebuildVariablesSchema()
@@ -6,7 +6,7 @@
class="absolute top-0 bottom-0 right-0 left-0" class="absolute top-0 bottom-0 right-0 left-0"
@click.prevent="$emit('close')" @click.prevent="$emit('close')"
/> />
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl"> <div class="modal-box pt-4 pb-6 px-6 mt-20 w-full max-w-xl">
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium"> <div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
<span class="modal-title"> <span class="modal-title">
{{ t('condition') }} - {{ (defaultField ? (defaultField.title || item.title || item.name) : item.name) || buildDefaultName(item) }} {{ t('condition') }} - {{ (defaultField ? (defaultField.title || item.title || item.name) : item.name) || buildDefaultName(item) }}
@@ -183,6 +183,9 @@ export default {
this.sectionRefs.push(ref) this.sectionRefs.push(ref)
} }
}, },
reloadContent () {
this.sectionRefs.forEach((ref) => ref.reloadContent())
},
onBeforeUnload (event) { onBeforeUnload (event) {
if (this.saveTimer) { if (this.saveTimer) {
event.preventDefault() event.preventDefault()
@@ -94,6 +94,8 @@ img.ProseMirror-separator {
} }
dynamic-variable { dynamic-variable {
background-color: #fef3c7; background-color: #fef3c7;
word-break: break-all;
overflow-wrap: anywhere;
}`) }`)
function collectDomAttrs (dom) { function collectDomAttrs (dom) {
@@ -136,11 +138,12 @@ function collectSpanDomAttrs (dom) {
return result return result
} }
function createBlockNode (name, tag, content) { function createBlockNode (name, tag, content, extra = {}) {
return Node.create({ return Node.create({
name, name,
group: 'block', group: 'block',
content: content || 'block+', content: content || 'block+',
...extra,
addAttributes () { addAttributes () {
return { return {
htmlAttrs: { default: {} } htmlAttrs: { default: {} }
@@ -194,9 +197,9 @@ const CustomHeading = Node.create({
}) })
const SectionNode = createBlockNode('section', 'section') const SectionNode = createBlockNode('section', 'section')
const ArticleNode = createBlockNode('article', 'article') const ArticleNode = createBlockNode('article', 'article', null, { isolating: true })
const HeaderNode = createBlockNode('header', 'header') const HeaderNode = createBlockNode('header', 'header', null, { isolating: true })
const FooterNode = createBlockNode('footer', 'footer') const FooterNode = createBlockNode('footer', 'footer', null, { isolating: true })
const DivNode = createBlockNode('div', 'div') const DivNode = createBlockNode('div', 'div')
const BlockquoteNode = createBlockNode('blockquote', 'blockquote') const BlockquoteNode = createBlockNode('blockquote', 'blockquote')
const PreNode = createBlockNode('pre', 'pre') const PreNode = createBlockNode('pre', 'pre')
@@ -281,6 +281,9 @@ export default {
} }
}, },
methods: { methods: {
reloadContent () {
this.editor.commands.setContent(this.section.innerHTML, { emitUpdate: false })
},
findAreaNodePos (areaUuid) { findAreaNodePos (areaUuid) {
const el = this.editor.view.dom.querySelector(`[data-area-uuid="${areaUuid}"]`) const el = this.editor.view.dom.querySelector(`[data-area-uuid="${areaUuid}"]`)
@@ -248,7 +248,7 @@ export default {
FieldType, FieldType,
IconSettings IconSettings
}, },
inject: ['t', 'save', 'backgroundColor', 'dateFormats'], inject: ['t', 'save', 'backgroundColor', 'dateFormats', 'locale'],
provide () { provide () {
return { return {
fieldTypes: ['text', 'number', 'date', 'checkbox', 'radio', 'select'] fieldTypes: ['text', 'number', 'date', 'checkbox', 'radio', 'select']
@@ -308,15 +308,18 @@ export default {
return { text: 'string', number: 'number', date: 'date', checkbox: 'boolean', radio: 'string', select: 'string' } return { text: 'string', number: 'number', date: 'date', checkbox: 'boolean', radio: 'string', select: 'string' }
}, },
numberFormats () { numberFormats () {
return [ const formats = ['none', 'usd', 'eur', 'gbp', 'comma', 'dot', 'space']
'none', const spaceLocales = ['fr-FR', 'es-ES', 'pt-PT', 'de-DE', 'it-IT', 'nl-NL']
'usd',
'eur', formats.push(spaceLocales.includes(this.locale) ? 'percent_space' : 'percent')
'gbp',
'comma', const selectedFormat = this.schema.format
'dot',
'space' if (selectedFormat && !formats.includes(selectedFormat)) {
] formats.push(selectedFormat)
}
return formats
}, },
availableDateFormats () { availableDateFormats () {
const formats = this.dateFormats.length const formats = this.dateFormats.length
@@ -397,6 +400,10 @@ export default {
return new Intl.NumberFormat('de-DE').format(number) return new Intl.NumberFormat('de-DE').format(number)
} else if (format === 'space') { } else if (format === 'space') {
return new Intl.NumberFormat('fr-FR').format(number) return new Intl.NumberFormat('fr-FR').format(number)
} else if (format === 'percent') {
return `${number}%`
} else if (format === 'percent_space') {
return `${String(number).replace('.', ',')} %`
} else { } else {
return number return number
} }
@@ -514,7 +514,7 @@ export default {
ContextSubmenu, ContextSubmenu,
ContextModal ContextModal
}, },
inject: ['t', 'getFieldTypeIndex', 'template', 'withCustomFields', 'currencies', 'dateFormats'], inject: ['t', 'getFieldTypeIndex', 'template', 'withCustomFields', 'currencies', 'dateFormats', 'locale'],
props: { props: {
contextMenu: { contextMenu: {
type: Object, type: Object,
@@ -610,7 +610,7 @@ export default {
IconTypography, IconTypography,
IconX IconX
}, },
inject: ['template', 't', 'dateFormats'], inject: ['template', 't', 'dateFormats', 'locale'],
props: { props: {
field: { field: {
type: Object, type: Object,
@@ -691,15 +691,18 @@ export default {
} }
}, },
numberFormats () { numberFormats () {
return [ const formats = ['none', 'usd', 'eur', 'gbp', 'comma', 'dot', 'space']
'none', const spaceLocales = ['fr-FR', 'es-ES', 'pt-PT', 'de-DE', 'it-IT', 'nl-NL']
'usd',
'eur', formats.push(spaceLocales.includes(this.locale) ? 'percent_space' : 'percent')
'gbp',
'comma', const selectedFormat = this.field.preferences?.format
'dot',
'space' if (selectedFormat && !formats.includes(selectedFormat)) {
] formats.push(selectedFormat)
}
return formats
}, },
availableDateFormats () { availableDateFormats () {
const formats = this.dateFormats.length const formats = this.dateFormats.length
@@ -824,6 +827,10 @@ export default {
return new Intl.NumberFormat('de-DE').format(number) return new Intl.NumberFormat('de-DE').format(number)
} else if (format === 'space') { } else if (format === 'space') {
return new Intl.NumberFormat('fr-FR').format(number) return new Intl.NumberFormat('fr-FR').format(number)
} else if (format === 'percent') {
return `${number}%`
} else if (format === 'percent_space') {
return `${String(number).replace('.', ',')} %`
} else { } else {
return number return number
} }
@@ -147,7 +147,7 @@
> >
<a <a
href="#" href="#"
class="flex px-2 group justify-between items-center" class="flex px-2 text-sm group justify-between items-center"
:class="{ 'active': submitter === selectedSubmitter, 'py-0.5': submitters.length > 8 }" :class="{ 'active': submitter === selectedSubmitter, 'py-0.5': submitters.length > 8 }"
@click.prevent="selectSubmitter(submitter)" @click.prevent="selectSubmitter(submitter)"
> >
@@ -198,7 +198,7 @@
> >
<a <a
href="#" href="#"
class="flex px-2" class="flex px-2 text-sm"
@click.prevent="addSubmitter" @click.prevent="addSubmitter"
> >
<IconUserPlus <IconUserPlus
+1 -1
View File
@@ -383,7 +383,7 @@
class="rounded py-2 px-4 w-full border border-dashed border-base-300" class="rounded py-2 px-4 w-full border border-dashed border-base-300"
> >
<div class="text-center text-sm"> <div class="text-center text-sm">
{{ t('start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document') }} {{ t('start_a_quick_tour_to_learn_how_to_create_and_send_your_first_document') }}
</div> </div>
<div class="flex justify-center"> <div class="flex justify-center">
<label <label
+65 -37
View File
@@ -200,7 +200,7 @@ const en = {
learn_more: 'Learn more', learn_more: 'Learn more',
and: 'and', and: 'and',
or: 'or', or: 'or',
start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: 'Start a quick tour to learn how to create and send your first document', start_a_quick_tour_to_learn_how_to_create_and_send_your_first_document: 'Start a quick tour to learn how to create and send your first document',
start_tour: 'Start Tour', start_tour: 'Start Tour',
or_add_from: 'Or add from', or_add_from: 'Or add from',
sync: 'Sync', sync: 'Sync',
@@ -215,7 +215,11 @@ const en = {
align_bottom: 'Align Bottom', align_bottom: 'Align Bottom',
fields_selected: '{count} Fields Selected', fields_selected: '{count} Fields Selected',
field_added: '{count} Field Added', field_added: '{count} Field Added',
fields_added: '{count} Fields Added' fields_added: '{count} Fields Added',
revisions: 'Revisions',
apply: 'Apply',
no_revisions_yet: 'No revisions yet',
viewing_revision_from: 'Viewing revision from {date}'
} }
const es = { const es = {
@@ -239,7 +243,7 @@ const es = {
min: 'Mín', min: 'Mín',
max: 'Máx', max: 'Máx',
date_signed: 'Fecha actual', date_signed: 'Fecha actual',
fuente: 'Fuente', font: 'Fuente',
party: 'Parte', party: 'Parte',
method: 'Método', method: 'Método',
reorder_fields: 'Reordenar campos', reorder_fields: 'Reordenar campos',
@@ -298,7 +302,7 @@ const es = {
set_signing_date: 'Establecer fecha de firma', set_signing_date: 'Establecer fecha de firma',
are_you_sure_: '¿Estás seguro?', are_you_sure_: '¿Estás seguro?',
sign_yourself: 'Firma tú mismo', sign_yourself: 'Firma tú mismo',
checked: 'Seleccionado', checked: 'Marcado',
current_date: 'Fecha actual', current_date: 'Fecha actual',
send: 'Enviar', send: 'Enviar',
remove: 'Eliminar', remove: 'Eliminar',
@@ -336,16 +340,16 @@ const es = {
eighth_party: 'Octava Parte', eighth_party: 'Octava Parte',
ninth_party: 'Novena Parte', ninth_party: 'Novena Parte',
tenth_party: 'Décima Parte', tenth_party: 'Décima Parte',
eleventh_party: 'Undécimo Partido', eleventh_party: 'Undécima Parte',
twelfth_party: 'Duodécimo Partido', twelfth_party: 'Duodécima Parte',
thirteenth_party: 'Decimotercer Partido', thirteenth_party: 'Decimotercera Parte',
fourteenth_party: 'Catorceavo Partido', fourteenth_party: 'Decimocuarta Parte',
fifteenth_party: 'Quinceavo Partido', fifteenth_party: 'Decimoquinta Parte',
sixteenth_party: 'Dieciséisavo Partido', sixteenth_party: 'Decimosexta Parte',
seventeenth_party: 'Diecisieteavo Partido', seventeenth_party: 'Decimoséptima Parte',
eighteenth_party: 'Dieciochoavo Partido', eighteenth_party: 'Decimoctava Parte',
nineteenth_party: 'Decimonovena Fiesta', nineteenth_party: 'Decimonovena Parte',
twentieth_party: 'Vigésima Fiesta', twentieth_party: 'Vigésima Parte',
draw: 'Dibujar', draw: 'Dibujar',
add: 'Agregar', add: 'Agregar',
text: 'Texto', text: 'Texto',
@@ -420,7 +424,7 @@ const es = {
learn_more: 'Aprende más', learn_more: 'Aprende más',
and: 'y', and: 'y',
or: 'o', or: 'o',
start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: 'Inicia una guía rápida para aprender a crear y enviar tu primer documento.', start_a_quick_tour_to_learn_how_to_create_and_send_your_first_document: 'Inicia una guía rápida para aprender a crear y enviar tu primer documento.',
start_tour: 'Iniciar guía', start_tour: 'Iniciar guía',
or_add_from: 'O agregar desde', or_add_from: 'O agregar desde',
sync: 'Sincronizar', sync: 'Sincronizar',
@@ -435,7 +439,11 @@ const es = {
align_bottom: 'Alinear abajo', align_bottom: 'Alinear abajo',
fields_selected: '{count} Campos Seleccionados', fields_selected: '{count} Campos Seleccionados',
field_added: '{count} Campo Añadido', field_added: '{count} Campo Añadido',
fields_added: '{count} Campos Añadidos' fields_added: '{count} Campos Añadidos',
revisions: 'Revisiones',
apply: 'Aplicar',
no_revisions_yet: 'Aún no hay revisiones',
viewing_revision_from: 'Viendo revisión del {date}'
} }
const it = { const it = {
@@ -458,15 +466,15 @@ const it = {
length: 'Lunghezza', length: 'Lunghezza',
min: 'Min', min: 'Min',
max: 'Max', max: 'Max',
date_signed: 'Data attuale', date_signed: 'Data corrente',
font: 'Carattere', font: 'Carattere',
party: 'Parte', party: 'Parte',
method: 'Metodo', method: 'Metodo',
reorder_fields: 'Riordina i campi', reorder_fields: 'Riordina i campi',
verify_id: 'Verifica ID', verify_id: 'Verifica ID',
obtain_qualified_electronic_signature_with_the_trusted_provider_click_to_learn_more: 'Ottieni una firma elettronica qualificata (QeS) con il fornitore di fiducia. Clicca per saperne di più.', obtain_qualified_electronic_signature_with_the_trusted_provider_click_to_learn_more: 'Ottieni una firma elettronica qualificata (QeS) con il fornitore di fiducia. Clicca per saperne di più.',
ricorrente: 'Ricorrente', recurrent: 'Ricorrente',
una_volta: 'Una volta', one_off: 'Una tantum',
editable: 'Modificabile', editable: 'Modificabile',
search_field: 'Campo di ricerca', search_field: 'Campo di ricerca',
field_not_found: 'Campo non trovato', field_not_found: 'Campo non trovato',
@@ -525,7 +533,7 @@ const it = {
drawn: 'Disegnato', drawn: 'Disegnato',
drawn_or_typed: 'Disegnato o Digitato', drawn_or_typed: 'Disegnato o Digitato',
drawn_or_upload: 'Disegnato o Caricato', drawn_or_upload: 'Disegnato o Caricato',
upload: 'Caricare', upload: 'Carica',
formula: 'Formula', formula: 'Formula',
typed: 'Digitato', typed: 'Digitato',
draw_field_on_the_document: 'Disegnare un campo sul documento', draw_field_on_the_document: 'Disegnare un campo sul documento',
@@ -640,7 +648,7 @@ const it = {
learn_more: 'Scopri di più', learn_more: 'Scopri di più',
and: 'e', and: 'e',
or: 'o', or: 'o',
start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: 'Inizia un tour rapido per imparare a creare e inviare il tuo primo documento.', start_a_quick_tour_to_learn_how_to_create_and_send_your_first_document: 'Inizia un tour rapido per imparare a creare e inviare il tuo primo documento.',
start_tour: 'Inizia il tour', start_tour: 'Inizia il tour',
or_add_from: 'O aggiungi da', or_add_from: 'O aggiungi da',
sync: 'Sincronizza', sync: 'Sincronizza',
@@ -655,7 +663,11 @@ const it = {
align_bottom: 'Allinea in basso', align_bottom: 'Allinea in basso',
fields_selected: '{count} Campi Selezionati', fields_selected: '{count} Campi Selezionati',
field_added: '{count} Campo Aggiunto', field_added: '{count} Campo Aggiunto',
fields_added: '{count} Campi Aggiunti' fields_added: '{count} Campi Aggiunti',
revisions: 'Revisioni',
apply: 'Applica',
no_revisions_yet: 'Nessuna revisione ancora',
viewing_revision_from: 'Visualizzazione revisione del {date}'
} }
const pt = { const pt = {
@@ -667,7 +679,7 @@ const pt = {
analyzing_: 'Analisando...', analyzing_: 'Analisando...',
download: 'Baixar', download: 'Baixar',
downloading_: 'Baixando...', downloading_: 'Baixando...',
view: 'Visualizar', view: 'Ver',
payment_link: 'Link de pagamento', payment_link: 'Link de pagamento',
strikeout: 'Tachado', strikeout: 'Tachado',
draw_strikethrough_the_document: 'Desenhe uma linha de tachado no documento', draw_strikethrough_the_document: 'Desenhe uma linha de tachado no documento',
@@ -679,13 +691,13 @@ const pt = {
min: 'Mín', min: 'Mín',
max: 'Máx', max: 'Máx',
date_signed: 'Data atual', date_signed: 'Data atual',
fonte: 'Fonte', font: 'Fonte',
party: 'Parte', party: 'Parte',
method: 'Método', method: 'Método',
reorder_fields: 'Reorganizar campos', reorder_fields: 'Reorganizar campos',
verify_id: 'Verificar ID', verify_id: 'Verificar ID',
obtain_qualified_electronic_signature_with_the_trusted_provider_click_to_learn_more: 'Obtenha a assinatura eletrônica qualificada (QeS) com o provedor confiável. Clique para saber mais.', obtain_qualified_electronic_signature_with_the_trusted_provider_click_to_learn_more: 'Obtenha a assinatura eletrônica qualificada (QeS) com o provedor confiável. Clique para saber mais.',
recurrent: 'Recurrente', recurrent: 'Recorrente',
one_off: 'Único', one_off: 'Único',
editable: 'Editável', editable: 'Editável',
search_field: 'Campo de busca', search_field: 'Campo de busca',
@@ -752,7 +764,7 @@ const pt = {
processing_: 'Processando...', processing_: 'Processando...',
add_pdf_documents_or_images: 'Adicionar documentos PDF ou imagens', add_pdf_documents_or_images: 'Adicionar documentos PDF ou imagens',
add_documents_or_images: 'Adicionar documentos ou imagens', add_documents_or_images: 'Adicionar documentos ou imagens',
add_a_new_document: 'Ajouter un nouveau document', add_a_new_document: 'Adicionar um novo documento',
replace_existing_document: 'Substituir documento existente', replace_existing_document: 'Substituir documento existente',
clone_and_replace_documents: 'Clonar e substituir documentos', clone_and_replace_documents: 'Clonar e substituir documentos',
required: 'Obrigatório', required: 'Obrigatório',
@@ -860,7 +872,7 @@ const pt = {
learn_more: 'Saiba mais', learn_more: 'Saiba mais',
and: 'e', and: 'e',
or: 'ou', or: 'ou',
start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: 'Comece um tour rápido para aprender a criar e enviar seu primeiro documento.', start_a_quick_tour_to_learn_how_to_create_and_send_your_first_document: 'Comece um tour rápido para aprender a criar e enviar seu primeiro documento.',
start_tour: 'Iniciar tour', start_tour: 'Iniciar tour',
or_add_from: 'Ou adicionar de', or_add_from: 'Ou adicionar de',
sync: 'Sincronizar', sync: 'Sincronizar',
@@ -875,7 +887,11 @@ const pt = {
align_bottom: 'Alinhar à parte inferior', align_bottom: 'Alinhar à parte inferior',
fields_selected: '{count} Campos Selecionados', fields_selected: '{count} Campos Selecionados',
field_added: '{count} Campo Adicionado', field_added: '{count} Campo Adicionado',
fields_added: '{count} Campos Adicionados' fields_added: '{count} Campos Adicionados',
revisions: 'Revisões',
apply: 'Aplicar',
no_revisions_yet: 'Nenhuma revisão ainda',
viewing_revision_from: 'Visualizando revisão de {date}'
} }
const fr = { const fr = {
@@ -900,7 +916,7 @@ const fr = {
max: 'Max', max: 'Max',
font: 'Police', font: 'Police',
party: 'Partie', party: 'Partie',
date_signed: 'Date de signature', date_signed: 'Date du jour',
method: 'Méthode', method: 'Méthode',
reorder_fields: 'Réorganiser les champs', reorder_fields: 'Réorganiser les champs',
verify_id: "Vérifier l'ID", verify_id: "Vérifier l'ID",
@@ -1080,7 +1096,7 @@ const fr = {
learn_more: 'En savoir plus', learn_more: 'En savoir plus',
and: 'et', and: 'et',
or: 'ou', or: 'ou',
start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: 'Commencez une visite rapide pour apprendre à créer et à envoyer votre premier document', start_a_quick_tour_to_learn_how_to_create_and_send_your_first_document: 'Commencez une visite rapide pour apprendre à créer et à envoyer votre premier document',
start_tour: 'Démarrer', start_tour: 'Démarrer',
or_add_from: 'Ou ajouter depuis', or_add_from: 'Ou ajouter depuis',
sync: 'Synchroniser', sync: 'Synchroniser',
@@ -1095,7 +1111,11 @@ const fr = {
align_bottom: 'Aligner en bas', align_bottom: 'Aligner en bas',
fields_selected: '{count} Champs Sélectionnés', fields_selected: '{count} Champs Sélectionnés',
field_added: '{count} Champ Ajouté', field_added: '{count} Champ Ajouté',
fields_added: '{count} Champs Ajoutés' fields_added: '{count} Champs Ajoutés',
revisions: 'Révisions',
apply: 'Appliquer',
no_revisions_yet: 'Aucune révision pour le moment',
viewing_revision_from: 'Affichage de la révision du {date}'
} }
const de = { const de = {
@@ -1120,7 +1140,7 @@ const de = {
max: 'Max.', max: 'Max.',
font: 'Schriftart', font: 'Schriftart',
party: 'Partei', party: 'Partei',
date_signed: 'Unterzeichnungsdatum', date_signed: 'Aktuelles Datum',
method: 'Methode', method: 'Methode',
reorder_fields: 'Felder neu anordnen', reorder_fields: 'Felder neu anordnen',
verify_id: 'ID überprüfen', verify_id: 'ID überprüfen',
@@ -1300,7 +1320,7 @@ const de = {
learn_more: 'Mehr erfahren', learn_more: 'Mehr erfahren',
and: 'und', and: 'und',
or: 'oder', or: 'oder',
start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: 'Starten Sie eine Kurztour, um zu lernen, wie Sie Ihr erstes Dokument erstellen und versenden.', start_a_quick_tour_to_learn_how_to_create_and_send_your_first_document: 'Starten Sie eine Kurztour, um zu lernen, wie Sie Ihr erstes Dokument erstellen und versenden.',
start_tour: 'Tour starten', start_tour: 'Tour starten',
or_add_from: 'Oder hinzufügen aus', or_add_from: 'Oder hinzufügen aus',
sync: 'Synchronisieren', sync: 'Synchronisieren',
@@ -1315,7 +1335,11 @@ const de = {
align_bottom: 'Unten ausrichten', align_bottom: 'Unten ausrichten',
fields_selected: '{count} Felder Ausgewählt', fields_selected: '{count} Felder Ausgewählt',
field_added: '{count} Feld Hinzugefügt', field_added: '{count} Feld Hinzugefügt',
fields_added: '{count} Felder Hinzugefügt' fields_added: '{count} Felder Hinzugefügt',
revisions: 'Revisionen',
apply: 'Anwenden',
no_revisions_yet: 'Noch keine Revisionen',
viewing_revision_from: 'Ansicht der Revision vom {date}'
} }
const nl = { const nl = {
@@ -1340,7 +1364,7 @@ const nl = {
max: 'Max', max: 'Max',
font: 'Lettertype', font: 'Lettertype',
party: 'Partij', party: 'Partij',
date_signed: 'Datum ondertekend', date_signed: 'Huidige datum',
method: 'Methode', method: 'Methode',
reorder_fields: 'Velden herschikken', reorder_fields: 'Velden herschikken',
verify_id: 'ID verifiëren', verify_id: 'ID verifiëren',
@@ -1520,7 +1544,7 @@ const nl = {
learn_more: 'Meer informatie', learn_more: 'Meer informatie',
and: 'en', and: 'en',
or: 'of', or: 'of',
start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: 'Start een korte rondleiding om te leren hoe u uw eerste document maakt en verzendt', start_a_quick_tour_to_learn_how_to_create_and_send_your_first_document: 'Start een korte rondleiding om te leren hoe u uw eerste document maakt en verzendt',
start_tour: 'Rondleiding starten', start_tour: 'Rondleiding starten',
or_add_from: 'Of toevoegen van', or_add_from: 'Of toevoegen van',
sync: 'Synchroniseren', sync: 'Synchroniseren',
@@ -1535,7 +1559,11 @@ const nl = {
align_bottom: 'Onder uitlijnen', align_bottom: 'Onder uitlijnen',
fields_selected: '{count} Velden Geselecteerd', fields_selected: '{count} Velden Geselecteerd',
field_added: '{count} Veld Toegevoegd', field_added: '{count} Veld Toegevoegd',
fields_added: '{count} Velden Toegevoegd' fields_added: '{count} Velden Toegevoegd',
revisions: 'Revisies',
apply: 'Toepassen',
no_revisions_yet: 'Nog geen revisies',
viewing_revision_from: 'Revisie van {date} bekijken'
} }
export { en, es, it, pt, fr, de, nl } export { en, es, it, pt, fr, de, nl }
+1
View File
@@ -57,6 +57,7 @@
@scroll-to="$emit('scroll-to', $event)" @scroll-to="$emit('scroll-to', $event)"
@add-custom-field="$emit('add-custom-field', $event)" @add-custom-field="$emit('add-custom-field', $event)"
@contextmenu="openAreaContextMenu($event, item.area, item.field)" @contextmenu="openAreaContextMenu($event, item.area, item.field)"
@click-title="closeContextMenu"
/> />
<FieldArea <FieldArea
v-for="(area, index) in newAreas" v-for="(area, index) in newAreas"
@@ -0,0 +1,104 @@
<template>
<div class="modal modal-open items-start !animate-none overflow-y-auto">
<div
class="absolute top-0 bottom-0 right-0 left-0"
@click.prevent="$emit('close')"
/>
<div class="modal-box pt-4 pb-6 mt-20 w-full">
<div class="flex justify-between items-center border-b pb-2 mb-3 font-medium">
<span>{{ t('revisions') }}</span>
<a
href="#"
class="text-xl"
@click.prevent="$emit('close')"
>&times;</a>
</div>
<ul class="space-y-1.5">
<li
v-for="revision in revisions"
:key="revision.id"
>
<button
type="button"
class="w-full text-left rounded-lg p-3 border border-dashed border-base-200 transition-colors disabled:cursor-default hover:bg-base-200 hover:border-base-200"
:disabled="loadingId !== null"
@click="viewRevision(revision.id)"
>
<div class="flex justify-between items-center gap-2">
<div class="flex flex-col">
<span>{{ formatDate(revision.created_at) }}</span>
<span class="-ml-0.5 flex items-center space-x-1 text-xs text-base-content/60 mt-0.5">
<IconUser class="w-3.5 h-3.5 flex-shrink-0" />
<span class="truncate">{{ revision.author.full_name || revision.author.email }}</span>
</span>
</div>
<span class="btn btn-sm btn-neutral text-white pointer-events-none flex-shrink-0">
<IconInnerShadowTop
v-if="loadingId === revision.id"
class="w-4 h-4 animate-spin"
/>
<span v-else>{{ t('view') }}</span>
</span>
</div>
</button>
</li>
<li
v-if="!revisions.length"
class="py-4 text-center text-base-content/60"
>
{{ t('no_revisions_yet') }}
</li>
</ul>
</div>
</div>
</template>
<script>
import { IconUser, IconInnerShadowTop } from '@tabler/icons-vue'
export default {
name: 'RevisionsModal',
components: { IconUser, IconInnerShadowTop },
inject: ['t', 'baseFetch'],
props: {
template: {
type: Object,
required: true
},
revisions: {
type: Array,
required: true
},
locale: {
type: String,
required: true
}
},
emits: ['close', 'apply'],
data () {
return {
loadingId: null
}
},
methods: {
viewRevision (id) {
if (this.loadingId !== null) return
this.loadingId = id
this.baseFetch(`/templates/${this.template.id}/versions/${id}`)
.then((r) => r.json())
.then((revision) => { this.$emit('apply', revision) })
.finally(() => { this.loadingId = null })
},
formatDate (string) {
return new Date(string).toLocaleString(this.locale || undefined, {
month: 'long',
day: 'numeric',
hour: 'numeric',
minute: '2-digit'
})
}
}
}
</script>
@@ -168,6 +168,65 @@
<script> <script>
import { IconUpload, IconInnerShadowTop, IconChevronDown, IconBrandGoogleDrive } from '@tabler/icons-vue' import { IconUpload, IconInnerShadowTop, IconChevronDown, IconBrandGoogleDrive } from '@tabler/icons-vue'
function convertImage (sourceFile, targetType, quality) {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = function (event) {
const img = new Image()
img.onload = function () {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
canvas.width = img.width
canvas.height = img.height
ctx.drawImage(img, 0, 0)
canvas.toBlob(function (blob) {
const ext = targetType === 'image/jpeg' ? '.jpg' : '.png'
const newFile = new File([blob], sourceFile.name.replace(/\.\w+$/, ext), { type: targetType })
resolve(newFile)
}, targetType, quality)
}
img.onerror = () => reject(new Error(`browser cannot decode ${sourceFile.type || sourceFile.name}`))
img.src = event.target.result
}
reader.onerror = reject
reader.readAsDataURL(sourceFile)
})
}
async function convertImagesInInput (input) {
if (!input.files || input.files.length === 0) return
const dt = new DataTransfer()
let didConvert = false
for (const file of Array.from(input.files)) {
let converted = file
try {
if (['image/bmp', 'image/vnd.microsoft.icon', 'image/svg+xml', 'image/gif'].includes(file.type)) {
converted = await convertImage(file, 'image/png')
didConvert = true
} else if (['image/heic', 'image/heif', 'image/heic-sequence', 'image/heif-sequence', 'image/avif', 'image/avif-sequence', 'image/webp'].includes(file.type)) {
converted = await convertImage(file, 'image/jpeg', 0.9)
didConvert = true
}
} catch (e) {
alert(e.message)
}
dt.items.add(converted)
}
if (didConvert) {
input.files = dt.files
}
}
export default { export default {
name: 'DocumentsUpload', name: 'DocumentsUpload',
components: { components: {
@@ -282,6 +341,10 @@ export default {
async upload ({ path } = {}) { async upload ({ path } = {}) {
this.isLoading = true this.isLoading = true
if (this.$refs.input) {
await convertImagesInInput(this.$refs.input)
}
return this.baseFetch(path || this.uploadUrl, { return this.baseFetch(path || this.uploadUrl, {
method: 'POST', method: 'POST',
headers: { Accept: 'application/json' }, headers: { Accept: 'application/json' },
-2
View File
@@ -3,8 +3,6 @@
class ReindexSearchEntryJob class ReindexSearchEntryJob
include Sidekiq::Job include Sidekiq::Job
InvalidFormat = Class.new(StandardError)
def perform(params = {}) def perform(params = {})
entry = SearchEntry.find_or_initialize_by(params.slice('record_type', 'record_id')) entry = SearchEntry.find_or_initialize_by(params.slice('record_type', 'record_id'))
@@ -30,13 +30,12 @@ class SendFormCompletedWebhookRequestJob
attempt:, attempt:,
data: Submitters::SerializeForWebhook.call(submitter)) data: Submitters::SerializeForWebhook.call(submitter))
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS && return if attempt > MAX_ATTEMPTS || (resp && resp.status.to_i < 400)
(!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
SendFormCompletedWebhookRequestJob.perform_in((2**attempt).minutes, { SendFormCompletedWebhookRequestJob.perform_in((2**attempt).minutes, {
**params, **params,
'attempt' => attempt + 1, 'attempt' => attempt + 1,
'last_status' => resp&.status.to_i 'last_status' => resp&.status.to_i
}) })
end end
end
end end
@@ -28,13 +28,12 @@ class SendFormDeclinedWebhookRequestJob
attempt:, attempt:,
data: Submitters::SerializeForWebhook.call(submitter)) data: Submitters::SerializeForWebhook.call(submitter))
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS && return if attempt > MAX_ATTEMPTS || (resp && resp.status.to_i < 400)
(!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
SendFormDeclinedWebhookRequestJob.perform_in((2**attempt).minutes, { SendFormDeclinedWebhookRequestJob.perform_in((2**attempt).minutes, {
**params, **params,
'attempt' => attempt + 1, 'attempt' => attempt + 1,
'last_status' => resp&.status.to_i 'last_status' => resp&.status.to_i
}) })
end end
end
end end
@@ -28,13 +28,12 @@ class SendFormStartedWebhookRequestJob
attempt:, attempt:,
data: Submitters::SerializeForWebhook.call(submitter)) data: Submitters::SerializeForWebhook.call(submitter))
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS && return if attempt > MAX_ATTEMPTS || (resp && resp.status.to_i < 400)
(!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
SendFormStartedWebhookRequestJob.perform_in((2**attempt).minutes, { SendFormStartedWebhookRequestJob.perform_in((2**attempt).minutes, {
**params, **params,
'attempt' => attempt + 1, 'attempt' => attempt + 1,
'last_status' => resp&.status.to_i 'last_status' => resp&.status.to_i
}) })
end end
end
end end
@@ -28,13 +28,12 @@ class SendFormViewedWebhookRequestJob
attempt:, attempt:,
data: Submitters::SerializeForWebhook.call(submitter)) data: Submitters::SerializeForWebhook.call(submitter))
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS && return if attempt > MAX_ATTEMPTS || (resp && resp.status.to_i < 400)
(!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
SendFormViewedWebhookRequestJob.perform_in((2**attempt).minutes, { SendFormViewedWebhookRequestJob.perform_in((2**attempt).minutes, {
**params, **params,
'attempt' => attempt + 1, 'attempt' => attempt + 1,
'last_status' => resp&.status.to_i 'last_status' => resp&.status.to_i
}) })
end end
end
end end
@@ -26,13 +26,12 @@ class SendSubmissionArchivedWebhookRequestJob
attempt:, attempt:,
data: submission.as_json(only: %i[id archived_at])) data: submission.as_json(only: %i[id archived_at]))
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS && return if attempt > MAX_ATTEMPTS || (resp && resp.status.to_i < 400)
(!Docuseal.multitenant? || submission.account.account_configs.exists?(key: :plan))
SendSubmissionArchivedWebhookRequestJob.perform_in((2**attempt).minutes, { SendSubmissionArchivedWebhookRequestJob.perform_in((2**attempt).minutes, {
**params, **params,
'attempt' => attempt + 1, 'attempt' => attempt + 1,
'last_status' => resp&.status.to_i 'last_status' => resp&.status.to_i
}) })
end end
end
end end
@@ -26,13 +26,12 @@ class SendSubmissionCompletedWebhookRequestJob
attempt:, attempt:,
data: Submissions::SerializeForApi.call(submission)) data: Submissions::SerializeForApi.call(submission))
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS && return if attempt > MAX_ATTEMPTS || (resp && resp.status.to_i < 400)
(!Docuseal.multitenant? || submission.account.account_configs.exists?(key: :plan))
SendSubmissionCompletedWebhookRequestJob.perform_in((2**attempt).minutes, { SendSubmissionCompletedWebhookRequestJob.perform_in((2**attempt).minutes, {
**params, **params,
'attempt' => attempt + 1, 'attempt' => attempt + 1,
'last_status' => resp&.status.to_i 'last_status' => resp&.status.to_i
}) })
end end
end
end end
@@ -26,13 +26,12 @@ class SendSubmissionCreatedWebhookRequestJob
attempt:, attempt:,
data: Submissions::SerializeForApi.call(submission)) data: Submissions::SerializeForApi.call(submission))
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS && return if attempt > MAX_ATTEMPTS || (resp && resp.status.to_i < 400)
(!Docuseal.multitenant? || submission.account.account_configs.exists?(key: :plan))
SendSubmissionCreatedWebhookRequestJob.perform_in((2**attempt).minutes, { SendSubmissionCreatedWebhookRequestJob.perform_in((2**attempt).minutes, {
**params, **params,
'attempt' => attempt + 1, 'attempt' => attempt + 1,
'last_status' => resp&.status.to_i 'last_status' => resp&.status.to_i
}) })
end end
end
end end
@@ -26,13 +26,12 @@ class SendSubmissionExpiredWebhookRequestJob
attempt:, attempt:,
data: Submissions::SerializeForApi.call(submission)) data: Submissions::SerializeForApi.call(submission))
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS && return if attempt > MAX_ATTEMPTS || (resp && resp.status.to_i < 400)
(!Docuseal.multitenant? || submission.account.account_configs.exists?(key: :plan))
SendSubmissionExpiredWebhookRequestJob.perform_in((2**attempt).minutes, { SendSubmissionExpiredWebhookRequestJob.perform_in((2**attempt).minutes, {
**params, **params,
'attempt' => attempt + 1, 'attempt' => attempt + 1,
'last_status' => resp&.status.to_i 'last_status' => resp&.status.to_i
}) })
end end
end
end end
@@ -26,13 +26,12 @@ class SendTemplateArchivedWebhookRequestJob
attempt:, attempt:,
data: template.as_json(only: %i[id archived_at])) data: template.as_json(only: %i[id archived_at]))
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS && return if attempt > MAX_ATTEMPTS || (resp && resp.status.to_i < 400)
(!Docuseal.multitenant? || template.account.account_configs.exists?(key: :plan))
SendTemplateArchivedWebhookRequestJob.perform_in((2**attempt).minutes, { SendTemplateArchivedWebhookRequestJob.perform_in((2**attempt).minutes, {
**params, **params,
'attempt' => attempt + 1, 'attempt' => attempt + 1,
'last_status' => resp&.status.to_i 'last_status' => resp&.status.to_i
}) })
end end
end
end end
@@ -26,13 +26,12 @@ class SendTemplateCreatedWebhookRequestJob
attempt:, attempt:,
data: Templates::SerializeForApi.call(template)) data: Templates::SerializeForApi.call(template))
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS && return if attempt > MAX_ATTEMPTS || (resp && resp.status.to_i < 400)
(!Docuseal.multitenant? || template.account.account_configs.exists?(key: :plan))
SendTemplateCreatedWebhookRequestJob.perform_in((2**attempt).minutes, { SendTemplateCreatedWebhookRequestJob.perform_in((2**attempt).minutes, {
**params, **params,
'attempt' => attempt + 1, 'attempt' => attempt + 1,
'last_status' => resp&.status.to_i 'last_status' => resp&.status.to_i
}) })
end end
end
end end
@@ -26,13 +26,12 @@ class SendTemplateUpdatedWebhookRequestJob
attempt:, attempt:,
data: Templates::SerializeForApi.call(template)) data: Templates::SerializeForApi.call(template))
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS && return if attempt > MAX_ATTEMPTS || (resp && resp.status.to_i < 400)
(!Docuseal.multitenant? || template.account.account_configs.exists?(key: :plan))
SendTemplateUpdatedWebhookRequestJob.perform_in((2**attempt).minutes, { SendTemplateUpdatedWebhookRequestJob.perform_in((2**attempt).minutes, {
**params, **params,
'attempt' => attempt + 1, 'attempt' => attempt + 1,
'last_status' => resp&.status.to_i 'last_status' => resp&.status.to_i
}) })
end end
end
end end
+6
View File
@@ -28,6 +28,12 @@ class DynamicDocument < ApplicationRecord
has_many :versions, class_name: 'DynamicDocumentVersion', dependent: :destroy has_many :versions, class_name: 'DynamicDocumentVersion', dependent: :destroy
has_one :current_version, class_name: 'DynamicDocumentVersion',
primary_key: %i[id sha1],
foreign_key: %i[dynamic_document_id sha1],
dependent: :destroy,
inverse_of: :dynamic_document
attribute :fields, :json attribute :fields, :json
before_validation :set_sha1 before_validation :set_sha1
+2 -1
View File
@@ -70,11 +70,12 @@ class Template < ApplicationRecord
has_many :submissions, dependent: :destroy has_many :submissions, dependent: :destroy
has_many :template_sharings, dependent: :destroy has_many :template_sharings, dependent: :destroy
has_many :template_accesses, dependent: :destroy has_many :template_accesses, dependent: :destroy
has_many :template_versions, dependent: :destroy
has_many :dynamic_documents, dependent: :destroy has_many :dynamic_documents, dependent: :destroy
has_many :dynamic_document_versions, through: :dynamic_documents, source: :versions has_many :dynamic_document_versions, through: :dynamic_documents, source: :versions
has_many :schema_dynamic_documents, lambda { |e| has_many :schema_dynamic_documents, lambda { |e|
where(uuid: e.schema.select { |e| e['dynamic'] }.pluck('attachment_uuid')) where(uuid: e.schema.select { |item| item['dynamic'] }.pluck('attachment_uuid'))
}, class_name: 'DynamicDocument', dependent: :destroy, inverse_of: :template }, class_name: 'DynamicDocument', dependent: :destroy, inverse_of: :template
scope :active, -> { where(archived_at: nil) } scope :active, -> { where(archived_at: nil) }
+44
View File
@@ -0,0 +1,44 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: template_versions
#
# id :bigint not null, primary key
# data :text not null
# sha1 :string not null
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint not null
# author_id :bigint not null
# template_id :bigint not null
#
# Indexes
#
# index_template_versions_on_account_id (account_id)
# index_template_versions_on_author_id (author_id)
# index_template_versions_on_template_id_and_sha1 (template_id,sha1) UNIQUE
#
# Foreign Keys
#
# fk_rails_... (account_id => accounts.id)
# fk_rails_... (author_id => users.id)
# fk_rails_... (template_id => templates.id)
#
class TemplateVersion < ApplicationRecord
belongs_to :template
belongs_to :account
belongs_to :author, class_name: 'User'
attribute :data, :string, default: -> { {} }
serialize :data, coder: JSON
before_validation :set_account, on: :create
private
def set_account
self.account ||= template.account
end
end
+7 -1
View File
@@ -6,6 +6,7 @@
# #
# id :bigint not null, primary key # id :bigint not null, primary key
# events :text not null # events :text not null
# hmac_secret :text not null
# secret :text not null # secret :text not null
# sha1 :string not null # sha1 :string not null
# url :text not null # url :text not null
@@ -47,10 +48,15 @@ class WebhookUrl < ApplicationRecord
serialize :secret, coder: JSON serialize :secret, coder: JSON
before_validation :set_sha1 before_validation :set_sha1
before_validation :set_hmac_secret
encrypts :url, :secret encrypts :url, :secret, :hmac_secret
def set_sha1 def set_sha1
self.sha1 = Digest::SHA1.hexdigest(url) self.sha1 = Digest::SHA1.hexdigest(url)
end end
def set_hmac_secret
self.hmac_secret ||= WebhookUrls::Signatures.generate_secret
end
end end
+2 -2
View File
@@ -36,7 +36,7 @@
<% if signature %> <% if signature %>
<div class="flex justify-center mb-4 relative"> <div class="flex justify-center mb-4 relative">
<%= button_to button_title(title: t('remove'), disabled_with: t('removing')), user_signature_path, method: :delete, class: 'right-0 top-0 absolute link' %> <%= button_to button_title(title: t('remove'), disabled_with: t('removing')), user_signature_path, method: :delete, class: 'right-0 top-0 absolute link' %>
<img src="<%= signature.url %>" style="max-height: 200px; width: auto" width="<%= signature.metadata['width'] %>" height="<%= signature.metadata['height'] %>"> <img src="<%= signature.url %>" style="height: 130px; width: 100%; object-fit: contain;">
</div> </div>
<% end %> <% end %>
<a href="<%= edit_user_signature_path %>" data-turbo-frame="modal" class="base-button w-full"> <a href="<%= edit_user_signature_path %>" data-turbo-frame="modal" class="base-button w-full">
@@ -49,7 +49,7 @@
<% if initials %> <% if initials %>
<div class="flex justify-center mb-4 relative"> <div class="flex justify-center mb-4 relative">
<%= button_to button_title(title: t('remove'), disabled_with: t('removing')), user_initials_path, method: :delete, class: 'right-0 top-0 absolute link' %> <%= button_to button_title(title: t('remove'), disabled_with: t('removing')), user_initials_path, method: :delete, class: 'right-0 top-0 absolute link' %>
<img src="<%= initials.url %>" style="max-height: 200px; width: auto" width="<%= initials.metadata['width'] %>" height="<%= initials.metadata['height'] %>"> <img src="<%= initials.url %>" style="height: 130px; width: 100%; object-fit: contain;">
</div> </div>
<% end %> <% end %>
<a href="<%= edit_user_initials_path %>" data-turbo-frame="modal" class="base-button w-full"> <a href="<%= edit_user_initials_path %>" data-turbo-frame="modal" class="base-button w-full">
+1 -1
View File
@@ -1,6 +1,6 @@
<a target="_blank" href="<%= Docuseal::GITHUB_URL %>" rel="noopener noreferrer nofollow" class="relative flex items-center rounded-full px-2 py-0.5 text-xs leading-4 mt-1 text-base-content border border-base-300 tooltip tooltip-bottom" data-tip="Give a star on GitHub"> <a target="_blank" href="<%= Docuseal::GITHUB_URL %>" rel="noopener noreferrer nofollow" class="relative flex items-center rounded-full px-2 py-0.5 text-xs leading-4 mt-1 text-base-content border border-base-300 tooltip tooltip-bottom" data-tip="Give a star on GitHub">
<span class="flex items-center justify-between space-x-0.5 font-medium"> <span class="flex items-center justify-between space-x-0.5 font-medium">
<%= svg_icon('start', class: 'h-3 w-3') %> <%= svg_icon('start', class: 'h-3 w-3') %>
<span>12k</span> <span>17k</span>
</span> </span>
</a> </a>
+1 -3
View File
@@ -52,9 +52,7 @@
<% if link_form_fields.include?('phone') %> <% if link_form_fields.include?('phone') %>
<div dir="auto" class="form-control !mt-0"> <div dir="auto" class="form-control !mt-0">
<%= f.label :phone, t('phone'), class: 'label' %> <%= f.label :phone, t('phone'), class: 'label' %>
<custom-validation data-invalid-message="<%= t('use_international_format_1xxx_') %>"> <%= f.telephone_field :phone, value: params[:phone] || @submitter.phone, pattern: '^\+[0-9\s\-]+$', required: true, title: t('use_international_format_1xxx_'), class: 'base-input w-full', placeholder: t(multiple_fields ? 'provide_your_phone_in_international_format' : 'provide_your_phone_in_international_format_to_start') %>
<%= f.telephone_field :phone, value: params[:phone] || @submitter.phone, pattern: '^\+[0-9\s\-]+$', required: true, class: 'base-input w-full', placeholder: t(multiple_fields ? 'provide_your_phone_in_international_format' : 'provide_your_phone_in_international_format_to_start') %>
</custom-validation>
</div> </div>
<% end %> <% end %>
<toggle-submit dir="auto" class="form-control"> <toggle-submit dir="auto" class="form-control">
@@ -34,13 +34,11 @@
</linked-input> </linked-input>
</submitters-autocomplete> </submitters-autocomplete>
<% has_phone_field = true %> <% has_phone_field = true %>
<custom-validation data-invalid-message="<%= t('use_international_format_1xxx_') %>">
<submitters-autocomplete data-field="phone"> <submitters-autocomplete data-field="phone">
<linked-input data-target-id="<%= "detailed_phone_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>"> <linked-input data-target-id="<%= "detailed_phone_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
<%= tag.input type: 'tel', pattern: '^\+[0-9\s\-]+$', name: 'submission[1][submitters][][phone]', autocomplete: 'off', class: 'base-input !h-10 mt-1.5 w-full', placeholder: local_assigns[:require_phone_2fa] == true || local_assigns[:prefillable_fields].present? ? t(:phone) : "#{t('phone')} (#{t('optional')})", id: "detailed_phone_#{item['uuid']}", required: local_assigns[:require_phone_2fa] == true && index.zero? %> <%= tag.input type: 'tel', pattern: '^\+[0-9\s\-]+$', name: 'submission[1][submitters][][phone]', autocomplete: 'off', title: t('use_international_format_1xxx_'), class: 'base-input !h-10 mt-1.5 w-full', placeholder: local_assigns[:require_phone_2fa] == true || local_assigns[:prefillable_fields].present? ? t(:phone) : "#{t('phone')} (#{t('optional')})", id: "detailed_phone_#{item['uuid']}", required: local_assigns[:require_phone_2fa] == true && index.zero? %>
</linked-input> </linked-input>
</submitters-autocomplete> </submitters-autocomplete>
</custom-validation>
</div> </div>
<% end %> <% end %>
<% if prefillable_fields.present? %> <% if prefillable_fields.present? %>
@@ -51,13 +49,11 @@
</submitters-autocomplete> </submitters-autocomplete>
<% if local_assigns[:require_phone_2fa] == true || prefillable_fields.any? { |f| f['type'] == 'phone' } %> <% if local_assigns[:require_phone_2fa] == true || prefillable_fields.any? { |f| f['type'] == 'phone' } %>
<% has_phone_field = true %> <% has_phone_field = true %>
<custom-validation data-invalid-message="<%= t('use_international_format_1xxx_') %>">
<submitters-autocomplete data-field="phone"> <submitters-autocomplete data-field="phone">
<linked-input data-target-id="<%= "detailed_phone_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>"> <linked-input data-target-id="<%= "detailed_phone_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
<%= tag.input type: 'tel', pattern: '^\+[0-9\s\-]+$', name: 'submission[1][submitters][][phone]', autocomplete: 'off', class: 'base-input !h-10 mt-1.5 w-full', placeholder: t(:phone), id: "detailed_phone_#{item['uuid']}", required: true %> <%= tag.input type: 'tel', pattern: '^\+[0-9\s\-]+$', name: 'submission[1][submitters][][phone]', autocomplete: 'off', title: t('use_international_format_1xxx_'), class: 'base-input !h-10 mt-1.5 w-full', placeholder: t(:phone), id: "detailed_phone_#{item['uuid']}", required: true %>
</linked-input> </linked-input>
</submitters-autocomplete> </submitters-autocomplete>
</custom-validation>
<% end %> <% end %>
<% prefillable_fields.each do |field| %> <% prefillable_fields.each do |field| %>
<% field_id = "detailed_field_#{index}_#{field['uuid'] || field['name'].parameterize}" %> <% field_id = "detailed_field_#{index}_#{field['uuid'] || field['name'].parameterize}" %>
+1 -3
View File
@@ -19,13 +19,11 @@
</label> </label>
<% end %> <% end %>
<input type="hidden" name="submission[1][submitters][][uuid]" value="<%= item['uuid'] %>"> <input type="hidden" name="submission[1][submitters][][uuid]" value="<%= item['uuid'] %>">
<custom-validation data-invalid-message="<%= t('use_international_format_1xxx_') %>">
<submitters-autocomplete data-field="phone"> <submitters-autocomplete data-field="phone">
<linked-input data-target-id="<%= "phone_phone_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>"> <linked-input data-target-id="<%= "phone_phone_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
<%= tag.input type: 'tel', pattern: '^\+[0-9\s\-]+$', name: 'submission[1][submitters][][phone]', autocomplete: 'off', class: 'base-input !h-10 w-full', placeholder: t('phone'), required: index.zero? || template.preferences['require_all_submitters'], id: "phone_phone_#{item['uuid']}" %> <%= tag.input type: 'tel', pattern: '^\+[0-9\s\-]+$', name: 'submission[1][submitters][][phone]', autocomplete: 'off', title: t('use_international_format_1xxx_'), class: 'base-input !h-10 w-full', placeholder: t('phone'), required: index.zero? || template.preferences['require_all_submitters'], id: "phone_phone_#{item['uuid']}" %>
</linked-input> </linked-input>
</submitters-autocomplete> </submitters-autocomplete>
</custom-validation>
<% if submitters.size > 1 %> <% if submitters.size > 1 %>
<submitters-autocomplete data-field="name"> <submitters-autocomplete data-field="name">
<linked-input data-target-id="<%= "phone_name_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>"> <linked-input data-target-id="<%= "phone_name_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
+10 -2
View File
@@ -30,6 +30,12 @@
<span class="hidden md:block"><%= t('event_log') %></span> <span class="hidden md:block"><%= t('event_log') %></span>
<% end %> <% end %>
<% end %> <% end %>
<% if signed_in? && !is_all_completed && can?(:manage, :resend_all) && @submission.submitters.to_a.size > 3 && !@submission.archived_at? && !@submission.template&.archived_at? && !@submission.expired? && can?(:update, @submission) %>
<% pending_submitters_count = @submission.submitters.to_a.count { |s| !s.completed_at? && s.email.present? && !s.declined_at? } %>
<% if pending_submitters_count.positive? %>
<%= button_to button_title(title: t('re_send_emails'), title_class: 'hidden md:inline', disabled_with: t('sending'), icon: svg_icon('mail_forward', class: 'w-6 h-6')), submission_resend_email_index_path(@submission), class: 'white-button', data: { turbo_confirm: t('are_you_sure_you_want_to_re_send_email_to_n_recipients', count: pending_submitters_count) } %>
<% end %>
<% end %>
<% if @submission.submitters.to_a.any?(&:completed_at?) %> <% if @submission.submitters.to_a.any?(&:completed_at?) %>
<% if is_all_completed || !is_combined_enabled %> <% if is_all_completed || !is_combined_enabled %>
<div class="join relative"> <div class="join relative">
@@ -260,10 +266,12 @@
<div dir="auto"> <div dir="auto">
<% if field['type'].in?(%w[signature initials]) %> <% if field['type'].in?(%w[signature initials]) %>
<div class="w-full bg-base-300 py-1"> <div class="w-full bg-base-300 py-1">
<img class="object-contain mx-auto" style="max-height: <%= field['type'] == 'signature' ? 100 : 50 %>px" height="<%= attachments_index[value].metadata['height'] %>" width="<%= attachments_index[value].metadata['width'] %>" src="<%= attachments_index[value].url %>" loading="lazy" alt="<%= field['name'] || field['title'] || field['type'] %>"> <% img_height = attachments_index[value].metadata['height'] %>
<img class="object-contain mx-auto" style="<%= 'max-' if img_height %>height: <%= field['type'] == 'signature' ? 100 : 50 %>px" height="<%= img_height %>" width="<%= attachments_index[value].metadata['width'] %>" src="<%= attachments_index[value].url %>" loading="lazy" alt="<%= field['name'] || field['title'] || field['type'] %>">
</div> </div>
<% elsif field['type'].in?(['image', 'stamp', 'kba']) && attachments_index[value].image? %> <% elsif field['type'].in?(['image', 'stamp', 'kba']) && attachments_index[value].image? %>
<img class="object-contain mx-auto max-h-28" style="max-height: 200px" height="<%= attachments_index[value].metadata['height'] %>" width="<%= attachments_index[value].metadata['width'] %>" src="<%= attachments_index[value].url %>" loading="lazy" alt="<%= field['name'] || field['title'] || field['type'] %>"> <% img_height = attachments_index[value].metadata['height'] %>
<img class="object-contain mx-auto" style="<%= 'max-' if img_height %>height: 200px" height="<%= img_height %>" width="<%= attachments_index[value].metadata['width'] %>" src="<%= attachments_index[value].url %>" loading="lazy" alt="<%= field['name'] || field['title'] || field['type'] %>">
<% elsif field['type'].in?(['file', 'payment', 'image']) %> <% elsif field['type'].in?(['file', 'payment', 'image']) %>
<div class="flex flex-col justify-center"> <div class="flex flex-col justify-center">
<% Array.wrap(value).each do |val| %> <% Array.wrap(value).each do |val| %>
+32 -19
View File
@@ -21,20 +21,31 @@
<h1 class="text-xl md:text-2xl font-medium focus:text-clip" style="width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"> <h1 class="text-xl md:text-2xl font-medium focus:text-clip" style="width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
<%= @submitter.submission.name || @submitter.submission.template.name %> <%= @submitter.submission.name || @submitter.submission.template.name %>
</h1> </h1>
<div class="flex items-center space-x-2" style="margin-left: 20px; flex-shrink: 0"> <div class="flex items-center gap-2 group" style="margin-left: 20px; flex-shrink: 0">
<% if @form_configs[:with_decline] %>
<modal-button data-target="<%= decline_modal_id = SecureRandom.uuid %>" class="hidden group-has-[.complete-button]:flex">
<button type="button" class="btn btn-sm md:!px-5 px-2" aria-label="<%= t(:decline) %>">
<span class="hidden md:inline"><%= t(:decline) %></span>
<span class="inline md:hidden">
<%= svg_icon('x', class: 'w-5 h-5') %>
</span>
</button>
</modal-button>
<% end %>
<span id="complete_button_container" class="peer contents"></span>
<% if @form_configs[:with_delegate] %> <% if @form_configs[:with_delegate] %>
<modal-button data-target="<%= delegate_modal_id = SecureRandom.uuid %>"> <modal-button data-target="<%= delegate_modal_id = SecureRandom.uuid %>" class="hidden peer-empty:flex">
<button id="delegate_button" type="button" class="btn btn-sm !px-5"><%= t(:delegate) %></button> <button id="delegate_button" type="button" class="btn btn-sm !px-5"><%= t(:delegate) %></button>
</modal-button> </modal-button>
<% if @form_configs[:with_decline] %> <% if @form_configs[:with_decline] %>
<modal-button data-target="<%= decline_modal_id = SecureRandom.uuid %>"> <modal-button data-target="<%= decline_modal_id %>" class="hidden peer-empty:flex">
<button id="decline_button" type="button" class="btn btn-sm px-2" aria-label="<%= t(:decline) %>"> <button id="decline_button" type="button" class="btn btn-sm px-2" aria-label="<%= t(:decline) %>">
<%= svg_icon('x', class: 'w-5 h-5') %> <%= svg_icon('x', class: 'w-5 h-5') %>
</button> </button>
</modal-button> </modal-button>
<% end %> <% end %>
<% if @form_configs[:with_partial_download] %> <% if @form_configs[:with_partial_download] %>
<download-button role="button" tabindex="0" data-src="<%= submit_form_download_index_path(@submitter.slug) %>" class="btn btn-neutral text-white btn-sm px-2" title="<%= t('download') %>" aria-label="<%= t('download') %>"> <download-button role="button" tabindex="0" data-src="<%= submit_form_download_index_path(@submitter.slug) %>" class="btn btn-neutral text-white btn-sm px-2 hidden peer-empty:flex" title="<%= t('download') %>" aria-label="<%= t('download') %>">
<span data-target="download-button.defaultButton"> <span data-target="download-button.defaultButton">
<%= svg_icon('download', class: 'w-5 h-5') %> <%= svg_icon('download', class: 'w-5 h-5') %>
</span> </span>
@@ -45,12 +56,12 @@
<% end %> <% end %>
<% else %> <% else %>
<% if @form_configs[:with_decline] %> <% if @form_configs[:with_decline] %>
<modal-button data-target="<%= decline_modal_id = SecureRandom.uuid %>"> <modal-button data-target="<%= decline_modal_id %>" class="hidden peer-empty:flex">
<button id="decline_button" type="button" class="btn btn-sm !px-5"><%= t(:decline) %></button> <button id="decline_button" type="button" class="btn btn-sm !px-5"><%= t(:decline) %></button>
</modal-button> </modal-button>
<% end %> <% end %>
<% if @form_configs[:with_partial_download] %> <% if @form_configs[:with_partial_download] %>
<download-button role="button" tabindex="0" data-src="<%= submit_form_download_index_path(@submitter.slug) %>" class="btn btn-neutral text-white btn-sm !px-4" aria-label="<%= t('download') %>"> <download-button role="button" tabindex="0" data-src="<%= submit_form_download_index_path(@submitter.slug) %>" class="btn btn-neutral text-white btn-sm !px-4 hidden peer-empty:flex" aria-label="<%= t('download') %>">
<span class="flex items-center justify-center" data-target="download-button.defaultButton"> <span class="flex items-center justify-center" data-target="download-button.defaultButton">
<%= svg_icon('download', class: 'w-6 h-6 inline md:hidden') %> <%= svg_icon('download', class: 'w-6 h-6 inline md:hidden') %>
<span class="hidden md:inline"><%= t('download') %></span> <span class="hidden md:inline"><%= t('download') %></span>
@@ -64,9 +75,17 @@
<% end %> <% end %>
</div> </div>
</header> </header>
<scroll-buttons inert class="fixed right-5 top-2 hidden md:flex gap-1 z-50 ease-in-out opacity-0 -translate-y-10"> <scroll-buttons inert class="fixed right-5 top-2 hidden md:flex gap-1 z-50 ease-in-out opacity-0 -translate-y-10 group">
<% if @form_configs[:with_decline] %>
<modal-button data-target="<%= decline_modal_id %>" class="hidden group-has-[.complete-button]:flex">
<button type="button" class="btn btn-sm px-2" aria-label="<%= t(:decline) %>">
<%= svg_icon('x', class: 'w-5 h-5') %>
</button>
</modal-button>
<% end %>
<span id="complete_button_container_scroll" class="peer contents"></span>
<% if @form_configs[:with_delegate] %> <% if @form_configs[:with_delegate] %>
<modal-button data-target="<%= delegate_modal_id %>"> <modal-button data-target="<%= delegate_modal_id %>" class="hidden peer-empty:flex">
<button id="delegate_button_mobile" type="button" class="btn btn-sm px-0" aria-label="<%= t(:delegate) %>"> <button id="delegate_button_mobile" type="button" class="btn btn-sm px-0" aria-label="<%= t(:delegate) %>">
<span class="min-[1366px]:inline hidden px-3"> <span class="min-[1366px]:inline hidden px-3">
<%= t(:delegate) %> <%= t(:delegate) %>
@@ -77,23 +96,15 @@
</button> </button>
</modal-button> </modal-button>
<% if @form_configs[:with_decline] %> <% if @form_configs[:with_decline] %>
<modal-button data-target="<%= decline_modal_id %>"> <modal-button data-target="<%= decline_modal_id %>" class="hidden peer-empty:flex">
<button id="decline_button_mobile" type="button" class="btn btn-sm px-2" aria-label="<%= t(:decline) %>"> <button id="decline_button_mobile" type="button" class="btn btn-sm px-2" aria-label="<%= t(:decline) %>">
<%= svg_icon('x', class: 'w-5 h-5') %> <%= svg_icon('x', class: 'w-5 h-5') %>
</button> </button>
</modal-button> </modal-button>
<% end %> <% end %>
<download-button role="button" tabindex="0" data-src="<%= submit_form_download_index_path(@submitter.slug) %>" class="btn btn-neutral text-white btn-sm px-2" aria-label="<%= t('download') %>">
<span data-target="download-button.defaultButton">
<%= svg_icon('download', class: 'w-5 h-5') %>
</span>
<span class="hidden" data-target="download-button.loadingButton">
<%= svg_icon('loader', class: 'w-5 h-5 animate-spin') %>
</span>
</download-button>
<% else %> <% else %>
<% if @form_configs[:with_decline] %> <% if @form_configs[:with_decline] %>
<modal-button data-target="<%= decline_modal_id %>"> <modal-button data-target="<%= decline_modal_id %>" class="hidden peer-empty:flex">
<button id="decline_button_mobile" type="button" class="btn btn-sm px-0" aria-label="<%= t(:decline) %>"> <button id="decline_button_mobile" type="button" class="btn btn-sm px-0" aria-label="<%= t(:decline) %>">
<span class="min-[1366px]:inline hidden px-3"> <span class="min-[1366px]:inline hidden px-3">
<%= t(:decline) %> <%= t(:decline) %>
@@ -104,7 +115,9 @@
</button> </button>
</modal-button> </modal-button>
<% end %> <% end %>
<download-button role="button" tabindex="0" data-src="<%= submit_form_download_index_path(@submitter.slug) %>" class="btn btn-neutral text-white btn-sm px-2" aria-label="<%= t('download') %>"> <% end %>
<% if @form_configs[:with_partial_download] %>
<download-button role="button" tabindex="0" data-src="<%= submit_form_download_index_path(@submitter.slug) %>" class="btn btn-neutral text-white btn-sm px-2 hidden peer-empty:flex" aria-label="<%= t('download') %>">
<span data-target="download-button.defaultButton"> <span data-target="download-button.defaultButton">
<%= svg_icon('download', class: 'w-5 h-5') %> <%= svg_icon('download', class: 'w-5 h-5') %>
</span> </span>
+1 -3
View File
@@ -10,11 +10,9 @@
<submitters-autocomplete data-field="email"> <submitters-autocomplete data-field="email">
<%= email_field_tag 'submitter[email]', @submitter.email, autocomplete: 'off', class: 'base-input !h-10 mt-1.5 w-full', placeholder: "#{t('email')} (#{t('optional')})" %> <%= email_field_tag 'submitter[email]', @submitter.email, autocomplete: 'off', class: 'base-input !h-10 mt-1.5 w-full', placeholder: "#{t('email')} (#{t('optional')})" %>
</submitters-autocomplete> </submitters-autocomplete>
<custom-validation data-invalid-message="<%= t('use_international_format_1xxx_') %>">
<submitters-autocomplete data-field="phone"> <submitters-autocomplete data-field="phone">
<%= telephone_field_tag 'submitter[phone]', @submitter.phone, autocomplete: 'off', pattern: '^\+[0-9\s\-]+$', class: 'base-input !h-10 mt-1.5 w-full', placeholder: "#{t('phone')} (#{t('optional')})" %> <%= telephone_field_tag 'submitter[phone]', @submitter.phone, autocomplete: 'off', pattern: '^\+[0-9\s\-]+$', title: t('use_international_format_1xxx_'), class: 'base-input !h-10 mt-1.5 w-full', placeholder: "#{t('phone')} (#{t('optional')})" %>
</submitters-autocomplete> </submitters-autocomplete>
</custom-validation>
</div> </div>
</submitter-item> </submitter-item>
</div> </div>
+2 -2
View File
@@ -60,7 +60,7 @@
</div> </div>
<div class="flex flex-wrap gap-2 w-full md:w-fit md:justify-between md:flex-none md:pt-1"> <div class="flex flex-wrap gap-2 w-full md:w-fit md:justify-between md:flex-none md:pt-1">
<% if !template.archived_at? && can?(:destroy, template) %> <% if !template.archived_at? && can?(:destroy, template) %>
<%= button_to button_title(title: t('archive'), disabled_with: t('archiving'), title_class: 'inline', icon: svg_icon('archive', class: 'w-6 h-6')), template_path(template), class: 'btn btn-outline btn-sm w-full', form_class: 'flex-1', method: :delete, data: { turbo_confirm: t('are_you_sure_') } %> <%= button_to button_title(title: t('archive'), disabled_with: t('archiving')[..-4], title_class: 'inline', icon: svg_icon('archive', class: 'w-6 h-6')), template_path(template), class: 'btn btn-outline btn-sm w-full', form_class: 'flex-1', method: :delete %>
<% end %> <% end %>
<% if can?(:create, current_account.templates.new(author: current_user)) %> <% if can?(:create, current_account.templates.new(author: current_user)) %>
<%= link_to new_template_clone_path(template), class: 'btn btn-outline btn-sm flex-1', data: { turbo_frame: :modal } do %> <%= link_to new_template_clone_path(template), class: 'btn btn-outline btn-sm flex-1', data: { turbo_frame: :modal } do %>
@@ -89,7 +89,7 @@
<% end %> <% end %>
<% if template.archived_at? %> <% if template.archived_at? %>
<% if can?(:create, template) %> <% if can?(:create, template) %>
<%= button_to button_title(title: t('restore'), disabled_with: t('restoring'), icon: svg_icon('rotate', class: 'w-6 h-6')), template_restore_index_path(template), class: 'btn btn-outline btn-sm flex-1' %> <%= button_to button_title(title: t('restore'), disabled_with: t('restoring')[..-4], icon: svg_icon('rotate', class: 'w-6 h-6')), template_restore_index_path(template), class: 'btn btn-outline btn-sm flex-1' %>
<% end %> <% end %>
<%= link_to template_preview_path(template), class: 'btn btn-outline btn-sm flex-1' do %> <%= link_to template_preview_path(template), class: 'btn btn-outline btn-sm flex-1' do %>
<span class="flex items-center justify-center space-x-2"> <span class="flex items-center justify-center space-x-2">
+2 -2
View File
@@ -15,8 +15,8 @@
</span> </span>
</label> </label>
<input type="hidden" name="form_id" value="<%= form_id %>"> <input type="hidden" name="form_id" value="<%= form_id %>">
<submit-form data-on="change" data-disable="true"> <convert-upload>
<input id="upload_template" name="files[]" class="hidden" type="file" accept="image/*, application/pdf, application/zip, application/json<%= ", #{Templates::CreateAttachments::DOCUMENT_EXTENSIONS.join(', ')}" if Docuseal.advanced_formats? %>" multiple> <input id="upload_template" name="files[]" class="hidden" type="file" accept="image/*, application/pdf, application/zip, application/json<%= ", #{Templates::CreateAttachments::DOCUMENT_EXTENSIONS.join(', ')}" if Docuseal.advanced_formats? %>" multiple>
</submit-form> </convert-upload>
<input hidden name="folder_name" value="<%= local_assigns[:folder_name] %>"> <input hidden name="folder_name" value="<%= local_assigns[:folder_name] %>">
<% end %> <% end %>
+1 -1
View File
@@ -6,4 +6,4 @@
<%= button_to nil, user_configs_path, method: :post, params: { user_config: { key: UserConfig::SHOW_APP_TOUR, value: true } }, class: 'hidden', id: 'start_tour_button' %> <%= button_to nil, user_configs_path, method: :post, params: { user_config: { key: UserConfig::SHOW_APP_TOUR, value: true } }, class: 'hidden', id: 'start_tour_button' %>
<% end %> <% end %>
<% end %> <% end %>
<template-builder class="grid" data-template="<%= @template_data %>" data-custom-fields="<%= (current_account.account_configs.find_or_initialize_by(key: AccountConfig::TEMPLATE_CUSTOM_FIELDS_KEY).value || []).to_json %>" data-with-sign-yourself-button="<%= !@template.archived_at? %>" data-with-fields-detection="true" data-with-send-button="<%= !@template.archived_at? && can?(:create, @template.submissions.new(account: current_account)) %>" data-locale="<%= I18n.locale %>" data-show-tour-start-form="<%= @show_tour_start_form %>"></template-builder> <template-builder class="grid" data-template="<%= @template_data.to_json %>" data-custom-fields="<%= (current_account.account_configs.find_or_initialize_by(key: AccountConfig::TEMPLATE_CUSTOM_FIELDS_KEY).value || []).to_json %>" data-with-sign-yourself-button="<%= !@template.archived_at? %>" data-with-fields-detection="true" data-with-send-button="<%= !@template.archived_at? && can?(:create, @template.submissions.new(account: current_account)) %>" data-with-revisions-menu="<%= @template.template_versions.exists? %>" data-locale="<%= I18n.locale %>" data-show-tour-start-form="<%= @show_tour_start_form %>"></template-builder>
+1 -1
View File
@@ -75,7 +75,7 @@
<% end %> <% end %>
<% if @submissions.present? %> <% if @submissions.present? %>
<div class="space-y-4"> <div class="space-y-4">
<%= render partial: 'submission', collection: @submissions, locals: { template: @template } %> <%= render partial: 'submission', collection: @submissions, locals: { template: @template, archived: @template.archived_at? } %>
</div> </div>
<% else %> <% else %>
<div class="card bg-base-200"> <div class="card bg-base-200">
+1 -1
View File
@@ -68,7 +68,7 @@
<%= t('welcome_to_docuseal') %> <%= t('welcome_to_docuseal') %>
</div> </div>
<div class="my-2 text-center text-xs text-base-content/70"> <div class="my-2 text-center text-xs text-base-content/70">
<%= t('start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document') %> <%= t('start_a_quick_tour_to_learn_how_to_create_and_send_your_first_document') %>
</div> </div>
<div class="flex gap-2 mt-3 w-full"> <div class="flex gap-2 mt-3 w-full">
<remove-on-event data-on="submit" data-selector-id="app_tour_manager" class="block w-full"> <remove-on-event data-on="submit" data-selector-id="app_tour_manager" class="block w-full">
+1 -1
View File
@@ -1 +1 @@
<template-builder class="grid" data-editable="false" data-with-sign-yourself-button="<%= !@template.archived_at? %>" data-with-send-button="<%= !@template.archived_at? && can?(:create, @template.submissions.new(account: current_account)) %>" data-template="<%= @template_data %>"></template-builder> <template-builder class="grid" data-editable="false" data-with-sign-yourself-button="<%= !@template.archived_at? %>" data-with-send-button="<%= !@template.archived_at? && can?(:create, @template.submissions.new(account: current_account)) %>" data-template="<%= @template_data.to_json %>"></template-builder>
+27 -15
View File
@@ -1,18 +1,30 @@
<div class="h-screen"> <confirm-upload>
<div <div class="h-screen">
class="text-center p-8 h-full flex items-center justify-center"> <div class="text-center p-8 h-full flex items-center justify-center">
<div> <div class="space-y-4">
<%= render 'shared/logo', width: 50, height: 50, class: 'mx-auto animate-bounce' %> <div data-target="confirm-upload.logo" class="mx-auto">
<span> <%= render 'shared/logo', width: 50, height: 50, class: 'mx-auto' %>
<%= t('processing') %>...
</span>
</div> </div>
<div data-target="confirm-upload.processing" class="hidden">
<span><%= t('processing') %>...</span>
</div> </div>
</div> <div data-target="confirm-upload.prompt">
<submit-form> <p class="text-lg">
<%= form_for '', url: templates_upload_path, method: :post, class: 'hidden' do %> <%= t('open_file_from') %>
<button type="submit"></button> <a href="<%= params[:url] %>" target="_blank" class="link" rel="noopener noreferrer nofollow">
<input name="url" value="<%= params[:url] %>"> <%= params[:filename].presence || params[:url] %>
<input name="filename" value="<%= params[:filename] %>"> </a>
</p>
<div class="flex items-center justify-center gap-2 mt-4">
<%= link_to t('cancel'), root_path, class: 'white-button w-44' %>
<%= form_for '', url: templates_upload_path, method: :post do |f| %>
<input type="hidden" name="url" value="<%= params[:url] %>">
<input type="hidden" name="filename" value="<%= params[:filename] %>">
<%= f.button button_title(title: t('open')), class: 'base-button w-44' %>
<% end %> <% end %>
</submit-form> </div>
</div>
</div>
</div>
</div>
</confirm-upload>
@@ -3,13 +3,14 @@
<% webhook_attempts = webhook_event.webhook_attempts.sort_by { |e| -e.id } %> <% webhook_attempts = webhook_event.webhook_attempts.sort_by { |e| -e.id } %>
<% if webhook_event.status == 'error' %> <% if webhook_event.status == 'error' %>
<% last_attempt = webhook_attempts.select { |e| e.attempt < SendWebhookRequest::MANUAL_ATTEMPT }.max_by(&:attempt) %> <% last_attempt = webhook_attempts.select { |e| e.attempt < SendWebhookRequest::MANUAL_ATTEMPT }.max_by(&:attempt) %>
<% if webhook_event.webhook_attempts.none?(&:success?) && last_attempt.attempt <= 10 %> <% next_attempt_at = last_attempt.created_at + (2**last_attempt.attempt).minutes %>
<% if webhook_event.webhook_attempts.none?(&:success?) && last_attempt.attempt <= 10 && next_attempt_at > 30.seconds.ago %>
<li class="ml-7"> <li class="ml-7">
<span class="btn btn-outline btn-xs btn-circle pointer-events-none absolute justify-center border-base-content-/60 text-base-content/60 bg-base-100" style="left: -12px;"> <span class="btn btn-outline btn-xs btn-circle pointer-events-none absolute justify-center border-base-content-/60 text-base-content/60 bg-base-100" style="left: -12px;">
<%= svg_icon('clock', class: 'w-4 h-4 shrink-0') %> <%= svg_icon('clock', class: 'w-4 h-4 shrink-0') %>
</span> </span>
<p class="leading-none text-base-content/90 pt-1"> <p class="leading-none text-base-content/90 pt-1">
<%= t('next_attempt_in_time_in_words', time_in_words: distance_of_time_in_words(Time.current, last_attempt.created_at + (2**last_attempt.attempt).minutes)) %> <%= t('next_attempt_in_time_in_words', time_in_words: distance_of_time_in_words(Time.current, next_attempt_at)) %>
</p> </p>
</li> </li>
<% end %> <% end %>
+20
View File
@@ -0,0 +1,20 @@
<%= render 'shared/turbo_modal', title: t('webhook_security') do %>
<div class="text-center mb-4">
<div class="inline-flex justify-center">
<%= link_to t('secret'), webhook_secret_path(@webhook_url), class: 'block bg-base-200 md:min-w-[112px] text-sm font-semibold whitespace-nowrap py-1.5 px-4 rounded-l-3xl', data: { turbo_frame: 'modal' } %>
<%= link_to t('hmac'), webhook_hmac_path(@webhook_url), class: 'block bg-base-300 md:min-w-[112px] text-sm font-semibold whitespace-nowrap py-1.5 px-4 rounded-r-3xl', data: { turbo_frame: 'modal' } %>
</div>
</div>
<div class="form-control">
<label class="label" for="hmac_secret"><%= t('hmac_signing_secret') %></label>
<% token = @webhook_url.hmac_secret %>
<% obscured = "#{token[0, 10]}#{'*' * [token.length - 10, 0].max}" %>
<div class="flex gap-2">
<masked-input class="block w-full" data-token="<%= token %>">
<input id="hmac_secret" type="text" value="<%= obscured %>" class="base-input font-mono w-full" autocomplete="off" readonly>
</masked-input>
<%= render 'shared/clipboard_copy', icon: 'copy', text: token, class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: t('copy'), copied_title: t('copied') %>
</div>
<p class="text-sm mt-2 opacity-70"><%= t('hmac_signature_header_hint_html', header: '<code>X-Docuseal-Signature</code>'.html_safe) %></p>
</div>
<% end %>
+7 -1
View File
@@ -1,4 +1,10 @@
<%= render 'shared/turbo_modal', title: t('webhook_secret') do %> <%= render 'shared/turbo_modal', title: t('webhook_security') do %>
<div class="text-center mb-4">
<div class="inline-flex justify-center">
<%= link_to t('secret'), webhook_secret_path(@webhook_url), class: 'block bg-base-300 md:min-w-[112px] text-sm font-semibold whitespace-nowrap py-1.5 px-4 rounded-l-3xl', data: { turbo_frame: 'modal' } %>
<%= link_to t('hmac'), webhook_hmac_path(@webhook_url), class: 'block bg-base-200 md:min-w-[112px] text-sm font-semibold whitespace-nowrap py-1.5 px-4 rounded-r-3xl', data: { turbo_frame: 'modal' } %>
</div>
</div>
<%= form_for @webhook_url, url: webhook_secret_path, method: :patch, html: { class: 'space-y-4' }, data: { turbo_frame: :_top } do |f| %> <%= form_for @webhook_url, url: webhook_secret_path, method: :patch, html: { class: 'space-y-4' }, data: { turbo_frame: :_top } do |f| %>
<div class="space-y-2"> <div class="space-y-2">
<%= f.fields_for :secret, Struct.new(:key, :value).new(*@webhook_url.secret.to_a.first) do |ff| %> <%= f.fields_for :secret, Struct.new(:key, :value).new(*@webhook_url.secret.to_a.first) do |ff| %>
+1 -1
View File
@@ -1,7 +1,7 @@
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0 md:space-x-10"> <div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0 md:space-x-10">
<%= render 'shared/settings_nav' %> <%= render 'shared/settings_nav' %>
<div class="flex-grow min-w-0"> <div class="flex-grow min-w-0">
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-center mb-4"> <div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-end mb-4">
<h1 class="text-4xl font-bold">Webhooks</h1> <h1 class="text-4xl font-bold">Webhooks</h1>
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-center"> <div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-center">
<%= render 'shared/test_mode_toggle' %> <%= render 'shared/test_mode_toggle' %>
+2 -2
View File
@@ -1,7 +1,7 @@
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0 md:space-x-10"> <div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0 md:space-x-10">
<%= render 'shared/settings_nav' %> <%= render 'shared/settings_nav' %>
<div class="flex-grow"> <div class="flex-grow">
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-center mb-4"> <div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-end mb-4">
<h1 class="text-4xl font-bold">Webhook</h1> <h1 class="text-4xl font-bold">Webhook</h1>
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-center"> <div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-center">
<% if params[:action] == 'index' && (current_user == true_user || current_account.testing?) %> <% if params[:action] == 'index' && (current_user == true_user || current_account.testing?) %>
@@ -23,7 +23,7 @@
<div class="flex items-center space-x-2 md:absolute md:right-0"> <div class="flex items-center space-x-2 md:absolute md:right-0">
<%= link_to webhook_secret_path(@webhook_url), class: 'btn btn-outline btn-sm bg-white', data: { turbo_frame: 'modal' } do %> <%= link_to webhook_secret_path(@webhook_url), class: 'btn btn-outline btn-sm bg-white', data: { turbo_frame: 'modal' } do %>
<%= svg_icon('lock', class: 'w-4 h-4') %> <%= svg_icon('lock', class: 'w-4 h-4') %>
<span><%= @webhook_url.secret.present? ? t('edit_secret') : t('add_secret') %></span> <span><%= t('security') %></span>
<% end %> <% end %>
<div class="tooltip tooltip-left md:tooltip-top" data-tip="<%= t('delete_webhook') %>"> <div class="tooltip tooltip-left md:tooltip-top" data-tip="<%= t('delete_webhook') %>">
<%= button_to settings_webhook_path(@webhook_url), class: 'btn btn-warning btn-sm', method: :delete, data: { turbo_confirm: t('are_you_sure_') } do %> <%= button_to settings_webhook_path(@webhook_url), class: 'btn btn-warning btn-sm', method: :delete, data: { turbo_confirm: t('are_you_sure_') } do %>
+12
View File
@@ -25,6 +25,18 @@ module DocuSeal
config.active_storage.draw_routes = ENV['MULTITENANT'] != 'true' config.active_storage.draw_routes = ENV['MULTITENANT'] != 'true'
config.active_storage.analyzers = []
config.active_storage.variant_processor = :disabled
config.active_storage.content_types_to_serve_as_binary += %w[
application/javascript
text/javascript
application/ecmascript
text/ecmascript
application/wasm
]
config.i18n.available_locales = %i[en en-US en-GB es-ES fr-FR pt-PT de-DE it-IT nl-NL config.i18n.available_locales = %i[en en-US en-GB es-ES fr-FR pt-PT de-DE it-IT nl-NL
es it de fr nl pl uk cs pt he ar ko ja] es it de fr nl pl uk cs pt he ar ko ja]
config.i18n.fallbacks = [:en] config.i18n.fallbacks = [:en]
+15
View File
@@ -0,0 +1,15 @@
# frozen_string_literal: true
priority = %w[application/pdf image/jpeg image/png]
indexes = Marcel::MAGIC.each_with_index.with_object({}) do |((type, _), i), acc|
acc[type] = i if priority.include?(type)
break acc if acc.size == priority.size
end
pdf_index, jpg_index, png_index = indexes.values_at(*priority)
Marcel::MAGIC[0], Marcel::MAGIC[pdf_index] = Marcel::MAGIC[pdf_index], Marcel::MAGIC[0]
Marcel::MAGIC[1], Marcel::MAGIC[jpg_index] = Marcel::MAGIC[jpg_index], Marcel::MAGIC[1]
Marcel::MAGIC[2], Marcel::MAGIC[png_index] = Marcel::MAGIC[png_index], Marcel::MAGIC[2]
+288 -205
View File
File diff suppressed because it is too large. Load diff
Loaded 100 of 132 files, more files were not shown because too many files have changed in this diff. Show more