mirror of
https://github.com/docusealco/docuseal.git
synced 2026-08-07 15:25:16 +00:00
Compare commits
60 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
@@ -12,7 +12,7 @@ jobs:
|
||||
- name: Install Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: 4.0.1
|
||||
ruby-version: 4.0.5
|
||||
- name: Cache gems
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
- name: Install Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: 4.0.1
|
||||
ruby-version: 4.0.5
|
||||
- name: Cache gems
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
@@ -89,7 +89,7 @@ jobs:
|
||||
- name: Install Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: 4.0.1
|
||||
ruby-version: 4.0.5
|
||||
- name: Cache gems
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
@@ -132,7 +132,7 @@ jobs:
|
||||
- name: Install Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: 4.0.1
|
||||
ruby-version: 4.0.5
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
@@ -163,7 +163,7 @@ jobs:
|
||||
yarn install
|
||||
sudo apt-get update
|
||||
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
|
||||
rm -f pdfium-linux.tgz
|
||||
- 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
|
||||
|
||||
@@ -13,7 +13,7 @@ RUN apk --no-cache add wget && \
|
||||
mkdir -p /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 NODE_ENV=production
|
||||
@@ -40,7 +40,7 @@ COPY ./app/views ./app/views
|
||||
|
||||
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 BUNDLE_WITHOUT="development:test"
|
||||
@@ -48,7 +48,8 @@ ENV OPENSSL_CONF=/etc/openssl_legacy.cnf
|
||||
|
||||
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
|
||||
|
||||
@@ -94,6 +95,7 @@ WORKDIR /data/docuseal
|
||||
ENV HOME=/home/docuseal
|
||||
ENV WORKDIR=/data/docuseal
|
||||
ENV VIPS_MAX_COORD=17000
|
||||
ENV VIPS_BLOCK_UNTRUSTED=1
|
||||
|
||||
EXPOSE 3000
|
||||
CMD ["/app/bin/bundle", "exec", "puma", "-C", "/app/config/puma.rb", "--dir", "/app"]
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
source 'https://rubygems.org'
|
||||
|
||||
ruby '4.0.1'
|
||||
ruby '4.0.5'
|
||||
|
||||
gem 'addressable'
|
||||
gem 'arabic-letter-connector', require: false
|
||||
@@ -21,7 +21,6 @@ gem 'faraday'
|
||||
gem 'faraday-follow_redirects'
|
||||
gem 'google-cloud-storage', require: false
|
||||
gem 'hexapdf'
|
||||
gem 'image_processing'
|
||||
gem 'jwt', require: false
|
||||
gem 'lograge'
|
||||
gem 'numo-narray-alt', require: false
|
||||
|
||||
+12
-18
@@ -116,7 +116,7 @@ GEM
|
||||
erubi (~> 1.4)
|
||||
parser (>= 2.4)
|
||||
smart_properties
|
||||
bigdecimal (4.1.0)
|
||||
bigdecimal (4.1.2)
|
||||
bindex (0.8.1)
|
||||
bootsnap (1.23.0)
|
||||
msgpack (~> 1.2)
|
||||
@@ -161,7 +161,7 @@ GEM
|
||||
irb (~> 1.10)
|
||||
reline (>= 0.3.8)
|
||||
declarative (0.0.20)
|
||||
devise (5.0.3)
|
||||
devise (5.0.4)
|
||||
bcrypt (~> 3.0)
|
||||
orm_adapter (~> 0.1)
|
||||
railties (>= 7.0)
|
||||
@@ -195,7 +195,7 @@ GEM
|
||||
railties (>= 6.1.0)
|
||||
faker (3.6.1)
|
||||
i18n (>= 1.8.11, < 2)
|
||||
faraday (2.14.1)
|
||||
faraday (2.14.2)
|
||||
faraday-net_http (>= 2.0, < 3.5)
|
||||
json
|
||||
logger
|
||||
@@ -264,18 +264,15 @@ GEM
|
||||
strscan (>= 3.1.2)
|
||||
i18n (1.14.8)
|
||||
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)
|
||||
irb (1.17.0)
|
||||
irb (1.18.0)
|
||||
pp (>= 0.6.0)
|
||||
prism (>= 1.3.0)
|
||||
rdoc (>= 4.0.0)
|
||||
reline (>= 0.4.2)
|
||||
jmespath (1.6.2)
|
||||
json (2.19.3)
|
||||
jwt (3.1.2)
|
||||
json (2.19.7)
|
||||
jwt (3.2.0)
|
||||
base64
|
||||
language_server-protocol (3.17.0.5)
|
||||
launchy (3.1.1)
|
||||
@@ -308,10 +305,8 @@ GEM
|
||||
marcel (1.1.0)
|
||||
matrix (0.4.3)
|
||||
method_source (1.1.0)
|
||||
mini_magick (5.3.1)
|
||||
logger
|
||||
mini_mime (1.1.5)
|
||||
minitest (6.0.3)
|
||||
minitest (6.0.6)
|
||||
drb (~> 2.0)
|
||||
prism (~> 1.5)
|
||||
msgpack (1.8.0)
|
||||
@@ -429,12 +424,12 @@ GEM
|
||||
tsort (>= 0.2)
|
||||
zeitwerk (~> 2.6)
|
||||
rainbow (3.1.1)
|
||||
rake (13.3.1)
|
||||
rake (13.4.2)
|
||||
rdoc (7.2.0)
|
||||
erb
|
||||
psych (>= 4.0.0)
|
||||
tsort
|
||||
redis-client (0.28.0)
|
||||
redis-client (0.29.0)
|
||||
connection_pool
|
||||
regexp_parser (2.11.3)
|
||||
reline (0.6.3)
|
||||
@@ -516,12 +511,12 @@ GEM
|
||||
rack-proxy (>= 0.6.1)
|
||||
railties (>= 5.2)
|
||||
semantic_range (>= 2.3.0)
|
||||
sidekiq (8.1.2)
|
||||
sidekiq (8.1.6)
|
||||
connection_pool (>= 3.0.0)
|
||||
json (>= 2.16.0)
|
||||
logger (>= 1.7.0)
|
||||
rack (>= 3.2.0)
|
||||
redis-client (>= 0.26.0)
|
||||
redis-client (>= 0.29.0)
|
||||
signet (0.21.0)
|
||||
addressable (~> 2.8)
|
||||
faraday (>= 0.17.5, < 3.a)
|
||||
@@ -624,7 +619,6 @@ DEPENDENCIES
|
||||
foreman
|
||||
google-cloud-storage
|
||||
hexapdf
|
||||
image_processing
|
||||
jwt
|
||||
letter_opener_web
|
||||
lograge
|
||||
@@ -662,7 +656,7 @@ DEPENDENCIES
|
||||
webmock
|
||||
|
||||
RUBY VERSION
|
||||
ruby 4.0.1
|
||||
ruby 4.0.5
|
||||
|
||||
BUNDLED WITH
|
||||
4.0.3
|
||||
|
||||
@@ -9,7 +9,9 @@ module Api
|
||||
|
||||
before_action :set_cors_headers
|
||||
before_action :set_noindex_headers
|
||||
before_action :set_security_headers
|
||||
|
||||
# rubocop:disable Metrics
|
||||
def show
|
||||
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)
|
||||
|
||||
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
|
||||
|
||||
@record = attachment.record
|
||||
@@ -45,6 +53,7 @@ module Api
|
||||
end
|
||||
end
|
||||
end
|
||||
# rubocop:enable Metrics
|
||||
|
||||
private
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ module Api
|
||||
|
||||
before_action :set_cors_headers
|
||||
before_action :set_noindex_headers
|
||||
before_action :set_security_headers
|
||||
|
||||
# rubocop:disable Metrics
|
||||
def show
|
||||
@@ -18,6 +19,12 @@ module Api
|
||||
|
||||
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|
|
||||
(current_user && a.record.account.id == current_user.account_id) ||
|
||||
a.record.account.account_configs.any? { |e| e.key == 'legacy_blob_proxy' } ||
|
||||
|
||||
@@ -102,6 +102,10 @@ module Api
|
||||
headers['X-Robots-Tag'] = 'noindex'
|
||||
end
|
||||
|
||||
def set_security_headers
|
||||
response.headers['X-Content-Type-Options'] = 'nosniff'
|
||||
end
|
||||
|
||||
def set_cors_headers
|
||||
headers['Access-Control-Allow-Origin'] = '*'
|
||||
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])
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
file = params[:file]
|
||||
|
||||
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)
|
||||
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" },
|
||||
status: :unprocessable_content
|
||||
end
|
||||
|
||||
metadata = { analyzed: true, identified: true, width: image.width, height: image.height }
|
||||
end
|
||||
|
||||
attachment = Submitters.create_attachment!(@submitter, params)
|
||||
attachment = Submitters.create_attachment!(@submitter, file, metadata:)
|
||||
|
||||
if params[:remember_signature] == 'true' && @submitter.email.present?
|
||||
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))
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
schema_documents, dynamic_documents, preview_image_attachments = preload_relations(templates)
|
||||
|
||||
expires_at = Accounts.link_expires_at(current_account)
|
||||
|
||||
@@ -30,6 +17,7 @@ module Api
|
||||
data: templates.map do |t|
|
||||
Templates::SerializeForApi.call(t,
|
||||
schema_documents: schema_documents.select { |e| e.record_id == t.id },
|
||||
dynamic_documents:,
|
||||
preview_image_attachments:,
|
||||
expires_at:)
|
||||
end,
|
||||
@@ -88,6 +76,41 @@ module Api
|
||||
|
||||
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)
|
||||
templates = Templates.search(current_user, templates, params[:q])
|
||||
templates = params[:archived].in?(['true', true]) ? templates.archived : templates.active
|
||||
|
||||
@@ -101,11 +101,18 @@ class StartFormController < ApplicationController
|
||||
def load_resubmit_submitter
|
||||
@resubmit_submitter =
|
||||
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
|
||||
|
||||
def can_resubmit?(submitter)
|
||||
submitter.account.account_configs.find_or_initialize_by(key: AccountConfig::ALLOW_TO_RESUBMIT).value != false
|
||||
end
|
||||
|
||||
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 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
|
||||
|
||||
def create
|
||||
authorize!(:update, @submission)
|
||||
|
||||
@submission.update!(archived_at: nil)
|
||||
|
||||
redirect_to submission_path(@submission), notice: I18n.t('submission_has_been_unarchived')
|
||||
|
||||
@@ -4,6 +4,8 @@ class SubmittersSendEmailController < ApplicationController
|
||||
load_and_authorize_resource :submitter
|
||||
|
||||
def create
|
||||
authorize!(:update, @submitter)
|
||||
|
||||
if Docuseal.multitenant? && SubmissionEvent.exists?(submitter: @submitter,
|
||||
event_type: 'send_email',
|
||||
created_at: 10.hours.ago..Time.current)
|
||||
|
||||
@@ -10,6 +10,8 @@ class TemplateDocumentsController < ApplicationController
|
||||
end
|
||||
|
||||
def create
|
||||
authorize!(:update, @template)
|
||||
|
||||
if params[:blobs].blank? && params[:files].blank?
|
||||
return render json: { error: I18n.t('file_is_missing') }, status: :unprocessable_content
|
||||
end
|
||||
|
||||
@@ -13,6 +13,9 @@ class TemplatesCloneAndReplaceController < ApplicationController
|
||||
|
||||
cloned_template = Templates::Clone.call(@template, author: current_user)
|
||||
cloned_template.name = File.basename(params[:files].first.original_filename, '.*')
|
||||
|
||||
authorize!(:create, cloned_template)
|
||||
|
||||
cloned_template.save!
|
||||
|
||||
documents = Templates::ReplaceAttachments.call(cloned_template, params, extract_fields: true)
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
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
|
||||
|
||||
def show
|
||||
@@ -31,19 +27,7 @@ class TemplatesController < ApplicationController
|
||||
def new; end
|
||||
|
||||
def edit
|
||||
ActiveRecord::Associations::Preloader.new(
|
||||
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
|
||||
@template_data = Templates.serialize_for_builder(@template)
|
||||
|
||||
render :edit, layout: 'plain'
|
||||
end
|
||||
@@ -77,8 +61,6 @@ class TemplatesController < ApplicationController
|
||||
|
||||
WebhookUrls.enqueue_events(@template, 'template.updated')
|
||||
|
||||
TemplateVersions.find_or_create_for(@template, author: current_user) if params[:revision]
|
||||
|
||||
head :ok
|
||||
end
|
||||
|
||||
|
||||
@@ -16,7 +16,12 @@ class TemplatesDetectFieldsController < ApplicationController
|
||||
page_number = params[:page].presence&.to_i
|
||||
|
||||
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)|
|
||||
sse.write({ attachment_uuid:, page:, fields: })
|
||||
|
||||
@@ -6,6 +6,8 @@ class TemplatesFoldersController < ApplicationController
|
||||
def edit; end
|
||||
|
||||
def update
|
||||
authorize!(:update, @template)
|
||||
|
||||
name = [params[:parent_name], params[:name]].compact_blank.join(' / ')
|
||||
|
||||
@template.folder = TemplateFolders.find_or_create_by_name(current_user, name)
|
||||
|
||||
@@ -4,18 +4,7 @@ class TemplatesPreviewController < ApplicationController
|
||||
load_and_authorize_resource :template
|
||||
|
||||
def show
|
||||
ActiveRecord::Associations::Preloader.new(
|
||||
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
|
||||
@template_data = Templates.serialize_for_builder(@template)
|
||||
|
||||
render :show, layout: 'plain'
|
||||
end
|
||||
|
||||
@@ -4,6 +4,8 @@ class TemplatesRestoreController < ApplicationController
|
||||
load_and_authorize_resource :template
|
||||
|
||||
def create
|
||||
authorize!(:update, @template)
|
||||
|
||||
@template.update!(archived_at: nil)
|
||||
|
||||
WebhookUrls.enqueue_events(@template, 'template.updated')
|
||||
|
||||
@@ -5,7 +5,9 @@ class TemplatesUploadsController < ApplicationController
|
||||
|
||||
layout 'plain'
|
||||
|
||||
def show; end
|
||||
def show
|
||||
redirect_to root_path if params[:url].blank?
|
||||
end
|
||||
|
||||
def create
|
||||
url_params = create_file_params_from_url if params[:url].present?
|
||||
|
||||
@@ -14,4 +14,12 @@ class TemplatesVersionsController < ApplicationController
|
||||
|
||||
render json: TemplateVersions.serialize(version)
|
||||
end
|
||||
|
||||
def create
|
||||
authorize!(:update, @template)
|
||||
|
||||
TemplateVersions.find_or_create_for(@template, author: current_user)
|
||||
|
||||
head :ok
|
||||
end
|
||||
end
|
||||
|
||||
@@ -11,6 +11,12 @@ class UserInitialsController < ApplicationController
|
||||
|
||||
return redirect_to settings_profile_index_path, notice: I18n.t('unable_to_save_initials') if file.blank?
|
||||
|
||||
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,
|
||||
filename: file.original_filename,
|
||||
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?
|
||||
|
||||
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,
|
||||
filename: file.original_filename,
|
||||
content_type: file.content_type)
|
||||
|
||||
@@ -21,6 +21,7 @@ import SubmittersAutocomplete from './elements/submitter_autocomplete'
|
||||
import FolderAutocomplete from './elements/folder_autocomplete'
|
||||
import SignatureForm from './elements/signature_form'
|
||||
import SubmitForm from './elements/submit_form'
|
||||
import ConvertUpload from './elements/convert_upload'
|
||||
import PromptPassword from './elements/prompt_password'
|
||||
import EmailsTextarea from './elements/emails_textarea'
|
||||
import ToggleSubmit from './elements/toggle_submit'
|
||||
@@ -47,13 +48,13 @@ import ScrollTo from './elements/scroll_to'
|
||||
import SetValue from './elements/set_value'
|
||||
import ReviewForm from './elements/review_form'
|
||||
import ShowOnValue from './elements/show_on_value'
|
||||
import CustomValidation from './elements/custom_validation'
|
||||
import ToggleClasses from './elements/toggle_classes'
|
||||
import AutosizeField from './elements/autosize_field'
|
||||
import GoogleDriveFilePicker from './elements/google_drive_file_picker'
|
||||
import OpenModal from './elements/open_modal'
|
||||
import BarChart from './elements/bar_chart'
|
||||
import FieldCondition from './elements/field_condition'
|
||||
import ConfirmUpload from './elements/confirm_upload'
|
||||
|
||||
import * as TurboInstantClick from './lib/turbo_instant_click'
|
||||
|
||||
@@ -112,6 +113,7 @@ safeRegisterElement('submitters-autocomplete', SubmittersAutocomplete)
|
||||
safeRegisterElement('folder-autocomplete', FolderAutocomplete)
|
||||
safeRegisterElement('signature-form', SignatureForm)
|
||||
safeRegisterElement('submit-form', SubmitForm)
|
||||
safeRegisterElement('convert-upload', ConvertUpload)
|
||||
safeRegisterElement('prompt-password', PromptPassword)
|
||||
safeRegisterElement('emails-textarea', EmailsTextarea)
|
||||
safeRegisterElement('toggle-cookies', ToggleCookies)
|
||||
@@ -139,13 +141,13 @@ safeRegisterElement('scroll-to', ScrollTo)
|
||||
safeRegisterElement('set-value', SetValue)
|
||||
safeRegisterElement('review-form', ReviewForm)
|
||||
safeRegisterElement('show-on-value', ShowOnValue)
|
||||
safeRegisterElement('custom-validation', CustomValidation)
|
||||
safeRegisterElement('toggle-classes', ToggleClasses)
|
||||
safeRegisterElement('autosize-field', AutosizeField)
|
||||
safeRegisterElement('google-drive-file-picker', GoogleDriveFilePicker)
|
||||
safeRegisterElement('open-modal', OpenModal)
|
||||
safeRegisterElement('bar-chart', BarChart)
|
||||
safeRegisterElement('field-condition', FieldCondition)
|
||||
safeRegisterElement('confirm-upload', ConfirmUpload)
|
||||
|
||||
safeRegisterElement('template-builder', class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
@@ -197,10 +199,35 @@ safeRegisterElement('template-builder', class extends HTMLElement {
|
||||
}
|
||||
|
||||
onSubmit = (e) => {
|
||||
if (e.detail.success && e.detail?.formSubmission?.formElement?.id === 'submitters_form') {
|
||||
e.detail.fetchResponse.response.json().then((data) => {
|
||||
this.component.template.submitters = data.submitters
|
||||
})
|
||||
if (e.detail.success) {
|
||||
if (e.detail?.formSubmission?.formElement?.id === 'submitters_form') {
|
||||
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 { 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">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
@@ -150,12 +151,16 @@ export default targetable(class extends HTMLElement {
|
||||
if (!this.isLoading) this.hideDraghover()
|
||||
}
|
||||
|
||||
uploadFiles (files, url) {
|
||||
async uploadFiles (files, url) {
|
||||
this.isLoading = true
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { actionable } from '@github/catalyst/lib/actionable'
|
||||
import { target, targetable } from '@github/catalyst/lib/targetable'
|
||||
import { convertImagesInInput } from './convert_upload'
|
||||
|
||||
export default actionable(targetable(class extends HTMLElement {
|
||||
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')
|
||||
}
|
||||
|
||||
onDrop (e) {
|
||||
async onDrop (e) {
|
||||
e.preventDefault()
|
||||
|
||||
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()
|
||||
|
||||
await convertImagesInInput(this.input)
|
||||
|
||||
this.uploadFiles(this.input.files)
|
||||
}
|
||||
|
||||
|
||||
@@ -52,7 +52,9 @@ safeRegisterElement('submission-form', class extends HTMLElement {
|
||||
completedMessage: JSON.parse(this.dataset.completedMessage || '{}'),
|
||||
completedRedirectUrl: this.dataset.completedRedirectUrl,
|
||||
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)
|
||||
|
||||
@@ -643,6 +643,10 @@ export default {
|
||||
return new Intl.NumberFormat('de-DE').format(number)
|
||||
} else if (format === 'space') {
|
||||
return new Intl.NumberFormat('fr-FR').format(number)
|
||||
} else if (format === 'percent') {
|
||||
return `${number}%`
|
||||
} else if (format === 'percent_space') {
|
||||
return `${String(number).replace('.', ',')} %`
|
||||
} else {
|
||||
return number
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
</label>
|
||||
<button
|
||||
v-if="withToday"
|
||||
type="button"
|
||||
class="btn btn-outline btn-sm !normal-case font-normal set-current-date-button"
|
||||
@click.prevent="[setCurrentDate(), $emit('focus')]"
|
||||
>
|
||||
|
||||
@@ -51,6 +51,36 @@
|
||||
<script>
|
||||
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 {
|
||||
name: 'FileDropzone',
|
||||
components: {
|
||||
@@ -153,8 +183,14 @@ export default {
|
||||
}
|
||||
})
|
||||
} else {
|
||||
if (file.type === 'image/bmp' || file.type === 'image/vnd.microsoft.icon') {
|
||||
file = await this.convertBmpToPng(file)
|
||||
try {
|
||||
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)
|
||||
@@ -181,32 +217,6 @@ export default {
|
||||
}).finally(() => {
|
||||
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
|
||||
id="complete_form_button"
|
||||
class="btn btn-sm btn-neutral text-white px-4 w-full flex justify-center"
|
||||
form="steps_form"
|
||||
form="complete_form"
|
||||
type="submit"
|
||||
name="completed"
|
||||
value="true"
|
||||
@@ -113,6 +113,39 @@
|
||||
</span>
|
||||
</button>
|
||||
</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
|
||||
v-if="!isFormVisible"
|
||||
id="expand_form_button"
|
||||
@@ -791,6 +824,16 @@ export default {
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
completeButtonContainer: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
completeButtonScrollContainer: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
schema: {
|
||||
type: Array,
|
||||
required: false,
|
||||
@@ -1008,6 +1051,7 @@ export default {
|
||||
isSubmitting: false,
|
||||
isSubmittingComplete: false,
|
||||
submittedValues: {},
|
||||
isFormStarted: false,
|
||||
recalculateButtonDisabledKey: '',
|
||||
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 () {
|
||||
if (this.alwaysMinimize) {
|
||||
return this.t('submit')
|
||||
@@ -1657,6 +1705,8 @@ export default {
|
||||
return Promise.reject(new Error(data.error))
|
||||
}
|
||||
|
||||
this.isFormStarted = true
|
||||
|
||||
const nextStep = (isLastStep && emptyRequiredField) || (forceComplete ? null : this.findNextStep(submitStepIndex))
|
||||
|
||||
if (nextStep) {
|
||||
@@ -1693,6 +1743,7 @@ export default {
|
||||
},
|
||||
async performComplete (resp) {
|
||||
this.isCompleted = true
|
||||
this.isFormVisible = true
|
||||
|
||||
if (resp?.text) {
|
||||
const respData = await resp.text()
|
||||
|
||||
@@ -7,6 +7,13 @@ const en = {
|
||||
kba: 'KBA',
|
||||
please_upload_an_image_file: 'Please upload an image file',
|
||||
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.',
|
||||
verify_id: 'Verify ID',
|
||||
identity_verification: 'Identity verification',
|
||||
@@ -116,6 +123,13 @@ const es = {
|
||||
kba: 'KBA',
|
||||
please_upload_an_image_file: 'Por favor, sube un archivo de imagen',
|
||||
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.',
|
||||
verify_id: 'Verificar ID',
|
||||
identity_verification: 'Verificación de identidad',
|
||||
@@ -225,6 +239,13 @@ const it = {
|
||||
kba: 'KBA',
|
||||
please_upload_an_image_file: 'Per favore carica un file immagine',
|
||||
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à.",
|
||||
verify_id: 'Verifica ID',
|
||||
identity_verification: "Verifica dell'identità",
|
||||
@@ -334,6 +355,13 @@ const de = {
|
||||
kba: 'KBA',
|
||||
please_upload_an_image_file: 'Bitte laden Sie eine Bilddatei hoch',
|
||||
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.',
|
||||
verify_id: 'ID überprüfen',
|
||||
identity_verification: 'Identitätsprüfung',
|
||||
@@ -443,6 +471,13 @@ const fr = {
|
||||
kba: 'KBA',
|
||||
please_upload_an_image_file: 'Veuillez téléverser un fichier image',
|
||||
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é.",
|
||||
verify_id: "Vérifier l'ID",
|
||||
identity_verification: "Vérification d'identité",
|
||||
@@ -552,6 +587,13 @@ const pl = {
|
||||
kba: 'KBA',
|
||||
please_upload_an_image_file: 'Proszę przesłać plik obrazu',
|
||||
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.',
|
||||
verify_id: 'Zweryfikuj ID',
|
||||
identity_verification: 'Weryfikacja tożsamości',
|
||||
@@ -661,6 +703,13 @@ const uk = {
|
||||
kba: 'KBA',
|
||||
please_upload_an_image_file: 'Будь ласка, завантажте файл зображення',
|
||||
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: "Заповніть всі обов'язкові поля, щоб продовжити перевірку особи.",
|
||||
verify_id: 'Підтвердження ідентичності',
|
||||
identity_verification: 'Ідентифікація особи',
|
||||
@@ -770,6 +819,13 @@ const cs = {
|
||||
kba: 'KBA',
|
||||
please_upload_an_image_file: 'Nahrajte prosím obrázkový soubor',
|
||||
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.',
|
||||
verify_id: 'Ověřit ID',
|
||||
identity_verification: 'Ověření identity',
|
||||
@@ -879,6 +935,13 @@ const pt = {
|
||||
kba: 'KBA',
|
||||
please_upload_an_image_file: 'Por favor, envie um arquivo de imagem',
|
||||
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.',
|
||||
verify_id: 'Verificar ID',
|
||||
identity_verification: 'Verificação de identidade',
|
||||
@@ -988,6 +1051,13 @@ const he = {
|
||||
kba: 'KBA',
|
||||
please_upload_an_image_file: 'אנא העלה קובץ תמונה',
|
||||
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: 'מלא את כל השדות הנדרשים כדי להמשיך עם אימות זהות.',
|
||||
verify_id: 'אמת מזהה',
|
||||
identity_verification: 'אימות זהות',
|
||||
@@ -1097,6 +1167,13 @@ const nl = {
|
||||
kba: 'KBA',
|
||||
please_upload_an_image_file: 'Upload alstublieft een afbeeldingsbestand',
|
||||
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.',
|
||||
verify_id: 'ID verifiëren',
|
||||
identity_verification: 'Identiteitsverificatie',
|
||||
@@ -1206,6 +1283,13 @@ const ar = {
|
||||
kba: 'KBA',
|
||||
please_upload_an_image_file: 'يرجى تحميل ملف صورة',
|
||||
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: 'أكمل جميع الحقول المطلوبة للمتابعة في التحقق من الهوية.',
|
||||
verify_id: 'تحقق من الهوية',
|
||||
identity_verification: 'التحقق من الهوية',
|
||||
@@ -1315,6 +1399,13 @@ const ko = {
|
||||
kba: 'KBA',
|
||||
please_upload_an_image_file: '이미지 파일을 업로드해 주세요',
|
||||
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: '신원 확인을 진행하려면 모든 필수 필드를 작성하십시오.',
|
||||
verify_id: '아이디 확인',
|
||||
identity_verification: '신원 확인',
|
||||
@@ -1424,6 +1515,13 @@ const ja = {
|
||||
kba: 'KBA',
|
||||
please_upload_an_image_file: '画像ファイルをアップロードしてください',
|
||||
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: '本人確認を進めるには、すべての必須項目を入力してください。',
|
||||
verify_id: '本人確認',
|
||||
identity_verification: '本人確認',
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
</template>
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline btn-sm reupload-button"
|
||||
@click.prevent="remove"
|
||||
>
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
{{ error }}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="base-button w-full flex justify-center submit-form-button"
|
||||
@click="restartKba"
|
||||
>
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
<div v-else>
|
||||
<button
|
||||
v-if="sessionId"
|
||||
type="button"
|
||||
disabled
|
||||
class="base-button w-full modal-save-button"
|
||||
>
|
||||
@@ -47,6 +48,7 @@
|
||||
<button
|
||||
v-else
|
||||
:id="field.uuid"
|
||||
type="button"
|
||||
class="btn bg-[#7B73FF] text-white hover:bg-[#0A2540] text-lg w-full"
|
||||
:class="{ disabled: isCreatingCheckout }"
|
||||
:disabled="isCreatingCheckout"
|
||||
|
||||
@@ -41,12 +41,11 @@
|
||||
:class="{ '!pr-11 -mr-10': !field.validation?.pattern }"
|
||||
:required="field.required"
|
||||
:pattern="field.validation?.pattern"
|
||||
:title="validationMessage"
|
||||
:aria-describedby="field.description ? field.uuid + '-desc' : undefined"
|
||||
:placeholder="`${t('type_here_')}${field.required ? '' : ` (${t('optional')})`}`"
|
||||
type="text"
|
||||
:name="`values[${field.uuid}]`"
|
||||
@invalid="validationMessage ? $event.target.setCustomValidity(validationMessage) : ''"
|
||||
@input="validationMessage ? $event.target.setCustomValidity('') : ''"
|
||||
@focus="$emit('focus')"
|
||||
>
|
||||
<textarea
|
||||
@@ -138,6 +137,17 @@ export default {
|
||||
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 () {
|
||||
if (this.field.validation?.message) {
|
||||
return this.field.validation.message
|
||||
@@ -149,6 +159,10 @@ export default {
|
||||
.join('-')
|
||||
|
||||
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 {
|
||||
return ''
|
||||
}
|
||||
|
||||
@@ -620,6 +620,10 @@ export default {
|
||||
return new Intl.NumberFormat('de-DE').format(number)
|
||||
} else if (format === 'space') {
|
||||
return new Intl.NumberFormat('fr-FR').format(number)
|
||||
} else if (format === 'percent') {
|
||||
return `${number}%`
|
||||
} else if (format === 'percent_space') {
|
||||
return `${String(number).replace('.', ',')} %`
|
||||
} else {
|
||||
return number
|
||||
}
|
||||
@@ -716,14 +720,6 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
drag (e) {
|
||||
if (e.target.id === 'mask' && this.editable) {
|
||||
this.isDragged = true
|
||||
|
||||
this.area.x = (e.offsetX - this.dragFrom.x) / e.target.clientWidth
|
||||
this.area.y = (e.offsetY - this.dragFrom.y) / e.target.clientHeight
|
||||
}
|
||||
},
|
||||
startTouchDrag (e) {
|
||||
if (e.target !== this.$refs.touchTarget && e.target !== this.$refs.touchValueTarget) {
|
||||
return
|
||||
@@ -769,6 +765,8 @@ export default {
|
||||
|
||||
this.maybeChangeAreaPage(this.area)
|
||||
|
||||
this.clampAreaBounds(this.area)
|
||||
|
||||
if (this.isDragged) {
|
||||
this.save()
|
||||
}
|
||||
@@ -836,6 +834,8 @@ export default {
|
||||
|
||||
this.maybeChangeAreaPage(this.area)
|
||||
|
||||
this.clampAreaBounds(this.area)
|
||||
|
||||
if (this.isMoved) {
|
||||
this.save()
|
||||
}
|
||||
@@ -860,18 +860,6 @@ export default {
|
||||
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 () {
|
||||
if (!this.selectedAreasRef.value.includes(this.area)) {
|
||||
this.selectedAreasRef.value = [this.area]
|
||||
@@ -886,6 +874,8 @@ export default {
|
||||
this.$el.getRootNode().removeEventListener('mousemove', this.resize)
|
||||
this.$el.getRootNode().removeEventListener('mouseup', this.stopResize)
|
||||
|
||||
this.clampAreaBounds(this.area)
|
||||
|
||||
this.$emit('stop-resize')
|
||||
|
||||
this.save()
|
||||
@@ -924,9 +914,17 @@ export default {
|
||||
this.$el.getRootNode().removeEventListener('touchmove', this.touchResize)
|
||||
this.$el.getRootNode().removeEventListener('touchend', this.stopTouchResize)
|
||||
|
||||
this.clampAreaBounds(this.area)
|
||||
|
||||
this.$emit('stop-resize')
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2512,6 +2512,11 @@ export default {
|
||||
|
||||
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()
|
||||
} else {
|
||||
const documentRef = this.documentRefs.find((e) => e.document.uuid === area.attachment_uuid)
|
||||
@@ -3141,7 +3146,11 @@ export default {
|
||||
|
||||
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}`)
|
||||
}).finally(() => {
|
||||
this.isSaving = false
|
||||
@@ -3372,7 +3381,7 @@ export default {
|
||||
}
|
||||
})
|
||||
},
|
||||
save ({ force = false, revision = false } = {}) {
|
||||
save ({ force = false } = {}) {
|
||||
this.pendingFieldAttachmentUuids = []
|
||||
|
||||
if (this.beforeRevisionSnapshot) {
|
||||
@@ -3404,8 +3413,7 @@ export default {
|
||||
submitters: this.template.submitters,
|
||||
fields: this.template.fields,
|
||||
variables_schema: this.template.variables_schema
|
||||
},
|
||||
...(revision ? { revision: true } : {})
|
||||
}
|
||||
}),
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
}).then(() => {
|
||||
@@ -3414,6 +3422,12 @@ export default {
|
||||
}
|
||||
})
|
||||
},
|
||||
captureRevision () {
|
||||
return this.baseFetch(`/templates/${this.template.id}/versions`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
},
|
||||
onDynamicDocumentUpdate () {
|
||||
this.rebuildVariablesSchema()
|
||||
|
||||
|
||||
@@ -94,6 +94,8 @@ img.ProseMirror-separator {
|
||||
}
|
||||
dynamic-variable {
|
||||
background-color: #fef3c7;
|
||||
word-break: break-all;
|
||||
overflow-wrap: anywhere;
|
||||
}`)
|
||||
|
||||
function collectDomAttrs (dom) {
|
||||
@@ -136,11 +138,12 @@ function collectSpanDomAttrs (dom) {
|
||||
return result
|
||||
}
|
||||
|
||||
function createBlockNode (name, tag, content) {
|
||||
function createBlockNode (name, tag, content, extra = {}) {
|
||||
return Node.create({
|
||||
name,
|
||||
group: 'block',
|
||||
content: content || 'block+',
|
||||
...extra,
|
||||
addAttributes () {
|
||||
return {
|
||||
htmlAttrs: { default: {} }
|
||||
@@ -194,9 +197,9 @@ const CustomHeading = Node.create({
|
||||
})
|
||||
|
||||
const SectionNode = createBlockNode('section', 'section')
|
||||
const ArticleNode = createBlockNode('article', 'article')
|
||||
const HeaderNode = createBlockNode('header', 'header')
|
||||
const FooterNode = createBlockNode('footer', 'footer')
|
||||
const ArticleNode = createBlockNode('article', 'article', null, { isolating: true })
|
||||
const HeaderNode = createBlockNode('header', 'header', null, { isolating: true })
|
||||
const FooterNode = createBlockNode('footer', 'footer', null, { isolating: true })
|
||||
const DivNode = createBlockNode('div', 'div')
|
||||
const BlockquoteNode = createBlockNode('blockquote', 'blockquote')
|
||||
const PreNode = createBlockNode('pre', 'pre')
|
||||
|
||||
@@ -248,7 +248,7 @@ export default {
|
||||
FieldType,
|
||||
IconSettings
|
||||
},
|
||||
inject: ['t', 'save', 'backgroundColor', 'dateFormats'],
|
||||
inject: ['t', 'save', 'backgroundColor', 'dateFormats', 'locale'],
|
||||
provide () {
|
||||
return {
|
||||
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' }
|
||||
},
|
||||
numberFormats () {
|
||||
return [
|
||||
'none',
|
||||
'usd',
|
||||
'eur',
|
||||
'gbp',
|
||||
'comma',
|
||||
'dot',
|
||||
'space'
|
||||
]
|
||||
const formats = ['none', 'usd', 'eur', 'gbp', 'comma', 'dot', 'space']
|
||||
const spaceLocales = ['fr-FR', 'es-ES', 'pt-PT', 'de-DE', 'it-IT', 'nl-NL']
|
||||
|
||||
formats.push(spaceLocales.includes(this.locale) ? 'percent_space' : 'percent')
|
||||
|
||||
const selectedFormat = this.schema.format
|
||||
|
||||
if (selectedFormat && !formats.includes(selectedFormat)) {
|
||||
formats.push(selectedFormat)
|
||||
}
|
||||
|
||||
return formats
|
||||
},
|
||||
availableDateFormats () {
|
||||
const formats = this.dateFormats.length
|
||||
@@ -397,6 +400,10 @@ export default {
|
||||
return new Intl.NumberFormat('de-DE').format(number)
|
||||
} else if (format === 'space') {
|
||||
return new Intl.NumberFormat('fr-FR').format(number)
|
||||
} else if (format === 'percent') {
|
||||
return `${number}%`
|
||||
} else if (format === 'percent_space') {
|
||||
return `${String(number).replace('.', ',')} %`
|
||||
} else {
|
||||
return number
|
||||
}
|
||||
|
||||
@@ -514,7 +514,7 @@ export default {
|
||||
ContextSubmenu,
|
||||
ContextModal
|
||||
},
|
||||
inject: ['t', 'getFieldTypeIndex', 'template', 'withCustomFields', 'currencies', 'dateFormats'],
|
||||
inject: ['t', 'getFieldTypeIndex', 'template', 'withCustomFields', 'currencies', 'dateFormats', 'locale'],
|
||||
props: {
|
||||
contextMenu: {
|
||||
type: Object,
|
||||
|
||||
@@ -610,7 +610,7 @@ export default {
|
||||
IconTypography,
|
||||
IconX
|
||||
},
|
||||
inject: ['template', 't', 'dateFormats'],
|
||||
inject: ['template', 't', 'dateFormats', 'locale'],
|
||||
props: {
|
||||
field: {
|
||||
type: Object,
|
||||
@@ -691,15 +691,18 @@ export default {
|
||||
}
|
||||
},
|
||||
numberFormats () {
|
||||
return [
|
||||
'none',
|
||||
'usd',
|
||||
'eur',
|
||||
'gbp',
|
||||
'comma',
|
||||
'dot',
|
||||
'space'
|
||||
]
|
||||
const formats = ['none', 'usd', 'eur', 'gbp', 'comma', 'dot', 'space']
|
||||
const spaceLocales = ['fr-FR', 'es-ES', 'pt-PT', 'de-DE', 'it-IT', 'nl-NL']
|
||||
|
||||
formats.push(spaceLocales.includes(this.locale) ? 'percent_space' : 'percent')
|
||||
|
||||
const selectedFormat = this.field.preferences?.format
|
||||
|
||||
if (selectedFormat && !formats.includes(selectedFormat)) {
|
||||
formats.push(selectedFormat)
|
||||
}
|
||||
|
||||
return formats
|
||||
},
|
||||
availableDateFormats () {
|
||||
const formats = this.dateFormats.length
|
||||
@@ -824,6 +827,10 @@ export default {
|
||||
return new Intl.NumberFormat('de-DE').format(number)
|
||||
} else if (format === 'space') {
|
||||
return new Intl.NumberFormat('fr-FR').format(number)
|
||||
} else if (format === 'percent') {
|
||||
return `${number}%`
|
||||
} else if (format === 'percent_space') {
|
||||
return `${String(number).replace('.', ',')} %`
|
||||
} else {
|
||||
return number
|
||||
}
|
||||
|
||||
@@ -679,7 +679,7 @@ const pt = {
|
||||
analyzing_: 'Analisando...',
|
||||
download: 'Baixar',
|
||||
downloading_: 'Baixando...',
|
||||
view: 'Visualizar',
|
||||
view: 'Ver',
|
||||
payment_link: 'Link de pagamento',
|
||||
strikeout: 'Tachado',
|
||||
draw_strikethrough_the_document: 'Desenhe uma linha de tachado no documento',
|
||||
|
||||
@@ -168,6 +168,65 @@
|
||||
<script>
|
||||
import { IconUpload, IconInnerShadowTop, IconChevronDown, IconBrandGoogleDrive } from '@tabler/icons-vue'
|
||||
|
||||
function convertImage (sourceFile, targetType, quality) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
|
||||
reader.onload = function (event) {
|
||||
const img = new Image()
|
||||
|
||||
img.onload = function () {
|
||||
const canvas = document.createElement('canvas')
|
||||
const ctx = canvas.getContext('2d')
|
||||
|
||||
canvas.width = img.width
|
||||
canvas.height = img.height
|
||||
ctx.drawImage(img, 0, 0)
|
||||
canvas.toBlob(function (blob) {
|
||||
const ext = targetType === 'image/jpeg' ? '.jpg' : '.png'
|
||||
const newFile = new File([blob], sourceFile.name.replace(/\.\w+$/, ext), { type: targetType })
|
||||
resolve(newFile)
|
||||
}, targetType, quality)
|
||||
}
|
||||
|
||||
img.onerror = () => reject(new Error(`browser cannot decode ${sourceFile.type || sourceFile.name}`))
|
||||
|
||||
img.src = event.target.result
|
||||
}
|
||||
reader.onerror = reject
|
||||
reader.readAsDataURL(sourceFile)
|
||||
})
|
||||
}
|
||||
|
||||
async function convertImagesInInput (input) {
|
||||
if (!input.files || input.files.length === 0) return
|
||||
|
||||
const dt = new DataTransfer()
|
||||
let didConvert = false
|
||||
|
||||
for (const file of Array.from(input.files)) {
|
||||
let converted = file
|
||||
|
||||
try {
|
||||
if (['image/bmp', 'image/vnd.microsoft.icon', 'image/svg+xml', 'image/gif'].includes(file.type)) {
|
||||
converted = await convertImage(file, 'image/png')
|
||||
didConvert = true
|
||||
} else if (['image/heic', 'image/heif', 'image/heic-sequence', 'image/heif-sequence', 'image/avif', 'image/avif-sequence', 'image/webp'].includes(file.type)) {
|
||||
converted = await convertImage(file, 'image/jpeg', 0.9)
|
||||
didConvert = true
|
||||
}
|
||||
} catch (e) {
|
||||
alert(e.message)
|
||||
}
|
||||
|
||||
dt.items.add(converted)
|
||||
}
|
||||
|
||||
if (didConvert) {
|
||||
input.files = dt.files
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'DocumentsUpload',
|
||||
components: {
|
||||
@@ -282,6 +341,10 @@ export default {
|
||||
async upload ({ path } = {}) {
|
||||
this.isLoading = true
|
||||
|
||||
if (this.$refs.input) {
|
||||
await convertImagesInInput(this.$refs.input)
|
||||
}
|
||||
|
||||
return this.baseFetch(path || this.uploadUrl, {
|
||||
method: 'POST',
|
||||
headers: { Accept: 'application/json' },
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
class ReindexSearchEntryJob
|
||||
include Sidekiq::Job
|
||||
|
||||
InvalidFormat = Class.new(StandardError)
|
||||
|
||||
def perform(params = {})
|
||||
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_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
|
||||
|
||||
before_validation :set_sha1
|
||||
|
||||
@@ -75,7 +75,7 @@ class Template < ApplicationRecord
|
||||
has_many :dynamic_document_versions, through: :dynamic_documents, source: :versions
|
||||
|
||||
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
|
||||
|
||||
scope :active, -> { where(archived_at: nil) }
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<% if signature %>
|
||||
<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' %>
|
||||
<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>
|
||||
<% end %>
|
||||
<a href="<%= edit_user_signature_path %>" data-turbo-frame="modal" class="base-button w-full">
|
||||
@@ -49,7 +49,7 @@
|
||||
<% if initials %>
|
||||
<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' %>
|
||||
<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>
|
||||
<% end %>
|
||||
<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">
|
||||
<span class="flex items-center justify-between space-x-0.5 font-medium">
|
||||
<%= svg_icon('start', class: 'h-3 w-3') %>
|
||||
<span>16k</span>
|
||||
<span>17k</span>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
@@ -52,9 +52,7 @@
|
||||
<% if link_form_fields.include?('phone') %>
|
||||
<div dir="auto" class="form-control !mt-0">
|
||||
<%= 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, 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>
|
||||
<%= 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') %>
|
||||
</div>
|
||||
<% end %>
|
||||
<toggle-submit dir="auto" class="form-control">
|
||||
|
||||
@@ -34,13 +34,11 @@
|
||||
</linked-input>
|
||||
</submitters-autocomplete>
|
||||
<% has_phone_field = true %>
|
||||
<custom-validation data-invalid-message="<%= t('use_international_format_1xxx_') %>">
|
||||
<submitters-autocomplete data-field="phone">
|
||||
<linked-input data-target-id="<%= "detailed_phone_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
|
||||
<%= tag.input type: 'tel', pattern: '^\+[0-9\s\-]+$', name: 'submission[1][submitters][][phone]', autocomplete: 'off', class: 'base-input !h-10 mt-1.5 w-full', placeholder: local_assigns[:require_phone_2fa] == true || local_assigns[:prefillable_fields].present? ? t(:phone) : "#{t('phone')} (#{t('optional')})", id: "detailed_phone_#{item['uuid']}", required: local_assigns[:require_phone_2fa] == true && index.zero? %>
|
||||
</linked-input>
|
||||
</submitters-autocomplete>
|
||||
</custom-validation>
|
||||
<submitters-autocomplete data-field="phone">
|
||||
<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? %>
|
||||
</linked-input>
|
||||
</submitters-autocomplete>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if prefillable_fields.present? %>
|
||||
@@ -51,13 +49,11 @@
|
||||
</submitters-autocomplete>
|
||||
<% if local_assigns[:require_phone_2fa] == true || prefillable_fields.any? { |f| f['type'] == 'phone' } %>
|
||||
<% has_phone_field = true %>
|
||||
<custom-validation data-invalid-message="<%= t('use_international_format_1xxx_') %>">
|
||||
<submitters-autocomplete data-field="phone">
|
||||
<linked-input data-target-id="<%= "detailed_phone_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
|
||||
<%= tag.input type: 'tel', pattern: '^\+[0-9\s\-]+$', name: 'submission[1][submitters][][phone]', autocomplete: 'off', class: 'base-input !h-10 mt-1.5 w-full', placeholder: t(:phone), id: "detailed_phone_#{item['uuid']}", required: true %>
|
||||
</linked-input>
|
||||
</submitters-autocomplete>
|
||||
</custom-validation>
|
||||
<submitters-autocomplete data-field="phone">
|
||||
<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 %>
|
||||
</linked-input>
|
||||
</submitters-autocomplete>
|
||||
<% end %>
|
||||
<% prefillable_fields.each do |field| %>
|
||||
<% field_id = "detailed_field_#{index}_#{field['uuid'] || field['name'].parameterize}" %>
|
||||
|
||||
@@ -19,13 +19,11 @@
|
||||
</label>
|
||||
<% end %>
|
||||
<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">
|
||||
<linked-input data-target-id="<%= "phone_phone_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
|
||||
<%= tag.input type: 'tel', pattern: '^\+[0-9\s\-]+$', name: 'submission[1][submitters][][phone]', autocomplete: 'off', class: 'base-input !h-10 w-full', placeholder: t('phone'), required: index.zero? || template.preferences['require_all_submitters'], id: "phone_phone_#{item['uuid']}" %>
|
||||
</linked-input>
|
||||
</submitters-autocomplete>
|
||||
</custom-validation>
|
||||
<submitters-autocomplete data-field="phone">
|
||||
<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']}" %>
|
||||
</linked-input>
|
||||
</submitters-autocomplete>
|
||||
<% if submitters.size > 1 %>
|
||||
<submitters-autocomplete data-field="name">
|
||||
<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>
|
||||
<% 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 is_all_completed || !is_combined_enabled %>
|
||||
<div class="join relative">
|
||||
@@ -260,10 +266,12 @@
|
||||
<div dir="auto">
|
||||
<% if field['type'].in?(%w[signature initials]) %>
|
||||
<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>
|
||||
<% 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']) %>
|
||||
<div class="flex flex-col justify-center">
|
||||
<% 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;">
|
||||
<%= @submitter.submission.name || @submitter.submission.template.name %>
|
||||
</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] %>
|
||||
<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>
|
||||
</modal-button>
|
||||
<% 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) %>">
|
||||
<%= svg_icon('x', class: 'w-5 h-5') %>
|
||||
</button>
|
||||
</modal-button>
|
||||
<% 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" 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">
|
||||
<%= svg_icon('download', class: 'w-5 h-5') %>
|
||||
</span>
|
||||
@@ -45,12 +56,12 @@
|
||||
<% end %>
|
||||
<% else %>
|
||||
<% 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>
|
||||
</modal-button>
|
||||
<% 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-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">
|
||||
<%= svg_icon('download', class: 'w-6 h-6 inline md:hidden') %>
|
||||
<span class="hidden md:inline"><%= t('download') %></span>
|
||||
@@ -64,9 +75,17 @@
|
||||
<% end %>
|
||||
</div>
|
||||
</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] %>
|
||||
<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) %>">
|
||||
<span class="min-[1366px]:inline hidden px-3">
|
||||
<%= t(:delegate) %>
|
||||
@@ -77,23 +96,15 @@
|
||||
</button>
|
||||
</modal-button>
|
||||
<% 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) %>">
|
||||
<%= svg_icon('x', class: 'w-5 h-5') %>
|
||||
</button>
|
||||
</modal-button>
|
||||
<% 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 %>
|
||||
<% 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) %>">
|
||||
<span class="min-[1366px]:inline hidden px-3">
|
||||
<%= t(:decline) %>
|
||||
@@ -104,7 +115,9 @@
|
||||
</button>
|
||||
</modal-button>
|
||||
<% 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">
|
||||
<%= svg_icon('download', class: 'w-5 h-5') %>
|
||||
</span>
|
||||
|
||||
@@ -10,11 +10,9 @@
|
||||
<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')})" %>
|
||||
</submitters-autocomplete>
|
||||
<custom-validation data-invalid-message="<%= t('use_international_format_1xxx_') %>">
|
||||
<submitters-autocomplete data-field="phone">
|
||||
<%= telephone_field_tag 'submitter[phone]', @submitter.phone, autocomplete: 'off', pattern: '^\+[0-9\s\-]+$', class: 'base-input !h-10 mt-1.5 w-full', placeholder: "#{t('phone')} (#{t('optional')})" %>
|
||||
</submitters-autocomplete>
|
||||
</custom-validation>
|
||||
<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')})" %>
|
||||
</submitters-autocomplete>
|
||||
</div>
|
||||
</submitter-item>
|
||||
</div>
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
</div>
|
||||
<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) %>
|
||||
<%= 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 %>
|
||||
<% 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 %>
|
||||
@@ -89,7 +89,7 @@
|
||||
<% end %>
|
||||
<% if template.archived_at? %>
|
||||
<% 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 %>
|
||||
<%= 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">
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
</span>
|
||||
</label>
|
||||
<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>
|
||||
</submit-form>
|
||||
</convert-upload>
|
||||
<input hidden name="folder_name" value="<%= local_assigns[:folder_name] %>">
|
||||
<% 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' %>
|
||||
<% 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">
|
||||
<div
|
||||
class="text-center p-8 h-full flex items-center justify-center">
|
||||
<div>
|
||||
<%= render 'shared/logo', width: 50, height: 50, class: 'mx-auto animate-bounce' %>
|
||||
<span>
|
||||
<%= t('processing') %>...
|
||||
</span>
|
||||
<confirm-upload>
|
||||
<div class="h-screen">
|
||||
<div class="text-center p-8 h-full flex items-center justify-center">
|
||||
<div class="space-y-4">
|
||||
<div data-target="confirm-upload.logo" class="mx-auto">
|
||||
<%= render 'shared/logo', width: 50, height: 50, class: 'mx-auto' %>
|
||||
</div>
|
||||
<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>
|
||||
<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>
|
||||
</confirm-upload>
|
||||
|
||||
@@ -25,6 +25,18 @@ module DocuSeal
|
||||
|
||||
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
|
||||
es it de fr nl pl uk cs pt he ar ko ja]
|
||||
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]
|
||||
+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>
|
||||
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_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
|
||||
copy_share_link: Copy Share Link
|
||||
copied_to_clipboard: Copied to Clipboard
|
||||
@@ -934,6 +937,9 @@ en: &en
|
||||
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}.
|
||||
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:
|
||||
confirmations:
|
||||
confirmed: Your email address has been successfully confirmed.
|
||||
@@ -1464,7 +1470,7 @@ es: &es
|
||||
type_emails_here: Escribe correos electrónicos aquí
|
||||
or_embed_on_your_website: O integra en tu sitio web
|
||||
phone: Teléfono
|
||||
send_emails: Enviar correos electrónicos
|
||||
send_emails: Enviar emails
|
||||
edit_message: Editar mensaje
|
||||
smtp_not_configured: SMTP no configurado
|
||||
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
|
||||
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.
|
||||
re_send_email: Reenviar correo electrónico
|
||||
send_email: Enviar correo electrónico
|
||||
re_send_email: Reenviar email
|
||||
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
|
||||
copied_to_clipboard: Copiado al portapapeles
|
||||
link: Enlace
|
||||
@@ -1981,6 +1990,9 @@ es: &es
|
||||
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}.
|
||||
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:
|
||||
confirmations:
|
||||
confirmed: Tu dirección de correo electrónico ha sido confirmada correctamente.
|
||||
@@ -2754,7 +2766,10 @@ it: &it
|
||||
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>
|
||||
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
|
||||
copy_share_link: Copia link di condivisione
|
||||
copied_to_clipboard: Copiato negli appunti
|
||||
@@ -3028,6 +3043,9 @@ it: &it
|
||||
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}.
|
||||
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:
|
||||
confirmations:
|
||||
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>
|
||||
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_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
|
||||
copy_share_link: Copier le lien de partage
|
||||
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
|
||||
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.
|
||||
open_file_from: 'Ouvrir le fichier depuis'
|
||||
cancel: Annuler
|
||||
open: Ouvrir
|
||||
devise:
|
||||
confirmations:
|
||||
confirmed: Votre adresse e-mail a été confirmée avec succès.
|
||||
@@ -4321,7 +4345,7 @@ pt: &pt
|
||||
|
||||
Obrigado,
|
||||
{account.name}
|
||||
view: Visualizar
|
||||
view: Ver
|
||||
email: Email
|
||||
form_expired_at_html: 'O formulário expirou em <span class="font-semibold">%{time}</span>'
|
||||
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>'
|
||||
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_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
|
||||
copy_share_link: Copiar link de compartilhamento
|
||||
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
|
||||
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.
|
||||
open_file_from: 'Abrir arquivo de'
|
||||
cancel: Cancelar
|
||||
open: Abrir
|
||||
devise:
|
||||
confirmations:
|
||||
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>
|
||||
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_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
|
||||
copy_share_link: Freigabelink kopieren
|
||||
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
|
||||
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.
|
||||
open_file_from: 'Datei öffnen von'
|
||||
cancel: Abbrechen
|
||||
open: Öffnen
|
||||
devise:
|
||||
confirmations:
|
||||
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>
|
||||
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_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
|
||||
copy_share_link: Deellink kopiëren
|
||||
copied_to_clipboard: Gekopieerd naar klembord
|
||||
@@ -7614,6 +7653,9 @@ nl: &nl
|
||||
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}.
|
||||
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:
|
||||
confirmations:
|
||||
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 :events, only: %i[index], controller: 'submission_events'
|
||||
resources :download, only: %i[index], controller: 'submissions_download'
|
||||
resources :resend_email, only: %i[create], controller: 'submissions_resend_email'
|
||||
end
|
||||
resources :submitters, only: %i[edit update]
|
||||
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 :code_modal, only: %i[show], controller: 'templates_code_modal'
|
||||
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_qr, only: %i[show], controller: 'templates_share_link_qr'
|
||||
resources :recipients, only: %i[create], controller: 'templates_recipients'
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Accounts
|
||||
LINK_EXPIRES_AT = 40.minutes
|
||||
LINK_EXPIRES_AT = ENV.fetch('FILE_URLS_EXPIRE_MINUTES', '40').to_i.minutes
|
||||
|
||||
module_function
|
||||
|
||||
|
||||
@@ -1,8 +1,25 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
stats = image.stats
|
||||
|
||||
|
||||
+1
-1
@@ -39,7 +39,7 @@ module LoadBmp
|
||||
bands = header_data[:bpp] / 8
|
||||
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
|
||||
|
||||
|
||||
+5
-3
@@ -43,7 +43,9 @@ module LoadIco
|
||||
|
||||
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])
|
||||
|
||||
@@ -200,13 +202,13 @@ module LoadIco
|
||||
|
||||
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,
|
||||
dib_width,
|
||||
image_pixel_height,
|
||||
4,
|
||||
:uchar
|
||||
)
|
||||
).copy(interpretation: :srgb)
|
||||
end
|
||||
# rubocop:enable Metrics
|
||||
end
|
||||
|
||||
@@ -25,6 +25,10 @@ module NumberUtils
|
||||
ApplicationController.helpers.number_to_currency(number, locale:, precision: 2, unit: CURRENCY_SYMBOLS[format])
|
||||
elsif locale
|
||||
ApplicationController.helpers.number_with_delimiter(number, locale:)
|
||||
elsif format == 'percent'
|
||||
"#{number}%"
|
||||
elsif format == 'percent_space'
|
||||
"#{number.to_s.tr('.', ',')} %"
|
||||
else
|
||||
number
|
||||
end
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'puma/plugin'
|
||||
require 'redis_client'
|
||||
|
||||
# rubocop:disable Metrics
|
||||
Puma::Plugin.create do
|
||||
@@ -68,7 +69,7 @@ Puma::Plugin.create do
|
||||
|
||||
break
|
||||
rescue RedisClient::CannotConnectError
|
||||
raise('Unable to connect to redis') if attempt > 10
|
||||
raise('Unable to connect to redis') if attempt > 30
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -39,7 +39,9 @@ module SendWebhookRequest
|
||||
data: data
|
||||
}.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.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 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?('.')
|
||||
|
||||
fixed_email = EmailTypo.call(email.delete_prefix('<'))
|
||||
@@ -175,7 +175,9 @@ module Submissions
|
||||
return email.downcase if domain == fixed_domain
|
||||
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}")
|
||||
|
||||
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_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 = last_submitter.timezone || account.timezone if with_submitter_timezone
|
||||
|
||||
@@ -363,7 +365,7 @@ module Submissions
|
||||
|
||||
image =
|
||||
begin
|
||||
Submissions::GenerateResultAttachments.load_vips_image(attachment).autorot
|
||||
ImageUtils.load_vips(attachment.download, content_type: attachment.content_type, autorot: true)
|
||||
rescue Vips::Error
|
||||
next unless attachment.content_type.starts_with?('image/')
|
||||
next if attachment.byte_size.zero?
|
||||
@@ -374,7 +376,13 @@ module Submissions
|
||||
scale = [600.0 / image.width, 600.0 / image.height].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
|
||||
height = resized_image.height * (width.to_f / resized_image.width)
|
||||
@@ -402,7 +410,7 @@ module Submissions
|
||||
|
||||
link =
|
||||
if with_file_links
|
||||
ActiveStorage::Blob.proxy_url(attachment.blob)
|
||||
ActiveStorage::Blob.proxy_url(attachment.blob, expires_at: file_links_expire_at)
|
||||
else
|
||||
r.submissions_preview_url(submission.slug, **Docuseal.default_url_options)
|
||||
end
|
||||
|
||||
@@ -29,6 +29,8 @@ module Submissions
|
||||
with_signature_id_reason =
|
||||
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,
|
||||
incremental: is_rotate_incremental)
|
||||
|
||||
@@ -42,7 +44,8 @@ module Submissions
|
||||
GenerateResultAttachments.fill_submitter_fields(s, submission.account, pdfs_index,
|
||||
with_signature_id:, is_flatten:, with_headings: index.zero?,
|
||||
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
|
||||
|
||||
template = submission.template
|
||||
|
||||
@@ -11,9 +11,6 @@ module Submissions
|
||||
'Helvetica'
|
||||
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_PATH
|
||||
else
|
||||
@@ -154,6 +151,8 @@ module Submissions
|
||||
with_signature_id_reason =
|
||||
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,
|
||||
incremental: is_rotate_incremental)
|
||||
|
||||
@@ -201,12 +200,14 @@ module Submissions
|
||||
with_submitter_timezone:,
|
||||
with_file_links:,
|
||||
with_timestamp_seconds:,
|
||||
with_signature_id_reason:)
|
||||
with_signature_id_reason:,
|
||||
file_links_expire_at:)
|
||||
end
|
||||
|
||||
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_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|
|
||||
hash[valign] = HexaPDF::Layout::TextLayouter.new(text_valign: valign.to_sym, text_align: :center)
|
||||
end
|
||||
@@ -313,7 +314,10 @@ module Submissions
|
||||
|
||||
image =
|
||||
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
|
||||
next unless attachment.content_type.starts_with?('image/')
|
||||
next if attachment.byte_size.zero?
|
||||
@@ -358,7 +362,8 @@ module Submissions
|
||||
image_x = area_x + ((half_width - image_width) / 2.0)
|
||||
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)
|
||||
|
||||
@@ -425,7 +430,8 @@ module Submissions
|
||||
|
||||
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)
|
||||
.draw(canvas, area_x + TEXT_LEFT_MARGIN,
|
||||
@@ -451,7 +457,10 @@ module Submissions
|
||||
|
||||
image =
|
||||
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
|
||||
next unless attachment.content_type.starts_with?('image/')
|
||||
next if attachment.byte_size.zero?
|
||||
@@ -462,7 +471,14 @@ module Submissions
|
||||
scale = [(area['w'] * width) / image.width,
|
||||
(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(
|
||||
io,
|
||||
@@ -504,7 +520,7 @@ module Submissions
|
||||
|
||||
url =
|
||||
if with_file_links
|
||||
ActiveStorage::Blob.proxy_url(attachment.blob)
|
||||
ActiveStorage::Blob.proxy_url(attachment.blob, expires_at: file_links_expire_at)
|
||||
else
|
||||
r.submissions_preview_url(submission.slug, **Docuseal.default_url_options)
|
||||
end
|
||||
@@ -731,7 +747,7 @@ module Submissions
|
||||
pdf.trailer.info[:Creator] = info_creator
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
@@ -747,12 +763,14 @@ module Submissions
|
||||
|
||||
begin
|
||||
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)
|
||||
|
||||
pdf.instance_variable_get(:@listeners)[:complete_objects].delete(pdfa_listener) if pdfa_listener
|
||||
|
||||
begin
|
||||
pdf.sign(io, write_options: { validate: false, incremental: false }, **sign_params)
|
||||
rescue HexaPDF::Error
|
||||
rescue HexaPDF::Error, TypeError
|
||||
pdf.validate(auto_correct: true)
|
||||
pdf.sign(io, write_options: { validate: false, incremental: false }, **sign_params)
|
||||
end
|
||||
@@ -1014,20 +1032,6 @@ module Submissions
|
||||
[]
|
||||
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
|
||||
Rails.application.routes.url_helpers
|
||||
end
|
||||
|
||||
+12
-18
@@ -122,27 +122,21 @@ module Submitters
|
||||
end
|
||||
end
|
||||
|
||||
def create_attachment!(submitter, params)
|
||||
blob =
|
||||
if (file = params[:file])
|
||||
extension = File.extname(file.original_filename).delete_prefix('.').downcase
|
||||
def create_attachment!(submitter, file, metadata: {})
|
||||
raise ParamsError, 'file param is missing' if file.blank?
|
||||
|
||||
if DANGEROUS_EXTENSIONS.include?(extension)
|
||||
raise MaliciousFileExtension, "File type '.#{extension}' is not allowed."
|
||||
end
|
||||
extension = File.extname(file.original_filename).delete_prefix('.').downcase
|
||||
|
||||
ActiveStorage::Blob.create_and_upload!(io: file.open,
|
||||
filename: file.original_filename,
|
||||
content_type: file.content_type)
|
||||
else
|
||||
raise ParamsError, 'file param is missing'
|
||||
end
|
||||
if DANGEROUS_EXTENSIONS.include?(extension)
|
||||
raise MaliciousFileExtension, "File type '.#{extension}' is not allowed."
|
||||
end
|
||||
|
||||
ActiveStorage::Attachment.create!(
|
||||
blob:,
|
||||
name: 'attachments',
|
||||
record: submitter
|
||||
)
|
||||
blob = ActiveStorage::Blob.create_and_upload!(io: file.tap(&:rewind).open,
|
||||
filename: file.original_filename,
|
||||
content_type: file.content_type,
|
||||
metadata:)
|
||||
|
||||
ActiveStorage::Attachment.create!(blob:, name: 'attachments', record: submitter)
|
||||
end
|
||||
|
||||
def normalize_preferences(account, user, params)
|
||||
|
||||
@@ -6,12 +6,6 @@ module Submitters
|
||||
HEIGHT = 200
|
||||
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
|
||||
|
||||
def call(submitter, with_logo: true)
|
||||
@@ -25,7 +19,7 @@ module Submitters
|
||||
def build_attachment(submitter, with_logo: true)
|
||||
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)
|
||||
|
||||
@@ -40,16 +34,17 @@ module Submitters
|
||||
def generate_stamp_image(submitter, with_logo: true)
|
||||
logo =
|
||||
if with_logo
|
||||
Vips::Image.new_from_buffer(load_logo(submitter).read, '')
|
||||
ImageUtils.load_vips(load_logo(submitter).read)
|
||||
else
|
||||
Vips::Image.new_from_buffer(TRANSPARENT_PIXEL, '').resize(WIDTH)
|
||||
Vips::Image.black(WIDTH, WIDTH, bands: 4).copy(interpretation: :srgb)
|
||||
end
|
||||
|
||||
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)
|
||||
|
||||
|
||||
@@ -27,7 +27,9 @@ module Submitters
|
||||
|
||||
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
|
||||
|
||||
@@ -212,8 +212,8 @@ module Submitters
|
||||
elsif type.in?(%w[signature initials]) && value.length < 60
|
||||
find_or_create_blob_from_text(account, value, type)
|
||||
elsif (data = Base64.decode64(value.sub(BASE64_PREFIX_REGEXP, ''))) &&
|
||||
Marcel::MimeType.for(data).exclude?('octet-stream')
|
||||
find_or_create_blob_from_base64(account, data, type)
|
||||
(mime_type = Marcel::MimeType.for(data)).exclude?('octet-stream')
|
||||
find_or_create_blob_from_base64(account, data, type, mime_type:)
|
||||
elsif type == 'image' && (value.starts_with?('<html>') || value.starts_with?('<!DOCTYPE'))
|
||||
raise InvalidDefaultValue, "Invalid #{type} value" unless purpose == :api
|
||||
|
||||
@@ -236,19 +236,31 @@ module Submitters
|
||||
raise InvalidDefaultValue, "HTML content is not allowed: #{value.first(200)}..."
|
||||
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)
|
||||
|
||||
blob = find_blob_by_checksum(checksum, account)
|
||||
|
||||
blob || ActiveStorage::Blob.create_and_upload!(
|
||||
io: StringIO.new(data),
|
||||
filename: "#{type}.png"
|
||||
)
|
||||
return blob if blob
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
|
||||
@@ -256,11 +268,20 @@ module Submitters
|
||||
|
||||
blob || ActiveStorage::Blob.create_and_upload!(
|
||||
io: StringIO.new(data),
|
||||
filename: "#{type}.png"
|
||||
filename: "#{type}.png",
|
||||
content_type: 'image/png',
|
||||
metadata: { analyzed: true, identified: true, width:, height: }
|
||||
)
|
||||
end
|
||||
|
||||
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(':')
|
||||
checksum = CHECKSUM_CACHE_STORE.fetch(cache_key)
|
||||
|
||||
@@ -276,10 +297,7 @@ module Submitters
|
||||
|
||||
blob = find_blob_by_checksum(checksum, account)
|
||||
|
||||
blob || ActiveStorage::Blob.create_and_upload!(
|
||||
io: StringIO.new(data),
|
||||
filename: Addressable::URI.parse(url).path.split('/').last
|
||||
)
|
||||
blob || ActiveStorage::Blob.create_and_upload!(io: StringIO.new(data), filename:)
|
||||
end
|
||||
|
||||
def find_blob_by_checksum(checksum, account)
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
module Templates
|
||||
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 = {
|
||||
one_day: 1.day,
|
||||
two_days: 2.days,
|
||||
@@ -91,4 +95,16 @@ module Templates
|
||||
Time.current + EXPIRATION_DURATIONS[default_expire_at_duration]
|
||||
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
|
||||
|
||||
@@ -77,7 +77,7 @@ module Templates
|
||||
split_page: false, aspect_ratio: false, padding: nil, page_number: nil)
|
||||
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:,
|
||||
temperature:, aspect_ratio:, padding:)
|
||||
@@ -193,7 +193,7 @@ module Templates
|
||||
|
||||
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
|
||||
|
||||
def sort_fields(fields, y_threshold: 0.01)
|
||||
|
||||
@@ -7,7 +7,6 @@ module Templates
|
||||
PREVIEW_FORMAT = '.jpg'
|
||||
ATTACHMENT_NAME = 'preview_images'
|
||||
|
||||
BMP_REGEXP = %r{\Aimage/(?:bmp|x-bmp|x-ms-bmp)\z}
|
||||
PDF_CONTENT_TYPE = 'application/pdf'
|
||||
CONCURRENCY = 2
|
||||
Q = 95
|
||||
@@ -59,19 +58,13 @@ module Templates
|
||||
def generate_preview_image(attachment, data)
|
||||
ActiveStorage::Attachment.where(name: ATTACHMENT_NAME, record: attachment).destroy_all
|
||||
|
||||
image =
|
||||
if BMP_REGEXP.match?(attachment.content_type)
|
||||
LoadBmp.call(data)
|
||||
else
|
||||
Vips::Image.new_from_buffer(data, '')
|
||||
end
|
||||
|
||||
image = image.autorot.resize(MAX_WIDTH / image.width.to_f)
|
||||
image = ImageUtils.load_vips(data, content_type: attachment.content_type, autorot: true)
|
||||
image = image.resize(MAX_WIDTH / image.width.to_f)
|
||||
|
||||
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:,
|
||||
palette: true, Q: Q, dither: 0))
|
||||
palette: true, Q: Q, dither: 0, strip: true))
|
||||
|
||||
ActiveStorage::Attachment.create!(
|
||||
blob: ActiveStorage::Blob.create_and_upload!(
|
||||
@@ -110,7 +103,15 @@ module Templates
|
||||
|
||||
promises =
|
||||
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
|
||||
|
||||
Concurrent::Promise.zip(*promises).value!.each do |blob|
|
||||
@@ -129,39 +130,27 @@ module Templates
|
||||
pool&.kill
|
||||
end
|
||||
|
||||
def build_and_upload_blob(doc, page_number, format = FORMAT)
|
||||
doc_page = doc.get_page(page_number)
|
||||
|
||||
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)
|
||||
def build_and_upload_blob(image, page_number, format = FORMAT)
|
||||
image = image.copy(interpretation: :srgb)
|
||||
|
||||
data =
|
||||
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:,
|
||||
palette: true, Q: Q, dither: 0)
|
||||
image.write_to_buffer(format, compression: 6, filter: 0, bitdepth:,
|
||||
palette: true, Q: Q, dither: 0)
|
||||
else
|
||||
page.write_to_buffer(format, interlace: true, Q: JPEG_Q)
|
||||
image.write_to_buffer(format, interlace: true, Q: JPEG_Q)
|
||||
end
|
||||
|
||||
blob = ActiveStorage::Blob.new(
|
||||
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
|
||||
rescue Vips::Error, Pdfium::PdfiumError => e
|
||||
Rollbar.warning(e) if defined?(Rollbar)
|
||||
|
||||
nil
|
||||
ensure
|
||||
doc_page&.close
|
||||
end
|
||||
|
||||
def maybe_flatten_form(data, pdf)
|
||||
@@ -206,7 +195,15 @@ module Templates
|
||||
def generate_pdf_preview_from_file(attachment, file_path, page_number)
|
||||
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
|
||||
ActiveStorage::Attachment.create!(
|
||||
|
||||
@@ -14,27 +14,25 @@ module Templates
|
||||
|
||||
module_function
|
||||
|
||||
def call(template, schema_documents: template.schema_documents.preload(:blob), preview_image_attachments: nil,
|
||||
expires_at: Accounts.link_expires_at(Account.new(id: template.account_id)))
|
||||
def call(template, schema_documents: template.schema_documents.preload(:blob), dynamic_documents: nil,
|
||||
preview_image_attachments: nil, expires_at: Accounts.link_expires_at(Account.new(id: template.account_id)))
|
||||
json = template.as_json(SERIALIZE_PARAMS)
|
||||
|
||||
preview_image_attachments ||=
|
||||
ActiveStorage::Attachment.joins(:blob)
|
||||
.where(blob: { filename: ['0.jpg', '0.png'] })
|
||||
.where(record_id: schema_documents.map(&:id),
|
||||
record_type: 'ActiveStorage::Attachment',
|
||||
name: :preview_images)
|
||||
.preload(:blob)
|
||||
dynamic_documents ||= preload_dynamic_documents(template)
|
||||
|
||||
preview_image_attachments ||= preload_preview_image_attachments(schema_documents, dynamic_documents)
|
||||
|
||||
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
|
||||
Rollbar.error("Documents missing: #{template.id}") if defined?(Rollbar)
|
||||
|
||||
next
|
||||
attachment = dynamic_document.current_version&.document_attachment
|
||||
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 ||= attachment.preview_images.joins(:blob).find_by(blob: { filename: ['0.jpg', '0.png'] })&.blob
|
||||
|
||||
@@ -49,5 +47,26 @@ module Templates
|
||||
|
||||
json
|
||||
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
|
||||
|
||||
@@ -133,7 +133,7 @@ RSpec.describe 'Signing Form' do
|
||||
|
||||
# Cell step
|
||||
fill_in 'Cell code', with: '123'
|
||||
click_on 'Complete'
|
||||
find('#submit_form_button').click
|
||||
|
||||
expect(page).to have_button('Download')
|
||||
expect(page).to have_content('Document has been signed!')
|
||||
@@ -220,7 +220,7 @@ RSpec.describe 'Signing Form' do
|
||||
|
||||
# Cell step
|
||||
fill_in 'Cell code', with: '123'
|
||||
click_on 'Complete'
|
||||
find('#submit_form_button').click
|
||||
|
||||
expect(page).to have_button('Download')
|
||||
expect(page).to have_content('Document has been signed!')
|
||||
@@ -308,7 +308,7 @@ RSpec.describe 'Signing Form' do
|
||||
click_button 'next'
|
||||
|
||||
fill_in 'Cell code', with: '123'
|
||||
click_on 'Complete'
|
||||
find('#submit_form_button').click
|
||||
|
||||
expect(page).to have_button('Download')
|
||||
expect(page).to have_content('Document has been signed!')
|
||||
@@ -398,7 +398,7 @@ RSpec.describe 'Signing Form' do
|
||||
|
||||
# Cell step
|
||||
fill_in 'Cell code', with: '123'
|
||||
click_on 'Complete'
|
||||
find('#submit_form_button').click
|
||||
|
||||
expect(page).to have_button('Download')
|
||||
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...'
|
||||
|
||||
fill_in 'First Name', with: 'Mary'
|
||||
click_button 'Complete'
|
||||
find('#submit_form_button').click
|
||||
|
||||
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"]')
|
||||
|
||||
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!')
|
||||
|
||||
@@ -494,7 +494,7 @@ RSpec.describe 'Signing Form' do
|
||||
expect(input[:required]).to be_truthy
|
||||
|
||||
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!')
|
||||
|
||||
@@ -517,7 +517,7 @@ RSpec.describe 'Signing Form' do
|
||||
|
||||
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!')
|
||||
|
||||
@@ -539,7 +539,7 @@ RSpec.describe 'Signing Form' do
|
||||
visit submit_form_path(slug: submitter.slug)
|
||||
|
||||
check 'Do you agree?'
|
||||
click_button 'Complete'
|
||||
find('#submit_form_button').click
|
||||
|
||||
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 }
|
||||
|
||||
choose 'Boy'
|
||||
click_button 'Complete'
|
||||
find('#submit_form_button').click
|
||||
|
||||
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...'
|
||||
|
||||
fill_in 'House number', with: '4'
|
||||
click_button 'Complete'
|
||||
find('#submit_form_button').click
|
||||
|
||||
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)
|
||||
|
||||
%w[Red Green].each { |color| check color }
|
||||
click_button 'Complete'
|
||||
find('#submit_form_button').click
|
||||
|
||||
expect(page).to have_content('Form has been completed!')
|
||||
|
||||
@@ -715,7 +715,7 @@ RSpec.describe 'Signing Form' do
|
||||
|
||||
select 'Female', from: 'Gender'
|
||||
|
||||
click_button 'Complete'
|
||||
find('#submit_form_button').click
|
||||
|
||||
expect(page).to have_content('Form has been completed!')
|
||||
|
||||
@@ -738,7 +738,7 @@ RSpec.describe 'Signing Form' do
|
||||
|
||||
find('#expand_form_button').click
|
||||
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!')
|
||||
|
||||
@@ -754,7 +754,7 @@ RSpec.describe 'Signing Form' do
|
||||
find('#expand_form_button').click
|
||||
click_button 'Draw'
|
||||
draw_canvas
|
||||
click_button 'Complete'
|
||||
find('#submit_form_button').click
|
||||
|
||||
expect(page).to have_content('Document has been signed!')
|
||||
|
||||
@@ -773,7 +773,7 @@ RSpec.describe 'Signing Form' do
|
||||
|
||||
sleep 0.1
|
||||
|
||||
click_button 'Complete'
|
||||
find('#submit_form_button').click
|
||||
|
||||
expect(page).to have_content('Document has been signed!')
|
||||
|
||||
@@ -797,7 +797,7 @@ RSpec.describe 'Signing Form' do
|
||||
find('#expand_form_button').click
|
||||
find('#dropzone').click
|
||||
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!')
|
||||
|
||||
@@ -821,7 +821,7 @@ RSpec.describe 'Signing Form' do
|
||||
find('#expand_form_button').click
|
||||
find('#dropzone').click
|
||||
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!')
|
||||
|
||||
@@ -848,7 +848,7 @@ RSpec.describe 'Signing Form' do
|
||||
expect(input[:placeholder]).to eq 'Type here...'
|
||||
|
||||
fill_in 'Cell code', with: '456'
|
||||
click_button 'Complete'
|
||||
find('#submit_form_button').click
|
||||
|
||||
expect(page).to have_content('Form has been completed!')
|
||||
|
||||
@@ -967,7 +967,7 @@ RSpec.describe 'Signing Form' do
|
||||
click_button 'next'
|
||||
|
||||
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!')
|
||||
|
||||
@@ -992,7 +992,7 @@ RSpec.describe 'Signing Form' do
|
||||
click_button 'next'
|
||||
|
||||
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!')
|
||||
|
||||
@@ -1015,7 +1015,7 @@ RSpec.describe 'Signing Form' do
|
||||
click_button 'next'
|
||||
|
||||
fill_in 'Phone (optional)', with: ''
|
||||
click_button 'Complete'
|
||||
find('#submit_form_button').click
|
||||
|
||||
expect(page).to have_content('Form has been completed!')
|
||||
|
||||
@@ -1038,7 +1038,7 @@ RSpec.describe 'Signing Form' do
|
||||
click_button 'next'
|
||||
|
||||
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!')
|
||||
|
||||
@@ -1068,7 +1068,7 @@ RSpec.describe 'Signing Form' do
|
||||
visit submit_form_path(slug: first_submitter.slug)
|
||||
|
||||
fill_in 'First Name', with: 'Jahn'
|
||||
click_button 'Complete'
|
||||
find('#submit_form_button').click
|
||||
|
||||
visit submit_form_path(slug: second_submitter.slug)
|
||||
|
||||
@@ -1229,7 +1229,7 @@ RSpec.describe 'Signing Form' do
|
||||
click_button 'Submit'
|
||||
|
||||
fill_in 'First Name', with: 'Mary'
|
||||
click_button 'Complete'
|
||||
find('#submit_form_button').click
|
||||
|
||||
expect(page).to have_content('Form has been completed!')
|
||||
|
||||
@@ -1264,7 +1264,7 @@ RSpec.describe 'Signing Form' do
|
||||
click_button 'Submit'
|
||||
|
||||
fill_in 'First Name', with: 'Mary'
|
||||
click_button 'Complete'
|
||||
find('#submit_form_button').click
|
||||
|
||||
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'
|
||||
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
|
||||
|
||||
@@ -42,9 +42,7 @@ RSpec.describe 'Template' do
|
||||
|
||||
it 'archives a template' do
|
||||
expect do
|
||||
accept_confirm('Are you sure?') do
|
||||
click_button 'Archive'
|
||||
end
|
||||
click_button 'Archive'
|
||||
end.to change { Template.active.count }.by(-1)
|
||||
|
||||
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