mirror of
https://github.com/docusealco/docuseal.git
synced 2026-08-07 15:25:16 +00:00
Compare commits
84 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 | |||
| 528a1216f8 | |||
| f2479bd259 | |||
| 135f0826d2 | |||
| 5710f0177b | |||
| 1a3a0528ba | |||
| 59793ff374 | |||
| 3c4ed42419 | |||
| 6a17b61550 | |||
| 76659497b7 | |||
| ee50c957cb | |||
| a9dd200919 | |||
| 1fa953104a | |||
| 50eb5b070e | |||
| 10fd624bec | |||
| 04ec2f8260 | |||
| d828d79574 | |||
| 01dd3fefe5 | |||
| 45ae954c0c | |||
| 1304849b55 | |||
| 0875faa079 | |||
| 3aa15d6ea6 | |||
| 6557329e97 | |||
| be27ce4161 | |||
| a3391a970e |
@@ -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
|
||||
|
||||
+19
-25
@@ -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
|
||||
@@ -257,25 +257,22 @@ GEM
|
||||
os (>= 0.9, < 2.0)
|
||||
signet (>= 0.16, < 2.a)
|
||||
hashdiff (1.2.1)
|
||||
hexapdf (1.6.0)
|
||||
hexapdf (1.7.0)
|
||||
cmdparse (~> 3.0, >= 3.0.3)
|
||||
geom2d (~> 0.4, >= 0.4.1)
|
||||
openssl (>= 2.2.1)
|
||||
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,17 +305,15 @@ 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)
|
||||
multi_json (1.19.1)
|
||||
net-http (0.9.1)
|
||||
uri (>= 0.11.1)
|
||||
net-imap (0.6.3)
|
||||
net-imap (0.6.4)
|
||||
date
|
||||
net-protocol
|
||||
net-pop (0.1.2)
|
||||
@@ -328,15 +323,15 @@ GEM
|
||||
net-smtp (0.5.1)
|
||||
net-protocol
|
||||
nio4r (2.7.5)
|
||||
nokogiri (1.19.2-aarch64-linux-gnu)
|
||||
nokogiri (1.19.3-aarch64-linux-gnu)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.19.2-aarch64-linux-musl)
|
||||
nokogiri (1.19.3-aarch64-linux-musl)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.19.2-arm64-darwin)
|
||||
nokogiri (1.19.3-arm64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.19.2-x86_64-linux-gnu)
|
||||
nokogiri (1.19.3-x86_64-linux-gnu)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.19.2-x86_64-linux-musl)
|
||||
nokogiri (1.19.3-x86_64-linux-musl)
|
||||
racc (~> 1.4)
|
||||
numo-narray-alt (0.10.3)
|
||||
oj (3.16.16)
|
||||
@@ -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
|
||||
|
||||
@@ -50,7 +50,7 @@ DocuSeal is an open source platform that provides secure and efficient digital d
|
||||
- Company logo and white-label
|
||||
- User roles
|
||||
- Automated reminders
|
||||
- Invitation and identify verification via SMS
|
||||
- Invitation and identity verification via SMS
|
||||
- Conditional fields and formulas
|
||||
- Bulk send with CSV, XLSX spreadsheet import
|
||||
- SSO / SAML
|
||||
@@ -74,7 +74,7 @@ DocuSeal is an open source platform that provides secure and efficient digital d
|
||||
docker run --name docuseal -p 3000:3000 -v.:/data docuseal/docuseal
|
||||
```
|
||||
|
||||
By default DocuSeal docker container uses an SQLite database to store data and configurations. Alternatively, it is possible use PostgreSQL or MySQL databases by specifying the `DATABASE_URL` env variable.
|
||||
By default DocuSeal docker container uses an SQLite database to store data and configurations. Alternatively, it is possible to use PostgreSQL or MySQL databases by specifying the `DATABASE_URL` env variable.
|
||||
|
||||
#### Docker Compose
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -8,35 +8,37 @@ module Api
|
||||
COOKIE_STORE_LIMIT = 10
|
||||
|
||||
def create
|
||||
submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
||||
|
||||
unless can_upload?(submitter)
|
||||
Rollbar.error("Can't upload: #{submitter.id}") if defined?(Rollbar)
|
||||
@submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
||||
|
||||
unless can_upload?(@submitter)
|
||||
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)
|
||||
Rollbar.error("Empty signature: #{@submitter.id}") if defined?(Rollbar)
|
||||
|
||||
return render json: { error: "#{params[:type]} is empty" }, status: :unprocessable_content
|
||||
end
|
||||
|
||||
if ImageUtils.error?(image)
|
||||
Rollbar.error("Error signature: #{submitter.id}") if defined?(Rollbar)
|
||||
Rollbar.error("Error signature: #{@submitter.id}") if defined?(Rollbar)
|
||||
|
||||
return render json: { error: "#{params[:type]} error, try to sign on another device" },
|
||||
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)
|
||||
if params[:remember_signature] == 'true' && @submitter.email.present?
|
||||
cookies.encrypted[:signature_uuids] = build_new_cookie_signatures_json(@submitter, attachment)
|
||||
end
|
||||
|
||||
render json: attachment.as_json(only: %i[uuid created_at], methods: %i[url filename content_type])
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -5,12 +5,12 @@ class SubmitFormInviteController < ApplicationController
|
||||
skip_authorization_check
|
||||
|
||||
def create
|
||||
submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||
|
||||
return head :unprocessable_content unless can_invite?(submitter)
|
||||
return head :unprocessable_content unless can_invite?(@submitter)
|
||||
|
||||
invite_submitters = filter_invite_submitters(submitter, 'invite_by_uuid')
|
||||
optional_invite_submitters = filter_invite_submitters(submitter, 'optional_invite_by_uuid')
|
||||
invite_submitters = filter_invite_submitters(@submitter, 'invite_by_uuid')
|
||||
optional_invite_submitters = filter_invite_submitters(@submitter, 'optional_invite_by_uuid')
|
||||
|
||||
ApplicationRecord.transaction do
|
||||
(invite_submitters + optional_invite_submitters).each do |item|
|
||||
@@ -21,18 +21,18 @@ class SubmitFormInviteController < ApplicationController
|
||||
|
||||
email = Submissions.normalize_email(attrs[:email])
|
||||
|
||||
submitter.submission.submitters.create!(uuid: attrs[:uuid], email:, account_id: submitter.account_id)
|
||||
@submitter.submission.submitters.create!(uuid: attrs[:uuid], email:, account_id: @submitter.account_id)
|
||||
|
||||
SubmissionEvents.create_with_tracking_data(submitter, 'invite_party', request, { uuid: submitter.uuid })
|
||||
SubmissionEvents.create_with_tracking_data(@submitter, 'invite_party', request, { uuid: @submitter.uuid })
|
||||
end
|
||||
|
||||
submitter.submission.update!(submitters_order: :preserved)
|
||||
@submitter.submission.update!(submitters_order: :preserved)
|
||||
end
|
||||
|
||||
submitter.submission.submitters.reload
|
||||
@submitter.submission.submitters.reload
|
||||
|
||||
if invite_submitters.all? { |s| submitter.submission.submitters.any? { |e| e.uuid == s['uuid'] } }
|
||||
Submitters::SubmitValues.call(submitter, ActionController::Parameters.new(completed: 'true'), request)
|
||||
if invite_submitters.all? { |s| @submitter.submission.submitters.any? { |e| e.uuid == s['uuid'] } }
|
||||
Submitters::SubmitValues.call(@submitter, ActionController::Parameters.new(completed: 'true'), request)
|
||||
|
||||
head :ok
|
||||
else
|
||||
|
||||
@@ -5,19 +5,19 @@ class SubmitFormMetadataController < ApplicationController
|
||||
skip_authorization_check
|
||||
|
||||
def index
|
||||
submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||
|
||||
return head :not_found if submitter.declined_at? ||
|
||||
submitter.completed_at? ||
|
||||
submitter.submission.archived_at? ||
|
||||
submitter.submission.expired? ||
|
||||
submitter.submission.template&.archived_at? ||
|
||||
submitter.account.archived_at? ||
|
||||
!Submitters::AuthorizedForForm.call(submitter, current_user, request)
|
||||
return head :not_found if @submitter.declined_at? ||
|
||||
@submitter.completed_at? ||
|
||||
@submitter.submission.archived_at? ||
|
||||
@submitter.submission.expired? ||
|
||||
@submitter.submission.template&.archived_at? ||
|
||||
@submitter.account.archived_at? ||
|
||||
!Submitters::AuthorizedForForm.call(@submitter, current_user, request)
|
||||
|
||||
submission = submitter.submission
|
||||
submission = @submitter.submission
|
||||
values = submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) }
|
||||
schema = Submissions.filtered_conditions_schema(submission, values:, include_submitter_uuid: submitter.uuid)
|
||||
schema = Submissions.filtered_conditions_schema(submission, values:, include_submitter_uuid: @submitter.uuid)
|
||||
|
||||
documents = schema.filter_map do |item|
|
||||
submission.schema_documents.find { |a| a.uuid == item['attachment_uuid'] }
|
||||
|
||||
@@ -5,21 +5,21 @@ class SubmitFormValuesController < ApplicationController
|
||||
skip_authorization_check
|
||||
|
||||
def index
|
||||
submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||
|
||||
return render json: {} if submitter.completed_at? ||
|
||||
submitter.declined_at? ||
|
||||
submitter.submission.template&.archived_at? ||
|
||||
submitter.submission.archived_at? ||
|
||||
submitter.submission.expired? ||
|
||||
!Submitters::AuthorizedForForm.call(submitter, current_user, request)
|
||||
return render json: {} if @submitter.completed_at? ||
|
||||
@submitter.declined_at? ||
|
||||
@submitter.submission.template&.archived_at? ||
|
||||
@submitter.submission.archived_at? ||
|
||||
@submitter.submission.expired? ||
|
||||
!Submitters::AuthorizedForForm.call(@submitter, current_user, request)
|
||||
|
||||
value = submitter.values[params['field_uuid']]
|
||||
attachment = submitter.attachments.where(created_at: params[:after]..).find_by(uuid: value) if value.present?
|
||||
value = @submitter.values[params['field_uuid']]
|
||||
attachment = @submitter.attachments.where(created_at: params[:after]..).find_by(uuid: value) if value.present?
|
||||
|
||||
render json: {
|
||||
value:,
|
||||
attachment: attachment&.as_json(only: %i[uuid created_at], methods: %i[url filename content_type])
|
||||
}, head: :ok
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class TemplatesVersionsController < ApplicationController
|
||||
load_and_authorize_resource :template
|
||||
|
||||
def index
|
||||
versions = @template.template_versions.order(id: :desc).preload(:author)
|
||||
|
||||
render json: versions.as_json(TemplateVersions::SERIALIZE_PARAMS)
|
||||
end
|
||||
|
||||
def show
|
||||
version = @template.template_versions.find(params[:id])
|
||||
|
||||
render json: TemplateVersions.serialize(version)
|
||||
end
|
||||
|
||||
def create
|
||||
authorize!(:update, @template)
|
||||
|
||||
TemplateVersions.find_or_create_for(@template, author: current_user)
|
||||
|
||||
head :ok
|
||||
end
|
||||
end
|
||||
@@ -11,6 +11,12 @@ class UserInitialsController < ApplicationController
|
||||
|
||||
return redirect_to settings_profile_index_path, notice: I18n.t('unable_to_save_initials') if file.blank?
|
||||
|
||||
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)
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class WebhookHmacController < ApplicationController
|
||||
load_and_authorize_resource :webhook_url, parent: false
|
||||
|
||||
def show; end
|
||||
end
|
||||
@@ -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 () {
|
||||
@@ -170,6 +172,8 @@ safeRegisterElement('template-builder', class extends HTMLElement {
|
||||
withLogo: this.dataset.withLogo !== 'false',
|
||||
withFieldsDetection: this.dataset.withFieldsDetection === 'true',
|
||||
withDetectExistingFields: this.dataset.withDetectExistingFields === 'true',
|
||||
withRevisions: true,
|
||||
withRevisionsMenu: this.dataset.withRevisionsMenu === 'true',
|
||||
editable: this.dataset.editable !== 'false',
|
||||
authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content,
|
||||
withCustomFields: true,
|
||||
@@ -195,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',
|
||||
@@ -136,7 +150,7 @@ const es = {
|
||||
verification_code_is_invalid: 'El código de verificación no es válido',
|
||||
sign_on_the_touchscreen: 'Firmar en pantalla táctil',
|
||||
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Escanea el código QR con la aplicación de la cámara para abrir el formulario en el móvil y dibujar tu firma',
|
||||
by_clicking_you_agree_to_the: 'Al hacer clic en "{button}", usted acepta el',
|
||||
by_clicking_you_agree_to_the: 'Al hacer clic en "{button}", usted acepta la',
|
||||
electronic_signature_disclosure: 'Divulgación de Firma Electrónica',
|
||||
esignature_disclosure: 'Divulgación de eFirma',
|
||||
already_paid: 'Ya pagado',
|
||||
@@ -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à",
|
||||
@@ -245,7 +266,7 @@ const it = {
|
||||
verification_code_is_invalid: 'Il codice di verifica non è valido',
|
||||
sign_on_the_touchscreen: 'Firma su schermo tattile',
|
||||
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: "Scansiona il codice QR con l'app della fotocamera per aprire il modulo sul cellulare e disegnare la tua firma",
|
||||
by_clicking_you_agree_to_the: 'Cliccando su "{button}", accetti il',
|
||||
by_clicking_you_agree_to_the: 'Cliccando su "{button}", accetti la',
|
||||
electronic_signature_disclosure: 'Divulgazione della Firma Elettronica',
|
||||
esignature_disclosure: 'Divulgazione della eFirma',
|
||||
minimize: 'Minimizza',
|
||||
@@ -258,7 +279,7 @@ const it = {
|
||||
date: 'Data',
|
||||
number: 'Numero',
|
||||
image: 'Immagine',
|
||||
pay: 'Pagamento',
|
||||
pay: 'Paga',
|
||||
file: 'File',
|
||||
select: 'Seleziona',
|
||||
checkbox: 'Checkbox',
|
||||
@@ -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',
|
||||
@@ -574,7 +616,7 @@ const pl = {
|
||||
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Zeskanuj kod QR za pomocą aplikacji aparatu, aby otworzyć formularz na telefonie i narysować swój podpis',
|
||||
by_clicking_you_agree_to_the: 'Klikając na "{button}", zgadzasz się na',
|
||||
electronic_signature_disclosure: 'Ujawnienie Elektronicznej Sygnatury',
|
||||
esignature_disclosure: 'Ujawnienie ePodpisu',
|
||||
esignature_disclosure: 'informację o e-podpisie',
|
||||
minimize: 'Zminimalizuj',
|
||||
text: 'Tekst',
|
||||
already_paid: 'Już zapłacono',
|
||||
@@ -642,7 +684,7 @@ const pl = {
|
||||
toggle_multiline_text: 'Przełącz Tekst Wielolinijkowy',
|
||||
email_has_been_sent: 'E-mail został wysłany',
|
||||
processing: 'Przetwarzanie',
|
||||
pay_with_stripe: 'Płatność za pomocą Stripe',
|
||||
pay_with_stripe: 'Zapłać za pomocą Stripe',
|
||||
reupload: 'Ponowne przesłanie',
|
||||
upload: 'Przesyłanie',
|
||||
files: 'Pliki',
|
||||
@@ -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: 'Ідентифікація особи',
|
||||
@@ -678,12 +727,12 @@ const uk = {
|
||||
authored_by: 'Автор',
|
||||
select_a_reason: 'Виберіть причину',
|
||||
value_is_invalid: 'Значення є неправильним',
|
||||
verification_code_is_invalid: 'Код підтвердження є неправильним',
|
||||
verification_code_is_invalid: 'Невірний код підтвердження',
|
||||
sign_on_the_touchscreen: 'Підписати на сенсорному екрані',
|
||||
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Скануйте QR-код за допомогою програми камери, щоб відкрити форму на мобільному пристрої та намалювати свій підпис',
|
||||
by_clicking_you_agree_to_the: 'Натиснувши на "{button}", ви погоджуєтеся з',
|
||||
electronic_signature_disclosure: 'Розголошення Електронного Підпису',
|
||||
esignature_disclosure: 'Розголошення еПідпису',
|
||||
electronic_signature_disclosure: 'інформацією про е-підпис',
|
||||
esignature_disclosure: 'інформацією про е-підпис',
|
||||
minimize: 'Зменшити',
|
||||
text: 'Текст',
|
||||
already_paid: 'Вже оплачено',
|
||||
@@ -751,7 +800,7 @@ const uk = {
|
||||
toggle_multiline_text: 'Перемкнути Багаторядковий Текст',
|
||||
email_has_been_sent: 'Електронний лист був відправлений',
|
||||
processing: 'Обробка',
|
||||
pay_with_strip: 'Сплатити за допомогою Stripe',
|
||||
pay_with_stripe: 'Сплатити за допомогою Stripe',
|
||||
reupload: 'Перезавантажити',
|
||||
upload: 'Завантажити',
|
||||
files: 'Файли',
|
||||
@@ -770,11 +819,18 @@ 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',
|
||||
complete: 'Dokončit',
|
||||
fill_all_required_fields_to_complete: 'Please complete all mandatory fields',
|
||||
fill_all_required_fields_to_complete: 'Vyplňte povinná pole pro dokončení',
|
||||
sign_and_complete: 'Podepsat a dokončit',
|
||||
invite: 'Pozvat',
|
||||
email: 'E-mail',
|
||||
@@ -782,8 +838,8 @@ const cs = {
|
||||
reviewed: 'Zkontrolováno',
|
||||
other: 'Jiné',
|
||||
authored_by_me: 'Autorem jsem já',
|
||||
approved_by: 'Schváleno kým',
|
||||
reviewed_by: 'Zkontrolováno kým',
|
||||
approved_by: 'Schválil(a)',
|
||||
reviewed_by: 'Zkontroloval(a)',
|
||||
authored_by: 'Autorem',
|
||||
select_a_reason: 'Vyberte důvod',
|
||||
value_is_invalid: 'Hodnota je neplatná',
|
||||
@@ -792,7 +848,7 @@ const cs = {
|
||||
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Naskenujte QR kód pomocí aplikace fotoaparátu, abyste otevřeli formulář na mobilním zařízení a nakreslili svůj podpis',
|
||||
by_clicking_you_agree_to_the: 'Kliknutím na "{button}" souhlasíte s',
|
||||
electronic_signature_disclosure: 'Zveřejněním Elektronického Podpisu',
|
||||
esignature_disclosure: 'Zveřejnění ePodpisu',
|
||||
esignature_disclosure: 'informací o e-podpisu',
|
||||
already_paid: 'Už zaplaceno',
|
||||
minimize: 'Minimalizovat',
|
||||
text: 'Text',
|
||||
@@ -860,7 +916,7 @@ const cs = {
|
||||
toggle_multiline_text: 'Přepnout Víceřádkový Text',
|
||||
email_has_been_sent: 'E-mail byl odeslán',
|
||||
processing: 'Zpracování',
|
||||
pay_with_stripe: 'Zaplacení přes Stripe',
|
||||
pay_with_stripe: 'Zaplatit přes Stripe',
|
||||
reupload: 'Znovu nahrát',
|
||||
upload: 'Nahrát',
|
||||
files: 'Soubory',
|
||||
@@ -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',
|
||||
@@ -890,18 +953,18 @@ const pt = {
|
||||
approved: 'Aprovado',
|
||||
reviewed: 'Revisado',
|
||||
other: 'Outro',
|
||||
authored_by_me: 'Autorizado por mim',
|
||||
authored_by_me: 'Escrito por mim',
|
||||
approved_by: 'Aprovado por',
|
||||
reviewed_by: 'Revisado por',
|
||||
authored_by: 'Autorizado por',
|
||||
authored_by: 'Escrito por',
|
||||
select_a_reason: 'Selecione um motivo',
|
||||
value_is_invalid: 'Valor é inválido',
|
||||
verification_code_is_invalid: 'Código de verificação é inválido',
|
||||
sign_on_the_touchscreen: 'Assinar na tela sensível',
|
||||
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Escaneie o código QR com o aplicativo da câmera para abrir o formulário no celular e desenhar sua assinatura',
|
||||
by_clicking_you_agree_to_the: 'Ao clicar em "{button}", você concorda com o',
|
||||
by_clicking_you_agree_to_the: 'Ao clicar em "{button}", você concorda com a',
|
||||
electronic_signature_disclosure: 'Divulgação de Assinatura Eletrônica',
|
||||
esignature_disclosure: 'Divulgação da eAssinatura',
|
||||
esignature_disclosure: 'Informação sobre eAssinatura',
|
||||
already_paid: 'Já pago',
|
||||
minimize: 'Minimizar',
|
||||
text: 'Texto',
|
||||
@@ -970,7 +1033,7 @@ const pt = {
|
||||
email_has_been_sent: 'Email enviado',
|
||||
processing: 'Processamento',
|
||||
pay_with_stripe: 'Pagar com Stripe',
|
||||
reupload: 'Reenviar',
|
||||
reupload: 'Recarregar',
|
||||
upload: 'Carregar',
|
||||
files: 'Arquivos',
|
||||
signature_is_too_small_or_simple_please_redraw: 'A assinatura é muito pequena ou simples. Por favor, redesenhe.',
|
||||
@@ -988,6 +1051,13 @@ const he = {
|
||||
kba: 'KBA',
|
||||
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: 'אימות זהות',
|
||||
@@ -1010,8 +1080,8 @@ const he = {
|
||||
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'סרוק את קוד ה-QR באמצעות אפליקציית המצלמה כדי לפתוח את הטופס במובייל ולצייר את החתימה שלך',
|
||||
by_clicking_you_agree_to_the: 'על ידי לחיצה על "{button}", אתה מסכים ל',
|
||||
electronic_signature_disclosure: 'חשיפת חתימה אלקטרונית',
|
||||
esignature_disclosure: 'חשיפת ה-eחתימה',
|
||||
minimize: 'לקטן',
|
||||
esignature_disclosure: 'חשיפת חתימה אלקטרונית',
|
||||
minimize: 'מזער',
|
||||
text: 'טקסט',
|
||||
already_paid: 'כבר שולם',
|
||||
signature: 'חתימה',
|
||||
@@ -1022,9 +1092,9 @@ const he = {
|
||||
number: 'מספר',
|
||||
image: 'תמונה',
|
||||
file: 'קובץ',
|
||||
pay: 'תשלום',
|
||||
pay: 'שלם',
|
||||
select: 'בחר',
|
||||
checkbox: 'תיק בחירה',
|
||||
checkbox: 'סימון',
|
||||
multiple: 'רב ערכים',
|
||||
radio: 'רדיו',
|
||||
cells: 'תאים',
|
||||
@@ -1051,9 +1121,9 @@ const he = {
|
||||
download: 'הורדה',
|
||||
clear: 'נקה',
|
||||
redraw: 'צייר מחדש',
|
||||
draw_initials: 'צייר ציוני ראשי תיבות',
|
||||
draw_initials: 'צייר ראשי תיבות',
|
||||
type_signature_here: 'הקלד חתימה כאן',
|
||||
type_initial_here: 'הקלד ציוני ראשי תיבות כאן',
|
||||
type_initial_here: 'הקלד ראשי תיבות כאן',
|
||||
form_has_been_completed: 'הטופס הושלם',
|
||||
document_has_been_signed: 'המסמך נחתם!',
|
||||
documents_have_been_signed: 'המסמכים נחתמו!',
|
||||
@@ -1063,7 +1133,7 @@ const he = {
|
||||
open_source_documents_software: 'תוכנה פתוחה למסמכים',
|
||||
verified_phone_number: 'אימות מספר טלפון',
|
||||
use_international_format: 'השתמש בפורמט בינלאומי: +1xxx',
|
||||
six_digits_code: 'קוד משתמש שש ספרות',
|
||||
six_digits_code: 'קוד שש ספרות',
|
||||
change_phone_number: 'שינוי מספר טלפון',
|
||||
sending: 'שולח',
|
||||
resend_code: 'שלח מחדש קוד',
|
||||
@@ -1072,7 +1142,7 @@ const he = {
|
||||
set_today: 'קבע היום',
|
||||
toggle_multiline_text: 'שנה בין טקסט במספר שורות לטקסט בשורה אחת',
|
||||
draw_signature: 'צייר חתימה',
|
||||
type_initial: 'הקלד ציוני ראשי תיבות',
|
||||
type_initial: 'הקלד ראשי תיבות',
|
||||
draw: 'צייר',
|
||||
type: 'הקלד',
|
||||
type_text: 'הקלד טקסט',
|
||||
@@ -1097,8 +1167,15 @@ 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: 'Verifiëren ID',
|
||||
verify_id: 'ID verifiëren',
|
||||
identity_verification: 'Identiteitsverificatie',
|
||||
complete: 'Voltooien',
|
||||
fill_all_required_fields_to_complete: 'Vul alle verplichte velden in om te voltooien',
|
||||
@@ -1146,7 +1223,7 @@ const nl = {
|
||||
continue: 'Doorgaan',
|
||||
sign_now: 'Nu ondertekenen',
|
||||
type_here_: 'Typ hier...',
|
||||
optional: 'Optioneel',
|
||||
optional: 'optioneel',
|
||||
option: 'Optie',
|
||||
appears_on: 'Verschijnt op',
|
||||
page: 'Pagina',
|
||||
@@ -1170,11 +1247,11 @@ const nl = {
|
||||
powered_by: 'Aangedreven door',
|
||||
please_check_the_box_to_continue: 'Vink het vakje aan om door te gaan.',
|
||||
open_source_documents_software: 'Open source documenten software',
|
||||
verified_phone_number: 'Geverifieerd telefoonnummer',
|
||||
verified_phone_number: 'Telefoonnummer verifiëren',
|
||||
use_international_format: 'Gebruik internationaal formaat: +1xxx',
|
||||
six_digits_code: '6-cijferige code',
|
||||
change_phone_number: 'Wijzig telefoonnummer',
|
||||
sending: 'Voltooien...',
|
||||
sending: 'Verzenden...',
|
||||
resend_code: 'Code opnieuw verzenden',
|
||||
verification_code_has_been_resent: 'Verificatiecode is opnieuw verzonden via SMS',
|
||||
please_fill_all_required_fields: 'Vul alle verplichte velden in',
|
||||
@@ -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: 'التحقق من الهوية',
|
||||
@@ -1232,7 +1316,7 @@ const ar = {
|
||||
esignature_disclosure: 'كشف التوقيع الإلكتروني',
|
||||
text: 'نص',
|
||||
signature: 'توقيع',
|
||||
initials: 'الاختصارات',
|
||||
initials: 'حروف أولى',
|
||||
date: 'تاريخ',
|
||||
number: 'رقم',
|
||||
digitally_signed_by: 'تم التوقيع رقميًا بواسطة',
|
||||
@@ -1269,9 +1353,9 @@ const ar = {
|
||||
download: 'تحميل',
|
||||
clear: 'مسح',
|
||||
redraw: 'إعادة الرسم',
|
||||
draw_initials: 'ارسم الاختصارات',
|
||||
draw_initials: 'ارسم حروف أولى',
|
||||
type_signature_here: 'اكتب التوقيع هنا',
|
||||
type_initial_here: 'اكتب الاختصارات هنا',
|
||||
type_initial_here: 'اكتب حروف أولى هنا',
|
||||
form_has_been_completed: 'تم إكمال النموذج!',
|
||||
document_has_been_signed: 'تم توقيع الوثيقة!',
|
||||
documents_have_been_signed: 'تم توقيع الوثائق!',
|
||||
@@ -1290,13 +1374,13 @@ const ar = {
|
||||
set_today: 'تعيين اليوم',
|
||||
toggle_multiline_text: 'تبديل النصوص متعددة الأسطر',
|
||||
draw_signature: 'ارسم التوقيع',
|
||||
type_initial: 'اكتب الاختصارات',
|
||||
type_initial: 'اكتب حروف أولى',
|
||||
draw: 'ارسم',
|
||||
type: 'اكتب',
|
||||
type_text: 'اكتب نصًا',
|
||||
email_has_been_sent: 'تم إرسال البريد الإلكتروني',
|
||||
processing: 'جارٍ المعالجة',
|
||||
pay_with_strip: 'الدفع بواسطة Stripe',
|
||||
pay_with_stripe: 'الدفع بواسطة Stripe',
|
||||
reupload: 'إعادة التحميل',
|
||||
upload: 'تحميل',
|
||||
files: 'الملفات',
|
||||
@@ -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: '신원 확인',
|
||||
@@ -1343,10 +1434,10 @@ const ko = {
|
||||
signature: '서명',
|
||||
initials: '이니셜',
|
||||
date: '날짜',
|
||||
digitally_signed_by: '디지털 서명이 완료되었습니다',
|
||||
digitally_signed_by: '디지털 서명자:',
|
||||
reason: '이유',
|
||||
number: '숫자',
|
||||
pay: '급여',
|
||||
pay: '결제',
|
||||
image: '이미지',
|
||||
take_photo: '사진 찍기',
|
||||
file: '파일',
|
||||
@@ -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 ''
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
@remove="$emit('remove')"
|
||||
@scroll-to="$emit('scroll-to', $event)"
|
||||
@add-custom-field="$emit('add-custom-field')"
|
||||
@click-title="$emit('click-title')"
|
||||
/>
|
||||
<div
|
||||
ref="touchValueTarget"
|
||||
@@ -317,7 +318,7 @@ export default {
|
||||
default: false
|
||||
}
|
||||
},
|
||||
emits: ['start-resize', 'stop-resize', 'start-drag', 'stop-drag', 'remove', 'scroll-to', 'add-custom-field'],
|
||||
emits: ['start-resize', 'stop-resize', 'start-drag', 'stop-drag', 'remove', 'scroll-to', 'add-custom-field', 'click-title'],
|
||||
data () {
|
||||
return {
|
||||
isContenteditable: false,
|
||||
@@ -619,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
|
||||
}
|
||||
@@ -715,14 +720,6 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
drag (e) {
|
||||
if (e.target.id === 'mask' && this.editable) {
|
||||
this.isDragged = true
|
||||
|
||||
this.area.x = (e.offsetX - this.dragFrom.x) / e.target.clientWidth
|
||||
this.area.y = (e.offsetY - this.dragFrom.y) / e.target.clientHeight
|
||||
}
|
||||
},
|
||||
startTouchDrag (e) {
|
||||
if (e.target !== this.$refs.touchTarget && e.target !== this.$refs.touchValueTarget) {
|
||||
return
|
||||
@@ -768,6 +765,8 @@ export default {
|
||||
|
||||
this.maybeChangeAreaPage(this.area)
|
||||
|
||||
this.clampAreaBounds(this.area)
|
||||
|
||||
if (this.isDragged) {
|
||||
this.save()
|
||||
}
|
||||
@@ -835,6 +834,8 @@ export default {
|
||||
|
||||
this.maybeChangeAreaPage(this.area)
|
||||
|
||||
this.clampAreaBounds(this.area)
|
||||
|
||||
if (this.isMoved) {
|
||||
this.save()
|
||||
}
|
||||
@@ -859,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]
|
||||
@@ -885,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()
|
||||
@@ -923,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
v-if="field?.type && (isSelected || isNameFocus) && !isInMultiSelection"
|
||||
class="absolute bg-white rounded-t border overflow-visible whitespace-nowrap flex z-10 field-area-controls"
|
||||
style="top: -25px; height: 25px"
|
||||
@mousedown.stop
|
||||
@mousedown.stop="$emit('click-title')"
|
||||
@pointerdown.stop
|
||||
>
|
||||
<FieldSubmitter
|
||||
@@ -279,7 +279,7 @@ export default {
|
||||
default: false
|
||||
}
|
||||
},
|
||||
emits: ['remove', 'scroll-to', 'add-custom-field', 'change'],
|
||||
emits: ['remove', 'scroll-to', 'add-custom-field', 'change', 'click-title'],
|
||||
data () {
|
||||
return {
|
||||
isShowFormulaModal: false,
|
||||
|
||||
@@ -51,6 +51,30 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="beforeRevisionSnapshot"
|
||||
class="top-1.5 sticky h-0 z-20 max-w-2xl mx-auto"
|
||||
>
|
||||
<div class="alert border-base-content/30 py-2 px-2.5">
|
||||
<IconInfoCircle class="stroke-info shrink-0 w-6 h-6" />
|
||||
<span>{{ t('viewing_revision_from').replace('{date}', formatRevisionTime(beforeRevisionSnapshot.revision.created_at)) }}</span>
|
||||
<div>
|
||||
<button
|
||||
class="btn btn-sm"
|
||||
@click.prevent="cancelRevision"
|
||||
>
|
||||
{{ t('cancel') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="editable"
|
||||
class="btn btn-sm btn-neutral text-white"
|
||||
@click.prevent="applyRevision"
|
||||
>
|
||||
{{ t('apply') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="$slots.buttons || withTitle"
|
||||
id="title_container"
|
||||
@@ -213,6 +237,18 @@
|
||||
<span class="whitespace-nowrap">{{ t('preferences') }}</span>
|
||||
</a>
|
||||
</li>
|
||||
<li v-if="withRevisionsMenu">
|
||||
<button
|
||||
class="flex space-x-2"
|
||||
@click.prevent="openRevisionsModal"
|
||||
@mouseenter="preloadRevisions"
|
||||
>
|
||||
<span class="w-6 h-6 flex-shrink-0 flex items-center justify-center">
|
||||
<IconHistory class="w-5 h-5" />
|
||||
</span>
|
||||
<span class="whitespace-nowrap">{{ t('revisions') }}</span>
|
||||
</button>
|
||||
</li>
|
||||
<li v-if="withDownload">
|
||||
<button
|
||||
class="flex space-x-2"
|
||||
@@ -600,7 +636,16 @@
|
||||
<div
|
||||
id="docuseal_modal_container"
|
||||
class="modal-container"
|
||||
/>
|
||||
>
|
||||
<RevisionsModal
|
||||
v-if="isRevisionsModalOpen"
|
||||
:template="template"
|
||||
:revisions="revisions"
|
||||
:locale="locale"
|
||||
@close="isRevisionsModalOpen = false"
|
||||
@apply="onRevisionApply"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -618,7 +663,8 @@ import DocumentPreview from './preview'
|
||||
import DocumentControls from './controls'
|
||||
import MobileFields from './mobile_fields'
|
||||
import FieldSubmitter from './field_submitter'
|
||||
import { IconPlus, IconUsersPlus, IconDeviceFloppy, IconChevronDown, IconEye, IconWritingSign, IconInnerShadowTop, IconInfoCircle, IconAdjustments, IconDownload } from '@tabler/icons-vue'
|
||||
import RevisionsModal from './revisions_modal'
|
||||
import { IconPlus, IconUsersPlus, IconDeviceFloppy, IconChevronDown, IconEye, IconWritingSign, IconInnerShadowTop, IconInfoCircle, IconAdjustments, IconDownload, IconHistory } from '@tabler/icons-vue'
|
||||
import { v4 } from 'uuid'
|
||||
import { ref, computed, toRaw, defineAsyncComponent } from 'vue'
|
||||
import * as i18n from './i18n'
|
||||
@@ -658,7 +704,9 @@ export default {
|
||||
IconDownload,
|
||||
IconAdjustments,
|
||||
IconEye,
|
||||
IconDeviceFloppy
|
||||
IconHistory,
|
||||
IconDeviceFloppy,
|
||||
RevisionsModal
|
||||
},
|
||||
provide () {
|
||||
return {
|
||||
@@ -980,6 +1028,16 @@ export default {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
withRevisions: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
withRevisionsMenu: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data () {
|
||||
@@ -1002,7 +1060,10 @@ export default {
|
||||
drawOption: null,
|
||||
dragField: null,
|
||||
isDragFile: false,
|
||||
isMathLoaded: false
|
||||
isMathLoaded: false,
|
||||
isRevisionsModalOpen: false,
|
||||
revisions: [],
|
||||
beforeRevisionSnapshot: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -1767,6 +1828,73 @@ export default {
|
||||
closeDropdown () {
|
||||
document.activeElement.blur()
|
||||
},
|
||||
preloadRevisions () {
|
||||
this.loadRevisionsPromise ||= this.baseFetch(`/templates/${this.template.id}/versions`)
|
||||
},
|
||||
openRevisionsModal () {
|
||||
this.closeDropdown()
|
||||
|
||||
this.loadRevisionsPromise ||= this.baseFetch(`/templates/${this.template.id}/versions`)
|
||||
|
||||
this.loadRevisionsPromise.then(async (resp) => {
|
||||
this.revisions = await resp.json()
|
||||
|
||||
this.isRevisionsModalOpen = true
|
||||
}).finally(() => {
|
||||
this.loadRevisionsPromise = null
|
||||
})
|
||||
},
|
||||
onRevisionApply (revision) {
|
||||
this.beforeRevisionSnapshot = {
|
||||
template: JSON.parse(JSON.stringify(this.template)),
|
||||
dynamicDocuments: JSON.parse(JSON.stringify(this.dynamicDocuments)),
|
||||
revision
|
||||
}
|
||||
|
||||
const { dynamic_documents: nextDynamicDocs = [], ...nextTemplate } = revision.data
|
||||
|
||||
Object.assign(this.template, nextTemplate)
|
||||
|
||||
this.dynamicDocuments.splice(0, this.dynamicDocuments.length, ...nextDynamicDocs)
|
||||
|
||||
this.$nextTick(() => this.reloadDynamicDocumentContent())
|
||||
|
||||
this.isRevisionsModalOpen = false
|
||||
},
|
||||
cancelRevision () {
|
||||
Object.assign(this.template, this.beforeRevisionSnapshot.template)
|
||||
|
||||
this.dynamicDocuments.splice(0, this.dynamicDocuments.length, ...this.beforeRevisionSnapshot.dynamicDocuments)
|
||||
|
||||
this.beforeRevisionSnapshot = null
|
||||
|
||||
this.$nextTick(() => this.reloadDynamicDocumentContent())
|
||||
},
|
||||
applyRevision () {
|
||||
this.beforeRevisionSnapshot = null
|
||||
|
||||
const dynamicDocumentRefs = this.documentRefs.filter((ref) => ref.isDynamic)
|
||||
|
||||
dynamicDocumentRefs.forEach((ref) => ref.update())
|
||||
|
||||
this.rebuildVariablesSchema({ disable: false })
|
||||
|
||||
return Promise.all([this.save({ force: true }), ...dynamicDocumentRefs.map((ref) => ref.saveBody())])
|
||||
},
|
||||
reloadDynamicDocumentContent () {
|
||||
this.documentRefs.forEach((ref) => {
|
||||
if (ref.isDynamic) ref.reloadContent()
|
||||
})
|
||||
},
|
||||
formatRevisionTime (string) {
|
||||
return new Date(string).toLocaleString(this.locale || undefined, {
|
||||
year: 'numeric',
|
||||
month: 'numeric',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: '2-digit'
|
||||
})
|
||||
},
|
||||
t (key) {
|
||||
return this.i18n[key] || i18n[this.language]?.[key] || i18n.en[key] || key
|
||||
},
|
||||
@@ -2384,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)
|
||||
@@ -3013,7 +3146,11 @@ export default {
|
||||
|
||||
const dynamicDocumentSaves = dynamicDocumentRefs.map((ref) => ref.saveBody())
|
||||
|
||||
Promise.all([this.save(), ...dynamicDocumentSaves]).then(() => {
|
||||
Promise.all([this.save({ force: true }), ...dynamicDocumentSaves]).then(() => {
|
||||
if (this.withRevisions) {
|
||||
this.captureRevision()
|
||||
}
|
||||
|
||||
window.Turbo.visit(`/templates/${this.template.id}`)
|
||||
}).finally(() => {
|
||||
this.isSaving = false
|
||||
@@ -3244,9 +3381,13 @@ export default {
|
||||
}
|
||||
})
|
||||
},
|
||||
save ({ force } = { force: false }) {
|
||||
save ({ force = false } = {}) {
|
||||
this.pendingFieldAttachmentUuids = []
|
||||
|
||||
if (this.beforeRevisionSnapshot) {
|
||||
this.beforeRevisionSnapshot = null
|
||||
}
|
||||
|
||||
if (this.onChange) {
|
||||
this.onChange(this.template)
|
||||
}
|
||||
@@ -3281,6 +3422,12 @@ export default {
|
||||
}
|
||||
})
|
||||
},
|
||||
captureRevision () {
|
||||
return this.baseFetch(`/templates/${this.template.id}/versions`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
},
|
||||
onDynamicDocumentUpdate () {
|
||||
this.rebuildVariablesSchema()
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
class="absolute top-0 bottom-0 right-0 left-0"
|
||||
@click.prevent="$emit('close')"
|
||||
/>
|
||||
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl">
|
||||
<div class="modal-box pt-4 pb-6 px-6 mt-20 w-full max-w-xl">
|
||||
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
|
||||
<span class="modal-title">
|
||||
{{ t('condition') }} - {{ (defaultField ? (defaultField.title || item.title || item.name) : item.name) || buildDefaultName(item) }}
|
||||
|
||||
@@ -183,6 +183,9 @@ export default {
|
||||
this.sectionRefs.push(ref)
|
||||
}
|
||||
},
|
||||
reloadContent () {
|
||||
this.sectionRefs.forEach((ref) => ref.reloadContent())
|
||||
},
|
||||
onBeforeUnload (event) {
|
||||
if (this.saveTimer) {
|
||||
event.preventDefault()
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -281,6 +281,9 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
reloadContent () {
|
||||
this.editor.commands.setContent(this.section.innerHTML, { emitUpdate: false })
|
||||
},
|
||||
findAreaNodePos (areaUuid) {
|
||||
const el = this.editor.view.dom.querySelector(`[data-area-uuid="${areaUuid}"]`)
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -147,7 +147,7 @@
|
||||
>
|
||||
<a
|
||||
href="#"
|
||||
class="flex px-2 group justify-between items-center"
|
||||
class="flex px-2 text-sm group justify-between items-center"
|
||||
:class="{ 'active': submitter === selectedSubmitter, 'py-0.5': submitters.length > 8 }"
|
||||
@click.prevent="selectSubmitter(submitter)"
|
||||
>
|
||||
@@ -198,7 +198,7 @@
|
||||
>
|
||||
<a
|
||||
href="#"
|
||||
class="flex px-2"
|
||||
class="flex px-2 text-sm"
|
||||
@click.prevent="addSubmitter"
|
||||
>
|
||||
<IconUserPlus
|
||||
|
||||
@@ -383,7 +383,7 @@
|
||||
class="rounded py-2 px-4 w-full border border-dashed border-base-300"
|
||||
>
|
||||
<div class="text-center text-sm">
|
||||
{{ t('start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document') }}
|
||||
{{ t('start_a_quick_tour_to_learn_how_to_create_and_send_your_first_document') }}
|
||||
</div>
|
||||
<div class="flex justify-center">
|
||||
<label
|
||||
|
||||
@@ -200,7 +200,7 @@ const en = {
|
||||
learn_more: 'Learn more',
|
||||
and: 'and',
|
||||
or: 'or',
|
||||
start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: 'Start a quick tour to learn how to create and send your first document',
|
||||
start_a_quick_tour_to_learn_how_to_create_and_send_your_first_document: 'Start a quick tour to learn how to create and send your first document',
|
||||
start_tour: 'Start Tour',
|
||||
or_add_from: 'Or add from',
|
||||
sync: 'Sync',
|
||||
@@ -215,7 +215,11 @@ const en = {
|
||||
align_bottom: 'Align Bottom',
|
||||
fields_selected: '{count} Fields Selected',
|
||||
field_added: '{count} Field Added',
|
||||
fields_added: '{count} Fields Added'
|
||||
fields_added: '{count} Fields Added',
|
||||
revisions: 'Revisions',
|
||||
apply: 'Apply',
|
||||
no_revisions_yet: 'No revisions yet',
|
||||
viewing_revision_from: 'Viewing revision from {date}'
|
||||
}
|
||||
|
||||
const es = {
|
||||
@@ -239,7 +243,7 @@ const es = {
|
||||
min: 'Mín',
|
||||
max: 'Máx',
|
||||
date_signed: 'Fecha actual',
|
||||
fuente: 'Fuente',
|
||||
font: 'Fuente',
|
||||
party: 'Parte',
|
||||
method: 'Método',
|
||||
reorder_fields: 'Reordenar campos',
|
||||
@@ -298,7 +302,7 @@ const es = {
|
||||
set_signing_date: 'Establecer fecha de firma',
|
||||
are_you_sure_: '¿Estás seguro?',
|
||||
sign_yourself: 'Firma tú mismo',
|
||||
checked: 'Seleccionado',
|
||||
checked: 'Marcado',
|
||||
current_date: 'Fecha actual',
|
||||
send: 'Enviar',
|
||||
remove: 'Eliminar',
|
||||
@@ -336,16 +340,16 @@ const es = {
|
||||
eighth_party: 'Octava Parte',
|
||||
ninth_party: 'Novena Parte',
|
||||
tenth_party: 'Décima Parte',
|
||||
eleventh_party: 'Undécimo Partido',
|
||||
twelfth_party: 'Duodécimo Partido',
|
||||
thirteenth_party: 'Decimotercer Partido',
|
||||
fourteenth_party: 'Catorceavo Partido',
|
||||
fifteenth_party: 'Quinceavo Partido',
|
||||
sixteenth_party: 'Dieciséisavo Partido',
|
||||
seventeenth_party: 'Diecisieteavo Partido',
|
||||
eighteenth_party: 'Dieciochoavo Partido',
|
||||
nineteenth_party: 'Decimonovena Fiesta',
|
||||
twentieth_party: 'Vigésima Fiesta',
|
||||
eleventh_party: 'Undécima Parte',
|
||||
twelfth_party: 'Duodécima Parte',
|
||||
thirteenth_party: 'Decimotercera Parte',
|
||||
fourteenth_party: 'Decimocuarta Parte',
|
||||
fifteenth_party: 'Decimoquinta Parte',
|
||||
sixteenth_party: 'Decimosexta Parte',
|
||||
seventeenth_party: 'Decimoséptima Parte',
|
||||
eighteenth_party: 'Decimoctava Parte',
|
||||
nineteenth_party: 'Decimonovena Parte',
|
||||
twentieth_party: 'Vigésima Parte',
|
||||
draw: 'Dibujar',
|
||||
add: 'Agregar',
|
||||
text: 'Texto',
|
||||
@@ -420,7 +424,7 @@ const es = {
|
||||
learn_more: 'Aprende más',
|
||||
and: 'y',
|
||||
or: 'o',
|
||||
start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: 'Inicia una guía rápida para aprender a crear y enviar tu primer documento.',
|
||||
start_a_quick_tour_to_learn_how_to_create_and_send_your_first_document: 'Inicia una guía rápida para aprender a crear y enviar tu primer documento.',
|
||||
start_tour: 'Iniciar guía',
|
||||
or_add_from: 'O agregar desde',
|
||||
sync: 'Sincronizar',
|
||||
@@ -435,7 +439,11 @@ const es = {
|
||||
align_bottom: 'Alinear abajo',
|
||||
fields_selected: '{count} Campos Seleccionados',
|
||||
field_added: '{count} Campo Añadido',
|
||||
fields_added: '{count} Campos Añadidos'
|
||||
fields_added: '{count} Campos Añadidos',
|
||||
revisions: 'Revisiones',
|
||||
apply: 'Aplicar',
|
||||
no_revisions_yet: 'Aún no hay revisiones',
|
||||
viewing_revision_from: 'Viendo revisión del {date}'
|
||||
}
|
||||
|
||||
const it = {
|
||||
@@ -458,15 +466,15 @@ const it = {
|
||||
length: 'Lunghezza',
|
||||
min: 'Min',
|
||||
max: 'Max',
|
||||
date_signed: 'Data attuale',
|
||||
date_signed: 'Data corrente',
|
||||
font: 'Carattere',
|
||||
party: 'Parte',
|
||||
method: 'Metodo',
|
||||
reorder_fields: 'Riordina i campi',
|
||||
verify_id: 'Verifica ID',
|
||||
obtain_qualified_electronic_signature_with_the_trusted_provider_click_to_learn_more: 'Ottieni una firma elettronica qualificata (QeS) con il fornitore di fiducia. Clicca per saperne di più.',
|
||||
ricorrente: 'Ricorrente',
|
||||
una_volta: 'Una volta',
|
||||
recurrent: 'Ricorrente',
|
||||
one_off: 'Una tantum',
|
||||
editable: 'Modificabile',
|
||||
search_field: 'Campo di ricerca',
|
||||
field_not_found: 'Campo non trovato',
|
||||
@@ -525,7 +533,7 @@ const it = {
|
||||
drawn: 'Disegnato',
|
||||
drawn_or_typed: 'Disegnato o Digitato',
|
||||
drawn_or_upload: 'Disegnato o Caricato',
|
||||
upload: 'Caricare',
|
||||
upload: 'Carica',
|
||||
formula: 'Formula',
|
||||
typed: 'Digitato',
|
||||
draw_field_on_the_document: 'Disegnare un campo sul documento',
|
||||
@@ -640,7 +648,7 @@ const it = {
|
||||
learn_more: 'Scopri di più',
|
||||
and: 'e',
|
||||
or: 'o',
|
||||
start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: 'Inizia un tour rapido per imparare a creare e inviare il tuo primo documento.',
|
||||
start_a_quick_tour_to_learn_how_to_create_and_send_your_first_document: 'Inizia un tour rapido per imparare a creare e inviare il tuo primo documento.',
|
||||
start_tour: 'Inizia il tour',
|
||||
or_add_from: 'O aggiungi da',
|
||||
sync: 'Sincronizza',
|
||||
@@ -655,7 +663,11 @@ const it = {
|
||||
align_bottom: 'Allinea in basso',
|
||||
fields_selected: '{count} Campi Selezionati',
|
||||
field_added: '{count} Campo Aggiunto',
|
||||
fields_added: '{count} Campi Aggiunti'
|
||||
fields_added: '{count} Campi Aggiunti',
|
||||
revisions: 'Revisioni',
|
||||
apply: 'Applica',
|
||||
no_revisions_yet: 'Nessuna revisione ancora',
|
||||
viewing_revision_from: 'Visualizzazione revisione del {date}'
|
||||
}
|
||||
|
||||
const pt = {
|
||||
@@ -667,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',
|
||||
@@ -679,13 +691,13 @@ const pt = {
|
||||
min: 'Mín',
|
||||
max: 'Máx',
|
||||
date_signed: 'Data atual',
|
||||
fonte: 'Fonte',
|
||||
font: 'Fonte',
|
||||
party: 'Parte',
|
||||
method: 'Método',
|
||||
reorder_fields: 'Reorganizar campos',
|
||||
verify_id: 'Verificar ID',
|
||||
obtain_qualified_electronic_signature_with_the_trusted_provider_click_to_learn_more: 'Obtenha a assinatura eletrônica qualificada (QeS) com o provedor confiável. Clique para saber mais.',
|
||||
recurrent: 'Recurrente',
|
||||
recurrent: 'Recorrente',
|
||||
one_off: 'Único',
|
||||
editable: 'Editável',
|
||||
search_field: 'Campo de busca',
|
||||
@@ -752,7 +764,7 @@ const pt = {
|
||||
processing_: 'Processando...',
|
||||
add_pdf_documents_or_images: 'Adicionar documentos PDF ou imagens',
|
||||
add_documents_or_images: 'Adicionar documentos ou imagens',
|
||||
add_a_new_document: 'Ajouter un nouveau document',
|
||||
add_a_new_document: 'Adicionar um novo documento',
|
||||
replace_existing_document: 'Substituir documento existente',
|
||||
clone_and_replace_documents: 'Clonar e substituir documentos',
|
||||
required: 'Obrigatório',
|
||||
@@ -860,7 +872,7 @@ const pt = {
|
||||
learn_more: 'Saiba mais',
|
||||
and: 'e',
|
||||
or: 'ou',
|
||||
start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: 'Comece um tour rápido para aprender a criar e enviar seu primeiro documento.',
|
||||
start_a_quick_tour_to_learn_how_to_create_and_send_your_first_document: 'Comece um tour rápido para aprender a criar e enviar seu primeiro documento.',
|
||||
start_tour: 'Iniciar tour',
|
||||
or_add_from: 'Ou adicionar de',
|
||||
sync: 'Sincronizar',
|
||||
@@ -875,7 +887,11 @@ const pt = {
|
||||
align_bottom: 'Alinhar à parte inferior',
|
||||
fields_selected: '{count} Campos Selecionados',
|
||||
field_added: '{count} Campo Adicionado',
|
||||
fields_added: '{count} Campos Adicionados'
|
||||
fields_added: '{count} Campos Adicionados',
|
||||
revisions: 'Revisões',
|
||||
apply: 'Aplicar',
|
||||
no_revisions_yet: 'Nenhuma revisão ainda',
|
||||
viewing_revision_from: 'Visualizando revisão de {date}'
|
||||
}
|
||||
|
||||
const fr = {
|
||||
@@ -900,7 +916,7 @@ const fr = {
|
||||
max: 'Max',
|
||||
font: 'Police',
|
||||
party: 'Partie',
|
||||
date_signed: 'Date de signature',
|
||||
date_signed: 'Date du jour',
|
||||
method: 'Méthode',
|
||||
reorder_fields: 'Réorganiser les champs',
|
||||
verify_id: "Vérifier l'ID",
|
||||
@@ -1080,7 +1096,7 @@ const fr = {
|
||||
learn_more: 'En savoir plus',
|
||||
and: 'et',
|
||||
or: 'ou',
|
||||
start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: 'Commencez une visite rapide pour apprendre à créer et à envoyer votre premier document',
|
||||
start_a_quick_tour_to_learn_how_to_create_and_send_your_first_document: 'Commencez une visite rapide pour apprendre à créer et à envoyer votre premier document',
|
||||
start_tour: 'Démarrer',
|
||||
or_add_from: 'Ou ajouter depuis',
|
||||
sync: 'Synchroniser',
|
||||
@@ -1095,7 +1111,11 @@ const fr = {
|
||||
align_bottom: 'Aligner en bas',
|
||||
fields_selected: '{count} Champs Sélectionnés',
|
||||
field_added: '{count} Champ Ajouté',
|
||||
fields_added: '{count} Champs Ajoutés'
|
||||
fields_added: '{count} Champs Ajoutés',
|
||||
revisions: 'Révisions',
|
||||
apply: 'Appliquer',
|
||||
no_revisions_yet: 'Aucune révision pour le moment',
|
||||
viewing_revision_from: 'Affichage de la révision du {date}'
|
||||
}
|
||||
|
||||
const de = {
|
||||
@@ -1120,7 +1140,7 @@ const de = {
|
||||
max: 'Max.',
|
||||
font: 'Schriftart',
|
||||
party: 'Partei',
|
||||
date_signed: 'Unterzeichnungsdatum',
|
||||
date_signed: 'Aktuelles Datum',
|
||||
method: 'Methode',
|
||||
reorder_fields: 'Felder neu anordnen',
|
||||
verify_id: 'ID überprüfen',
|
||||
@@ -1300,7 +1320,7 @@ const de = {
|
||||
learn_more: 'Mehr erfahren',
|
||||
and: 'und',
|
||||
or: 'oder',
|
||||
start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: 'Starten Sie eine Kurztour, um zu lernen, wie Sie Ihr erstes Dokument erstellen und versenden.',
|
||||
start_a_quick_tour_to_learn_how_to_create_and_send_your_first_document: 'Starten Sie eine Kurztour, um zu lernen, wie Sie Ihr erstes Dokument erstellen und versenden.',
|
||||
start_tour: 'Tour starten',
|
||||
or_add_from: 'Oder hinzufügen aus',
|
||||
sync: 'Synchronisieren',
|
||||
@@ -1315,7 +1335,11 @@ const de = {
|
||||
align_bottom: 'Unten ausrichten',
|
||||
fields_selected: '{count} Felder Ausgewählt',
|
||||
field_added: '{count} Feld Hinzugefügt',
|
||||
fields_added: '{count} Felder Hinzugefügt'
|
||||
fields_added: '{count} Felder Hinzugefügt',
|
||||
revisions: 'Revisionen',
|
||||
apply: 'Anwenden',
|
||||
no_revisions_yet: 'Noch keine Revisionen',
|
||||
viewing_revision_from: 'Ansicht der Revision vom {date}'
|
||||
}
|
||||
|
||||
const nl = {
|
||||
@@ -1340,7 +1364,7 @@ const nl = {
|
||||
max: 'Max',
|
||||
font: 'Lettertype',
|
||||
party: 'Partij',
|
||||
date_signed: 'Datum ondertekend',
|
||||
date_signed: 'Huidige datum',
|
||||
method: 'Methode',
|
||||
reorder_fields: 'Velden herschikken',
|
||||
verify_id: 'ID verifiëren',
|
||||
@@ -1520,7 +1544,7 @@ const nl = {
|
||||
learn_more: 'Meer informatie',
|
||||
and: 'en',
|
||||
or: 'of',
|
||||
start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: 'Start een korte rondleiding om te leren hoe u uw eerste document maakt en verzendt',
|
||||
start_a_quick_tour_to_learn_how_to_create_and_send_your_first_document: 'Start een korte rondleiding om te leren hoe u uw eerste document maakt en verzendt',
|
||||
start_tour: 'Rondleiding starten',
|
||||
or_add_from: 'Of toevoegen van',
|
||||
sync: 'Synchroniseren',
|
||||
@@ -1535,7 +1559,11 @@ const nl = {
|
||||
align_bottom: 'Onder uitlijnen',
|
||||
fields_selected: '{count} Velden Geselecteerd',
|
||||
field_added: '{count} Veld Toegevoegd',
|
||||
fields_added: '{count} Velden Toegevoegd'
|
||||
fields_added: '{count} Velden Toegevoegd',
|
||||
revisions: 'Revisies',
|
||||
apply: 'Toepassen',
|
||||
no_revisions_yet: 'Nog geen revisies',
|
||||
viewing_revision_from: 'Revisie van {date} bekijken'
|
||||
}
|
||||
|
||||
export { en, es, it, pt, fr, de, nl }
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
@scroll-to="$emit('scroll-to', $event)"
|
||||
@add-custom-field="$emit('add-custom-field', $event)"
|
||||
@contextmenu="openAreaContextMenu($event, item.area, item.field)"
|
||||
@click-title="closeContextMenu"
|
||||
/>
|
||||
<FieldArea
|
||||
v-for="(area, index) in newAreas"
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
<template>
|
||||
<div class="modal modal-open items-start !animate-none overflow-y-auto">
|
||||
<div
|
||||
class="absolute top-0 bottom-0 right-0 left-0"
|
||||
@click.prevent="$emit('close')"
|
||||
/>
|
||||
<div class="modal-box pt-4 pb-6 mt-20 w-full">
|
||||
<div class="flex justify-between items-center border-b pb-2 mb-3 font-medium">
|
||||
<span>{{ t('revisions') }}</span>
|
||||
<a
|
||||
href="#"
|
||||
class="text-xl"
|
||||
@click.prevent="$emit('close')"
|
||||
>×</a>
|
||||
</div>
|
||||
<ul class="space-y-1.5">
|
||||
<li
|
||||
v-for="revision in revisions"
|
||||
:key="revision.id"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="w-full text-left rounded-lg p-3 border border-dashed border-base-200 transition-colors disabled:cursor-default hover:bg-base-200 hover:border-base-200"
|
||||
:disabled="loadingId !== null"
|
||||
@click="viewRevision(revision.id)"
|
||||
>
|
||||
<div class="flex justify-between items-center gap-2">
|
||||
<div class="flex flex-col">
|
||||
<span>{{ formatDate(revision.created_at) }}</span>
|
||||
<span class="-ml-0.5 flex items-center space-x-1 text-xs text-base-content/60 mt-0.5">
|
||||
<IconUser class="w-3.5 h-3.5 flex-shrink-0" />
|
||||
<span class="truncate">{{ revision.author.full_name || revision.author.email }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<span class="btn btn-sm btn-neutral text-white pointer-events-none flex-shrink-0">
|
||||
<IconInnerShadowTop
|
||||
v-if="loadingId === revision.id"
|
||||
class="w-4 h-4 animate-spin"
|
||||
/>
|
||||
<span v-else>{{ t('view') }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
</li>
|
||||
<li
|
||||
v-if="!revisions.length"
|
||||
class="py-4 text-center text-base-content/60"
|
||||
>
|
||||
{{ t('no_revisions_yet') }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { IconUser, IconInnerShadowTop } from '@tabler/icons-vue'
|
||||
|
||||
export default {
|
||||
name: 'RevisionsModal',
|
||||
components: { IconUser, IconInnerShadowTop },
|
||||
inject: ['t', 'baseFetch'],
|
||||
props: {
|
||||
template: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
revisions: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
locale: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
emits: ['close', 'apply'],
|
||||
data () {
|
||||
return {
|
||||
loadingId: null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
viewRevision (id) {
|
||||
if (this.loadingId !== null) return
|
||||
|
||||
this.loadingId = id
|
||||
|
||||
this.baseFetch(`/templates/${this.template.id}/versions/${id}`)
|
||||
.then((r) => r.json())
|
||||
.then((revision) => { this.$emit('apply', revision) })
|
||||
.finally(() => { this.loadingId = null })
|
||||
},
|
||||
formatDate (string) {
|
||||
return new Date(string).toLocaleString(this.locale || undefined, {
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: '2-digit'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -168,6 +168,65 @@
|
||||
<script>
|
||||
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'))
|
||||
|
||||
|
||||
@@ -30,13 +30,12 @@ class SendFormCompletedWebhookRequestJob
|
||||
attempt:,
|
||||
data: Submitters::SerializeForWebhook.call(submitter))
|
||||
|
||||
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
|
||||
(!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
|
||||
SendFormCompletedWebhookRequestJob.perform_in((2**attempt).minutes, {
|
||||
**params,
|
||||
'attempt' => attempt + 1,
|
||||
'last_status' => resp&.status.to_i
|
||||
})
|
||||
end
|
||||
return if attempt > MAX_ATTEMPTS || (resp && resp.status.to_i < 400)
|
||||
|
||||
SendFormCompletedWebhookRequestJob.perform_in((2**attempt).minutes, {
|
||||
**params,
|
||||
'attempt' => attempt + 1,
|
||||
'last_status' => resp&.status.to_i
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
@@ -28,13 +28,12 @@ class SendFormDeclinedWebhookRequestJob
|
||||
attempt:,
|
||||
data: Submitters::SerializeForWebhook.call(submitter))
|
||||
|
||||
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
|
||||
(!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
|
||||
SendFormDeclinedWebhookRequestJob.perform_in((2**attempt).minutes, {
|
||||
**params,
|
||||
'attempt' => attempt + 1,
|
||||
'last_status' => resp&.status.to_i
|
||||
})
|
||||
end
|
||||
return if attempt > MAX_ATTEMPTS || (resp && resp.status.to_i < 400)
|
||||
|
||||
SendFormDeclinedWebhookRequestJob.perform_in((2**attempt).minutes, {
|
||||
**params,
|
||||
'attempt' => attempt + 1,
|
||||
'last_status' => resp&.status.to_i
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
@@ -28,13 +28,12 @@ class SendFormStartedWebhookRequestJob
|
||||
attempt:,
|
||||
data: Submitters::SerializeForWebhook.call(submitter))
|
||||
|
||||
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
|
||||
(!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
|
||||
SendFormStartedWebhookRequestJob.perform_in((2**attempt).minutes, {
|
||||
**params,
|
||||
'attempt' => attempt + 1,
|
||||
'last_status' => resp&.status.to_i
|
||||
})
|
||||
end
|
||||
return if attempt > MAX_ATTEMPTS || (resp && resp.status.to_i < 400)
|
||||
|
||||
SendFormStartedWebhookRequestJob.perform_in((2**attempt).minutes, {
|
||||
**params,
|
||||
'attempt' => attempt + 1,
|
||||
'last_status' => resp&.status.to_i
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
@@ -28,13 +28,12 @@ class SendFormViewedWebhookRequestJob
|
||||
attempt:,
|
||||
data: Submitters::SerializeForWebhook.call(submitter))
|
||||
|
||||
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
|
||||
(!Docuseal.multitenant? || submitter.account.account_configs.exists?(key: :plan))
|
||||
SendFormViewedWebhookRequestJob.perform_in((2**attempt).minutes, {
|
||||
**params,
|
||||
'attempt' => attempt + 1,
|
||||
'last_status' => resp&.status.to_i
|
||||
})
|
||||
end
|
||||
return if attempt > MAX_ATTEMPTS || (resp && resp.status.to_i < 400)
|
||||
|
||||
SendFormViewedWebhookRequestJob.perform_in((2**attempt).minutes, {
|
||||
**params,
|
||||
'attempt' => attempt + 1,
|
||||
'last_status' => resp&.status.to_i
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
@@ -26,13 +26,12 @@ class SendSubmissionArchivedWebhookRequestJob
|
||||
attempt:,
|
||||
data: submission.as_json(only: %i[id archived_at]))
|
||||
|
||||
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
|
||||
(!Docuseal.multitenant? || submission.account.account_configs.exists?(key: :plan))
|
||||
SendSubmissionArchivedWebhookRequestJob.perform_in((2**attempt).minutes, {
|
||||
**params,
|
||||
'attempt' => attempt + 1,
|
||||
'last_status' => resp&.status.to_i
|
||||
})
|
||||
end
|
||||
return if attempt > MAX_ATTEMPTS || (resp && resp.status.to_i < 400)
|
||||
|
||||
SendSubmissionArchivedWebhookRequestJob.perform_in((2**attempt).minutes, {
|
||||
**params,
|
||||
'attempt' => attempt + 1,
|
||||
'last_status' => resp&.status.to_i
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
@@ -26,13 +26,12 @@ class SendSubmissionCompletedWebhookRequestJob
|
||||
attempt:,
|
||||
data: Submissions::SerializeForApi.call(submission))
|
||||
|
||||
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
|
||||
(!Docuseal.multitenant? || submission.account.account_configs.exists?(key: :plan))
|
||||
SendSubmissionCompletedWebhookRequestJob.perform_in((2**attempt).minutes, {
|
||||
**params,
|
||||
'attempt' => attempt + 1,
|
||||
'last_status' => resp&.status.to_i
|
||||
})
|
||||
end
|
||||
return if attempt > MAX_ATTEMPTS || (resp && resp.status.to_i < 400)
|
||||
|
||||
SendSubmissionCompletedWebhookRequestJob.perform_in((2**attempt).minutes, {
|
||||
**params,
|
||||
'attempt' => attempt + 1,
|
||||
'last_status' => resp&.status.to_i
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
@@ -26,13 +26,12 @@ class SendSubmissionCreatedWebhookRequestJob
|
||||
attempt:,
|
||||
data: Submissions::SerializeForApi.call(submission))
|
||||
|
||||
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
|
||||
(!Docuseal.multitenant? || submission.account.account_configs.exists?(key: :plan))
|
||||
SendSubmissionCreatedWebhookRequestJob.perform_in((2**attempt).minutes, {
|
||||
**params,
|
||||
'attempt' => attempt + 1,
|
||||
'last_status' => resp&.status.to_i
|
||||
})
|
||||
end
|
||||
return if attempt > MAX_ATTEMPTS || (resp && resp.status.to_i < 400)
|
||||
|
||||
SendSubmissionCreatedWebhookRequestJob.perform_in((2**attempt).minutes, {
|
||||
**params,
|
||||
'attempt' => attempt + 1,
|
||||
'last_status' => resp&.status.to_i
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
@@ -26,13 +26,12 @@ class SendSubmissionExpiredWebhookRequestJob
|
||||
attempt:,
|
||||
data: Submissions::SerializeForApi.call(submission))
|
||||
|
||||
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
|
||||
(!Docuseal.multitenant? || submission.account.account_configs.exists?(key: :plan))
|
||||
SendSubmissionExpiredWebhookRequestJob.perform_in((2**attempt).minutes, {
|
||||
**params,
|
||||
'attempt' => attempt + 1,
|
||||
'last_status' => resp&.status.to_i
|
||||
})
|
||||
end
|
||||
return if attempt > MAX_ATTEMPTS || (resp && resp.status.to_i < 400)
|
||||
|
||||
SendSubmissionExpiredWebhookRequestJob.perform_in((2**attempt).minutes, {
|
||||
**params,
|
||||
'attempt' => attempt + 1,
|
||||
'last_status' => resp&.status.to_i
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
@@ -26,13 +26,12 @@ class SendTemplateArchivedWebhookRequestJob
|
||||
attempt:,
|
||||
data: template.as_json(only: %i[id archived_at]))
|
||||
|
||||
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
|
||||
(!Docuseal.multitenant? || template.account.account_configs.exists?(key: :plan))
|
||||
SendTemplateArchivedWebhookRequestJob.perform_in((2**attempt).minutes, {
|
||||
**params,
|
||||
'attempt' => attempt + 1,
|
||||
'last_status' => resp&.status.to_i
|
||||
})
|
||||
end
|
||||
return if attempt > MAX_ATTEMPTS || (resp && resp.status.to_i < 400)
|
||||
|
||||
SendTemplateArchivedWebhookRequestJob.perform_in((2**attempt).minutes, {
|
||||
**params,
|
||||
'attempt' => attempt + 1,
|
||||
'last_status' => resp&.status.to_i
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
@@ -26,13 +26,12 @@ class SendTemplateCreatedWebhookRequestJob
|
||||
attempt:,
|
||||
data: Templates::SerializeForApi.call(template))
|
||||
|
||||
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
|
||||
(!Docuseal.multitenant? || template.account.account_configs.exists?(key: :plan))
|
||||
SendTemplateCreatedWebhookRequestJob.perform_in((2**attempt).minutes, {
|
||||
**params,
|
||||
'attempt' => attempt + 1,
|
||||
'last_status' => resp&.status.to_i
|
||||
})
|
||||
end
|
||||
return if attempt > MAX_ATTEMPTS || (resp && resp.status.to_i < 400)
|
||||
|
||||
SendTemplateCreatedWebhookRequestJob.perform_in((2**attempt).minutes, {
|
||||
**params,
|
||||
'attempt' => attempt + 1,
|
||||
'last_status' => resp&.status.to_i
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
@@ -26,13 +26,12 @@ class SendTemplateUpdatedWebhookRequestJob
|
||||
attempt:,
|
||||
data: Templates::SerializeForApi.call(template))
|
||||
|
||||
if (resp.nil? || resp.status.to_i >= 400) && attempt <= MAX_ATTEMPTS &&
|
||||
(!Docuseal.multitenant? || template.account.account_configs.exists?(key: :plan))
|
||||
SendTemplateUpdatedWebhookRequestJob.perform_in((2**attempt).minutes, {
|
||||
**params,
|
||||
'attempt' => attempt + 1,
|
||||
'last_status' => resp&.status.to_i
|
||||
})
|
||||
end
|
||||
return if attempt > MAX_ATTEMPTS || (resp && resp.status.to_i < 400)
|
||||
|
||||
SendTemplateUpdatedWebhookRequestJob.perform_in((2**attempt).minutes, {
|
||||
**params,
|
||||
'attempt' => attempt + 1,
|
||||
'last_status' => resp&.status.to_i
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -70,11 +70,12 @@ class Template < ApplicationRecord
|
||||
has_many :submissions, dependent: :destroy
|
||||
has_many :template_sharings, dependent: :destroy
|
||||
has_many :template_accesses, dependent: :destroy
|
||||
has_many :template_versions, dependent: :destroy
|
||||
has_many :dynamic_documents, dependent: :destroy
|
||||
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) }
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: template_versions
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# data :text not null
|
||||
# sha1 :string not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# account_id :bigint not null
|
||||
# author_id :bigint not null
|
||||
# template_id :bigint not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_template_versions_on_account_id (account_id)
|
||||
# index_template_versions_on_author_id (author_id)
|
||||
# index_template_versions_on_template_id_and_sha1 (template_id,sha1) UNIQUE
|
||||
#
|
||||
# Foreign Keys
|
||||
#
|
||||
# fk_rails_... (account_id => accounts.id)
|
||||
# fk_rails_... (author_id => users.id)
|
||||
# fk_rails_... (template_id => templates.id)
|
||||
#
|
||||
class TemplateVersion < ApplicationRecord
|
||||
belongs_to :template
|
||||
belongs_to :account
|
||||
belongs_to :author, class_name: 'User'
|
||||
|
||||
attribute :data, :string, default: -> { {} }
|
||||
|
||||
serialize :data, coder: JSON
|
||||
|
||||
before_validation :set_account, on: :create
|
||||
|
||||
private
|
||||
|
||||
def set_account
|
||||
self.account ||= template.account
|
||||
end
|
||||
end
|
||||
@@ -4,14 +4,15 @@
|
||||
#
|
||||
# Table name: webhook_urls
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# events :text not null
|
||||
# secret :text not null
|
||||
# sha1 :string not null
|
||||
# url :text not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# account_id :bigint not null
|
||||
# id :bigint not null, primary key
|
||||
# events :text not null
|
||||
# hmac_secret :text not null
|
||||
# secret :text not null
|
||||
# sha1 :string not null
|
||||
# url :text not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# account_id :bigint not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
@@ -47,10 +48,15 @@ class WebhookUrl < ApplicationRecord
|
||||
serialize :secret, coder: JSON
|
||||
|
||||
before_validation :set_sha1
|
||||
before_validation :set_hmac_secret
|
||||
|
||||
encrypts :url, :secret
|
||||
encrypts :url, :secret, :hmac_secret
|
||||
|
||||
def set_sha1
|
||||
self.sha1 = Digest::SHA1.hexdigest(url)
|
||||
end
|
||||
|
||||
def set_hmac_secret
|
||||
self.hmac_secret ||= WebhookUrls::Signatures.generate_secret
|
||||
end
|
||||
end
|
||||
|
||||
@@ -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>12k</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-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>
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
<% end %>
|
||||
<% if @submissions.present? %>
|
||||
<div class="space-y-4">
|
||||
<%= render partial: 'submission', collection: @submissions, locals: { template: @template } %>
|
||||
<%= render partial: 'submission', collection: @submissions, locals: { template: @template, archived: @template.archived_at? } %>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="card bg-base-200">
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
<%= t('welcome_to_docuseal') %>
|
||||
</div>
|
||||
<div class="my-2 text-center text-xs text-base-content/70">
|
||||
<%= t('start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document') %>
|
||||
<%= t('start_a_quick_tour_to_learn_how_to_create_and_send_your_first_document') %>
|
||||
</div>
|
||||
<div class="flex gap-2 mt-3 w-full">
|
||||
<remove-on-event data-on="submit" data-selector-id="app_tour_manager" class="block w-full">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -3,13 +3,14 @@
|
||||
<% webhook_attempts = webhook_event.webhook_attempts.sort_by { |e| -e.id } %>
|
||||
<% if webhook_event.status == 'error' %>
|
||||
<% last_attempt = webhook_attempts.select { |e| e.attempt < SendWebhookRequest::MANUAL_ATTEMPT }.max_by(&:attempt) %>
|
||||
<% if webhook_event.webhook_attempts.none?(&:success?) && last_attempt.attempt <= 10 %>
|
||||
<% next_attempt_at = last_attempt.created_at + (2**last_attempt.attempt).minutes %>
|
||||
<% if webhook_event.webhook_attempts.none?(&:success?) && last_attempt.attempt <= 10 && next_attempt_at > 30.seconds.ago %>
|
||||
<li class="ml-7">
|
||||
<span class="btn btn-outline btn-xs btn-circle pointer-events-none absolute justify-center border-base-content-/60 text-base-content/60 bg-base-100" style="left: -12px;">
|
||||
<%= svg_icon('clock', class: 'w-4 h-4 shrink-0') %>
|
||||
</span>
|
||||
<p class="leading-none text-base-content/90 pt-1">
|
||||
<%= t('next_attempt_in_time_in_words', time_in_words: distance_of_time_in_words(Time.current, last_attempt.created_at + (2**last_attempt.attempt).minutes)) %>
|
||||
<%= t('next_attempt_in_time_in_words', time_in_words: distance_of_time_in_words(Time.current, next_attempt_at)) %>
|
||||
</p>
|
||||
</li>
|
||||
<% end %>
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<%= render 'shared/turbo_modal', title: t('webhook_security') do %>
|
||||
<div class="text-center mb-4">
|
||||
<div class="inline-flex justify-center">
|
||||
<%= link_to t('secret'), webhook_secret_path(@webhook_url), class: 'block bg-base-200 md:min-w-[112px] text-sm font-semibold whitespace-nowrap py-1.5 px-4 rounded-l-3xl', data: { turbo_frame: 'modal' } %>
|
||||
<%= link_to t('hmac'), webhook_hmac_path(@webhook_url), class: 'block bg-base-300 md:min-w-[112px] text-sm font-semibold whitespace-nowrap py-1.5 px-4 rounded-r-3xl', data: { turbo_frame: 'modal' } %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label" for="hmac_secret"><%= t('hmac_signing_secret') %></label>
|
||||
<% token = @webhook_url.hmac_secret %>
|
||||
<% obscured = "#{token[0, 10]}#{'*' * [token.length - 10, 0].max}" %>
|
||||
<div class="flex gap-2">
|
||||
<masked-input class="block w-full" data-token="<%= token %>">
|
||||
<input id="hmac_secret" type="text" value="<%= obscured %>" class="base-input font-mono w-full" autocomplete="off" readonly>
|
||||
</masked-input>
|
||||
<%= render 'shared/clipboard_copy', icon: 'copy', text: token, class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: t('copy'), copied_title: t('copied') %>
|
||||
</div>
|
||||
<p class="text-sm mt-2 opacity-70"><%= t('hmac_signature_header_hint_html', header: '<code>X-Docuseal-Signature</code>'.html_safe) %></p>
|
||||
</div>
|
||||
<% end %>
|
||||
@@ -1,4 +1,10 @@
|
||||
<%= render 'shared/turbo_modal', title: t('webhook_secret') do %>
|
||||
<%= render 'shared/turbo_modal', title: t('webhook_security') do %>
|
||||
<div class="text-center mb-4">
|
||||
<div class="inline-flex justify-center">
|
||||
<%= link_to t('secret'), webhook_secret_path(@webhook_url), class: 'block bg-base-300 md:min-w-[112px] text-sm font-semibold whitespace-nowrap py-1.5 px-4 rounded-l-3xl', data: { turbo_frame: 'modal' } %>
|
||||
<%= link_to t('hmac'), webhook_hmac_path(@webhook_url), class: 'block bg-base-200 md:min-w-[112px] text-sm font-semibold whitespace-nowrap py-1.5 px-4 rounded-r-3xl', data: { turbo_frame: 'modal' } %>
|
||||
</div>
|
||||
</div>
|
||||
<%= form_for @webhook_url, url: webhook_secret_path, method: :patch, html: { class: 'space-y-4' }, data: { turbo_frame: :_top } do |f| %>
|
||||
<div class="space-y-2">
|
||||
<%= f.fields_for :secret, Struct.new(:key, :value).new(*@webhook_url.secret.to_a.first) do |ff| %>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0 md:space-x-10">
|
||||
<%= render 'shared/settings_nav' %>
|
||||
<div class="flex-grow min-w-0">
|
||||
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-center mb-4">
|
||||
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-end mb-4">
|
||||
<h1 class="text-4xl font-bold">Webhooks</h1>
|
||||
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-center">
|
||||
<%= render 'shared/test_mode_toggle' %>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0 md:space-x-10">
|
||||
<%= render 'shared/settings_nav' %>
|
||||
<div class="flex-grow">
|
||||
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-center mb-4">
|
||||
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-end mb-4">
|
||||
<h1 class="text-4xl font-bold">Webhook</h1>
|
||||
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:items-center">
|
||||
<% if params[:action] == 'index' && (current_user == true_user || current_account.testing?) %>
|
||||
@@ -23,7 +23,7 @@
|
||||
<div class="flex items-center space-x-2 md:absolute md:right-0">
|
||||
<%= link_to webhook_secret_path(@webhook_url), class: 'btn btn-outline btn-sm bg-white', data: { turbo_frame: 'modal' } do %>
|
||||
<%= svg_icon('lock', class: 'w-4 h-4') %>
|
||||
<span><%= @webhook_url.secret.present? ? t('edit_secret') : t('add_secret') %></span>
|
||||
<span><%= t('security') %></span>
|
||||
<% end %>
|
||||
<div class="tooltip tooltip-left md:tooltip-top" data-tip="<%= t('delete_webhook') %>">
|
||||
<%= button_to settings_webhook_path(@webhook_url), class: 'btn btn-warning btn-sm', method: :delete, data: { turbo_confirm: t('are_you_sure_') } do %>
|
||||
|
||||
@@ -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]
|
||||
+288
-205
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user