mirror of
https://github.com/docusealco/docuseal
synced 2026-11-05 14:42:33 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed46af8418 | ||
|
|
095f25a6e9 | ||
|
|
3cdab30dc8 | ||
|
|
8e462047ba | ||
|
|
b7f8b65d9d | ||
|
|
da21c37254 | ||
|
|
a89aef4a89 | ||
|
|
dd6516fd11 | ||
|
|
344a3ce198 | ||
|
|
62f8283587 | ||
|
|
1f89accac3 | ||
|
|
9fcaef4cf7 | ||
|
|
d5738a0631 | ||
|
|
8bf7a1f95a | ||
|
|
04cf36891e | ||
|
|
b2d9948c30 | ||
|
|
a2c9ac1707 | ||
|
|
46a6bd0108 | ||
|
|
5f069e7a40 | ||
|
|
ff57e5c6ae | ||
|
|
f65b6e2d76 | ||
|
|
58fd180ae0 | ||
|
|
cd6503c4c3 | ||
|
|
e8b36c2b6d | ||
|
|
51743f1359 | ||
|
|
504c42646b | ||
|
|
c61e84d1b4 | ||
|
|
b8ab01c46c | ||
|
|
9558060bde | ||
|
|
0741c879f1 | ||
|
|
2e6dd2867e | ||
|
|
9f3fcccb61 | ||
|
|
ae08d7abad | ||
|
|
16808a7ec6 | ||
|
|
b1dfcd6283 | ||
|
|
369f4c0f08 | ||
|
|
827de03579 | ||
|
|
0c7f4e5b43 | ||
|
|
9c700a3fb6 | ||
|
|
89bf83febb | ||
|
|
d44b35cf0a | ||
|
|
891833e273 | ||
|
|
29d3a80d3f | ||
|
|
90fef58684 | ||
|
|
07841722ac | ||
|
|
9e4da5948b | ||
|
|
dc6e4313a1 | ||
|
|
a68cc0b689 | ||
|
|
d057fb0f67 | ||
|
|
7e8f045a29 | ||
|
|
5249d8d18d | ||
|
|
f0fafbadd4 | ||
|
|
8b51079dcc | ||
|
|
354bccd6e8 | ||
|
|
6806772346 | ||
|
|
60082655d4 | ||
|
|
7fe56941fd | ||
|
|
abd498dd33 | ||
|
|
99ca0136ed | ||
|
|
e52830c9b4 | ||
|
|
755decca27 | ||
|
|
41604008d1 | ||
|
|
5bddce8969 | ||
|
|
b25503f141 | ||
|
|
bdf1850448 | ||
|
|
230d3ccf69 | ||
|
|
e378025a2d | ||
|
|
04129ded90 | ||
|
|
a7891f89f8 | ||
|
|
37d4a8e834 |
No files matched your search
@@ -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
@@ -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"]
|
||||||
@@ -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,11 +21,9 @@ 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
|
||||||
gem 'oj'
|
|
||||||
gem 'onnxruntime', require: false
|
gem 'onnxruntime', require: false
|
||||||
gem 'pagy'
|
gem 'pagy'
|
||||||
gem 'pg', require: false
|
gem 'pg', require: false
|
||||||
|
|||||||
+12
-23
@@ -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
|
||||||
@@ -264,18 +264,15 @@ GEM
|
|||||||
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.8)
|
||||||
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,10 +305,8 @@ 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)
|
||||||
@@ -339,9 +334,6 @@ GEM
|
|||||||
nokogiri (1.19.3-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)
|
|
||||||
bigdecimal (>= 3.0)
|
|
||||||
ostruct (>= 0.2)
|
|
||||||
onnxruntime (0.10.1-aarch64-linux)
|
onnxruntime (0.10.1-aarch64-linux)
|
||||||
ffi
|
ffi
|
||||||
onnxruntime (0.10.1-arm64-darwin)
|
onnxruntime (0.10.1-arm64-darwin)
|
||||||
@@ -351,7 +343,6 @@ GEM
|
|||||||
openssl (4.0.1)
|
openssl (4.0.1)
|
||||||
orm_adapter (0.5.0)
|
orm_adapter (0.5.0)
|
||||||
os (1.1.4)
|
os (1.1.4)
|
||||||
ostruct (0.6.3)
|
|
||||||
package_json (0.2.0)
|
package_json (0.2.0)
|
||||||
pagy (43.4.4)
|
pagy (43.4.4)
|
||||||
json
|
json
|
||||||
@@ -429,12 +420,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 +507,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,12 +615,10 @@ DEPENDENCIES
|
|||||||
foreman
|
foreman
|
||||||
google-cloud-storage
|
google-cloud-storage
|
||||||
hexapdf
|
hexapdf
|
||||||
image_processing
|
|
||||||
jwt
|
jwt
|
||||||
letter_opener_web
|
letter_opener_web
|
||||||
lograge
|
lograge
|
||||||
numo-narray-alt
|
numo-narray-alt
|
||||||
oj
|
|
||||||
onnxruntime
|
onnxruntime
|
||||||
pagy
|
pagy
|
||||||
pg
|
pg
|
||||||
@@ -662,7 +651,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
|
||||||
@@ -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'
|
||||||
|
|||||||
@@ -11,13 +11,13 @@ module Api
|
|||||||
@submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
@submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
||||||
|
|
||||||
unless can_upload?(@submitter)
|
unless can_upload?(@submitter)
|
||||||
Rollbar.error("Can't upload: #{@submitter.id}") if defined?(Rollbar)
|
|
||||||
|
|
||||||
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)
|
||||||
@@ -31,9 +31,11 @@ module Api
|
|||||||
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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
@@ -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,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
|
||||||
@@ -77,8 +61,6 @@ class TemplatesController < ApplicationController
|
|||||||
|
|
||||||
WebhookUrls.enqueue_events(@template, 'template.updated')
|
WebhookUrls.enqueue_events(@template, 'template.updated')
|
||||||
|
|
||||||
TemplateVersions.find_or_create_for(@template, author: current_user) if params[:revision]
|
|
||||||
|
|
||||||
head :ok
|
head :ok
|
||||||
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?
|
||||||
|
|||||||
@@ -14,4 +14,12 @@ class TemplatesVersionsController < ApplicationController
|
|||||||
|
|
||||||
render json: TemplateVersions.serialize(version)
|
render json: TemplateVersions.serialize(version)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
authorize!(:update, @template)
|
||||||
|
|
||||||
|
TemplateVersions.find_or_create_for(@template, author: current_user)
|
||||||
|
|
||||||
|
head :ok
|
||||||
|
end
|
||||||
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)
|
||||||
|
|||||||
@@ -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 () {
|
||||||
@@ -197,10 +199,35 @@ 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) {
|
||||||
e.detail.fetchResponse.response.json().then((data) => {
|
if (e.detail?.formSubmission?.formElement?.id === 'submitters_form') {
|
||||||
this.component.template.submitters = data.submitters
|
e.detail.fetchResponse.response.json().then((data) => {
|
||||||
})
|
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
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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')
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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')]"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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',
|
||||||
@@ -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à",
|
||||||
@@ -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',
|
||||||
@@ -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: 'Ідентифікація особи',
|
||||||
@@ -770,6 +819,13 @@ 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',
|
||||||
@@ -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',
|
||||||
@@ -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: 'אימות זהות',
|
||||||
@@ -1097,6 +1167,13 @@ 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: 'ID verifiëren',
|
verify_id: 'ID verifiëren',
|
||||||
identity_verification: 'Identiteitsverificatie',
|
identity_verification: 'Identiteitsverificatie',
|
||||||
@@ -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: 'التحقق من الهوية',
|
||||||
@@ -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: '신원 확인',
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -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 ''
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -197,7 +197,8 @@
|
|||||||
v-else-if="!isCheckboxInput"
|
v-else-if="!isCheckboxInput"
|
||||||
width="100%"
|
width="100%"
|
||||||
height="100%"
|
height="100%"
|
||||||
class="max-h-10 opacity-50"
|
class="opacity-50"
|
||||||
|
:style="{ maxHeight: isInlineSize ? '4.4cqmin' : '40px' }"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -620,6 +621,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
|
||||||
}
|
}
|
||||||
@@ -716,14 +721,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
|
||||||
@@ -769,6 +766,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()
|
||||||
}
|
}
|
||||||
@@ -836,6 +835,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()
|
||||||
}
|
}
|
||||||
@@ -860,18 +861,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]
|
||||||
@@ -886,6 +875,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()
|
||||||
@@ -924,9 +915,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -309,6 +309,8 @@
|
|||||||
:data-document-uuid="item.attachment_uuid"
|
:data-document-uuid="item.attachment_uuid"
|
||||||
:accept-file-types="acceptFileTypes"
|
:accept-file-types="acceptFileTypes"
|
||||||
:with-replace-button="withUploadButton"
|
:with-replace-button="withUploadButton"
|
||||||
|
:with-google-drive="withGoogleDrive"
|
||||||
|
:authenticity-token="authenticityToken"
|
||||||
:editable="editable"
|
:editable="editable"
|
||||||
:dynamic-documents="dynamicDocuments"
|
:dynamic-documents="dynamicDocuments"
|
||||||
:with-dynamic-documents="withDynamicDocuments"
|
:with-dynamic-documents="withDynamicDocuments"
|
||||||
@@ -355,12 +357,17 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
id="pages_container"
|
id="pages_container"
|
||||||
class="w-full overflow-x-hidden mt-0.5 pt-0.5"
|
ref="pagesContainer"
|
||||||
:class="isMobile ? 'overflow-y-auto' : 'overflow-y-hidden md:overflow-y-auto'"
|
class="w-full mt-0.5 pt-0.5"
|
||||||
|
:class="[
|
||||||
|
isMobile ? 'overflow-y-auto' : 'overflow-y-hidden md:overflow-y-auto',
|
||||||
|
zoomLevel > 1 ? 'overflow-x-auto' : 'overflow-x-hidden'
|
||||||
|
]"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
ref="documents"
|
ref="documents"
|
||||||
class="pr-3.5 pl-0.5"
|
class="pr-3.5 pl-0.5"
|
||||||
|
:style="zoomLevel > 1 ? { width: `${zoomLevel * 100}%` } : null"
|
||||||
>
|
>
|
||||||
<template v-if="!sortedDocuments.length && (withUploadButton || withAddPageButton)">
|
<template v-if="!sortedDocuments.length && (withUploadButton || withAddPageButton)">
|
||||||
<Dropzone
|
<Dropzone
|
||||||
@@ -451,6 +458,8 @@
|
|||||||
:with-arrows="template.schema.length > 1"
|
:with-arrows="template.schema.length > 1"
|
||||||
:item="template.schema.find((item) => item.attachment_uuid === document.uuid)"
|
:item="template.schema.find((item) => item.attachment_uuid === document.uuid)"
|
||||||
:with-replace-button="withUploadButton"
|
:with-replace-button="withUploadButton"
|
||||||
|
:with-google-drive="withGoogleDrive"
|
||||||
|
:authenticity-token="authenticityToken"
|
||||||
:accept-file-types="acceptFileTypes"
|
:accept-file-types="acceptFileTypes"
|
||||||
:document="document"
|
:document="document"
|
||||||
:template="template"
|
:template="template"
|
||||||
@@ -633,6 +642,36 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
|
<Transition
|
||||||
|
enter-active-class="transition-all duration-200 ease-out"
|
||||||
|
enter-from-class="translate-y-4 opacity-0"
|
||||||
|
enter-to-class="translate-y-0 opacity-100"
|
||||||
|
leave-active-class="transition-all duration-200 ease-in"
|
||||||
|
leave-from-class="translate-y-0 opacity-100"
|
||||||
|
leave-to-class="translate-y-4 opacity-0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="zoomLevel > 1"
|
||||||
|
class="sticky bottom-0 z-40 pointer-events-none"
|
||||||
|
>
|
||||||
|
<div class="absolute left-0 right-0 bottom-4 flex justify-center">
|
||||||
|
<div class="join shadow pointer-events-auto">
|
||||||
|
<span class="join-item bg-base-content text-white pl-2 pr-2.5 h-9 items-center text-sm font-medium cursor-default w-16 flex justify-end">
|
||||||
|
<span>
|
||||||
|
{{ Math.round(zoomLevel * 100) }}%
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="join-item bg-base-content text-white h-9 pl-2 pr-3 inline-flex items-center justify-center cursor-pointer hover:opacity-90 border-l border-white/20"
|
||||||
|
@click="zoomLevel = 1"
|
||||||
|
>
|
||||||
|
<IconX class="w-4 h-4 stroke-2" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
<div
|
<div
|
||||||
id="docuseal_modal_container"
|
id="docuseal_modal_container"
|
||||||
class="modal-container"
|
class="modal-container"
|
||||||
@@ -664,7 +703,7 @@ 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 RevisionsModal from './revisions_modal'
|
import RevisionsModal from './revisions_modal'
|
||||||
import { IconPlus, IconUsersPlus, IconDeviceFloppy, IconChevronDown, IconEye, IconWritingSign, IconInnerShadowTop, IconInfoCircle, IconAdjustments, IconDownload, IconHistory } from '@tabler/icons-vue'
|
import { IconPlus, IconUsersPlus, IconDeviceFloppy, IconChevronDown, IconEye, IconWritingSign, IconInnerShadowTop, IconInfoCircle, IconAdjustments, IconDownload, IconHistory, IconX } 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'
|
||||||
@@ -706,6 +745,7 @@ export default {
|
|||||||
IconEye,
|
IconEye,
|
||||||
IconHistory,
|
IconHistory,
|
||||||
IconDeviceFloppy,
|
IconDeviceFloppy,
|
||||||
|
IconX,
|
||||||
RevisionsModal
|
RevisionsModal
|
||||||
},
|
},
|
||||||
provide () {
|
provide () {
|
||||||
@@ -1063,7 +1103,8 @@ export default {
|
|||||||
isMathLoaded: false,
|
isMathLoaded: false,
|
||||||
isRevisionsModalOpen: false,
|
isRevisionsModalOpen: false,
|
||||||
revisions: [],
|
revisions: [],
|
||||||
beforeRevisionSnapshot: null
|
beforeRevisionSnapshot: null,
|
||||||
|
zoomLevel: 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -1313,6 +1354,8 @@ export default {
|
|||||||
this.pendingFieldAttachmentUuids.push(item.attachment_uuid)
|
this.pendingFieldAttachmentUuids.push(item.attachment_uuid)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.$refs.pagesContainer.addEventListener('wheel', this.onPagesWheel, { passive: false })
|
||||||
},
|
},
|
||||||
unmounted () {
|
unmounted () {
|
||||||
document.removeEventListener('keyup', this.onKeyUp)
|
document.removeEventListener('keyup', this.onKeyUp)
|
||||||
@@ -1320,6 +1363,8 @@ export default {
|
|||||||
|
|
||||||
window.removeEventListener('resize', this.onWindowResize)
|
window.removeEventListener('resize', this.onWindowResize)
|
||||||
window.removeEventListener('dragleave', this.onWindowDragLeave)
|
window.removeEventListener('dragleave', this.onWindowDragLeave)
|
||||||
|
|
||||||
|
this.$refs.pagesContainer.removeEventListener('wheel', this.onPagesWheel)
|
||||||
},
|
},
|
||||||
beforeUpdate () {
|
beforeUpdate () {
|
||||||
this.documentRefs = []
|
this.documentRefs = []
|
||||||
@@ -2018,6 +2063,30 @@ export default {
|
|||||||
|
|
||||||
this.isBreakpointLg = this.$el.getRootNode().querySelector('div[data-v-app]').offsetWidth < breakpointLg
|
this.isBreakpointLg = this.$el.getRootNode().querySelector('div[data-v-app]').offsetWidth < breakpointLg
|
||||||
},
|
},
|
||||||
|
onPagesWheel (event) {
|
||||||
|
if (!event.ctrlKey && !event.metaKey) return
|
||||||
|
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
const oldZoom = this.zoomLevel
|
||||||
|
const nextZoom = Math.max(1, Math.min(3, oldZoom - event.deltaY * 0.006))
|
||||||
|
|
||||||
|
if (nextZoom === oldZoom) return
|
||||||
|
|
||||||
|
const rect = this.$refs.pagesContainer.getBoundingClientRect()
|
||||||
|
const cursorX = event.clientX - rect.left
|
||||||
|
const cursorY = event.clientY - rect.top
|
||||||
|
const ratio = nextZoom / oldZoom
|
||||||
|
const nextScrollLeft = (this.$refs.pagesContainer.scrollLeft + cursorX) * ratio - cursorX
|
||||||
|
const nextScrollTop = (this.$refs.pagesContainer.scrollTop + cursorY) * ratio - cursorY
|
||||||
|
|
||||||
|
this.zoomLevel = nextZoom
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.pagesContainer.scrollLeft = nextScrollLeft
|
||||||
|
this.$refs.pagesContainer.scrollTop = nextScrollTop
|
||||||
|
})
|
||||||
|
},
|
||||||
setDocumentRefs (el) {
|
setDocumentRefs (el) {
|
||||||
if (el) {
|
if (el) {
|
||||||
this.documentRefs.push(el)
|
this.documentRefs.push(el)
|
||||||
@@ -2512,6 +2581,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)
|
||||||
@@ -3141,7 +3215,11 @@ export default {
|
|||||||
|
|
||||||
const dynamicDocumentSaves = dynamicDocumentRefs.map((ref) => ref.saveBody())
|
const dynamicDocumentSaves = dynamicDocumentRefs.map((ref) => ref.saveBody())
|
||||||
|
|
||||||
Promise.all([this.save({ force: true, revision: this.withRevisions }), ...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
|
||||||
@@ -3372,7 +3450,7 @@ export default {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
save ({ force = false, revision = false } = {}) {
|
save ({ force = false } = {}) {
|
||||||
this.pendingFieldAttachmentUuids = []
|
this.pendingFieldAttachmentUuids = []
|
||||||
|
|
||||||
if (this.beforeRevisionSnapshot) {
|
if (this.beforeRevisionSnapshot) {
|
||||||
@@ -3404,8 +3482,7 @@ export default {
|
|||||||
submitters: this.template.submitters,
|
submitters: this.template.submitters,
|
||||||
fields: this.template.fields,
|
fields: this.template.fields,
|
||||||
variables_schema: this.template.variables_schema
|
variables_schema: this.template.variables_schema
|
||||||
},
|
}
|
||||||
...(revision ? { revision: true } : {})
|
|
||||||
}),
|
}),
|
||||||
headers: { 'Content-Type': 'application/json' }
|
headers: { 'Content-Type': 'application/json' }
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
@@ -3414,6 +3491,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()
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,9 @@
|
|||||||
v-if="withReplaceButton"
|
v-if="withReplaceButton"
|
||||||
:template-id="template.id"
|
:template-id="template.id"
|
||||||
:accept-file-types="acceptFileTypes"
|
:accept-file-types="acceptFileTypes"
|
||||||
|
:authenticity-token="authenticityToken"
|
||||||
|
:with-google-drive="withGoogleDrive"
|
||||||
|
:google-drive-file-id="item.google_drive_file_id"
|
||||||
@click.stop
|
@click.stop
|
||||||
@success="$emit('replace', { replaceSchemaItem: item, ...$event })"
|
@success="$emit('replace', { replaceSchemaItem: item, ...$event })"
|
||||||
/>
|
/>
|
||||||
@@ -73,6 +76,16 @@ export default {
|
|||||||
required: true,
|
required: true,
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
|
withGoogleDrive: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
authenticityToken: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
withArrows: {
|
withArrows: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
|
|||||||
@@ -62,7 +62,7 @@
|
|||||||
name="files[]"
|
name="files[]"
|
||||||
:accept="acceptFileTypes"
|
:accept="acceptFileTypes"
|
||||||
multiple
|
multiple
|
||||||
@change="upload"
|
@change="upload()"
|
||||||
>
|
>
|
||||||
</form>
|
</form>
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,165 @@
|
|||||||
|
<template>
|
||||||
|
<Teleport :to="modalContainerEl">
|
||||||
|
<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 px-6 mt-20 max-h-none w-full max-w-xl">
|
||||||
|
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
|
||||||
|
<span class="modal-title">
|
||||||
|
Google Drive
|
||||||
|
</span>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="text-xl modal-close-button"
|
||||||
|
@click.prevent="$emit('close')"
|
||||||
|
>×</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<form
|
||||||
|
v-if="showOauthButton"
|
||||||
|
method="post"
|
||||||
|
:action="oauthPath"
|
||||||
|
@submit="isConnectClicked = true"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
name="authenticity_token"
|
||||||
|
:value="authenticityToken"
|
||||||
|
autocomplete="off"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="btn bg-white btn-outline w-full text-base font-medium mt-4"
|
||||||
|
data-turbo="false"
|
||||||
|
type="submit"
|
||||||
|
:disabled="isConnectClicked"
|
||||||
|
>
|
||||||
|
<span v-if="isConnectClicked">
|
||||||
|
<span class="flex items-center justify-center space-x-2">
|
||||||
|
<IconInnerShadowTop class="animate-spin" />
|
||||||
|
<span>{{ t('submitting') }}...</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span v-else>
|
||||||
|
<span class="flex items-center justify-center space-x-2">
|
||||||
|
<IconBrandGoogleDrive />
|
||||||
|
<span>{{ t('connect_google_drive') }}</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="relative"
|
||||||
|
>
|
||||||
|
<iframe
|
||||||
|
class="border border-base-300 rounded-lg"
|
||||||
|
style="width: 100%; height: 440px; background: white;"
|
||||||
|
src="/template_google_drive"
|
||||||
|
/>
|
||||||
|
<div v-if="loading">
|
||||||
|
<div
|
||||||
|
class="bg-white absolute top-0 bottom-0 left-0 right-0 opacity-80 rounded-lg"
|
||||||
|
style="margin: 1px"
|
||||||
|
/>
|
||||||
|
<div class="absolute top-0 bottom-0 left-0 right-0 flex items-center justify-center">
|
||||||
|
<IconInnerShadowTop class="animate-spin" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { IconBrandGoogleDrive, IconInnerShadowTop } from '@tabler/icons-vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'GoogleDrivePickerModal',
|
||||||
|
components: {
|
||||||
|
IconBrandGoogleDrive,
|
||||||
|
IconInnerShadowTop
|
||||||
|
},
|
||||||
|
inject: ['t'],
|
||||||
|
props: {
|
||||||
|
templateId: {
|
||||||
|
type: [Number, String],
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
authenticityToken: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
reopenAfterAuth: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
modalContainerEl: {
|
||||||
|
type: [Object, String],
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['close', 'picked', 'update:loading'],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
isConnectClicked: false,
|
||||||
|
showOauthButton: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
oauthPath () {
|
||||||
|
const redir = this.reopenAfterAuth
|
||||||
|
? `/templates/${this.templateId}/edit?google_drive_open=1`
|
||||||
|
: `/templates/${this.templateId}/edit`
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
access_type: 'offline',
|
||||||
|
include_granted_scopes: 'true',
|
||||||
|
prompt: 'consent',
|
||||||
|
scope: [
|
||||||
|
'https://www.googleapis.com/auth/userinfo.email',
|
||||||
|
'https://www.googleapis.com/auth/drive.file'
|
||||||
|
].join(' '),
|
||||||
|
oauth_data: new URLSearchParams({ redir }).toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
return `/auth/google_oauth2?${new URLSearchParams(params).toString()}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
window.addEventListener('message', this.messageHandler)
|
||||||
|
},
|
||||||
|
beforeUnmount () {
|
||||||
|
window.removeEventListener('message', this.messageHandler)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
messageHandler (event) {
|
||||||
|
if (event.data.type === 'google-drive-files-picked') {
|
||||||
|
const files = event.data.files || []
|
||||||
|
|
||||||
|
if (!files.length) return
|
||||||
|
|
||||||
|
this.$emit('update:loading', true)
|
||||||
|
this.$emit('picked', files)
|
||||||
|
} else if (event.data.type === 'google-drive-picker-loaded') {
|
||||||
|
this.$emit('update:loading', false)
|
||||||
|
} else if (event.data.type === 'google-drive-picker-request-oauth') {
|
||||||
|
this.showOauthButton = true
|
||||||
|
this.$emit('update:loading', false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -219,7 +219,9 @@ const en = {
|
|||||||
revisions: 'Revisions',
|
revisions: 'Revisions',
|
||||||
apply: 'Apply',
|
apply: 'Apply',
|
||||||
no_revisions_yet: 'No revisions yet',
|
no_revisions_yet: 'No revisions yet',
|
||||||
viewing_revision_from: 'Viewing revision from {date}'
|
viewing_revision_from: 'Viewing revision from {date}',
|
||||||
|
connect_google_drive: 'Connect Google Drive',
|
||||||
|
submitting: 'Submitting'
|
||||||
}
|
}
|
||||||
|
|
||||||
const es = {
|
const es = {
|
||||||
@@ -443,7 +445,9 @@ const es = {
|
|||||||
revisions: 'Revisiones',
|
revisions: 'Revisiones',
|
||||||
apply: 'Aplicar',
|
apply: 'Aplicar',
|
||||||
no_revisions_yet: 'Aún no hay revisiones',
|
no_revisions_yet: 'Aún no hay revisiones',
|
||||||
viewing_revision_from: 'Viendo revisión del {date}'
|
viewing_revision_from: 'Viendo revisión del {date}',
|
||||||
|
connect_google_drive: 'Conectar Google Drive',
|
||||||
|
submitting: 'Enviando'
|
||||||
}
|
}
|
||||||
|
|
||||||
const it = {
|
const it = {
|
||||||
@@ -667,7 +671,9 @@ const it = {
|
|||||||
revisions: 'Revisioni',
|
revisions: 'Revisioni',
|
||||||
apply: 'Applica',
|
apply: 'Applica',
|
||||||
no_revisions_yet: 'Nessuna revisione ancora',
|
no_revisions_yet: 'Nessuna revisione ancora',
|
||||||
viewing_revision_from: 'Visualizzazione revisione del {date}'
|
viewing_revision_from: 'Visualizzazione revisione del {date}',
|
||||||
|
connect_google_drive: 'Connetti Google Drive',
|
||||||
|
submitting: 'Invio'
|
||||||
}
|
}
|
||||||
|
|
||||||
const pt = {
|
const pt = {
|
||||||
@@ -679,7 +685,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',
|
||||||
@@ -891,7 +897,9 @@ const pt = {
|
|||||||
revisions: 'Revisões',
|
revisions: 'Revisões',
|
||||||
apply: 'Aplicar',
|
apply: 'Aplicar',
|
||||||
no_revisions_yet: 'Nenhuma revisão ainda',
|
no_revisions_yet: 'Nenhuma revisão ainda',
|
||||||
viewing_revision_from: 'Visualizando revisão de {date}'
|
viewing_revision_from: 'Visualizando revisão de {date}',
|
||||||
|
connect_google_drive: 'Conectar Google Drive',
|
||||||
|
submitting: 'Enviando'
|
||||||
}
|
}
|
||||||
|
|
||||||
const fr = {
|
const fr = {
|
||||||
@@ -1115,7 +1123,9 @@ const fr = {
|
|||||||
revisions: 'Révisions',
|
revisions: 'Révisions',
|
||||||
apply: 'Appliquer',
|
apply: 'Appliquer',
|
||||||
no_revisions_yet: 'Aucune révision pour le moment',
|
no_revisions_yet: 'Aucune révision pour le moment',
|
||||||
viewing_revision_from: 'Affichage de la révision du {date}'
|
viewing_revision_from: 'Affichage de la révision du {date}',
|
||||||
|
connect_google_drive: 'Connecter Google Drive',
|
||||||
|
submitting: 'Soumission'
|
||||||
}
|
}
|
||||||
|
|
||||||
const de = {
|
const de = {
|
||||||
@@ -1339,7 +1349,9 @@ const de = {
|
|||||||
revisions: 'Revisionen',
|
revisions: 'Revisionen',
|
||||||
apply: 'Anwenden',
|
apply: 'Anwenden',
|
||||||
no_revisions_yet: 'Noch keine Revisionen',
|
no_revisions_yet: 'Noch keine Revisionen',
|
||||||
viewing_revision_from: 'Ansicht der Revision vom {date}'
|
viewing_revision_from: 'Ansicht der Revision vom {date}',
|
||||||
|
connect_google_drive: 'Google Drive verbinden',
|
||||||
|
submitting: 'Übermittlung'
|
||||||
}
|
}
|
||||||
|
|
||||||
const nl = {
|
const nl = {
|
||||||
@@ -1563,7 +1575,9 @@ const nl = {
|
|||||||
revisions: 'Revisies',
|
revisions: 'Revisies',
|
||||||
apply: 'Toepassen',
|
apply: 'Toepassen',
|
||||||
no_revisions_yet: 'Nog geen revisies',
|
no_revisions_yet: 'Nog geen revisies',
|
||||||
viewing_revision_from: 'Revisie van {date} bekijken'
|
viewing_revision_from: 'Revisie van {date} bekijken',
|
||||||
|
connect_google_drive: 'Verbind Google Drive',
|
||||||
|
submitting: 'Indienen'
|
||||||
}
|
}
|
||||||
|
|
||||||
export { en, es, it, pt, fr, de, nl }
|
export { en, es, it, pt, fr, de, nl }
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
:src="image.url"
|
:src="image.url"
|
||||||
:width="width"
|
:width="width"
|
||||||
:height="height"
|
:height="height"
|
||||||
class="rounded"
|
class="w-full h-full rounded"
|
||||||
@load="onImageLoad"
|
@load="onImageLoad"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -36,7 +36,10 @@
|
|||||||
v-if="withReplaceButton"
|
v-if="withReplaceButton"
|
||||||
:template-id="template.id"
|
:template-id="template.id"
|
||||||
:accept-file-types="acceptFileTypes"
|
:accept-file-types="acceptFileTypes"
|
||||||
class="opacity-0 group-hover:opacity-100"
|
:authenticity-token="authenticityToken"
|
||||||
|
:with-google-drive="withGoogleDrive"
|
||||||
|
:google-drive-file-id="item.google_drive_file_id"
|
||||||
|
class="opacity-0 group-hover:opacity-100 has-[label:focus]:opacity-100"
|
||||||
@click.stop
|
@click.stop
|
||||||
@success="$emit('replace', { replaceSchemaItem: item, ...$event })"
|
@success="$emit('replace', { replaceSchemaItem: item, ...$event })"
|
||||||
/>
|
/>
|
||||||
@@ -232,6 +235,16 @@ export default {
|
|||||||
required: true,
|
required: true,
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
|
withGoogleDrive: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
authenticityToken: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
dynamicDocuments: {
|
dynamicDocuments: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
@@ -1,37 +1,106 @@
|
|||||||
<template>
|
<template>
|
||||||
<label
|
<div class="inline-flex items-stretch replace-document-control">
|
||||||
:for="inputId"
|
<label
|
||||||
class="btn btn-neutral btn-xs text-white transition-none replace-document-button"
|
:for="inputId"
|
||||||
:class="{ 'opacity-100': isLoading }"
|
class="btn btn-neutral btn-xs transition-none replace-document-button"
|
||||||
>
|
:class="{ 'opacity-100': isLoading, 'pr-0': showGoogleDriveDropdown && !isLoading }"
|
||||||
{{ message }}
|
>
|
||||||
|
<div class="flex items-center justify-between w-full h-full">
|
||||||
|
<span class="flex items-center space-x-2 w-full justify-center text-white">
|
||||||
|
{{ message }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="showGoogleDriveDropdown && !isLoading"
|
||||||
|
class="dropdown dropdown-end inline h-full"
|
||||||
|
style="width: 30px"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
tabindex="0"
|
||||||
|
class="flex items-center h-full cursor-pointer text-white"
|
||||||
|
>
|
||||||
|
<IconChevronDown class="w-4 h-4 flex-shrink-0" />
|
||||||
|
</label>
|
||||||
|
<ul
|
||||||
|
tabindex="0"
|
||||||
|
:style="{ backgroundColor }"
|
||||||
|
class="dropdown-content p-2 mt-2 shadow menu text-base mb-1 rounded-box text-right !text-base-content"
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click.prevent="openGoogleDriveModal"
|
||||||
|
>
|
||||||
|
<IconBrandGoogleDrive class="w-4 h-4 flex-shrink-0" />
|
||||||
|
<span class="whitespace-nowrap text-sm normal-case font-medium">Google Drive</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
<form
|
<form
|
||||||
ref="form"
|
ref="form"
|
||||||
class="hidden"
|
class="hidden"
|
||||||
>
|
>
|
||||||
|
<input
|
||||||
|
v-if="googleDriveFile"
|
||||||
|
name="google_drive_file_ids[]"
|
||||||
|
:value="googleDriveFile.id"
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
:id="inputId"
|
:id="inputId"
|
||||||
ref="input"
|
ref="input"
|
||||||
name="files[]"
|
name="files[]"
|
||||||
type="file"
|
type="file"
|
||||||
:accept="acceptFileTypes"
|
:accept="acceptFileTypes"
|
||||||
@change="upload"
|
@change="upload()"
|
||||||
>
|
>
|
||||||
</form>
|
</form>
|
||||||
</label>
|
<GoogleDrivePickerModal
|
||||||
|
v-if="showGoogleDriveModal"
|
||||||
|
v-model:loading="isLoadingGoogleDrive"
|
||||||
|
:template-id="templateId"
|
||||||
|
:authenticity-token="authenticityToken"
|
||||||
|
:modal-container-el="modalContainerEl"
|
||||||
|
@close="showGoogleDriveModal = false"
|
||||||
|
@picked="onGoogleDrivePicked"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Upload from './upload'
|
import Upload from './upload'
|
||||||
|
import GoogleDrivePickerModal from './google_drive_picker_modal'
|
||||||
|
import { IconChevronDown, IconBrandGoogleDrive } from '@tabler/icons-vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ReplaceDocument',
|
name: 'ReplaceDocument',
|
||||||
inject: ['baseFetch', 't'],
|
components: {
|
||||||
|
IconChevronDown,
|
||||||
|
IconBrandGoogleDrive,
|
||||||
|
GoogleDrivePickerModal
|
||||||
|
},
|
||||||
|
inject: ['baseFetch', 't', 'backgroundColor'],
|
||||||
props: {
|
props: {
|
||||||
templateId: {
|
templateId: {
|
||||||
type: [Number, String],
|
type: [Number, String],
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
authenticityToken: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
googleDriveFileId: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
withGoogleDrive: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
acceptFileTypes: {
|
acceptFileTypes: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -41,16 +110,25 @@ export default {
|
|||||||
emits: ['success'],
|
emits: ['success'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
isLoading: false
|
isLoading: false,
|
||||||
|
isLoadingGoogleDrive: true,
|
||||||
|
googleDriveFile: null,
|
||||||
|
showGoogleDriveModal: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
inputId () {
|
inputId () {
|
||||||
return 'el' + Math.random().toString(32).split('.')[1]
|
return 'el' + Math.random().toString(32).split('.')[1]
|
||||||
},
|
},
|
||||||
|
showGoogleDriveDropdown () {
|
||||||
|
return this.withGoogleDrive && !!this.googleDriveFileId
|
||||||
|
},
|
||||||
uploadUrl () {
|
uploadUrl () {
|
||||||
return `/templates/${this.templateId}/documents`
|
return `/templates/${this.templateId}/documents`
|
||||||
},
|
},
|
||||||
|
modalContainerEl () {
|
||||||
|
return this.$el.getRootNode().querySelector('#docuseal_modal_container')
|
||||||
|
},
|
||||||
message () {
|
message () {
|
||||||
if (this.isLoading) {
|
if (this.isLoading) {
|
||||||
return this.t('uploading_')
|
return this.t('uploading_')
|
||||||
@@ -60,7 +138,27 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
upload: Upload.methods.upload
|
upload: Upload.methods.upload,
|
||||||
|
openGoogleDriveModal () {
|
||||||
|
this.showGoogleDriveModal = true
|
||||||
|
this.isLoadingGoogleDrive = true
|
||||||
|
this.googleDriveFile = null
|
||||||
|
this.$el.getRootNode().activeElement?.blur()
|
||||||
|
},
|
||||||
|
onGoogleDrivePicked (files) {
|
||||||
|
this.googleDriveFile = files[0]
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.upload({ path: `/templates/${this.templateId}/google_drive_documents` }).then((resp) => {
|
||||||
|
if (resp.ok) {
|
||||||
|
this.showGoogleDriveModal = false
|
||||||
|
}
|
||||||
|
}).finally(() => {
|
||||||
|
this.isLoadingGoogleDrive = false
|
||||||
|
this.googleDriveFile = null
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -61,87 +61,16 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
<Teleport
|
<GoogleDrivePickerModal
|
||||||
v-if="showGoogleDriveModal"
|
v-if="showGoogleDriveModal"
|
||||||
:to="modalContainerEl"
|
v-model:loading="isLoadingGoogleDrive"
|
||||||
>
|
:template-id="templateId"
|
||||||
<div
|
:authenticity-token="authenticityToken"
|
||||||
class="modal modal-open items-start !animate-none overflow-y-auto"
|
:modal-container-el="modalContainerEl"
|
||||||
>
|
:reopen-after-auth="true"
|
||||||
<div
|
@close="showGoogleDriveModal = false"
|
||||||
class="absolute top-0 bottom-0 right-0 left-0"
|
@picked="onGoogleDrivePicked"
|
||||||
@click.prevent="showGoogleDriveModal = false"
|
/>
|
||||||
/>
|
|
||||||
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl">
|
|
||||||
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
|
|
||||||
<span class="modal-title">
|
|
||||||
Google Drive
|
|
||||||
</span>
|
|
||||||
<a
|
|
||||||
href="#"
|
|
||||||
class="text-xl modal-close-button"
|
|
||||||
@click.prevent="showGoogleDriveModal = false"
|
|
||||||
>×</a>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<form
|
|
||||||
v-if="showGoogleDriveOauthButton"
|
|
||||||
method="post"
|
|
||||||
:action="googleDriveOauthPath"
|
|
||||||
@submit="isConnectGoogleDriveClicked = true"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="hidden"
|
|
||||||
name="authenticity_token"
|
|
||||||
:value="authenticityToken"
|
|
||||||
autocomplete="off"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
id="gdrive_oauth_button"
|
|
||||||
class="btn bg-white btn-outline w-full text-base font-medium mt-4"
|
|
||||||
data-turbo="false"
|
|
||||||
type="submit"
|
|
||||||
:disabled="isConnectGoogleDriveClicked"
|
|
||||||
>
|
|
||||||
<span v-if="isConnectGoogleDriveClicked">
|
|
||||||
<span class="flex items-center justify-center space-x-2">
|
|
||||||
<IconInnerShadowTop class="animate-spin" />
|
|
||||||
<span class="">Submitting...</span>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
v-else
|
|
||||||
>
|
|
||||||
<span class="flex items-center justify-center space-x-2">
|
|
||||||
<IconBrandGoogleDrive />
|
|
||||||
<span>Connect Google Drive</span>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
class="relative"
|
|
||||||
>
|
|
||||||
<iframe
|
|
||||||
class="border border-base-300 rounded-lg"
|
|
||||||
style="width: 100%; height: 440px; background: white;"
|
|
||||||
src="/template_google_drive"
|
|
||||||
/>
|
|
||||||
<div v-if="isLoadingGoogleDrive">
|
|
||||||
<div
|
|
||||||
class="bg-white absolute top-0 bottom-0 left-0 right-0 opacity-80 rounded-lg"
|
|
||||||
style="margin: 1px"
|
|
||||||
/>
|
|
||||||
<div class="absolute top-0 bottom-0 left-0 right-0 flex items-center justify-center">
|
|
||||||
<IconInnerShadowTop class="animate-spin" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Teleport>
|
|
||||||
<form
|
<form
|
||||||
ref="form"
|
ref="form"
|
||||||
class="hidden"
|
class="hidden"
|
||||||
@@ -159,7 +88,7 @@
|
|||||||
type="file"
|
type="file"
|
||||||
:accept="acceptFileTypes"
|
:accept="acceptFileTypes"
|
||||||
multiple
|
multiple
|
||||||
@change="upload"
|
@change="upload()"
|
||||||
>
|
>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -167,6 +96,66 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { IconUpload, IconInnerShadowTop, IconChevronDown, IconBrandGoogleDrive } from '@tabler/icons-vue'
|
import { IconUpload, IconInnerShadowTop, IconChevronDown, IconBrandGoogleDrive } from '@tabler/icons-vue'
|
||||||
|
import GoogleDrivePickerModal from './google_drive_picker_modal'
|
||||||
|
|
||||||
|
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',
|
||||||
@@ -174,7 +163,8 @@ export default {
|
|||||||
IconUpload,
|
IconUpload,
|
||||||
IconInnerShadowTop,
|
IconInnerShadowTop,
|
||||||
IconChevronDown,
|
IconChevronDown,
|
||||||
IconBrandGoogleDrive
|
IconBrandGoogleDrive,
|
||||||
|
GoogleDrivePickerModal
|
||||||
},
|
},
|
||||||
inject: ['baseFetch', 't', 'backgroundColor'],
|
inject: ['baseFetch', 't', 'backgroundColor'],
|
||||||
props: {
|
props: {
|
||||||
@@ -202,11 +192,9 @@ export default {
|
|||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
isConnectGoogleDriveClicked: false,
|
isLoadingGoogleDrive: true,
|
||||||
isLoadingGoogleDrive: false,
|
|
||||||
googleDriveFiles: [],
|
googleDriveFiles: [],
|
||||||
showGoogleDriveModal: false,
|
showGoogleDriveModal: false
|
||||||
showGoogleDriveOauthButton: false
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -219,69 +207,43 @@ export default {
|
|||||||
uploadUrl () {
|
uploadUrl () {
|
||||||
return `/templates/${this.templateId}/documents`
|
return `/templates/${this.templateId}/documents`
|
||||||
},
|
},
|
||||||
googleDriveOauthPath () {
|
|
||||||
const params = {
|
|
||||||
access_type: 'offline',
|
|
||||||
include_granted_scopes: 'true',
|
|
||||||
prompt: 'consent',
|
|
||||||
scope: [
|
|
||||||
'https://www.googleapis.com/auth/userinfo.email',
|
|
||||||
'https://www.googleapis.com/auth/drive.file'
|
|
||||||
].join(' '),
|
|
||||||
oauth_data: new URLSearchParams({
|
|
||||||
redir: `/templates/${this.templateId}/edit?google_drive_open=1`
|
|
||||||
}).toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
const query = new URLSearchParams(params).toString()
|
|
||||||
|
|
||||||
return `/auth/google_oauth2?${query}`
|
|
||||||
},
|
|
||||||
modalContainerEl () {
|
modalContainerEl () {
|
||||||
return this.$el.getRootNode().querySelector('#docuseal_modal_container')
|
return this.$el.getRootNode().querySelector('#docuseal_modal_container')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
window.addEventListener('message', this.messageHandler)
|
|
||||||
|
|
||||||
if (this.queryParams.get('google_drive_open') === '1') {
|
if (this.queryParams.get('google_drive_open') === '1') {
|
||||||
this.openGoogleDriveModal()
|
this.openGoogleDriveModal()
|
||||||
|
|
||||||
window.history.replaceState({}, document.title, window.location.pathname)
|
window.history.replaceState({}, document.title, window.location.pathname)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeUnmount () {
|
|
||||||
window.removeEventListener('message', this.messageHandler)
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
openGoogleDriveModal () {
|
openGoogleDriveModal () {
|
||||||
this.showGoogleDriveModal = true
|
this.showGoogleDriveModal = true
|
||||||
this.isLoadingGoogleDrive = true
|
this.isLoadingGoogleDrive = true
|
||||||
},
|
},
|
||||||
messageHandler (event) {
|
onGoogleDrivePicked (files) {
|
||||||
if (event.data.type === 'google-drive-files-picked') {
|
this.googleDriveFiles = files
|
||||||
this.googleDriveFiles = event.data.files || []
|
|
||||||
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.isLoadingGoogleDrive = true
|
this.upload({ path: `/templates/${this.templateId}/google_drive_documents` }).then((resp) => {
|
||||||
|
if (resp.ok) {
|
||||||
this.upload({ path: `/templates/${this.templateId}/google_drive_documents` }).then((resp) => {
|
this.showGoogleDriveModal = false
|
||||||
if (resp.ok) {
|
}
|
||||||
this.showGoogleDriveModal = false
|
}).finally(() => {
|
||||||
}
|
this.isLoadingGoogleDrive = false
|
||||||
}).finally(() => {
|
this.googleDriveFiles = []
|
||||||
this.isLoadingGoogleDrive = false
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
} else if (event.data.type === 'google-drive-picker-loaded') {
|
})
|
||||||
this.isLoadingGoogleDrive = false
|
|
||||||
} else if (event.data.type === 'google-drive-picker-request-oauth') {
|
|
||||||
this.showGoogleDriveOauthButton = true
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
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' },
|
||||||
|
|||||||
@@ -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'))
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ class Template < ApplicationRecord
|
|||||||
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) }
|
||||||
|
|||||||
@@ -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,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>16k</span>
|
<span>17k</span>
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
@@ -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', 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? %>
|
||||||
<%= 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? %>
|
</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', 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 %>
|
||||||
<%= 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 %>
|
</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}" %>
|
||||||
|
|||||||
@@ -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', 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']}" %>
|
||||||
<%= 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']}" %>
|
</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? %>">
|
||||||
|
|||||||
@@ -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">
|
||||||
@@ -221,7 +227,7 @@
|
|||||||
<div class="flex items-center space-x-1 mt-1">
|
<div class="flex items-center space-x-1 mt-1">
|
||||||
<span>
|
<span>
|
||||||
<%= t('reason') %>:
|
<%= t('reason') %>:
|
||||||
<%= simple_format(h(submitter.submission_events.find_by(event_type: :decline_form).data['reason'])) %>
|
<%= simple_format(h(submitter.submission_events.find_by(event_type: :decline_form).data['reason']), {}, sanitize: false) %>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
@@ -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| %>
|
||||||
|
|||||||
@@ -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,4 +1,4 @@
|
|||||||
<p><%= t('hi_there') %>,</p>
|
<p><%= t('hi_there') %>,</p>
|
||||||
<p><%= t('name_declined_by_submitter_with_the_following_reason', name: @submitter.submission.name || @submitter.submission.template.name, submitter: @submitter.name || @submitter.email || @submitter.phone) %></p>
|
<p><%= t('name_declined_by_submitter_with_the_following_reason', name: @submitter.submission.name || @submitter.submission.template.name, submitter: @submitter.name || @submitter.email || @submitter.phone) %></p>
|
||||||
<%= simple_format(h(@submitter.submission_events.find_by(event_type: :decline_form).data['reason'])) %>
|
<%= simple_format(h(@submitter.submission_events.find_by(event_type: :decline_form).data['reason']), {}, sanitize: false) %>
|
||||||
<p><%= link_to submission_url(@submitter.submission), submission_url(@submitter.submission) %></p>
|
<p><%= link_to submission_url(@submitter.submission), submission_url(@submitter.submission) %></p>
|
||||||
@@ -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\-]+$', title: t('use_international_format_1xxx_'), 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\-]+$', 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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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 %>
|
||||||
@@ -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-with-revisions-menu="<%= @template.template_versions.exists? %>" 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 @@
|
|||||||
<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>
|
||||||
@@ -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') %>...
|
</div>
|
||||||
</span>
|
<div data-target="confirm-upload.processing" class="hidden">
|
||||||
|
<span><%= t('processing') %>...</span>
|
||||||
|
</div>
|
||||||
|
<div data-target="confirm-upload.prompt">
|
||||||
|
<p class="text-lg">
|
||||||
|
<%= t('open_file_from') %>
|
||||||
|
<a href="<%= params[:url] %>" target="_blank" class="link" rel="noopener noreferrer nofollow">
|
||||||
|
<%= params[:filename].presence || params[:url] %>
|
||||||
|
</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 %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</confirm-upload>
|
||||||
<submit-form>
|
|
||||||
<%= form_for '', url: templates_upload_path, method: :post, class: 'hidden' do %>
|
|
||||||
<button type="submit"></button>
|
|
||||||
<input name="url" value="<%= params[:url] %>">
|
|
||||||
<input name="filename" value="<%= params[:filename] %>">
|
|
||||||
<% end %>
|
|
||||||
</submit-form>
|
|
||||||
@@ -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]
|
||||||
|
|||||||
@@ -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]
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
Oj.optimize_rails
|
|
||||||
+47
-5
@@ -658,6 +658,9 @@ en: &en
|
|||||||
create_a_new_template_document_form_or_submit_the_existing_one_html: <a href="%{new_template_link}" data-turbo-frame="modal" class="inline underline font-medium">Create a new template</a> document form or <a href="%{start_form_link}" target="_blank" class="inline underline font-medium">submit the existing one</a>
|
create_a_new_template_document_form_or_submit_the_existing_one_html: <a href="%{new_template_link}" data-turbo-frame="modal" class="inline underline font-medium">Create a new template</a> document form or <a href="%{start_form_link}" target="_blank" class="inline underline font-medium">submit the existing one</a>
|
||||||
send_email_copy_with_completed_documents_to_a_specified_bcc_address: Send email copy with completed documents to a specified BCC address.
|
send_email_copy_with_completed_documents_to_a_specified_bcc_address: Send email copy with completed documents to a specified BCC address.
|
||||||
re_send_email: Re-send Email
|
re_send_email: Re-send Email
|
||||||
|
re_send_emails: Re-send Emails
|
||||||
|
are_you_sure_you_want_to_re_send_email_to_n_recipients: Are you sure you want to re-send the invitation email to %{count} recipients?
|
||||||
|
emails_have_been_sent_to_n_recipients: Emails have been sent to %{count} recipients.
|
||||||
send_email: Send Email
|
send_email: Send Email
|
||||||
copy_share_link: Copy Share Link
|
copy_share_link: Copy Share Link
|
||||||
copied_to_clipboard: Copied to Clipboard
|
copied_to_clipboard: Copied to Clipboard
|
||||||
@@ -934,6 +937,9 @@ en: &en
|
|||||||
your_email_address_has_been_changed: Your email address has been changed
|
your_email_address_has_been_changed: Your email address has been changed
|
||||||
the_email_address_for_your_account_has_been_changed_to_new_email: The email address for your account has been changed to %{new_email}.
|
the_email_address_for_your_account_has_been_changed_to_new_email: The email address for your account has been changed to %{new_email}.
|
||||||
if_you_did_not_make_this_change_please_contact_us_by_replying_to_this_email: If you did not make this change, please contact us by replying to this email.
|
if_you_did_not_make_this_change_please_contact_us_by_replying_to_this_email: If you did not make this change, please contact us by replying to this email.
|
||||||
|
open_file_from: 'Open file from'
|
||||||
|
cancel: Cancel
|
||||||
|
open: Open
|
||||||
devise:
|
devise:
|
||||||
confirmations:
|
confirmations:
|
||||||
confirmed: Your email address has been successfully confirmed.
|
confirmed: Your email address has been successfully confirmed.
|
||||||
@@ -1464,7 +1470,7 @@ es: &es
|
|||||||
type_emails_here: Escribe correos electrónicos aquí
|
type_emails_here: Escribe correos electrónicos aquí
|
||||||
or_embed_on_your_website: O integra en tu sitio web
|
or_embed_on_your_website: O integra en tu sitio web
|
||||||
phone: Teléfono
|
phone: Teléfono
|
||||||
send_emails: Enviar correos electrónicos
|
send_emails: Enviar emails
|
||||||
edit_message: Editar mensaje
|
edit_message: Editar mensaje
|
||||||
smtp_not_configured: SMTP no configurado
|
smtp_not_configured: SMTP no configurado
|
||||||
configure_smtp_settings_in_order_to_send_emails_: 'Configura los ajustes de SMTP para enviar correos electrónicos:'
|
configure_smtp_settings_in_order_to_send_emails_: 'Configura los ajustes de SMTP para enviar correos electrónicos:'
|
||||||
@@ -1707,8 +1713,11 @@ es: &es
|
|||||||
sign_in_person: Firma en persona
|
sign_in_person: Firma en persona
|
||||||
create_a_new_template_document_form_or_submit_the_existing_one_html: <a href="%{new_template_link}" data-turbo-frame="modal" class="inline underline font-medium">Crear una nueva plantilla</a> de documento o <a href="%{start_form_link}" target="_blank" class="inline underline font-medium">enviar el existente</a>
|
create_a_new_template_document_form_or_submit_the_existing_one_html: <a href="%{new_template_link}" data-turbo-frame="modal" class="inline underline font-medium">Crear una nueva plantilla</a> de documento o <a href="%{start_form_link}" target="_blank" class="inline underline font-medium">enviar el existente</a>
|
||||||
send_email_copy_with_completed_documents_to_a_specified_bcc_address: Enviar una copia del correo electrónico con los documentos completados a una dirección BCC especificada.
|
send_email_copy_with_completed_documents_to_a_specified_bcc_address: Enviar una copia del correo electrónico con los documentos completados a una dirección BCC especificada.
|
||||||
re_send_email: Reenviar correo electrónico
|
re_send_email: Reenviar email
|
||||||
send_email: Enviar correo electrónico
|
re_send_emails: Reenviar emails
|
||||||
|
are_you_sure_you_want_to_re_send_email_to_n_recipients: ¿Estás seguro de que quieres reenviar el email de invitación a %{count} destinatarios?
|
||||||
|
emails_have_been_sent_to_n_recipients: Los emails han sido enviados a %{count} destinatarios.
|
||||||
|
send_email: Enviar email
|
||||||
copy_share_link: Copiar enlace de compartir
|
copy_share_link: Copiar enlace de compartir
|
||||||
copied_to_clipboard: Copiado al portapapeles
|
copied_to_clipboard: Copiado al portapapeles
|
||||||
link: Enlace
|
link: Enlace
|
||||||
@@ -1981,6 +1990,9 @@ es: &es
|
|||||||
your_email_address_has_been_changed: Tu dirección de correo electrónico ha sido cambiada
|
your_email_address_has_been_changed: Tu dirección de correo electrónico ha sido cambiada
|
||||||
the_email_address_for_your_account_has_been_changed_to_new_email: La dirección de correo electrónico de tu cuenta ha sido cambiada a %{new_email}.
|
the_email_address_for_your_account_has_been_changed_to_new_email: La dirección de correo electrónico de tu cuenta ha sido cambiada a %{new_email}.
|
||||||
if_you_did_not_make_this_change_please_contact_us_by_replying_to_this_email: Si no realizaste este cambio, contáctanos respondiendo a este correo electrónico.
|
if_you_did_not_make_this_change_please_contact_us_by_replying_to_this_email: Si no realizaste este cambio, contáctanos respondiendo a este correo electrónico.
|
||||||
|
open_file_from: 'Abrir archivo desde'
|
||||||
|
cancel: Cancelar
|
||||||
|
open: Abrir
|
||||||
devise:
|
devise:
|
||||||
confirmations:
|
confirmations:
|
||||||
confirmed: Tu dirección de correo electrónico ha sido confirmada correctamente.
|
confirmed: Tu dirección de correo electrónico ha sido confirmada correctamente.
|
||||||
@@ -2754,7 +2766,10 @@ it: &it
|
|||||||
sign_in_person: Firma di persona
|
sign_in_person: Firma di persona
|
||||||
create_a_new_template_document_form_or_submit_the_existing_one_html: <a href="%{new_template_link}" data-turbo-frame="modal" class="inline underline font-medium">Crea un nuovo modello</a> di documento o <a href="%{start_form_link}" target="_blank" class="inline underline font-medium">invia quello esistente</a>
|
create_a_new_template_document_form_or_submit_the_existing_one_html: <a href="%{new_template_link}" data-turbo-frame="modal" class="inline underline font-medium">Crea un nuovo modello</a> di documento o <a href="%{start_form_link}" target="_blank" class="inline underline font-medium">invia quello esistente</a>
|
||||||
send_email_copy_with_completed_documents_to_a_specified_bcc_address: Invia una copia dell'email con i documenti completati a un indirizzo BCC specificato.
|
send_email_copy_with_completed_documents_to_a_specified_bcc_address: Invia una copia dell'email con i documenti completati a un indirizzo BCC specificato.
|
||||||
re_send_email: Invia di nuovo l'email
|
re_send_email: Reinvia email
|
||||||
|
re_send_emails: Reinvia le email
|
||||||
|
are_you_sure_you_want_to_re_send_email_to_n_recipients: Sei sicuro di voler inviare di nuovo l'email di invito a %{count} destinatari?
|
||||||
|
emails_have_been_sent_to_n_recipients: Le email sono state inviate a %{count} destinatari.
|
||||||
send_email: Invia email
|
send_email: Invia email
|
||||||
copy_share_link: Copia link di condivisione
|
copy_share_link: Copia link di condivisione
|
||||||
copied_to_clipboard: Copiato negli appunti
|
copied_to_clipboard: Copiato negli appunti
|
||||||
@@ -3028,6 +3043,9 @@ it: &it
|
|||||||
your_email_address_has_been_changed: Il tuo indirizzo email è stato modificato
|
your_email_address_has_been_changed: Il tuo indirizzo email è stato modificato
|
||||||
the_email_address_for_your_account_has_been_changed_to_new_email: L'indirizzo email del tuo account è stato modificato in %{new_email}.
|
the_email_address_for_your_account_has_been_changed_to_new_email: L'indirizzo email del tuo account è stato modificato in %{new_email}.
|
||||||
if_you_did_not_make_this_change_please_contact_us_by_replying_to_this_email: Se non hai effettuato questa modifica, contattaci rispondendo a questa email.
|
if_you_did_not_make_this_change_please_contact_us_by_replying_to_this_email: Se non hai effettuato questa modifica, contattaci rispondendo a questa email.
|
||||||
|
open_file_from: 'Aprire file da'
|
||||||
|
cancel: Annulla
|
||||||
|
open: Apri
|
||||||
devise:
|
devise:
|
||||||
confirmations:
|
confirmations:
|
||||||
confirmed: Il tuo indirizzo email è stato confermato con successo.
|
confirmed: Il tuo indirizzo email è stato confermato con successo.
|
||||||
@@ -3802,6 +3820,9 @@ fr: &fr
|
|||||||
create_a_new_template_document_form_or_submit_the_existing_one_html: <a href="%{new_template_link}" data-turbo-frame="modal" class="inline underline font-medium">Créez un nouveau modèle</a> de document ou <a href="%{start_form_link}" target="_blank" class="inline underline font-medium">soumettez l’existant</a>
|
create_a_new_template_document_form_or_submit_the_existing_one_html: <a href="%{new_template_link}" data-turbo-frame="modal" class="inline underline font-medium">Créez un nouveau modèle</a> de document ou <a href="%{start_form_link}" target="_blank" class="inline underline font-medium">soumettez l’existant</a>
|
||||||
send_email_copy_with_completed_documents_to_a_specified_bcc_address: Envoyer une copie e‑mail avec les documents complétés à une adresse Cci spécifiée.
|
send_email_copy_with_completed_documents_to_a_specified_bcc_address: Envoyer une copie e‑mail avec les documents complétés à une adresse Cci spécifiée.
|
||||||
re_send_email: Renvoyer l’e‑mail
|
re_send_email: Renvoyer l’e‑mail
|
||||||
|
re_send_emails: Renvoyer e‑mails
|
||||||
|
are_you_sure_you_want_to_re_send_email_to_n_recipients: Êtes‑vous sûr de vouloir renvoyer l’e‑mail d’invitation à %{count} destinataires ?
|
||||||
|
emails_have_been_sent_to_n_recipients: Les e‑mails ont été envoyés à %{count} destinataires.
|
||||||
send_email: Envoyer l’e‑mail
|
send_email: Envoyer l’e‑mail
|
||||||
copy_share_link: Copier le lien de partage
|
copy_share_link: Copier le lien de partage
|
||||||
copied_to_clipboard: Copié dans le presse‑papiers
|
copied_to_clipboard: Copié dans le presse‑papiers
|
||||||
@@ -4072,6 +4093,9 @@ fr: &fr
|
|||||||
your_email_address_has_been_changed: Votre adresse e-mail a été modifiée
|
your_email_address_has_been_changed: Votre adresse e-mail a été modifiée
|
||||||
the_email_address_for_your_account_has_been_changed_to_new_email: "L'adresse e-mail de votre compte a été modifiée en %{new_email}."
|
the_email_address_for_your_account_has_been_changed_to_new_email: "L'adresse e-mail de votre compte a été modifiée en %{new_email}."
|
||||||
if_you_did_not_make_this_change_please_contact_us_by_replying_to_this_email: Si vous n'avez pas effectué ce changement, veuillez nous contacter en répondant à cet e-mail.
|
if_you_did_not_make_this_change_please_contact_us_by_replying_to_this_email: Si vous n'avez pas effectué ce changement, veuillez nous contacter en répondant à cet e-mail.
|
||||||
|
open_file_from: 'Ouvrir le fichier depuis'
|
||||||
|
cancel: Annuler
|
||||||
|
open: Ouvrir
|
||||||
devise:
|
devise:
|
||||||
confirmations:
|
confirmations:
|
||||||
confirmed: Votre adresse e-mail a été confirmée avec succès.
|
confirmed: Votre adresse e-mail a été confirmée avec succès.
|
||||||
@@ -4321,7 +4345,7 @@ pt: &pt
|
|||||||
|
|
||||||
Obrigado,
|
Obrigado,
|
||||||
{account.name}
|
{account.name}
|
||||||
view: Visualizar
|
view: Ver
|
||||||
email: Email
|
email: Email
|
||||||
form_expired_at_html: 'O formulário expirou em <span class="font-semibold">%{time}</span>'
|
form_expired_at_html: 'O formulário expirou em <span class="font-semibold">%{time}</span>'
|
||||||
verification_code_code: 'Código de verificação: %{code}'
|
verification_code_code: 'Código de verificação: %{code}'
|
||||||
@@ -4846,6 +4870,9 @@ pt: &pt
|
|||||||
create_a_new_template_document_form_or_submit_the_existing_one_html: '<a href="%{new_template_link}" data-turbo-frame="modal" class="inline underline font-medium">Criar um novo modelo</a> de documento ou <a href="%{start_form_link}" target="_blank" class="inline underline font-medium">submeter o existente</a>'
|
create_a_new_template_document_form_or_submit_the_existing_one_html: '<a href="%{new_template_link}" data-turbo-frame="modal" class="inline underline font-medium">Criar um novo modelo</a> de documento ou <a href="%{start_form_link}" target="_blank" class="inline underline font-medium">submeter o existente</a>'
|
||||||
send_email_copy_with_completed_documents_to_a_specified_bcc_address: Envie uma cópia do e-mail com documentos concluídos para um endereço BCC especificado.
|
send_email_copy_with_completed_documents_to_a_specified_bcc_address: Envie uma cópia do e-mail com documentos concluídos para um endereço BCC especificado.
|
||||||
re_send_email: Reenviar e-mail
|
re_send_email: Reenviar e-mail
|
||||||
|
re_send_emails: Reenviar e-mails
|
||||||
|
are_you_sure_you_want_to_re_send_email_to_n_recipients: Tem certeza de que deseja reenviar o e-mail de convite para %{count} destinatários?
|
||||||
|
emails_have_been_sent_to_n_recipients: Os e-mails foram enviados para %{count} destinatários.
|
||||||
send_email: Enviar e-mail
|
send_email: Enviar e-mail
|
||||||
copy_share_link: Copiar link de compartilhamento
|
copy_share_link: Copiar link de compartilhamento
|
||||||
copied_to_clipboard: Copiado para a área de transferência
|
copied_to_clipboard: Copiado para a área de transferência
|
||||||
@@ -5119,6 +5146,9 @@ pt: &pt
|
|||||||
your_email_address_has_been_changed: Seu endereço de e-mail foi alterado
|
your_email_address_has_been_changed: Seu endereço de e-mail foi alterado
|
||||||
the_email_address_for_your_account_has_been_changed_to_new_email: O endereço de e-mail da sua conta foi alterado para %{new_email}.
|
the_email_address_for_your_account_has_been_changed_to_new_email: O endereço de e-mail da sua conta foi alterado para %{new_email}.
|
||||||
if_you_did_not_make_this_change_please_contact_us_by_replying_to_this_email: Se você não fez essa alteração, entre em contato conosco respondendo a este e-mail.
|
if_you_did_not_make_this_change_please_contact_us_by_replying_to_this_email: Se você não fez essa alteração, entre em contato conosco respondendo a este e-mail.
|
||||||
|
open_file_from: 'Abrir arquivo de'
|
||||||
|
cancel: Cancelar
|
||||||
|
open: Abrir
|
||||||
devise:
|
devise:
|
||||||
confirmations:
|
confirmations:
|
||||||
confirmed: Seu endereço de e-mail foi confirmado com sucesso.
|
confirmed: Seu endereço de e-mail foi confirmado com sucesso.
|
||||||
@@ -5893,6 +5923,9 @@ de: &de
|
|||||||
create_a_new_template_document_form_or_submit_the_existing_one_html: <a href="%{new_template_link}" data-turbo-frame="modal" class="inline underline font-medium">Neue Vorlage erstellen</a> oder <a href="%{start_form_link}" target="_blank" class="inline underline font-medium">bestehende einreichen</a>
|
create_a_new_template_document_form_or_submit_the_existing_one_html: <a href="%{new_template_link}" data-turbo-frame="modal" class="inline underline font-medium">Neue Vorlage erstellen</a> oder <a href="%{start_form_link}" target="_blank" class="inline underline font-medium">bestehende einreichen</a>
|
||||||
send_email_copy_with_completed_documents_to_a_specified_bcc_address: Senden Sie eine E-Mail-Kopie mit abgeschlossenen Dokumenten an eine angegebene BCC-Adresse.
|
send_email_copy_with_completed_documents_to_a_specified_bcc_address: Senden Sie eine E-Mail-Kopie mit abgeschlossenen Dokumenten an eine angegebene BCC-Adresse.
|
||||||
re_send_email: E-Mail erneut senden
|
re_send_email: E-Mail erneut senden
|
||||||
|
re_send_emails: E-Mails senden
|
||||||
|
are_you_sure_you_want_to_re_send_email_to_n_recipients: Sind Sie sicher, dass Sie die Einladungs-E-Mail erneut an %{count} Empfänger senden möchten?
|
||||||
|
emails_have_been_sent_to_n_recipients: Die E-Mails wurden an %{count} Empfänger gesendet.
|
||||||
send_email: E-Mail senden
|
send_email: E-Mail senden
|
||||||
copy_share_link: Freigabelink kopieren
|
copy_share_link: Freigabelink kopieren
|
||||||
copied_to_clipboard: In die Zwischenablage kopiert
|
copied_to_clipboard: In die Zwischenablage kopiert
|
||||||
@@ -6166,6 +6199,9 @@ de: &de
|
|||||||
your_email_address_has_been_changed: Ihre E-Mail-Adresse wurde geändert
|
your_email_address_has_been_changed: Ihre E-Mail-Adresse wurde geändert
|
||||||
the_email_address_for_your_account_has_been_changed_to_new_email: Die E-Mail-Adresse Ihres Kontos wurde in %{new_email} geändert.
|
the_email_address_for_your_account_has_been_changed_to_new_email: Die E-Mail-Adresse Ihres Kontos wurde in %{new_email} geändert.
|
||||||
if_you_did_not_make_this_change_please_contact_us_by_replying_to_this_email: Wenn Sie diese Änderung nicht vorgenommen haben, kontaktieren Sie uns bitte, indem Sie auf diese E-Mail antworten.
|
if_you_did_not_make_this_change_please_contact_us_by_replying_to_this_email: Wenn Sie diese Änderung nicht vorgenommen haben, kontaktieren Sie uns bitte, indem Sie auf diese E-Mail antworten.
|
||||||
|
open_file_from: 'Datei öffnen von'
|
||||||
|
cancel: Abbrechen
|
||||||
|
open: Öffnen
|
||||||
devise:
|
devise:
|
||||||
confirmations:
|
confirmations:
|
||||||
confirmed: Ihre E-Mail-Adresse wurde erfolgreich bestätigt.
|
confirmed: Ihre E-Mail-Adresse wurde erfolgreich bestätigt.
|
||||||
@@ -7345,6 +7381,9 @@ nl: &nl
|
|||||||
create_a_new_template_document_form_or_submit_the_existing_one_html: <a href="%{new_template_link}" data-turbo-frame="modal" class="inline underline font-medium">Maak een nieuw sjabloon</a> voor een documentformulier of <a href="%{start_form_link}" target="_blank" class="inline underline font-medium">dien het bestaande in</a>
|
create_a_new_template_document_form_or_submit_the_existing_one_html: <a href="%{new_template_link}" data-turbo-frame="modal" class="inline underline font-medium">Maak een nieuw sjabloon</a> voor een documentformulier of <a href="%{start_form_link}" target="_blank" class="inline underline font-medium">dien het bestaande in</a>
|
||||||
send_email_copy_with_completed_documents_to_a_specified_bcc_address: Stuur een e-mailkopie met voltooide documenten naar een opgegeven BCC-adres.
|
send_email_copy_with_completed_documents_to_a_specified_bcc_address: Stuur een e-mailkopie met voltooide documenten naar een opgegeven BCC-adres.
|
||||||
re_send_email: E-mail opnieuw verzenden
|
re_send_email: E-mail opnieuw verzenden
|
||||||
|
re_send_emails: E-mails verzenden
|
||||||
|
are_you_sure_you_want_to_re_send_email_to_n_recipients: Weet je zeker dat je de uitnodigings-e-mail opnieuw wilt verzenden naar %{count} ontvangers?
|
||||||
|
emails_have_been_sent_to_n_recipients: De e-mails zijn verzonden naar %{count} ontvangers.
|
||||||
send_email: E-mail verzenden
|
send_email: E-mail verzenden
|
||||||
copy_share_link: Deellink kopiëren
|
copy_share_link: Deellink kopiëren
|
||||||
copied_to_clipboard: Gekopieerd naar klembord
|
copied_to_clipboard: Gekopieerd naar klembord
|
||||||
@@ -7614,6 +7653,9 @@ nl: &nl
|
|||||||
your_email_address_has_been_changed: Uw e-mailadres is gewijzigd
|
your_email_address_has_been_changed: Uw e-mailadres is gewijzigd
|
||||||
the_email_address_for_your_account_has_been_changed_to_new_email: Het e-mailadres van uw account is gewijzigd naar %{new_email}.
|
the_email_address_for_your_account_has_been_changed_to_new_email: Het e-mailadres van uw account is gewijzigd naar %{new_email}.
|
||||||
if_you_did_not_make_this_change_please_contact_us_by_replying_to_this_email: Als u deze wijziging niet hebt aangebracht, neem dan contact met ons op door op deze e-mail te antwoorden.
|
if_you_did_not_make_this_change_please_contact_us_by_replying_to_this_email: Als u deze wijziging niet hebt aangebracht, neem dan contact met ons op door op deze e-mail te antwoorden.
|
||||||
|
open_file_from: 'Bestand openen van'
|
||||||
|
cancel: Annuleren
|
||||||
|
open: Openen
|
||||||
devise:
|
devise:
|
||||||
confirmations:
|
confirmations:
|
||||||
confirmed: Uw e-mailadres is succesvol bevestigd.
|
confirmed: Uw e-mailadres is succesvol bevestigd.
|
||||||
|
|||||||
+2
-1
@@ -72,6 +72,7 @@ Rails.application.routes.draw do
|
|||||||
resources :unarchive, only: %i[create], controller: 'submissions_unarchive'
|
resources :unarchive, only: %i[create], controller: 'submissions_unarchive'
|
||||||
resources :events, only: %i[index], controller: 'submission_events'
|
resources :events, only: %i[index], controller: 'submission_events'
|
||||||
resources :download, only: %i[index], controller: 'submissions_download'
|
resources :download, only: %i[index], controller: 'submissions_download'
|
||||||
|
resources :resend_email, only: %i[create], controller: 'submissions_resend_email'
|
||||||
end
|
end
|
||||||
resources :submitters, only: %i[edit update]
|
resources :submitters, only: %i[edit update]
|
||||||
resources :console_redirect, only: %i[index]
|
resources :console_redirect, only: %i[index]
|
||||||
@@ -108,7 +109,7 @@ Rails.application.routes.draw do
|
|||||||
resource :form, only: %i[show], controller: 'templates_form_preview'
|
resource :form, only: %i[show], controller: 'templates_form_preview'
|
||||||
resource :code_modal, only: %i[show], controller: 'templates_code_modal'
|
resource :code_modal, only: %i[show], controller: 'templates_code_modal'
|
||||||
resource :preferences, only: %i[show create destroy], controller: 'templates_preferences'
|
resource :preferences, only: %i[show create destroy], controller: 'templates_preferences'
|
||||||
resources :versions, only: %i[index show], controller: 'templates_versions'
|
resources :versions, only: %i[index show create], controller: 'templates_versions'
|
||||||
resource :share_link, only: %i[show create], controller: 'templates_share_link'
|
resource :share_link, only: %i[show create], controller: 'templates_share_link'
|
||||||
resource :share_link_qr, only: %i[show], controller: 'templates_share_link_qr'
|
resource :share_link_qr, only: %i[show], controller: 'templates_share_link_qr'
|
||||||
resources :recipients, only: %i[create], controller: 'templates_recipients'
|
resources :recipients, only: %i[create], controller: 'templates_recipients'
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module Accounts
|
module Accounts
|
||||||
LINK_EXPIRES_AT = 40.minutes
|
LINK_EXPIRES_AT = ENV.fetch('FILE_URLS_EXPIRE_MINUTES', '40').to_i.minutes
|
||||||
|
|
||||||
module_function
|
module_function
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ module HtmlToPlainText
|
|||||||
|
|
||||||
doc = Nokogiri::HTML.fragment(cleaned)
|
doc = Nokogiri::HTML.fragment(cleaned)
|
||||||
|
|
||||||
doc.css('script').each(&:remove)
|
doc.xpath('.//script').each(&:remove)
|
||||||
|
|
||||||
result = process_nodes(doc, line_length)
|
result = process_nodes(doc, line_length)
|
||||||
|
|
||||||
@@ -33,12 +33,9 @@ module HtmlToPlainText
|
|||||||
result = +''
|
result = +''
|
||||||
|
|
||||||
node.children.each do |child|
|
node.children.each do |child|
|
||||||
case child
|
if child.text? || child.cdata?
|
||||||
when Nokogiri::XML::Text
|
|
||||||
result << child.text
|
result << child.text
|
||||||
when Nokogiri::XML::Comment
|
elsif child.element?
|
||||||
next
|
|
||||||
when Nokogiri::XML::Element
|
|
||||||
result << process_element(child, line_length)
|
result << process_element(child, line_length)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,8 +1,25 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module ImageUtils
|
module ImageUtils
|
||||||
|
ICO_REGEXP = %r{\Aimage/(?:x-icon|vnd\.microsoft\.icon)\z}
|
||||||
|
BMP_REGEXP = %r{\Aimage/(?:bmp|x-bmp|x-ms-bmp)\z}
|
||||||
|
|
||||||
module_function
|
module_function
|
||||||
|
|
||||||
|
def load_vips(data, content_type: nil, autorot: false)
|
||||||
|
content_type ||= Marcel::MimeType.for(data)
|
||||||
|
|
||||||
|
if ICO_REGEXP.match?(content_type)
|
||||||
|
LoadIco.call(data)
|
||||||
|
elsif BMP_REGEXP.match?(content_type)
|
||||||
|
LoadBmp.call(data)
|
||||||
|
else
|
||||||
|
image = Vips::Image.new_from_buffer(data, '')
|
||||||
|
|
||||||
|
autorot ? image.autorot : image
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def blank?(image)
|
def blank?(image)
|
||||||
stats = image.stats
|
stats = image.stats
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -39,7 +39,7 @@ module LoadBmp
|
|||||||
bands = header_data[:bpp] / 8
|
bands = header_data[:bpp] / 8
|
||||||
end
|
end
|
||||||
|
|
||||||
image = Vips::Image.new_from_memory(final_pixel_data, header_data[:width], header_data[:height], bands, :uchar)
|
image = Vips::Image.new_from_memory_copy(final_pixel_data, header_data[:width], header_data[:height], bands, :uchar)
|
||||||
|
|
||||||
image = image.flip(:vertical) if header_data[:orientation] == -1
|
image = image.flip(:vertical) if header_data[:orientation] == -1
|
||||||
|
|
||||||
|
|||||||
+5
-3
@@ -43,7 +43,9 @@ module LoadIco
|
|||||||
|
|
||||||
raise ArgumentError, 'Unable to load' unless image_data_bytes && image_data_bytes.bytesize == best_entry[:size]
|
raise ArgumentError, 'Unable to load' unless image_data_bytes && image_data_bytes.bytesize == best_entry[:size]
|
||||||
|
|
||||||
return Vips::Image.new_from_buffer(image_data_bytes, '') if image_data_bytes.start_with?(PNG_SIGNATURE)
|
if image_data_bytes.start_with?(PNG_SIGNATURE)
|
||||||
|
return ImageUtils.load_vips(image_data_bytes, content_type: 'image/png')
|
||||||
|
end
|
||||||
|
|
||||||
image = load_image_entry(image_data_bytes, best_entry[:width], best_entry[:height])
|
image = load_image_entry(image_data_bytes, best_entry[:width], best_entry[:height])
|
||||||
|
|
||||||
@@ -200,13 +202,13 @@ module LoadIco
|
|||||||
|
|
||||||
return nil unless pixel_data_string.bytesize == expected_bytes && expected_bytes.positive?
|
return nil unless pixel_data_string.bytesize == expected_bytes && expected_bytes.positive?
|
||||||
|
|
||||||
Vips::Image.new_from_memory(
|
Vips::Image.new_from_memory_copy(
|
||||||
pixel_data_string,
|
pixel_data_string,
|
||||||
dib_width,
|
dib_width,
|
||||||
image_pixel_height,
|
image_pixel_height,
|
||||||
4,
|
4,
|
||||||
:uchar
|
:uchar
|
||||||
)
|
).copy(interpretation: :srgb)
|
||||||
end
|
end
|
||||||
# rubocop:enable Metrics
|
# rubocop:enable Metrics
|
||||||
end
|
end
|
||||||
@@ -25,6 +25,10 @@ module NumberUtils
|
|||||||
ApplicationController.helpers.number_to_currency(number, locale:, precision: 2, unit: CURRENCY_SYMBOLS[format])
|
ApplicationController.helpers.number_to_currency(number, locale:, precision: 2, unit: CURRENCY_SYMBOLS[format])
|
||||||
elsif locale
|
elsif locale
|
||||||
ApplicationController.helpers.number_with_delimiter(number, locale:)
|
ApplicationController.helpers.number_with_delimiter(number, locale:)
|
||||||
|
elsif format == 'percent'
|
||||||
|
"#{number}%"
|
||||||
|
elsif format == 'percent_space'
|
||||||
|
"#{number.to_s.tr('.', ',')} %"
|
||||||
else
|
else
|
||||||
number
|
number
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require 'puma/plugin'
|
require 'puma/plugin'
|
||||||
|
require 'redis_client'
|
||||||
|
|
||||||
# rubocop:disable Metrics
|
# rubocop:disable Metrics
|
||||||
Puma::Plugin.create do
|
Puma::Plugin.create do
|
||||||
@@ -68,7 +69,7 @@ Puma::Plugin.create do
|
|||||||
|
|
||||||
break
|
break
|
||||||
rescue RedisClient::CannotConnectError
|
rescue RedisClient::CannotConnectError
|
||||||
raise('Unable to connect to redis') if attempt > 10
|
raise('Unable to connect to redis') if attempt > 30
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -39,7 +39,9 @@ module SendWebhookRequest
|
|||||||
data: data
|
data: data
|
||||||
}.to_json
|
}.to_json
|
||||||
|
|
||||||
req.headers['X-Docuseal-Signature'] = WebhookUrls::Signatures.sign(webhook_url.hmac_secret, body: req.body)
|
if req.headers['X-Docuseal-Signature'].blank?
|
||||||
|
req.headers['X-Docuseal-Signature'] = WebhookUrls::Signatures.sign(webhook_url.hmac_secret, body: req.body)
|
||||||
|
end
|
||||||
|
|
||||||
req.options.read_timeout = 15
|
req.options.read_timeout = 15
|
||||||
req.options.open_timeout = 8
|
req.options.open_timeout = 8
|
||||||
|
|||||||
+4
-2
@@ -162,7 +162,7 @@ module Submissions
|
|||||||
return email.downcase.sub(/@gmail?\z/i, '@gmail.com') if email.match?(/@gmail?\z/i)
|
return email.downcase.sub(/@gmail?\z/i, '@gmail.com') if email.match?(/@gmail?\z/i)
|
||||||
|
|
||||||
return email.downcase if email.include?(',') ||
|
return email.downcase if email.include?(',') ||
|
||||||
email.match?(/\.(?:gob|om|mm|cm|et|mo|nz|za|ie|ed\.jp)\z/i) ||
|
email.match?(/\.(?:gob(?:\.\w+)?|om|mm|cm|et|mo|nz|za|ie|ed\.jp)\z/i) ||
|
||||||
email.exclude?('.')
|
email.exclude?('.')
|
||||||
|
|
||||||
fixed_email = EmailTypo.call(email.delete_prefix('<'))
|
fixed_email = EmailTypo.call(email.delete_prefix('<'))
|
||||||
@@ -175,7 +175,9 @@ module Submissions
|
|||||||
return email.downcase if domain == fixed_domain
|
return email.downcase if domain == fixed_domain
|
||||||
return email.downcase if fixed_domain.match?(/\Agmail\.(?!com\z)/i)
|
return email.downcase if fixed_domain.match?(/\Agmail\.(?!com\z)/i)
|
||||||
|
|
||||||
if DidYouMean::Levenshtein.distance(domain, fixed_domain) > 3
|
threshold = fixed_domain.start_with?('hotmail.') ? 2 : 3
|
||||||
|
|
||||||
|
if DidYouMean::Levenshtein.distance(domain, fixed_domain) > threshold
|
||||||
Rails.logger.info("Skipped email fix #{domain}")
|
Rails.logger.info("Skipped email fix #{domain}")
|
||||||
|
|
||||||
return email.downcase
|
return email.downcase
|
||||||
|
|||||||
@@ -129,6 +129,8 @@ module Submissions
|
|||||||
with_submitter_timezone = configs.find { |c| c.key == AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY }&.value == true
|
with_submitter_timezone = configs.find { |c| c.key == AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY }&.value == true
|
||||||
with_timestamp_seconds = configs.find { |c| c.key == AccountConfig::WITH_TIMESTAMP_SECONDS_KEY }&.value == true
|
with_timestamp_seconds = configs.find { |c| c.key == AccountConfig::WITH_TIMESTAMP_SECONDS_KEY }&.value == true
|
||||||
|
|
||||||
|
file_links_expire_at = Accounts.link_expires_at(submission.account) if with_file_links
|
||||||
|
|
||||||
timezone = account.timezone
|
timezone = account.timezone
|
||||||
timezone = last_submitter.timezone || account.timezone if with_submitter_timezone
|
timezone = last_submitter.timezone || account.timezone if with_submitter_timezone
|
||||||
|
|
||||||
@@ -345,6 +347,13 @@ module Submissions
|
|||||||
|
|
||||||
field_name = grouped_value_field_names[value].presence || field['title'].presence || field['name'].to_s
|
field_name = grouped_value_field_names[value].presence || field['title'].presence || field['name'].to_s
|
||||||
|
|
||||||
|
field_type = field['type']
|
||||||
|
|
||||||
|
if field_type == 'image' &&
|
||||||
|
submitter.attachments.find { |a| a.uuid == value }.then { |a| !a.image? || a.content_type == 'image/heic' }
|
||||||
|
field_type = 'file'
|
||||||
|
end
|
||||||
|
|
||||||
[
|
[
|
||||||
composer.formatted_text_box(
|
composer.formatted_text_box(
|
||||||
[
|
[
|
||||||
@@ -357,13 +366,13 @@ module Submissions
|
|||||||
text_align: field_name.to_s.match?(RTL_REGEXP) ? :right : :left,
|
text_align: field_name.to_s.match?(RTL_REGEXP) ? :right : :left,
|
||||||
line_spacing: 1.3, padding: [0, 0, 2, 0]
|
line_spacing: 1.3, padding: [0, 0, 2, 0]
|
||||||
),
|
),
|
||||||
if field['type'].in?(%w[image signature initials stamp kba]) &&
|
if field_type.in?(%w[image signature initials stamp kba]) &&
|
||||||
(attachment = submitter.attachments.find { |a| a.uuid == value }) &&
|
(attachment = submitter.attachments.find { |a| a.uuid == value }) &&
|
||||||
attachment.image?
|
attachment.image?
|
||||||
|
|
||||||
image =
|
image =
|
||||||
begin
|
begin
|
||||||
Submissions::GenerateResultAttachments.load_vips_image(attachment).autorot
|
ImageUtils.load_vips(attachment.download, content_type: attachment.content_type, autorot: true)
|
||||||
rescue Vips::Error
|
rescue Vips::Error
|
||||||
next unless attachment.content_type.starts_with?('image/')
|
next unless attachment.content_type.starts_with?('image/')
|
||||||
next if attachment.byte_size.zero?
|
next if attachment.byte_size.zero?
|
||||||
@@ -374,7 +383,13 @@ module Submissions
|
|||||||
scale = [600.0 / image.width, 600.0 / image.height].min
|
scale = [600.0 / image.width, 600.0 / image.height].min
|
||||||
|
|
||||||
resized_image = image.resize([scale, 1].min)
|
resized_image = image.resize([scale, 1].min)
|
||||||
io = StringIO.new(resized_image.write_to_buffer('.png'))
|
|
||||||
|
io =
|
||||||
|
if field['type'] == 'image' && !resized_image.has_alpha?
|
||||||
|
StringIO.new(resized_image.colourspace(:srgb).write_to_buffer('.jpg', strip: true))
|
||||||
|
else
|
||||||
|
StringIO.new(resized_image.write_to_buffer('.png', strip: true))
|
||||||
|
end
|
||||||
|
|
||||||
width = field['type'] == 'initials' ? 50 : 200
|
width = field['type'] == 'initials' ? 50 : 200
|
||||||
height = resized_image.height * (width.to_f / resized_image.width)
|
height = resized_image.height * (width.to_f / resized_image.width)
|
||||||
@@ -386,7 +401,7 @@ module Submissions
|
|||||||
|
|
||||||
composer.image(io, width:, height:, margin: [5, 0, 10, 0])
|
composer.image(io, width:, height:, margin: [5, 0, 10, 0])
|
||||||
composer.formatted_text_box([{ text: '' }])
|
composer.formatted_text_box([{ text: '' }])
|
||||||
elsif field['type'].in?(%w[file payment image])
|
elsif field_type.in?(%w[file payment image])
|
||||||
if field['type'] == 'payment'
|
if field['type'] == 'payment'
|
||||||
unit = CURRENCY_SYMBOLS[field['preferences']['currency']] || field['preferences']['currency']
|
unit = CURRENCY_SYMBOLS[field['preferences']['currency']] || field['preferences']['currency']
|
||||||
|
|
||||||
@@ -402,7 +417,7 @@ module Submissions
|
|||||||
|
|
||||||
link =
|
link =
|
||||||
if with_file_links
|
if with_file_links
|
||||||
ActiveStorage::Blob.proxy_url(attachment.blob)
|
ActiveStorage::Blob.proxy_url(attachment.blob, expires_at: file_links_expire_at)
|
||||||
else
|
else
|
||||||
r.submissions_preview_url(submission.slug, **Docuseal.default_url_options)
|
r.submissions_preview_url(submission.slug, **Docuseal.default_url_options)
|
||||||
end
|
end
|
||||||
@@ -411,7 +426,7 @@ module Submissions
|
|||||||
end,
|
end,
|
||||||
padding: [0, 0, 10, 0]
|
padding: [0, 0, 10, 0]
|
||||||
)
|
)
|
||||||
elsif field['type'] == 'checkbox'
|
elsif field_type == 'checkbox'
|
||||||
composer.formatted_text_box([{ text: value.to_s.titleize }], padding: [0, 0, 10, 0])
|
composer.formatted_text_box([{ text: value.to_s.titleize }], padding: [0, 0, 10, 0])
|
||||||
else
|
else
|
||||||
if field['type'] == 'date'
|
if field['type'] == 'date'
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ module Submissions
|
|||||||
with_signature_id_reason =
|
with_signature_id_reason =
|
||||||
configs.find { |c| c.key == AccountConfig::WITH_SIGNATURE_ID_REASON_KEY }&.value != false
|
configs.find { |c| c.key == AccountConfig::WITH_SIGNATURE_ID_REASON_KEY }&.value != false
|
||||||
|
|
||||||
|
file_links_expire_at = Accounts.link_expires_at(submission.account) if with_file_links
|
||||||
|
|
||||||
pdfs_index = GenerateResultAttachments.build_pdfs_index(submission, flatten: is_flatten,
|
pdfs_index = GenerateResultAttachments.build_pdfs_index(submission, flatten: is_flatten,
|
||||||
incremental: is_rotate_incremental)
|
incremental: is_rotate_incremental)
|
||||||
|
|
||||||
@@ -42,7 +44,8 @@ module Submissions
|
|||||||
GenerateResultAttachments.fill_submitter_fields(s, submission.account, pdfs_index,
|
GenerateResultAttachments.fill_submitter_fields(s, submission.account, pdfs_index,
|
||||||
with_signature_id:, is_flatten:, with_headings: index.zero?,
|
with_signature_id:, is_flatten:, with_headings: index.zero?,
|
||||||
with_submitter_timezone:, with_file_links:,
|
with_submitter_timezone:, with_file_links:,
|
||||||
with_signature_id_reason:, with_timestamp_seconds:)
|
file_links_expire_at:, with_signature_id_reason:,
|
||||||
|
with_timestamp_seconds:)
|
||||||
end
|
end
|
||||||
|
|
||||||
template = submission.template
|
template = submission.template
|
||||||
|
|||||||
@@ -11,9 +11,6 @@ module Submissions
|
|||||||
'Helvetica'
|
'Helvetica'
|
||||||
end
|
end
|
||||||
|
|
||||||
ICO_REGEXP = %r{\Aimage/(?:x-icon|vnd\.microsoft\.icon)\z}
|
|
||||||
BMP_REGEXP = %r{\Aimage/(?:bmp|x-bmp|x-ms-bmp)\z}
|
|
||||||
|
|
||||||
FONT_BOLD_NAME = if File.exist?(FONT_BOLD_PATH)
|
FONT_BOLD_NAME = if File.exist?(FONT_BOLD_PATH)
|
||||||
FONT_BOLD_PATH
|
FONT_BOLD_PATH
|
||||||
else
|
else
|
||||||
@@ -154,6 +151,8 @@ module Submissions
|
|||||||
with_signature_id_reason =
|
with_signature_id_reason =
|
||||||
configs.find { |c| c.key == AccountConfig::WITH_SIGNATURE_ID_REASON_KEY }&.value != false
|
configs.find { |c| c.key == AccountConfig::WITH_SIGNATURE_ID_REASON_KEY }&.value != false
|
||||||
|
|
||||||
|
file_links_expire_at = Accounts.link_expires_at(submitter.account) if with_file_links
|
||||||
|
|
||||||
pdfs_index = build_pdfs_index(submitter.submission, submitter:, flatten: is_flatten,
|
pdfs_index = build_pdfs_index(submitter.submission, submitter:, flatten: is_flatten,
|
||||||
incremental: is_rotate_incremental)
|
incremental: is_rotate_incremental)
|
||||||
|
|
||||||
@@ -201,12 +200,14 @@ module Submissions
|
|||||||
with_submitter_timezone:,
|
with_submitter_timezone:,
|
||||||
with_file_links:,
|
with_file_links:,
|
||||||
with_timestamp_seconds:,
|
with_timestamp_seconds:,
|
||||||
with_signature_id_reason:)
|
with_signature_id_reason:,
|
||||||
|
file_links_expire_at:)
|
||||||
end
|
end
|
||||||
|
|
||||||
def fill_submitter_fields(submitter, account, pdfs_index, with_signature_id:, is_flatten:, with_headings: nil,
|
def fill_submitter_fields(submitter, account, pdfs_index, with_signature_id:, is_flatten:, with_headings: nil,
|
||||||
with_submitter_timezone: false, with_signature_id_reason: true,
|
with_submitter_timezone: false, with_signature_id_reason: true,
|
||||||
with_timestamp_seconds: false, with_file_links: nil)
|
with_timestamp_seconds: false, with_file_links: nil,
|
||||||
|
file_links_expire_at: Accounts.link_expires_at(account))
|
||||||
cell_layouters = Hash.new do |hash, valign|
|
cell_layouters = Hash.new do |hash, valign|
|
||||||
hash[valign] = HexaPDF::Layout::TextLayouter.new(text_valign: valign.to_sym, text_align: :center)
|
hash[valign] = HexaPDF::Layout::TextLayouter.new(text_valign: valign.to_sym, text_align: :center)
|
||||||
end
|
end
|
||||||
@@ -293,8 +294,11 @@ module Submissions
|
|||||||
canvas.font(FONT_NAME, size: font_size)
|
canvas.font(FONT_NAME, size: font_size)
|
||||||
|
|
||||||
field_type = field['type']
|
field_type = field['type']
|
||||||
field_type = 'file' if field_type == 'image' &&
|
|
||||||
!submitter.attachments.find { |a| a.uuid == value }.image?
|
if field_type == 'image' &&
|
||||||
|
submitter.attachments.find { |a| a.uuid == value }.then { |a| !a.image? || a.content_type == 'image/heic' }
|
||||||
|
field_type = 'file'
|
||||||
|
end
|
||||||
|
|
||||||
if field_type == 'signature' && field.dig('preferences', 'with_signature_id').in?([true, false])
|
if field_type == 'signature' && field.dig('preferences', 'with_signature_id').in?([true, false])
|
||||||
with_signature_id = field['preferences']['with_signature_id']
|
with_signature_id = field['preferences']['with_signature_id']
|
||||||
@@ -313,7 +317,10 @@ module Submissions
|
|||||||
|
|
||||||
image =
|
image =
|
||||||
begin
|
begin
|
||||||
load_vips_image(attachment, attachments_data_cache).autorot
|
attachments_data_cache[attachment.uuid] ||= attachment.download
|
||||||
|
|
||||||
|
ImageUtils.load_vips(attachments_data_cache[attachment.uuid],
|
||||||
|
content_type: attachment.content_type, autorot: true)
|
||||||
rescue Vips::Error
|
rescue Vips::Error
|
||||||
next unless attachment.content_type.starts_with?('image/')
|
next unless attachment.content_type.starts_with?('image/')
|
||||||
next if attachment.byte_size.zero?
|
next if attachment.byte_size.zero?
|
||||||
@@ -358,7 +365,8 @@ module Submissions
|
|||||||
image_x = area_x + ((half_width - image_width) / 2.0)
|
image_x = area_x + ((half_width - image_width) / 2.0)
|
||||||
image_y = height - area_y - image_height
|
image_y = height - area_y - image_height
|
||||||
|
|
||||||
io = StringIO.new(image.resize([scale * 4, 1].select(&:positive?).min).write_to_buffer('.png'))
|
io =
|
||||||
|
StringIO.new(image.resize([scale * 4, 1].select(&:positive?).min).write_to_buffer('.png', strip: true))
|
||||||
|
|
||||||
canvas.image(io, at: [image_x, image_y], width: image_width, height: image_height)
|
canvas.image(io, at: [image_x, image_y], width: image_width, height: image_height)
|
||||||
|
|
||||||
@@ -425,7 +433,8 @@ module Submissions
|
|||||||
|
|
||||||
scale = [area_w / image.width, image_height / image.height].min
|
scale = [area_w / image.width, image_height / image.height].min
|
||||||
|
|
||||||
io = StringIO.new(image.resize([scale * 4, 1].select(&:positive?).min).write_to_buffer('.png'))
|
io =
|
||||||
|
StringIO.new(image.resize([scale * 4, 1].select(&:positive?).min).write_to_buffer('.png', strip: true))
|
||||||
|
|
||||||
layouter.fit([text], area_w, base_font_size / 0.65)
|
layouter.fit([text], area_w, base_font_size / 0.65)
|
||||||
.draw(canvas, area_x + TEXT_LEFT_MARGIN,
|
.draw(canvas, area_x + TEXT_LEFT_MARGIN,
|
||||||
@@ -451,7 +460,10 @@ module Submissions
|
|||||||
|
|
||||||
image =
|
image =
|
||||||
begin
|
begin
|
||||||
load_vips_image(attachment, attachments_data_cache).autorot
|
attachments_data_cache[attachment.uuid] ||= attachment.download
|
||||||
|
|
||||||
|
ImageUtils.load_vips(attachments_data_cache[attachment.uuid],
|
||||||
|
content_type: attachment.content_type, autorot: true)
|
||||||
rescue Vips::Error
|
rescue Vips::Error
|
||||||
next unless attachment.content_type.starts_with?('image/')
|
next unless attachment.content_type.starts_with?('image/')
|
||||||
next if attachment.byte_size.zero?
|
next if attachment.byte_size.zero?
|
||||||
@@ -462,7 +474,14 @@ module Submissions
|
|||||||
scale = [(area['w'] * width) / image.width,
|
scale = [(area['w'] * width) / image.width,
|
||||||
(area['h'] * height) / image.height].min
|
(area['h'] * height) / image.height].min
|
||||||
|
|
||||||
io = StringIO.new(image.resize([scale * 4, 1].select(&:positive?).min).write_to_buffer('.png'))
|
resized_image = image.resize([scale * 4, 1].select(&:positive?).min)
|
||||||
|
|
||||||
|
io =
|
||||||
|
if field_type == 'image' && !resized_image.has_alpha?
|
||||||
|
StringIO.new(resized_image.colourspace(:srgb).write_to_buffer('.jpg', strip: true))
|
||||||
|
else
|
||||||
|
StringIO.new(resized_image.write_to_buffer('.png', strip: true))
|
||||||
|
end
|
||||||
|
|
||||||
canvas.image(
|
canvas.image(
|
||||||
io,
|
io,
|
||||||
@@ -504,7 +523,7 @@ module Submissions
|
|||||||
|
|
||||||
url =
|
url =
|
||||||
if with_file_links
|
if with_file_links
|
||||||
ActiveStorage::Blob.proxy_url(attachment.blob)
|
ActiveStorage::Blob.proxy_url(attachment.blob, expires_at: file_links_expire_at)
|
||||||
else
|
else
|
||||||
r.submissions_preview_url(submission.slug, **Docuseal.default_url_options)
|
r.submissions_preview_url(submission.slug, **Docuseal.default_url_options)
|
||||||
end
|
end
|
||||||
@@ -731,7 +750,7 @@ module Submissions
|
|||||||
pdf.trailer.info[:Creator] = info_creator
|
pdf.trailer.info[:Creator] = info_creator
|
||||||
|
|
||||||
if Docuseal.pdf_format == 'pdf/a-3b'
|
if Docuseal.pdf_format == 'pdf/a-3b'
|
||||||
pdf.task(:pdfa, level: '3b')
|
pdfa_listener = pdf.task(:pdfa, level: '3b')
|
||||||
pdf.config['font.map'] = PDFA_FONT_MAP
|
pdf.config['font.map'] = PDFA_FONT_MAP
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -747,12 +766,14 @@ module Submissions
|
|||||||
|
|
||||||
begin
|
begin
|
||||||
pdf.sign(io, write_options: { validate: false }, **sign_params)
|
pdf.sign(io, write_options: { validate: false }, **sign_params)
|
||||||
rescue HexaPDF::Error, NoMethodError => e
|
rescue HexaPDF::Error, NoMethodError, TypeError => e
|
||||||
Rollbar.error(e) if defined?(Rollbar)
|
Rollbar.error(e) if defined?(Rollbar)
|
||||||
|
|
||||||
|
pdf.instance_variable_get(:@listeners)[:complete_objects].delete(pdfa_listener) if pdfa_listener
|
||||||
|
|
||||||
begin
|
begin
|
||||||
pdf.sign(io, write_options: { validate: false, incremental: false }, **sign_params)
|
pdf.sign(io, write_options: { validate: false, incremental: false }, **sign_params)
|
||||||
rescue HexaPDF::Error
|
rescue HexaPDF::Error, TypeError
|
||||||
pdf.validate(auto_correct: true)
|
pdf.validate(auto_correct: true)
|
||||||
pdf.sign(io, write_options: { validate: false, incremental: false }, **sign_params)
|
pdf.sign(io, write_options: { validate: false, incremental: false }, **sign_params)
|
||||||
end
|
end
|
||||||
@@ -1014,20 +1035,6 @@ module Submissions
|
|||||||
[]
|
[]
|
||||||
end
|
end
|
||||||
|
|
||||||
def load_vips_image(attachment, cache = {})
|
|
||||||
cache[attachment.uuid] ||= attachment.download
|
|
||||||
|
|
||||||
data = cache[attachment.uuid]
|
|
||||||
|
|
||||||
if ICO_REGEXP.match?(attachment.content_type)
|
|
||||||
LoadIco.call(data)
|
|
||||||
elsif BMP_REGEXP.match?(attachment.content_type)
|
|
||||||
LoadBmp.call(data)
|
|
||||||
else
|
|
||||||
Vips::Image.new_from_buffer(data, '')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def r
|
def r
|
||||||
Rails.application.routes.url_helpers
|
Rails.application.routes.url_helpers
|
||||||
end
|
end
|
||||||
|
|||||||
+12
-18
@@ -122,27 +122,21 @@ module Submitters
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_attachment!(submitter, params)
|
def create_attachment!(submitter, file, metadata: {})
|
||||||
blob =
|
raise ParamsError, 'file param is missing' if file.blank?
|
||||||
if (file = params[:file])
|
|
||||||
extension = File.extname(file.original_filename).delete_prefix('.').downcase
|
|
||||||
|
|
||||||
if DANGEROUS_EXTENSIONS.include?(extension)
|
extension = File.extname(file.original_filename).delete_prefix('.').downcase
|
||||||
raise MaliciousFileExtension, "File type '.#{extension}' is not allowed."
|
|
||||||
end
|
|
||||||
|
|
||||||
ActiveStorage::Blob.create_and_upload!(io: file.open,
|
if DANGEROUS_EXTENSIONS.include?(extension)
|
||||||
filename: file.original_filename,
|
raise MaliciousFileExtension, "File type '.#{extension}' is not allowed."
|
||||||
content_type: file.content_type)
|
end
|
||||||
else
|
|
||||||
raise ParamsError, 'file param is missing'
|
|
||||||
end
|
|
||||||
|
|
||||||
ActiveStorage::Attachment.create!(
|
blob = ActiveStorage::Blob.create_and_upload!(io: file.tap(&:rewind).open,
|
||||||
blob:,
|
filename: file.original_filename,
|
||||||
name: 'attachments',
|
content_type: file.content_type,
|
||||||
record: submitter
|
metadata:)
|
||||||
)
|
|
||||||
|
ActiveStorage::Attachment.create!(blob:, name: 'attachments', record: submitter)
|
||||||
end
|
end
|
||||||
|
|
||||||
def normalize_preferences(account, user, params)
|
def normalize_preferences(account, user, params)
|
||||||
|
|||||||
@@ -6,12 +6,6 @@ module Submitters
|
|||||||
HEIGHT = 200
|
HEIGHT = 200
|
||||||
LRM = "\u200E"
|
LRM = "\u200E"
|
||||||
|
|
||||||
TRANSPARENT_PIXEL = "\x89PNG\r\n\u001A\n\u0000\u0000\u0000\rIHDR\u0000" \
|
|
||||||
"\u0000\u0000\u0001\u0000\u0000\u0000\u0001\b\u0004" \
|
|
||||||
"\u0000\u0000\u0000\xB5\u001C\f\u0002\u0000\u0000\u0000" \
|
|
||||||
"\vIDATx\xDAc\xFC_\u000F\u0000\u0002\x83\u0001\x804\xC3ڨ" \
|
|
||||||
"\u0000\u0000\u0000\u0000IEND\xAEB`\x82"
|
|
||||||
|
|
||||||
module_function
|
module_function
|
||||||
|
|
||||||
def call(submitter, with_logo: true)
|
def call(submitter, with_logo: true)
|
||||||
@@ -25,7 +19,7 @@ module Submitters
|
|||||||
def build_attachment(submitter, with_logo: true)
|
def build_attachment(submitter, with_logo: true)
|
||||||
image = generate_stamp_image(submitter, with_logo:)
|
image = generate_stamp_image(submitter, with_logo:)
|
||||||
|
|
||||||
image_data = image.write_to_buffer('.png')
|
image_data = image.write_to_buffer('.png', strip: true)
|
||||||
|
|
||||||
checksum = Digest::MD5.base64digest(image_data)
|
checksum = Digest::MD5.base64digest(image_data)
|
||||||
|
|
||||||
@@ -40,16 +34,17 @@ module Submitters
|
|||||||
def generate_stamp_image(submitter, with_logo: true)
|
def generate_stamp_image(submitter, with_logo: true)
|
||||||
logo =
|
logo =
|
||||||
if with_logo
|
if with_logo
|
||||||
Vips::Image.new_from_buffer(load_logo(submitter).read, '')
|
ImageUtils.load_vips(load_logo(submitter).read)
|
||||||
else
|
else
|
||||||
Vips::Image.new_from_buffer(TRANSPARENT_PIXEL, '').resize(WIDTH)
|
Vips::Image.black(WIDTH, WIDTH, bands: 4).copy(interpretation: :srgb)
|
||||||
end
|
end
|
||||||
|
|
||||||
logo = logo.resize([WIDTH / logo.width.to_f, HEIGHT / logo.height.to_f].min)
|
logo = logo.resize([WIDTH / logo.width.to_f, HEIGHT / logo.height.to_f].min)
|
||||||
|
logo = logo.copy(interpretation: :srgb) if logo.interpretation == :multiband
|
||||||
|
|
||||||
base_layer = Vips::Image.black(WIDTH, HEIGHT).new_from_image([255, 255, 255]).copy(interpretation: :srgb)
|
base_layer = Vips::Image.black(WIDTH, HEIGHT).new_from_image([255, 255, 255, 255]).copy(interpretation: :srgb)
|
||||||
|
|
||||||
opacity_layer = Vips::Image.new_from_buffer(TRANSPARENT_PIXEL, '').resize(WIDTH)
|
opacity_layer = Vips::Image.black(WIDTH, HEIGHT).new_from_image([255, 255, 255, 127]).copy(interpretation: :srgb)
|
||||||
|
|
||||||
text = build_text_image(submitter)
|
text = build_text_image(submitter)
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,9 @@ module Submitters
|
|||||||
|
|
||||||
text_mask = Vips::Image.black(text_image.width, text_image.height)
|
text_mask = Vips::Image.black(text_image.width, text_image.height)
|
||||||
|
|
||||||
text_mask.bandjoin(text_image).copy(interpretation: :b_w).write_to_buffer('.png')
|
image = text_mask.bandjoin(text_image).copy(interpretation: :b_w)
|
||||||
|
|
||||||
|
[image.write_to_buffer('.png'), image.width, image.height]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -212,8 +212,8 @@ module Submitters
|
|||||||
elsif type.in?(%w[signature initials]) && value.length < 60
|
elsif type.in?(%w[signature initials]) && value.length < 60
|
||||||
find_or_create_blob_from_text(account, value, type)
|
find_or_create_blob_from_text(account, value, type)
|
||||||
elsif (data = Base64.decode64(value.sub(BASE64_PREFIX_REGEXP, ''))) &&
|
elsif (data = Base64.decode64(value.sub(BASE64_PREFIX_REGEXP, ''))) &&
|
||||||
Marcel::MimeType.for(data).exclude?('octet-stream')
|
(mime_type = Marcel::MimeType.for(data)).exclude?('octet-stream')
|
||||||
find_or_create_blob_from_base64(account, data, type)
|
find_or_create_blob_from_base64(account, data, type, mime_type:)
|
||||||
elsif type == 'image' && (value.starts_with?('<html>') || value.starts_with?('<!DOCTYPE'))
|
elsif type == 'image' && (value.starts_with?('<html>') || value.starts_with?('<!DOCTYPE'))
|
||||||
raise InvalidDefaultValue, "Invalid #{type} value" unless purpose == :api
|
raise InvalidDefaultValue, "Invalid #{type} value" unless purpose == :api
|
||||||
|
|
||||||
@@ -236,19 +236,31 @@ module Submitters
|
|||||||
raise InvalidDefaultValue, "HTML content is not allowed: #{value.first(200)}..."
|
raise InvalidDefaultValue, "HTML content is not allowed: #{value.first(200)}..."
|
||||||
end
|
end
|
||||||
|
|
||||||
def find_or_create_blob_from_base64(account, data, type)
|
def find_or_create_blob_from_base64(account, data, type, mime_type: nil)
|
||||||
checksum = Digest::MD5.base64digest(data)
|
checksum = Digest::MD5.base64digest(data)
|
||||||
|
|
||||||
blob = find_blob_by_checksum(checksum, account)
|
blob = find_blob_by_checksum(checksum, account)
|
||||||
|
|
||||||
blob || ActiveStorage::Blob.create_and_upload!(
|
return blob if blob
|
||||||
io: StringIO.new(data),
|
|
||||||
filename: "#{type}.png"
|
mime_type ||= Marcel::MimeType.for(data)
|
||||||
)
|
|
||||||
|
detected_extensions = Marcel::TYPE_EXTS[mime_type].to_a.map(&:downcase)
|
||||||
|
|
||||||
|
if detected_extensions.any? { |e| Submitters::DANGEROUS_EXTENSIONS.include?(e) }
|
||||||
|
raise InvalidDefaultValue, "File type '.#{detected_extensions.first}' is not allowed."
|
||||||
|
end
|
||||||
|
|
||||||
|
extension = detected_extensions.first
|
||||||
|
extension = 'png' if extension.blank? && type.in?(%w[signature initials stamp image])
|
||||||
|
|
||||||
|
filename = extension.present? ? "#{type}.#{extension}" : type
|
||||||
|
|
||||||
|
ActiveStorage::Blob.create_and_upload!(io: StringIO.new(data), filename:)
|
||||||
end
|
end
|
||||||
|
|
||||||
def find_or_create_blob_from_text(account, text, type)
|
def find_or_create_blob_from_text(account, text, type)
|
||||||
data = Submitters::GenerateFontImage.call(text, font: type)
|
data, width, height = Submitters::GenerateFontImage.call(text, font: type)
|
||||||
|
|
||||||
checksum = Digest::MD5.base64digest(data)
|
checksum = Digest::MD5.base64digest(data)
|
||||||
|
|
||||||
@@ -256,11 +268,20 @@ module Submitters
|
|||||||
|
|
||||||
blob || ActiveStorage::Blob.create_and_upload!(
|
blob || ActiveStorage::Blob.create_and_upload!(
|
||||||
io: StringIO.new(data),
|
io: StringIO.new(data),
|
||||||
filename: "#{type}.png"
|
filename: "#{type}.png",
|
||||||
|
content_type: 'image/png',
|
||||||
|
metadata: { analyzed: true, identified: true, width:, height: }
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def find_or_create_blob_from_url(account, url)
|
def find_or_create_blob_from_url(account, url)
|
||||||
|
filename = Addressable::URI.parse(url).path.split('/').last.to_s
|
||||||
|
extension = File.extname(filename).delete_prefix('.').downcase
|
||||||
|
|
||||||
|
if Submitters::DANGEROUS_EXTENSIONS.include?(extension)
|
||||||
|
raise InvalidDefaultValue, "File type '.#{extension}' is not allowed."
|
||||||
|
end
|
||||||
|
|
||||||
cache_key = [account.id, url].join(':')
|
cache_key = [account.id, url].join(':')
|
||||||
checksum = CHECKSUM_CACHE_STORE.fetch(cache_key)
|
checksum = CHECKSUM_CACHE_STORE.fetch(cache_key)
|
||||||
|
|
||||||
@@ -276,10 +297,7 @@ module Submitters
|
|||||||
|
|
||||||
blob = find_blob_by_checksum(checksum, account)
|
blob = find_blob_by_checksum(checksum, account)
|
||||||
|
|
||||||
blob || ActiveStorage::Blob.create_and_upload!(
|
blob || ActiveStorage::Blob.create_and_upload!(io: StringIO.new(data), filename:)
|
||||||
io: StringIO.new(data),
|
|
||||||
filename: Addressable::URI.parse(url).path.split('/').last
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def find_blob_by_checksum(checksum, account)
|
def find_blob_by_checksum(checksum, account)
|
||||||
|
|||||||
@@ -3,6 +3,10 @@
|
|||||||
module Templates
|
module Templates
|
||||||
COLOR_REGEXP = /\A(#(?:[0-9a-f]{3}|[0-9a-f]{6})|[a-z]+)\z/i
|
COLOR_REGEXP = /\A(#(?:[0-9a-f]{3}|[0-9a-f]{6})|[a-z]+)\z/i
|
||||||
|
|
||||||
|
TEMPLATE_BUILDER_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
|
||||||
|
|
||||||
EXPIRATION_DURATIONS = {
|
EXPIRATION_DURATIONS = {
|
||||||
one_day: 1.day,
|
one_day: 1.day,
|
||||||
two_days: 2.days,
|
two_days: 2.days,
|
||||||
@@ -91,4 +95,16 @@ module Templates
|
|||||||
Time.current + EXPIRATION_DURATIONS[default_expire_at_duration]
|
Time.current + EXPIRATION_DURATIONS[default_expire_at_duration]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def serialize_for_builder(template)
|
||||||
|
data = template.as_json(only: TEMPLATE_BUILDER_FIELDS)
|
||||||
|
|
||||||
|
data['documents'] = template.schema_documents.preload(:blob, { preview_images_attachments: :blob }).as_json(
|
||||||
|
only: %i[id uuid],
|
||||||
|
methods: %i[metadata signed_key],
|
||||||
|
include: { preview_images: { only: %i[id], methods: %i[url metadata filename] } }
|
||||||
|
)
|
||||||
|
|
||||||
|
data
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -77,7 +77,7 @@ module Templates
|
|||||||
split_page: false, aspect_ratio: false, padding: nil, page_number: nil)
|
split_page: false, aspect_ratio: false, padding: nil, page_number: nil)
|
||||||
return [[], nil] if page_number && page_number != 0
|
return [[], nil] if page_number && page_number != 0
|
||||||
|
|
||||||
image = Vips::Image.new_from_buffer(io.read, '')
|
image = ImageUtils.load_vips(io.read)
|
||||||
|
|
||||||
fields = inference.call(image, confidence:, nms:, nmm:, split_page:,
|
fields = inference.call(image, confidence:, nms:, nmm:, split_page:,
|
||||||
temperature:, aspect_ratio:, padding:)
|
temperature:, aspect_ratio:, padding:)
|
||||||
@@ -193,7 +193,7 @@ module Templates
|
|||||||
|
|
||||||
data, width, height = page.render_to_bitmap(size_key => size)
|
data, width, height = page.render_to_bitmap(size_key => size)
|
||||||
|
|
||||||
Vips::Image.new_from_memory(data, width, height, 4, :uchar)
|
Vips::Image.new_from_memory_copy(data, width, height, 4, :uchar)
|
||||||
end
|
end
|
||||||
|
|
||||||
def sort_fields(fields, y_threshold: 0.01)
|
def sort_fields(fields, y_threshold: 0.01)
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ module Templates
|
|||||||
PREVIEW_FORMAT = '.jpg'
|
PREVIEW_FORMAT = '.jpg'
|
||||||
ATTACHMENT_NAME = 'preview_images'
|
ATTACHMENT_NAME = 'preview_images'
|
||||||
|
|
||||||
BMP_REGEXP = %r{\Aimage/(?:bmp|x-bmp|x-ms-bmp)\z}
|
|
||||||
PDF_CONTENT_TYPE = 'application/pdf'
|
PDF_CONTENT_TYPE = 'application/pdf'
|
||||||
CONCURRENCY = 2
|
CONCURRENCY = 2
|
||||||
Q = 95
|
Q = 95
|
||||||
@@ -59,19 +58,13 @@ module Templates
|
|||||||
def generate_preview_image(attachment, data)
|
def generate_preview_image(attachment, data)
|
||||||
ActiveStorage::Attachment.where(name: ATTACHMENT_NAME, record: attachment).destroy_all
|
ActiveStorage::Attachment.where(name: ATTACHMENT_NAME, record: attachment).destroy_all
|
||||||
|
|
||||||
image =
|
image = ImageUtils.load_vips(data, content_type: attachment.content_type, autorot: true)
|
||||||
if BMP_REGEXP.match?(attachment.content_type)
|
image = image.resize(MAX_WIDTH / image.width.to_f)
|
||||||
LoadBmp.call(data)
|
|
||||||
else
|
|
||||||
Vips::Image.new_from_buffer(data, '')
|
|
||||||
end
|
|
||||||
|
|
||||||
image = image.autorot.resize(MAX_WIDTH / image.width.to_f)
|
|
||||||
|
|
||||||
bitdepth = 2**image.stats.to_a[1..3].pluck(2).uniq.size
|
bitdepth = 2**image.stats.to_a[1..3].pluck(2).uniq.size
|
||||||
|
|
||||||
io = StringIO.new(image.write_to_buffer(FORMAT, compression: 6, filter: 0, bitdepth:,
|
io = StringIO.new(image.write_to_buffer(FORMAT, compression: 6, filter: 0, bitdepth:,
|
||||||
palette: true, Q: Q, dither: 0))
|
palette: true, Q: Q, dither: 0, strip: true))
|
||||||
|
|
||||||
ActiveStorage::Attachment.create!(
|
ActiveStorage::Attachment.create!(
|
||||||
blob: ActiveStorage::Blob.create_and_upload!(
|
blob: ActiveStorage::Blob.create_and_upload!(
|
||||||
@@ -110,7 +103,15 @@ module Templates
|
|||||||
|
|
||||||
promises =
|
promises =
|
||||||
range.map do |page_number|
|
range.map do |page_number|
|
||||||
Concurrent::Promise.execute(executor: pool) { build_and_upload_blob(doc, page_number) }
|
doc_page = doc.get_page(page_number)
|
||||||
|
|
||||||
|
bytes, width, height = doc_page.render_to_bitmap(width: MAX_WIDTH)
|
||||||
|
|
||||||
|
image = Vips::Image.new_from_memory_copy(bytes, width, height, 4, :uchar)
|
||||||
|
|
||||||
|
Concurrent::Promise.execute(executor: pool) { build_and_upload_blob(image, page_number) }
|
||||||
|
ensure
|
||||||
|
doc_page&.close
|
||||||
end
|
end
|
||||||
|
|
||||||
Concurrent::Promise.zip(*promises).value!.each do |blob|
|
Concurrent::Promise.zip(*promises).value!.each do |blob|
|
||||||
@@ -129,39 +130,27 @@ module Templates
|
|||||||
pool&.kill
|
pool&.kill
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_and_upload_blob(doc, page_number, format = FORMAT)
|
def build_and_upload_blob(image, page_number, format = FORMAT)
|
||||||
doc_page = doc.get_page(page_number)
|
image = image.copy(interpretation: :srgb)
|
||||||
|
|
||||||
data, width, height = doc_page.render_to_bitmap(width: MAX_WIDTH)
|
|
||||||
|
|
||||||
page = Vips::Image.new_from_memory(data, width, height, 4, :uchar)
|
|
||||||
|
|
||||||
page = page.copy(interpretation: :srgb)
|
|
||||||
|
|
||||||
data =
|
data =
|
||||||
if format == FORMAT
|
if format == FORMAT
|
||||||
bitdepth = 2**page.stats.to_a[1..3].pluck(2).uniq.size
|
bitdepth = 2**image.stats.to_a[1..3].pluck(2).uniq.size
|
||||||
|
|
||||||
page.write_to_buffer(format, compression: 6, filter: 0, bitdepth:,
|
image.write_to_buffer(format, compression: 6, filter: 0, bitdepth:,
|
||||||
palette: true, Q: Q, dither: 0)
|
palette: true, Q: Q, dither: 0)
|
||||||
else
|
else
|
||||||
page.write_to_buffer(format, interlace: true, Q: JPEG_Q)
|
image.write_to_buffer(format, interlace: true, Q: JPEG_Q)
|
||||||
end
|
end
|
||||||
|
|
||||||
blob = ActiveStorage::Blob.new(
|
blob = ActiveStorage::Blob.new(
|
||||||
filename: "#{page_number}#{format}",
|
filename: "#{page_number}#{format}",
|
||||||
metadata: { analyzed: true, identified: true, width: page.width, height: page.height }
|
metadata: { analyzed: true, identified: true, width: image.width, height: image.height }
|
||||||
)
|
)
|
||||||
|
|
||||||
blob.upload(StringIO.new(data))
|
blob.upload(StringIO.new(data))
|
||||||
|
|
||||||
blob
|
blob
|
||||||
rescue Vips::Error, Pdfium::PdfiumError => e
|
|
||||||
Rollbar.warning(e) if defined?(Rollbar)
|
|
||||||
|
|
||||||
nil
|
|
||||||
ensure
|
|
||||||
doc_page&.close
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def maybe_flatten_form(data, pdf)
|
def maybe_flatten_form(data, pdf)
|
||||||
@@ -206,7 +195,15 @@ module Templates
|
|||||||
def generate_pdf_preview_from_file(attachment, file_path, page_number)
|
def generate_pdf_preview_from_file(attachment, file_path, page_number)
|
||||||
doc = Pdfium::Document.open_file(file_path)
|
doc = Pdfium::Document.open_file(file_path)
|
||||||
|
|
||||||
blob = build_and_upload_blob(doc, page_number, PREVIEW_FORMAT)
|
doc_page = doc.get_page(page_number)
|
||||||
|
|
||||||
|
bytes, width, height = doc_page.render_to_bitmap(width: MAX_WIDTH)
|
||||||
|
|
||||||
|
doc_page.close
|
||||||
|
|
||||||
|
image = Vips::Image.new_from_memory_copy(bytes, width, height, 4, :uchar)
|
||||||
|
|
||||||
|
blob = build_and_upload_blob(image, page_number, PREVIEW_FORMAT)
|
||||||
|
|
||||||
ApplicationRecord.no_touching do
|
ApplicationRecord.no_touching do
|
||||||
ActiveStorage::Attachment.create!(
|
ActiveStorage::Attachment.create!(
|
||||||
|
|||||||
@@ -14,27 +14,25 @@ module Templates
|
|||||||
|
|
||||||
module_function
|
module_function
|
||||||
|
|
||||||
def call(template, schema_documents: template.schema_documents.preload(:blob), preview_image_attachments: nil,
|
def call(template, schema_documents: template.schema_documents.preload(:blob), dynamic_documents: nil,
|
||||||
expires_at: Accounts.link_expires_at(Account.new(id: template.account_id)))
|
preview_image_attachments: nil, expires_at: Accounts.link_expires_at(Account.new(id: template.account_id)))
|
||||||
json = template.as_json(SERIALIZE_PARAMS)
|
json = template.as_json(SERIALIZE_PARAMS)
|
||||||
|
|
||||||
preview_image_attachments ||=
|
dynamic_documents ||= preload_dynamic_documents(template)
|
||||||
ActiveStorage::Attachment.joins(:blob)
|
|
||||||
.where(blob: { filename: ['0.jpg', '0.png'] })
|
preview_image_attachments ||= preload_preview_image_attachments(schema_documents, dynamic_documents)
|
||||||
.where(record_id: schema_documents.map(&:id),
|
|
||||||
record_type: 'ActiveStorage::Attachment',
|
|
||||||
name: :preview_images)
|
|
||||||
.preload(:blob)
|
|
||||||
|
|
||||||
json['documents'] = template.schema.filter_map do |item|
|
json['documents'] = template.schema.filter_map do |item|
|
||||||
attachment = schema_documents.find { |e| e.uuid == item['attachment_uuid'] }
|
if item['dynamic']
|
||||||
|
dynamic_document = dynamic_documents.find { |e| e.uuid == item['attachment_uuid'] }
|
||||||
|
|
||||||
unless attachment
|
attachment = dynamic_document.current_version&.document_attachment
|
||||||
Rollbar.error("Documents missing: #{template.id}") if defined?(Rollbar)
|
|
||||||
|
|
||||||
next
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
attachment ||= schema_documents.find { |e| e.uuid == item['attachment_uuid'] }
|
||||||
|
|
||||||
|
next unless attachment
|
||||||
|
|
||||||
first_page_blob = preview_image_attachments.find { |e| e.record_id == attachment.id }&.blob
|
first_page_blob = preview_image_attachments.find { |e| e.record_id == attachment.id }&.blob
|
||||||
first_page_blob ||= attachment.preview_images.joins(:blob).find_by(blob: { filename: ['0.jpg', '0.png'] })&.blob
|
first_page_blob ||= attachment.preview_images.joins(:blob).find_by(blob: { filename: ['0.jpg', '0.png'] })&.blob
|
||||||
|
|
||||||
@@ -49,5 +47,26 @@ module Templates
|
|||||||
|
|
||||||
json
|
json
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def preload_dynamic_documents(template)
|
||||||
|
return DynamicDocument.none if template.schema.none? { |item| item['dynamic'] }
|
||||||
|
|
||||||
|
template.schema_dynamic_documents
|
||||||
|
.preload(current_version: { document_attachment: :blob })
|
||||||
|
.select(:id, :uuid, :template_id, :sha1, :created_at, :updated_at)
|
||||||
|
end
|
||||||
|
|
||||||
|
def preload_preview_image_attachments(schema_documents, dynamic_documents)
|
||||||
|
record_ids =
|
||||||
|
schema_documents.map(&:id) +
|
||||||
|
dynamic_documents.filter_map { |d| d.current_version&.document_attachment&.id }
|
||||||
|
|
||||||
|
ActiveStorage::Attachment.joins(:blob)
|
||||||
|
.where(blob: { filename: ['0.jpg', '0.png'] })
|
||||||
|
.where(record_id: record_ids,
|
||||||
|
record_type: 'ActiveStorage::Attachment',
|
||||||
|
name: :preview_images)
|
||||||
|
.preload(:blob)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -133,7 +133,7 @@ RSpec.describe 'Signing Form' do
|
|||||||
|
|
||||||
# Cell step
|
# Cell step
|
||||||
fill_in 'Cell code', with: '123'
|
fill_in 'Cell code', with: '123'
|
||||||
click_on 'Complete'
|
find('#submit_form_button').click
|
||||||
|
|
||||||
expect(page).to have_button('Download')
|
expect(page).to have_button('Download')
|
||||||
expect(page).to have_content('Document has been signed!')
|
expect(page).to have_content('Document has been signed!')
|
||||||
@@ -220,7 +220,7 @@ RSpec.describe 'Signing Form' do
|
|||||||
|
|
||||||
# Cell step
|
# Cell step
|
||||||
fill_in 'Cell code', with: '123'
|
fill_in 'Cell code', with: '123'
|
||||||
click_on 'Complete'
|
find('#submit_form_button').click
|
||||||
|
|
||||||
expect(page).to have_button('Download')
|
expect(page).to have_button('Download')
|
||||||
expect(page).to have_content('Document has been signed!')
|
expect(page).to have_content('Document has been signed!')
|
||||||
@@ -308,7 +308,7 @@ RSpec.describe 'Signing Form' do
|
|||||||
click_button 'next'
|
click_button 'next'
|
||||||
|
|
||||||
fill_in 'Cell code', with: '123'
|
fill_in 'Cell code', with: '123'
|
||||||
click_on 'Complete'
|
find('#submit_form_button').click
|
||||||
|
|
||||||
expect(page).to have_button('Download')
|
expect(page).to have_button('Download')
|
||||||
expect(page).to have_content('Document has been signed!')
|
expect(page).to have_content('Document has been signed!')
|
||||||
@@ -398,7 +398,7 @@ RSpec.describe 'Signing Form' do
|
|||||||
|
|
||||||
# Cell step
|
# Cell step
|
||||||
fill_in 'Cell code', with: '123'
|
fill_in 'Cell code', with: '123'
|
||||||
click_on 'Complete'
|
find('#submit_form_button').click
|
||||||
|
|
||||||
expect(page).to have_button('Download')
|
expect(page).to have_button('Download')
|
||||||
expect(page).to have_content('Document has been signed!')
|
expect(page).to have_content('Document has been signed!')
|
||||||
@@ -443,7 +443,7 @@ RSpec.describe 'Signing Form' do
|
|||||||
expect(input[:placeholder]).to eq 'Type here...'
|
expect(input[:placeholder]).to eq 'Type here...'
|
||||||
|
|
||||||
fill_in 'First Name', with: 'Mary'
|
fill_in 'First Name', with: 'Mary'
|
||||||
click_button 'Complete'
|
find('#submit_form_button').click
|
||||||
|
|
||||||
expect(page).to have_content('Form has been completed!')
|
expect(page).to have_content('Form has been completed!')
|
||||||
|
|
||||||
@@ -468,7 +468,7 @@ RSpec.describe 'Signing Form' do
|
|||||||
expect(page).not_to have_selector(:css, 'div[data-tip="Toggle Multiline Text"]')
|
expect(page).not_to have_selector(:css, 'div[data-tip="Toggle Multiline Text"]')
|
||||||
|
|
||||||
fill_in 'First Name', with: 'Very long text'
|
fill_in 'First Name', with: 'Very long text'
|
||||||
click_button 'Complete'
|
find('#submit_form_button').click
|
||||||
|
|
||||||
expect(page).to have_content('Form has been completed!')
|
expect(page).to have_content('Form has been completed!')
|
||||||
|
|
||||||
@@ -494,7 +494,7 @@ RSpec.describe 'Signing Form' do
|
|||||||
expect(input[:required]).to be_truthy
|
expect(input[:required]).to be_truthy
|
||||||
|
|
||||||
fill_in 'Birthday', with: I18n.l(25.years.ago, format: '%Y-%m-%d')
|
fill_in 'Birthday', with: I18n.l(25.years.ago, format: '%Y-%m-%d')
|
||||||
click_button 'Complete'
|
find('#submit_form_button').click
|
||||||
|
|
||||||
expect(page).to have_content('Form has been completed!')
|
expect(page).to have_content('Form has been completed!')
|
||||||
|
|
||||||
@@ -517,7 +517,7 @@ RSpec.describe 'Signing Form' do
|
|||||||
|
|
||||||
expect(input[:value]).to eq Time.zone.now.strftime('%Y-%m-%d')
|
expect(input[:value]).to eq Time.zone.now.strftime('%Y-%m-%d')
|
||||||
|
|
||||||
click_button 'Complete'
|
find('#submit_form_button').click
|
||||||
|
|
||||||
expect(page).to have_content('Form has been completed!')
|
expect(page).to have_content('Form has been completed!')
|
||||||
|
|
||||||
@@ -539,7 +539,7 @@ RSpec.describe 'Signing Form' do
|
|||||||
visit submit_form_path(slug: submitter.slug)
|
visit submit_form_path(slug: submitter.slug)
|
||||||
|
|
||||||
check 'Do you agree?'
|
check 'Do you agree?'
|
||||||
click_button 'Complete'
|
find('#submit_form_button').click
|
||||||
|
|
||||||
expect(page).to have_content('Form has been completed!')
|
expect(page).to have_content('Form has been completed!')
|
||||||
|
|
||||||
@@ -563,7 +563,7 @@ RSpec.describe 'Signing Form' do
|
|||||||
%w[Girl Boy].map { |v| find_field(v) }.each { |input| expect(input[:required]).to be_truthy }
|
%w[Girl Boy].map { |v| find_field(v) }.each { |input| expect(input[:required]).to be_truthy }
|
||||||
|
|
||||||
choose 'Boy'
|
choose 'Boy'
|
||||||
click_button 'Complete'
|
find('#submit_form_button').click
|
||||||
|
|
||||||
expect(page).to have_content('Form has been completed!')
|
expect(page).to have_content('Form has been completed!')
|
||||||
|
|
||||||
@@ -670,7 +670,7 @@ RSpec.describe 'Signing Form' do
|
|||||||
expect(input[:placeholder]).to eq 'Type here...'
|
expect(input[:placeholder]).to eq 'Type here...'
|
||||||
|
|
||||||
fill_in 'House number', with: '4'
|
fill_in 'House number', with: '4'
|
||||||
click_button 'Complete'
|
find('#submit_form_button').click
|
||||||
|
|
||||||
expect(page).to have_content('Form has been completed!')
|
expect(page).to have_content('Form has been completed!')
|
||||||
|
|
||||||
@@ -692,7 +692,7 @@ RSpec.describe 'Signing Form' do
|
|||||||
visit submit_form_path(slug: submitter.slug)
|
visit submit_form_path(slug: submitter.slug)
|
||||||
|
|
||||||
%w[Red Green].each { |color| check color }
|
%w[Red Green].each { |color| check color }
|
||||||
click_button 'Complete'
|
find('#submit_form_button').click
|
||||||
|
|
||||||
expect(page).to have_content('Form has been completed!')
|
expect(page).to have_content('Form has been completed!')
|
||||||
|
|
||||||
@@ -715,7 +715,7 @@ RSpec.describe 'Signing Form' do
|
|||||||
|
|
||||||
select 'Female', from: 'Gender'
|
select 'Female', from: 'Gender'
|
||||||
|
|
||||||
click_button 'Complete'
|
find('#submit_form_button').click
|
||||||
|
|
||||||
expect(page).to have_content('Form has been completed!')
|
expect(page).to have_content('Form has been completed!')
|
||||||
|
|
||||||
@@ -738,7 +738,7 @@ RSpec.describe 'Signing Form' do
|
|||||||
|
|
||||||
find('#expand_form_button').click
|
find('#expand_form_button').click
|
||||||
fill_in 'initials_text_input', with: 'John Doe'
|
fill_in 'initials_text_input', with: 'John Doe'
|
||||||
click_button 'Complete'
|
find('#submit_form_button').click
|
||||||
|
|
||||||
expect(page).to have_content('Document has been signed!')
|
expect(page).to have_content('Document has been signed!')
|
||||||
|
|
||||||
@@ -754,7 +754,7 @@ RSpec.describe 'Signing Form' do
|
|||||||
find('#expand_form_button').click
|
find('#expand_form_button').click
|
||||||
click_button 'Draw'
|
click_button 'Draw'
|
||||||
draw_canvas
|
draw_canvas
|
||||||
click_button 'Complete'
|
find('#submit_form_button').click
|
||||||
|
|
||||||
expect(page).to have_content('Document has been signed!')
|
expect(page).to have_content('Document has been signed!')
|
||||||
|
|
||||||
@@ -773,7 +773,7 @@ RSpec.describe 'Signing Form' do
|
|||||||
|
|
||||||
sleep 0.1
|
sleep 0.1
|
||||||
|
|
||||||
click_button 'Complete'
|
find('#submit_form_button').click
|
||||||
|
|
||||||
expect(page).to have_content('Document has been signed!')
|
expect(page).to have_content('Document has been signed!')
|
||||||
|
|
||||||
@@ -797,7 +797,7 @@ RSpec.describe 'Signing Form' do
|
|||||||
find('#expand_form_button').click
|
find('#expand_form_button').click
|
||||||
find('#dropzone').click
|
find('#dropzone').click
|
||||||
find('input[type="file"]', visible: false).attach_file(Rails.root.join('spec/fixtures/sample-image.png'))
|
find('input[type="file"]', visible: false).attach_file(Rails.root.join('spec/fixtures/sample-image.png'))
|
||||||
click_button 'Complete'
|
find('#submit_form_button').click
|
||||||
|
|
||||||
expect(page).to have_content('Form has been completed!')
|
expect(page).to have_content('Form has been completed!')
|
||||||
|
|
||||||
@@ -821,7 +821,7 @@ RSpec.describe 'Signing Form' do
|
|||||||
find('#expand_form_button').click
|
find('#expand_form_button').click
|
||||||
find('#dropzone').click
|
find('#dropzone').click
|
||||||
find('input[type="file"]', visible: false).attach_file(Rails.root.join('spec/fixtures/sample-document.pdf'))
|
find('input[type="file"]', visible: false).attach_file(Rails.root.join('spec/fixtures/sample-document.pdf'))
|
||||||
click_button 'Complete'
|
find('#submit_form_button').click
|
||||||
|
|
||||||
expect(page).to have_content('Form has been completed!')
|
expect(page).to have_content('Form has been completed!')
|
||||||
|
|
||||||
@@ -848,7 +848,7 @@ RSpec.describe 'Signing Form' do
|
|||||||
expect(input[:placeholder]).to eq 'Type here...'
|
expect(input[:placeholder]).to eq 'Type here...'
|
||||||
|
|
||||||
fill_in 'Cell code', with: '456'
|
fill_in 'Cell code', with: '456'
|
||||||
click_button 'Complete'
|
find('#submit_form_button').click
|
||||||
|
|
||||||
expect(page).to have_content('Form has been completed!')
|
expect(page).to have_content('Form has been completed!')
|
||||||
|
|
||||||
@@ -967,7 +967,7 @@ RSpec.describe 'Signing Form' do
|
|||||||
click_button 'next'
|
click_button 'next'
|
||||||
|
|
||||||
fill_in 'Comment', with: 'This is a comment'
|
fill_in 'Comment', with: 'This is a comment'
|
||||||
click_button 'Complete'
|
find('#submit_form_button').click
|
||||||
|
|
||||||
expect(page).to have_content('Form has been completed!')
|
expect(page).to have_content('Form has been completed!')
|
||||||
|
|
||||||
@@ -992,7 +992,7 @@ RSpec.describe 'Signing Form' do
|
|||||||
click_button 'next'
|
click_button 'next'
|
||||||
|
|
||||||
fill_in 'Comment', with: 'This is a comment'
|
fill_in 'Comment', with: 'This is a comment'
|
||||||
click_button 'Complete'
|
find('#submit_form_button').click
|
||||||
|
|
||||||
expect(page).to have_content('Form has been completed!')
|
expect(page).to have_content('Form has been completed!')
|
||||||
|
|
||||||
@@ -1015,7 +1015,7 @@ RSpec.describe 'Signing Form' do
|
|||||||
click_button 'next'
|
click_button 'next'
|
||||||
|
|
||||||
fill_in 'Phone (optional)', with: ''
|
fill_in 'Phone (optional)', with: ''
|
||||||
click_button 'Complete'
|
find('#submit_form_button').click
|
||||||
|
|
||||||
expect(page).to have_content('Form has been completed!')
|
expect(page).to have_content('Form has been completed!')
|
||||||
|
|
||||||
@@ -1038,7 +1038,7 @@ RSpec.describe 'Signing Form' do
|
|||||||
click_button 'next'
|
click_button 'next'
|
||||||
|
|
||||||
fill_in 'Phone (optional)', with: '+1 (773) 229-8825'
|
fill_in 'Phone (optional)', with: '+1 (773) 229-8825'
|
||||||
click_button 'Complete'
|
find('#submit_form_button').click
|
||||||
|
|
||||||
expect(page).to have_content('Form has been completed!')
|
expect(page).to have_content('Form has been completed!')
|
||||||
|
|
||||||
@@ -1068,7 +1068,7 @@ RSpec.describe 'Signing Form' do
|
|||||||
visit submit_form_path(slug: first_submitter.slug)
|
visit submit_form_path(slug: first_submitter.slug)
|
||||||
|
|
||||||
fill_in 'First Name', with: 'Jahn'
|
fill_in 'First Name', with: 'Jahn'
|
||||||
click_button 'Complete'
|
find('#submit_form_button').click
|
||||||
|
|
||||||
visit submit_form_path(slug: second_submitter.slug)
|
visit submit_form_path(slug: second_submitter.slug)
|
||||||
|
|
||||||
@@ -1229,7 +1229,7 @@ RSpec.describe 'Signing Form' do
|
|||||||
click_button 'Submit'
|
click_button 'Submit'
|
||||||
|
|
||||||
fill_in 'First Name', with: 'Mary'
|
fill_in 'First Name', with: 'Mary'
|
||||||
click_button 'Complete'
|
find('#submit_form_button').click
|
||||||
|
|
||||||
expect(page).to have_content('Form has been completed!')
|
expect(page).to have_content('Form has been completed!')
|
||||||
|
|
||||||
@@ -1264,7 +1264,7 @@ RSpec.describe 'Signing Form' do
|
|||||||
click_button 'Submit'
|
click_button 'Submit'
|
||||||
|
|
||||||
fill_in 'First Name', with: 'Mary'
|
fill_in 'First Name', with: 'Mary'
|
||||||
click_button 'Complete'
|
find('#submit_form_button').click
|
||||||
|
|
||||||
expect(page).to have_content('Form has been completed!')
|
expect(page).to have_content('Form has been completed!')
|
||||||
|
|
||||||
@@ -1274,4 +1274,47 @@ RSpec.describe 'Signing Form' do
|
|||||||
expect(field_value(submitter, 'First Name')).to eq 'Mary'
|
expect(field_value(submitter, 'First Name')).to eq 'Mary'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when header complete button' do
|
||||||
|
let(:template) { create(:template, account:, author:, only_field_types: %w[text date]) }
|
||||||
|
let(:submission) { create(:submission, :with_submitters, template:) }
|
||||||
|
let(:submitter) { submission.submitters.first }
|
||||||
|
|
||||||
|
before do
|
||||||
|
template.fields.second['required'] = false
|
||||||
|
template.save!
|
||||||
|
submission.update(template_fields: template.fields)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'shows header complete button and hides download after filling required fields' do
|
||||||
|
visit submit_form_path(slug: submitter.slug)
|
||||||
|
|
||||||
|
expect(page).to have_button('Decline')
|
||||||
|
expect(page).to have_css('download-button[aria-label="Download"]', visible: true)
|
||||||
|
expect(page).not_to have_css('#complete_button_container button')
|
||||||
|
|
||||||
|
fill_in 'First Name', with: 'John Doe'
|
||||||
|
click_button 'next'
|
||||||
|
|
||||||
|
expect(page).to have_css('#complete_button_container button')
|
||||||
|
expect(page).to have_button('Decline')
|
||||||
|
expect(page).to have_css('download-button[aria-label="Download"]', visible: :hidden)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'completes the form via header complete button skipping optional fields' do
|
||||||
|
visit submit_form_path(slug: submitter.slug)
|
||||||
|
|
||||||
|
fill_in 'First Name', with: 'John Doe'
|
||||||
|
click_button 'next'
|
||||||
|
|
||||||
|
find('#complete_button_container button').click
|
||||||
|
|
||||||
|
expect(page).to have_content('Form has been completed!')
|
||||||
|
|
||||||
|
submitter.reload
|
||||||
|
|
||||||
|
expect(submitter.completed_at).to be_present
|
||||||
|
expect(field_value(submitter, 'First Name')).to eq 'John Doe'
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -20,7 +20,7 @@ RSpec.describe 'Template Builder' do
|
|||||||
|
|
||||||
expect do
|
expect do
|
||||||
doc.find('.replace-document-button').click
|
doc.find('.replace-document-button').click
|
||||||
doc.find('.replace-document-button input[type="file"]', visible: false)
|
doc.find('.replace-document-control input[type="file"]', visible: false)
|
||||||
.attach_file(Rails.root.join('spec/fixtures/sample-image.png'))
|
.attach_file(Rails.root.join('spec/fixtures/sample-image.png'))
|
||||||
|
|
||||||
page.driver.wait_for_network_idle
|
page.driver.wait_for_network_idle
|
||||||
|
|||||||
@@ -42,9 +42,7 @@ RSpec.describe 'Template' do
|
|||||||
|
|
||||||
it 'archives a template' do
|
it 'archives a template' do
|
||||||
expect do
|
expect do
|
||||||
accept_confirm('Are you sure?') do
|
click_button 'Archive'
|
||||||
click_button 'Archive'
|
|
||||||
end
|
|
||||||
end.to change { Template.active.count }.by(-1)
|
end.to change { Template.active.count }.by(-1)
|
||||||
|
|
||||||
expect(page).to have_content('Template has been archived')
|
expect(page).to have_content('Template has been archived')
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
RSpec.describe 'Templates Upload' do
|
||||||
|
let!(:account) { create(:account) }
|
||||||
|
let!(:user) { create(:user, account:) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
sign_in(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when url param is present' do
|
||||||
|
let(:file_url) { 'https://example.com/document.pdf' }
|
||||||
|
|
||||||
|
before do
|
||||||
|
stub_request(:get, file_url).to_return(
|
||||||
|
body: Rails.root.join('spec/fixtures/sample-document.pdf').read,
|
||||||
|
headers: { 'Content-Type' => 'application/pdf' }
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'shows a confirm page and creates template on submit' do
|
||||||
|
visit "/new?url=#{CGI.escape(file_url)}"
|
||||||
|
|
||||||
|
expect(page).to have_text('Open file from')
|
||||||
|
expect(page).to have_link('example.com/document.pdf')
|
||||||
|
|
||||||
|
click_button 'Open'
|
||||||
|
|
||||||
|
expect(Template.last.name).to eq('document')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when url param is missing' do
|
||||||
|
it 'redirects to root' do
|
||||||
|
visit '/new'
|
||||||
|
|
||||||
|
expect(page).to have_current_path(root_path)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Reference in new issue
Block a user