mirror of
https://github.com/docusealco/docuseal
synced 2026-11-05 06:42:32 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2521eb5a4a | ||
|
|
bf6afc5e61 | ||
|
|
291d869843 | ||
|
|
cdfecb2a8f | ||
|
|
c0df8e0ec3 | ||
|
|
a21f7d1653 | ||
|
|
79e2514416 | ||
|
|
80a7385183 | ||
|
|
1c8a7b6a7c | ||
|
|
b46ced2b6f | ||
|
|
5ae479644e | ||
|
|
cb66eb1293 | ||
|
|
b4d5a2cad4 | ||
|
|
32691b0c0d | ||
|
|
9163f744a3 | ||
|
|
23ecce51a3 | ||
|
|
747ffa8cf1 | ||
|
|
4e230899ed | ||
|
|
aff9f753c7 | ||
|
|
1127fac500 | ||
|
|
7707e1cd2b | ||
|
|
30274f7f1a | ||
|
|
a5da6cc781 | ||
|
|
201e4010c5 | ||
|
|
b9bbdbffae | ||
|
|
9e8e1996e8 | ||
|
|
b99235397a | ||
|
|
c0fd06daf3 | ||
|
|
90536294df | ||
|
|
f8e9787675 | ||
|
|
bf9b785946 | ||
|
|
dab3a6f031 | ||
|
|
88466193c5 | ||
|
|
c2f7601dad | ||
|
|
189f7b2d7b | ||
|
|
5d87160ee9 | ||
|
|
0cdcdb8898 | ||
|
|
c20619b5c8 |
No files matched your search
@@ -37,3 +37,4 @@ yarn-debug.log*
|
|||||||
/docuseal
|
/docuseal
|
||||||
/ee
|
/ee
|
||||||
dump.rdb
|
dump.rdb
|
||||||
|
*.onnx
|
||||||
@@ -42,6 +42,12 @@ Metrics/CyclomaticComplexity:
|
|||||||
Metrics/PerceivedComplexity:
|
Metrics/PerceivedComplexity:
|
||||||
Max: 15
|
Max: 15
|
||||||
|
|
||||||
|
Style/MultipleComparison:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Naming/PredicateMethod:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
Layout/LineLength:
|
Layout/LineLength:
|
||||||
AllowedPatterns: ['\A\s*#']
|
AllowedPatterns: ['\A\s*#']
|
||||||
|
|
||||||
|
|||||||
+6
-2
@@ -9,6 +9,7 @@ RUN apk --no-cache add fontforge wget && \
|
|||||||
wget https://cdn.jsdelivr.net/gh/notofonts/notofonts.github.io/fonts/NotoSansSymbols2/hinted/ttf/NotoSansSymbols2-Regular.ttf && \
|
wget https://cdn.jsdelivr.net/gh/notofonts/notofonts.github.io/fonts/NotoSansSymbols2/hinted/ttf/NotoSansSymbols2-Regular.ttf && \
|
||||||
wget https://github.com/Maxattax97/gnu-freefont/raw/master/ttf/FreeSans.ttf && \
|
wget https://github.com/Maxattax97/gnu-freefont/raw/master/ttf/FreeSans.ttf && \
|
||||||
wget https://github.com/impallari/DancingScript/raw/master/OFL.txt && \
|
wget https://github.com/impallari/DancingScript/raw/master/OFL.txt && \
|
||||||
|
wget -O /model.onnx "https://github.com/docusealco/fields-detection/releases/download/1.0.0/model_704_int8.onnx" && \
|
||||||
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/docusealco/pdfium-binaries/releases/latest/download/pdfium-linux-$(uname -m | sed 's/x86_64/x64/;s/aarch64/arm64/').tgz" && \
|
||||||
mkdir -p /pdfium-linux && \
|
mkdir -p /pdfium-linux && \
|
||||||
tar -xzf pdfium-linux.tgz -C /pdfium-linux
|
tar -xzf pdfium-linux.tgz -C /pdfium-linux
|
||||||
@@ -50,7 +51,7 @@ ENV OPENSSL_CONF=/app/openssl_legacy.cnf
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN echo '@edge https://dl-cdn.alpinelinux.org/alpine/edge/community' >> /etc/apk/repositories && apk add --no-cache sqlite-dev libpq-dev mariadb-dev vips-dev@edge yaml-dev redis libheif@edge vips-heif@edge gcompat ttf-freefont && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf
|
RUN apk add --no-cache sqlite-dev libpq-dev mariadb-dev vips-dev yaml-dev redis libheif vips-heif gcompat ttf-freefont && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf
|
||||||
|
|
||||||
RUN echo $'.include = /etc/ssl/openssl.cnf\n\
|
RUN echo $'.include = /etc/ssl/openssl.cnf\n\
|
||||||
\n\
|
\n\
|
||||||
@@ -66,7 +67,9 @@ activate = 1' >> /app/openssl_legacy.cnf
|
|||||||
|
|
||||||
COPY ./Gemfile ./Gemfile.lock ./
|
COPY ./Gemfile ./Gemfile.lock ./
|
||||||
|
|
||||||
RUN apk add --no-cache build-base && bundle install && apk del --no-cache build-base && rm -rf ~/.bundle /usr/local/bundle/cache && ruby -e "puts Dir['/usr/local/bundle/**/{spec,rdoc,resources/shared,resources/collation,resources/locales}']" | xargs rm -rf
|
RUN apk add --no-cache build-base && bundle install && apk del --no-cache build-base && rm -rf ~/.bundle /usr/local/bundle/cache && ruby -e "puts Dir['/usr/local/bundle/**/{spec,rdoc,resources/shared,resources/collation,resources/locales}']" | xargs rm -rf && ln -sf /usr/lib/libonnxruntime.so.1 $(ruby -e "print Dir[Gem::Specification.find_by_name('onnxruntime').gem_dir + '/vendor/*.so'].first")
|
||||||
|
|
||||||
|
RUN echo 'https://dl-cdn.alpinelinux.org/alpine/edge/main' >> /etc/apk/repositories && echo 'https://dl-cdn.alpinelinux.org/alpine/edge/community' >> /etc/apk/repositories && apk add --no-cache onnxruntime
|
||||||
|
|
||||||
COPY ./bin ./bin
|
COPY ./bin ./bin
|
||||||
COPY ./app ./app
|
COPY ./app ./app
|
||||||
@@ -83,6 +86,7 @@ COPY --from=download /fonts/GoNotoKurrent-Regular.ttf /fonts/GoNotoKurrent-Bold.
|
|||||||
COPY --from=download /fonts/FreeSans.ttf /usr/share/fonts/freefont
|
COPY --from=download /fonts/FreeSans.ttf /usr/share/fonts/freefont
|
||||||
COPY --from=download /pdfium-linux/lib/libpdfium.so /usr/lib/libpdfium.so
|
COPY --from=download /pdfium-linux/lib/libpdfium.so /usr/lib/libpdfium.so
|
||||||
COPY --from=download /pdfium-linux/licenses/pdfium.txt /usr/lib/libpdfium-LICENSE.txt
|
COPY --from=download /pdfium-linux/licenses/pdfium.txt /usr/lib/libpdfium-LICENSE.txt
|
||||||
|
COPY --from=download /model.onnx /app/tmp/model.onnx
|
||||||
COPY --from=webpack /app/public/packs ./public/packs
|
COPY --from=webpack /app/public/packs ./public/packs
|
||||||
|
|
||||||
RUN ln -s /fonts /app/public/fonts
|
RUN ln -s /fonts /app/public/fonts
|
||||||
|
|||||||
@@ -24,7 +24,9 @@ gem 'image_processing'
|
|||||||
gem 'jwt'
|
gem 'jwt'
|
||||||
gem 'lograge'
|
gem 'lograge'
|
||||||
gem 'mysql2', require: false
|
gem 'mysql2', require: false
|
||||||
|
gem 'numo-narray'
|
||||||
gem 'oj'
|
gem 'oj'
|
||||||
|
gem 'onnxruntime'
|
||||||
gem 'pagy'
|
gem 'pagy'
|
||||||
gem 'pg', require: false
|
gem 'pg', require: false
|
||||||
gem 'premailer-rails'
|
gem 'premailer-rails'
|
||||||
|
|||||||
+30
-15
@@ -76,7 +76,7 @@ GEM
|
|||||||
public_suffix (>= 2.0.2, < 7.0)
|
public_suffix (>= 2.0.2, < 7.0)
|
||||||
annotaterb (4.14.0)
|
annotaterb (4.14.0)
|
||||||
arabic-letter-connector (0.1.1)
|
arabic-letter-connector (0.1.1)
|
||||||
ast (2.4.2)
|
ast (2.4.3)
|
||||||
aws-eventstream (1.3.0)
|
aws-eventstream (1.3.0)
|
||||||
aws-partitions (1.1027.0)
|
aws-partitions (1.1027.0)
|
||||||
aws-sdk-core (3.214.0)
|
aws-sdk-core (3.214.0)
|
||||||
@@ -291,10 +291,10 @@ GEM
|
|||||||
rdoc (>= 4.0.0)
|
rdoc (>= 4.0.0)
|
||||||
reline (>= 0.4.2)
|
reline (>= 0.4.2)
|
||||||
jmespath (1.6.2)
|
jmespath (1.6.2)
|
||||||
json (2.13.2)
|
json (2.15.0)
|
||||||
jwt (2.9.3)
|
jwt (2.9.3)
|
||||||
base64
|
base64
|
||||||
language_server-protocol (3.17.0.3)
|
language_server-protocol (3.17.0.5)
|
||||||
launchy (3.0.1)
|
launchy (3.0.1)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
childprocess (~> 5.0)
|
childprocess (~> 5.0)
|
||||||
@@ -305,6 +305,7 @@ GEM
|
|||||||
letter_opener (~> 1.9)
|
letter_opener (~> 1.9)
|
||||||
railties (>= 6.1)
|
railties (>= 6.1)
|
||||||
rexml
|
rexml
|
||||||
|
lint_roller (1.1.0)
|
||||||
logger (1.7.0)
|
logger (1.7.0)
|
||||||
lograge (0.14.0)
|
lograge (0.14.0)
|
||||||
actionpack (>= 4)
|
actionpack (>= 4)
|
||||||
@@ -356,17 +357,26 @@ GEM
|
|||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.9-x86_64-linux-musl)
|
nokogiri (1.18.9-x86_64-linux-musl)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
|
numo-narray (0.9.2.1)
|
||||||
oj (3.16.11)
|
oj (3.16.11)
|
||||||
bigdecimal (>= 3.0)
|
bigdecimal (>= 3.0)
|
||||||
ostruct (>= 0.2)
|
ostruct (>= 0.2)
|
||||||
|
onnxruntime (0.10.1)
|
||||||
|
ffi
|
||||||
|
onnxruntime (0.10.1-aarch64-linux)
|
||||||
|
ffi
|
||||||
|
onnxruntime (0.10.1-arm64-darwin)
|
||||||
|
ffi
|
||||||
|
onnxruntime (0.10.1-x86_64-linux)
|
||||||
|
ffi
|
||||||
openssl (3.3.0)
|
openssl (3.3.0)
|
||||||
orm_adapter (0.5.0)
|
orm_adapter (0.5.0)
|
||||||
os (1.1.4)
|
os (1.1.4)
|
||||||
ostruct (0.6.3)
|
ostruct (0.6.3)
|
||||||
package_json (0.1.0)
|
package_json (0.1.0)
|
||||||
pagy (9.3.3)
|
pagy (9.3.3)
|
||||||
parallel (1.26.3)
|
parallel (1.27.0)
|
||||||
parser (3.3.6.0)
|
parser (3.3.9.0)
|
||||||
ast (~> 2.4.1)
|
ast (~> 2.4.1)
|
||||||
racc
|
racc
|
||||||
pg (1.5.9)
|
pg (1.5.9)
|
||||||
@@ -383,6 +393,7 @@ GEM
|
|||||||
pretender (0.5.0)
|
pretender (0.5.0)
|
||||||
actionpack (>= 6.1)
|
actionpack (>= 6.1)
|
||||||
prettyprint (0.2.0)
|
prettyprint (0.2.0)
|
||||||
|
prism (1.5.1)
|
||||||
pry (0.15.0)
|
pry (0.15.0)
|
||||||
coderay (~> 1.1)
|
coderay (~> 1.1)
|
||||||
method_source (~> 1.0)
|
method_source (~> 1.0)
|
||||||
@@ -395,7 +406,7 @@ GEM
|
|||||||
puma (6.5.0)
|
puma (6.5.0)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
racc (1.8.1)
|
racc (1.8.1)
|
||||||
rack (3.2.0)
|
rack (3.2.3)
|
||||||
rack-proxy (0.7.7)
|
rack-proxy (0.7.7)
|
||||||
rack
|
rack
|
||||||
rack-session (2.1.1)
|
rack-session (2.1.1)
|
||||||
@@ -448,7 +459,7 @@ GEM
|
|||||||
psych (>= 4.0.0)
|
psych (>= 4.0.0)
|
||||||
redis-client (0.23.0)
|
redis-client (0.23.0)
|
||||||
connection_pool
|
connection_pool
|
||||||
regexp_parser (2.9.3)
|
regexp_parser (2.11.3)
|
||||||
reline (0.6.2)
|
reline (0.6.2)
|
||||||
io-console (~> 0.5)
|
io-console (~> 0.5)
|
||||||
representable (3.2.0)
|
representable (3.2.0)
|
||||||
@@ -485,18 +496,20 @@ GEM
|
|||||||
rspec-mocks (~> 3.13)
|
rspec-mocks (~> 3.13)
|
||||||
rspec-support (~> 3.13)
|
rspec-support (~> 3.13)
|
||||||
rspec-support (3.13.2)
|
rspec-support (3.13.2)
|
||||||
rubocop (1.69.2)
|
rubocop (1.81.1)
|
||||||
json (~> 2.3)
|
json (~> 2.3)
|
||||||
language_server-protocol (>= 3.17.0)
|
language_server-protocol (~> 3.17.0.2)
|
||||||
|
lint_roller (~> 1.1.0)
|
||||||
parallel (~> 1.10)
|
parallel (~> 1.10)
|
||||||
parser (>= 3.3.0.2)
|
parser (>= 3.3.0.2)
|
||||||
rainbow (>= 2.2.2, < 4.0)
|
rainbow (>= 2.2.2, < 4.0)
|
||||||
regexp_parser (>= 2.9.3, < 3.0)
|
regexp_parser (>= 2.9.3, < 3.0)
|
||||||
rubocop-ast (>= 1.36.2, < 2.0)
|
rubocop-ast (>= 1.47.1, < 2.0)
|
||||||
ruby-progressbar (~> 1.7)
|
ruby-progressbar (~> 1.7)
|
||||||
unicode-display_width (>= 2.4.0, < 4.0)
|
unicode-display_width (>= 2.4.0, < 4.0)
|
||||||
rubocop-ast (1.37.0)
|
rubocop-ast (1.47.1)
|
||||||
parser (>= 3.3.1.0)
|
parser (>= 3.3.7.2)
|
||||||
|
prism (~> 1.4)
|
||||||
rubocop-performance (1.23.0)
|
rubocop-performance (1.23.0)
|
||||||
rubocop (>= 1.48.1, < 2.0)
|
rubocop (>= 1.48.1, < 2.0)
|
||||||
rubocop-ast (>= 1.31.1, < 2.0)
|
rubocop-ast (>= 1.31.1, < 2.0)
|
||||||
@@ -567,9 +580,9 @@ GEM
|
|||||||
tzinfo-data (1.2024.2)
|
tzinfo-data (1.2024.2)
|
||||||
tzinfo (>= 1.0.0)
|
tzinfo (>= 1.0.0)
|
||||||
uber (0.1.0)
|
uber (0.1.0)
|
||||||
unicode-display_width (3.1.2)
|
unicode-display_width (3.2.0)
|
||||||
unicode-emoji (~> 4.0, >= 4.0.4)
|
unicode-emoji (~> 4.1)
|
||||||
unicode-emoji (4.0.4)
|
unicode-emoji (4.1.0)
|
||||||
uniform_notifier (1.16.0)
|
uniform_notifier (1.16.0)
|
||||||
uri (1.0.3)
|
uri (1.0.3)
|
||||||
useragent (0.16.11)
|
useragent (0.16.11)
|
||||||
@@ -634,7 +647,9 @@ DEPENDENCIES
|
|||||||
letter_opener_web
|
letter_opener_web
|
||||||
lograge
|
lograge
|
||||||
mysql2
|
mysql2
|
||||||
|
numo-narray
|
||||||
oj
|
oj
|
||||||
|
onnxruntime
|
||||||
pagy
|
pagy
|
||||||
pg
|
pg
|
||||||
premailer-rails
|
premailer-rails
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ DocuSeal is an open source platform that provides secure and efficient digital d
|
|||||||
- PDF signature verification
|
- PDF signature verification
|
||||||
- Users management
|
- Users management
|
||||||
- Mobile-optimized
|
- Mobile-optimized
|
||||||
- 6 UI languages with signing available in 14 languages
|
- 7 UI languages with signing available in 14 languages
|
||||||
- API and Webhooks for integrations
|
- API and Webhooks for integrations
|
||||||
- Easy to deploy in minutes
|
- Easy to deploy in minutes
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ class AccountsController < ApplicationController
|
|||||||
'es-ES' => 'Español',
|
'es-ES' => 'Español',
|
||||||
'pt-PT' => 'Português',
|
'pt-PT' => 'Português',
|
||||||
'de-DE' => 'Deutsch',
|
'de-DE' => 'Deutsch',
|
||||||
'it-IT' => 'Italiano'
|
'it-IT' => 'Italiano',
|
||||||
|
'nl-NL' => 'Nederlands'
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
before_action :load_account
|
before_action :load_account
|
||||||
@@ -46,6 +47,7 @@ class AccountsController < ApplicationController
|
|||||||
authorize!(:manage, current_account)
|
authorize!(:manage, current_account)
|
||||||
|
|
||||||
true_user.update!(locked_at: Time.current, email: true_user.email.sub('@', '+removed@'))
|
true_user.update!(locked_at: Time.current, email: true_user.email.sub('@', '+removed@'))
|
||||||
|
true_user.account.update!(archived_at: Time.current)
|
||||||
|
|
||||||
# rubocop:disable Layout/LineLength
|
# rubocop:disable Layout/LineLength
|
||||||
render turbo_stream: turbo_stream.replace(
|
render turbo_stream: turbo_stream.replace(
|
||||||
|
|||||||
@@ -34,6 +34,10 @@ module Api
|
|||||||
end
|
end
|
||||||
|
|
||||||
render json: attachment.as_json(only: %i[uuid created_at], methods: %i[url filename content_type])
|
render json: attachment.as_json(only: %i[uuid created_at], methods: %i[url filename content_type])
|
||||||
|
rescue Submitters::MaliciousFileExtension => e
|
||||||
|
Rollbar.error(e) if defined?(Rollbar)
|
||||||
|
|
||||||
|
render json: { error: e.message }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_new_cookie_signatures_json(submitter, attachment)
|
def build_new_cookie_signatures_json(submitter, attachment)
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ module Api
|
|||||||
is_send_email = !params[:send_email].in?(['false', false])
|
is_send_email = !params[:send_email].in?(['false', false])
|
||||||
|
|
||||||
if (emails = (params[:emails] || params[:email]).presence) &&
|
if (emails = (params[:emails] || params[:email]).presence) &&
|
||||||
(params[:submission].blank? && params[:submitters].blank?)
|
params[:submission].blank? && params[:submitters].blank?
|
||||||
Submissions.create_from_emails(template:,
|
Submissions.create_from_emails(template:,
|
||||||
user: current_user,
|
user: current_user,
|
||||||
source: :api,
|
source: :api,
|
||||||
|
|||||||
@@ -17,8 +17,10 @@ class ConsoleRedirectController < ApplicationController
|
|||||||
scope: :console,
|
scope: :console,
|
||||||
exp: 1.minute.from_now.to_i)
|
exp: 1.minute.from_now.to_i)
|
||||||
|
|
||||||
path = Addressable::URI.parse(params[:redir]).path if params[:redir].to_s.starts_with?(Docuseal::CONSOLE_URL)
|
redir_uri = Addressable::URI.parse(params[:redir])
|
||||||
|
path = redir_uri.path if params[:redir].to_s.starts_with?(Docuseal::CONSOLE_URL)
|
||||||
|
|
||||||
redirect_to("#{Docuseal::CONSOLE_URL}#{path}?#{{ auth: }.to_query}", allow_other_host: true)
|
redirect_to "#{Docuseal::CONSOLE_URL}#{path}?#{{ **redir_uri&.query_values, 'auth' => auth }.to_query}",
|
||||||
|
allow_other_host: true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -29,7 +29,7 @@ class SendSubmissionEmailController < ApplicationController
|
|||||||
|
|
||||||
RateLimit.call("send-email-#{@submitter.id}", limit: 2, ttl: 5.minutes)
|
RateLimit.call("send-email-#{@submitter.id}", limit: 2, ttl: 5.minutes)
|
||||||
|
|
||||||
SubmitterMailer.documents_copy_email(@submitter, sig: true).deliver_later! unless already_sent?(@submitter)
|
SubmitterMailer.documents_copy_email(@submitter, sig: true).deliver_later! if can_send?(@submitter)
|
||||||
|
|
||||||
respond_to do |f|
|
respond_to do |f|
|
||||||
f.html { render :success }
|
f.html { render :success }
|
||||||
@@ -39,8 +39,11 @@ class SendSubmissionEmailController < ApplicationController
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def already_sent?(submitter)
|
def can_send?(submitter)
|
||||||
EmailEvent.exists?(tag: :submitter_documents_copy, email: submitter.email, emailable: submitter,
|
return false if submitter.account.archived_at?
|
||||||
event_type: :send, created_at: SEND_DURATION.ago..Time.current)
|
return false if EmailEvent.exists?(tag: :submitter_documents_copy, email: submitter.email, emailable: submitter,
|
||||||
|
event_type: :send, created_at: SEND_DURATION.ago..Time.current)
|
||||||
|
|
||||||
|
true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -41,6 +41,9 @@ class SubmissionsPreviewController < ApplicationController
|
|||||||
|
|
||||||
def completed
|
def completed
|
||||||
@submission = Submission.find_by!(slug: params[:submissions_preview_slug])
|
@submission = Submission.find_by!(slug: params[:submissions_preview_slug])
|
||||||
|
|
||||||
|
raise ActionController::RoutingError, I18n.t('not_found') if @submission.account.archived_at?
|
||||||
|
|
||||||
@template = @submission.template
|
@template = @submission.template
|
||||||
|
|
||||||
render :completed, layout: 'form'
|
render :completed, layout: 'form'
|
||||||
|
|||||||
@@ -75,7 +75,9 @@ class SubmitFormController < ApplicationController
|
|||||||
render json: { error: e.message }, status: :unprocessable_content
|
render json: { error: e.message }, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
|
||||||
def completed; end
|
def completed
|
||||||
|
raise ActionController::RoutingError, I18n.t('not_found') if @submitter.account.archived_at?
|
||||||
|
end
|
||||||
|
|
||||||
def success; end
|
def success; end
|
||||||
|
|
||||||
|
|||||||
@@ -118,7 +118,8 @@ class TemplatesController < ApplicationController
|
|||||||
def template_params
|
def template_params
|
||||||
params.require(:template).permit(
|
params.require(:template).permit(
|
||||||
:name,
|
:name,
|
||||||
{ schema: [[:attachment_uuid, :name, { conditions: [%i[field_uuid value action operation]] }]],
|
{ schema: [[:attachment_uuid, :google_drive_file_id, :name,
|
||||||
|
{ conditions: [%i[field_uuid value action operation]] }]],
|
||||||
submitters: [%i[name uuid is_requester linked_to_uuid invite_by_uuid optional_invite_by_uuid email order]],
|
submitters: [%i[name uuid is_requester linked_to_uuid invite_by_uuid optional_invite_by_uuid email order]],
|
||||||
fields: [[:uuid, :submitter_uuid, :name, :type,
|
fields: [[:uuid, :submitter_uuid, :name, :type,
|
||||||
:required, :readonly, :default_value,
|
:required, :readonly, :default_value,
|
||||||
|
|||||||
@@ -6,12 +6,18 @@ class TemplatesDebugController < ApplicationController
|
|||||||
DEBUG_FILE = ''
|
DEBUG_FILE = ''
|
||||||
|
|
||||||
def show
|
def show
|
||||||
attachment = @template.documents.first
|
schema_uuids = @template.schema.index_by { |e| e['attachment_uuid'] }
|
||||||
|
attachment = @template.documents.find { |a| schema_uuids[a.uuid] }
|
||||||
|
|
||||||
data = attachment.download
|
data = attachment.download
|
||||||
pdf = HexaPDF::Document.new(io: StringIO.new(data))
|
|
||||||
|
|
||||||
fields = Templates::FindAcroFields.call(pdf, attachment, data)
|
unless attachment.image?
|
||||||
|
pdf = HexaPDF::Document.new(io: StringIO.new(data))
|
||||||
|
|
||||||
|
fields = Templates::FindAcroFields.call(pdf, attachment, data)
|
||||||
|
end
|
||||||
|
|
||||||
|
fields = Templates::DetectFields.call(StringIO.new(data), attachment:) if fields.blank?
|
||||||
|
|
||||||
attachment.metadata['pdf'] ||= {}
|
attachment.metadata['pdf'] ||= {}
|
||||||
attachment.metadata['pdf']['fields'] = fields
|
attachment.metadata['pdf']['fields'] = fields
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class TemplatesDetectFieldsController < ApplicationController
|
||||||
|
include ActionController::Live
|
||||||
|
|
||||||
|
load_and_authorize_resource :template
|
||||||
|
|
||||||
|
def create
|
||||||
|
response.headers['Content-Type'] = 'text/event-stream'
|
||||||
|
|
||||||
|
sse = SSE.new(response.stream)
|
||||||
|
|
||||||
|
documents = @template.schema_documents.preload(:blob)
|
||||||
|
|
||||||
|
documents.each do |document|
|
||||||
|
io = StringIO.new(document.download)
|
||||||
|
|
||||||
|
Templates::DetectFields.call(io, attachment: document) do |(attachment_uuid, page, fields)|
|
||||||
|
sse.write({ attachment_uuid:, page:, fields: })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
sse.write({ completed: true })
|
||||||
|
ensure
|
||||||
|
response.stream.close
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -49,6 +49,8 @@ import ShowOnValue from './elements/show_on_value'
|
|||||||
import CustomValidation from './elements/custom_validation'
|
import CustomValidation from './elements/custom_validation'
|
||||||
import ToggleClasses from './elements/toggle_classes'
|
import ToggleClasses from './elements/toggle_classes'
|
||||||
import AutosizeField from './elements/autosize_field'
|
import AutosizeField from './elements/autosize_field'
|
||||||
|
import GoogleDriveFilePicker from './elements/google_drive_file_picker'
|
||||||
|
import OpenModal from './elements/open_modal'
|
||||||
|
|
||||||
import * as TurboInstantClick from './lib/turbo_instant_click'
|
import * as TurboInstantClick from './lib/turbo_instant_click'
|
||||||
|
|
||||||
@@ -136,6 +138,8 @@ safeRegisterElement('show-on-value', ShowOnValue)
|
|||||||
safeRegisterElement('custom-validation', CustomValidation)
|
safeRegisterElement('custom-validation', CustomValidation)
|
||||||
safeRegisterElement('toggle-classes', ToggleClasses)
|
safeRegisterElement('toggle-classes', ToggleClasses)
|
||||||
safeRegisterElement('autosize-field', AutosizeField)
|
safeRegisterElement('autosize-field', AutosizeField)
|
||||||
|
safeRegisterElement('google-drive-file-picker', GoogleDriveFilePicker)
|
||||||
|
safeRegisterElement('open-modal', OpenModal)
|
||||||
|
|
||||||
safeRegisterElement('template-builder', class extends HTMLElement {
|
safeRegisterElement('template-builder', class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
@@ -152,6 +156,7 @@ safeRegisterElement('template-builder', class extends HTMLElement {
|
|||||||
withPhone: this.dataset.withPhone === 'true',
|
withPhone: this.dataset.withPhone === 'true',
|
||||||
withVerification: ['true', 'false'].includes(this.dataset.withVerification) ? this.dataset.withVerification === 'true' : null,
|
withVerification: ['true', 'false'].includes(this.dataset.withVerification) ? this.dataset.withVerification === 'true' : null,
|
||||||
withLogo: this.dataset.withLogo !== 'false',
|
withLogo: this.dataset.withLogo !== 'false',
|
||||||
|
withFieldsDetection: this.dataset.withFieldsDetection === 'true',
|
||||||
editable: this.dataset.editable !== 'false',
|
editable: this.dataset.editable !== 'false',
|
||||||
authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content,
|
authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content,
|
||||||
withPayment: this.dataset.withPayment === 'true',
|
withPayment: this.dataset.withPayment === 'true',
|
||||||
@@ -160,6 +165,7 @@ safeRegisterElement('template-builder', class extends HTMLElement {
|
|||||||
withSendButton: this.dataset.withSendButton !== 'false',
|
withSendButton: this.dataset.withSendButton !== 'false',
|
||||||
withSignYourselfButton: this.dataset.withSignYourselfButton !== 'false',
|
withSignYourselfButton: this.dataset.withSignYourselfButton !== 'false',
|
||||||
withConditions: this.dataset.withConditions === 'true',
|
withConditions: this.dataset.withConditions === 'true',
|
||||||
|
withGoogleDrive: this.dataset.withGoogleDrive === 'true',
|
||||||
withReplaceAndCloneUpload: true,
|
withReplaceAndCloneUpload: true,
|
||||||
currencies: (this.dataset.currencies || '').split(',').filter(Boolean),
|
currencies: (this.dataset.currencies || '').split(',').filter(Boolean),
|
||||||
acceptFileTypes: this.dataset.acceptFileTypes,
|
acceptFileTypes: this.dataset.acceptFileTypes,
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
const iframeTemplate = this.querySelector('template')
|
||||||
|
|
||||||
|
this.observer = new IntersectionObserver((entries) => {
|
||||||
|
if (entries.some(e => e.isIntersecting)) {
|
||||||
|
iframeTemplate.parentElement.prepend(iframeTemplate.content)
|
||||||
|
|
||||||
|
this.observer.disconnect()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.observer.observe(this)
|
||||||
|
|
||||||
|
window.addEventListener('message', this.messageHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
messageHandler = (event) => {
|
||||||
|
if (event.data.type === 'google-drive-files-picked') {
|
||||||
|
this.form.querySelectorAll('input[name="google_drive_file_ids[]"]').forEach(el => el.remove())
|
||||||
|
|
||||||
|
const files = event.data.files || []
|
||||||
|
|
||||||
|
files.forEach((file) => {
|
||||||
|
const input = document.createElement('input')
|
||||||
|
input.type = 'hidden'
|
||||||
|
input.name = 'google_drive_file_ids[]'
|
||||||
|
input.value = file.id
|
||||||
|
this.form.appendChild(input)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.form.querySelector('button[type="submit"]').click()
|
||||||
|
this.loader.classList.remove('hidden')
|
||||||
|
} else if (event.data.type === 'google-drive-picker-loaded') {
|
||||||
|
this.loader.classList.add('hidden')
|
||||||
|
this.form.classList.remove('hidden')
|
||||||
|
} else if (event.data.type === 'google-drive-picker-request-oauth') {
|
||||||
|
document.getElementById(this.dataset.oauthButtonId).classList.remove('hidden')
|
||||||
|
this.classList.add('hidden')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback () {
|
||||||
|
this.observer?.unobserve(this)
|
||||||
|
window.removeEventListener('message', this.messageHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
get form () {
|
||||||
|
return this.querySelector('form')
|
||||||
|
}
|
||||||
|
|
||||||
|
get loader () {
|
||||||
|
return document.getElementById('google_drive_loader')
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
const src = this.getAttribute('src')
|
||||||
|
const link = document.createElement('a')
|
||||||
|
|
||||||
|
link.href = src
|
||||||
|
link.setAttribute('data-turbo-frame', 'modal')
|
||||||
|
link.style.display = 'none'
|
||||||
|
|
||||||
|
this.appendChild(link)
|
||||||
|
|
||||||
|
link.click()
|
||||||
|
|
||||||
|
window.history.replaceState({}, document.title, window.location.pathname)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -553,6 +553,10 @@ export default {
|
|||||||
style.color = this.field.preferences.color
|
style.color = this.field.preferences.color
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.field.preferences?.background) {
|
||||||
|
style.background = this.field.preferences.background
|
||||||
|
}
|
||||||
|
|
||||||
return style
|
return style
|
||||||
},
|
},
|
||||||
isNarrow () {
|
isNarrow () {
|
||||||
|
|||||||
@@ -163,12 +163,20 @@ export default {
|
|||||||
return fetch(this.baseUrl + '/api/attachments', {
|
return fetch(this.baseUrl + '/api/attachments', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData
|
body: formData
|
||||||
}).then(resp => resp.json()).then((data) => {
|
}).then(async (resp) => {
|
||||||
return data
|
const data = await resp.json()
|
||||||
|
|
||||||
|
if (resp.status === 422) {
|
||||||
|
alert(data.error)
|
||||||
|
} else {
|
||||||
|
return data
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})).then((result) => {
|
})).then((result) => {
|
||||||
this.$emit('upload', result)
|
if (result && result[0]) {
|
||||||
|
this.$emit('upload', result)
|
||||||
|
}
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
this.isLoading = false
|
this.isLoading = false
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ const en = {
|
|||||||
type_text: 'Type text',
|
type_text: 'Type text',
|
||||||
email_has_been_sent: 'Email has been sent',
|
email_has_been_sent: 'Email has been sent',
|
||||||
processing: 'Processing',
|
processing: 'Processing',
|
||||||
pay_with_strip: 'Pay with Stripe',
|
pay_with_stripe: 'Pay with Stripe',
|
||||||
reupload: 'Reupload',
|
reupload: 'Reupload',
|
||||||
upload: 'Upload',
|
upload: 'Upload',
|
||||||
files: 'Files',
|
files: 'Files',
|
||||||
@@ -192,7 +192,7 @@ const es = {
|
|||||||
type_text: 'Escribir texto',
|
type_text: 'Escribir texto',
|
||||||
email_has_been_sent: 'El correo electrónico ha sido enviado',
|
email_has_been_sent: 'El correo electrónico ha sido enviado',
|
||||||
processing: 'Procesando',
|
processing: 'Procesando',
|
||||||
pay_with_strip: 'Pagar con Stripe',
|
pay_with_stripe: 'Pagar con Stripe',
|
||||||
reupload: 'Volver a subir',
|
reupload: 'Volver a subir',
|
||||||
upload: 'Subir',
|
upload: 'Subir',
|
||||||
files: 'Archivos',
|
files: 'Archivos',
|
||||||
@@ -222,7 +222,7 @@ const it = {
|
|||||||
value_is_invalid: 'Il valore non è valido',
|
value_is_invalid: 'Il valore non è valido',
|
||||||
verification_code_is_invalid: 'Il codice di verifica non è valido',
|
verification_code_is_invalid: 'Il codice di verifica non è valido',
|
||||||
drawn_signature_on_a_touchscreen_device: 'Firma disegnata su un dispositivo con schermo tattile',
|
drawn_signature_on_a_touchscreen_device: 'Firma disegnata su un dispositivo con schermo tattile',
|
||||||
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Scansiona il codice QR con l\'app della fotocamera per aprire il modulo sul cellulare e disegnare la tua firma',
|
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: "Scansiona il codice QR con l'app della fotocamera per aprire il modulo sul cellulare e disegnare la tua firma",
|
||||||
by_clicking_you_agree_to_the: 'Cliccando su "{button}", accetti il',
|
by_clicking_you_agree_to_the: 'Cliccando su "{button}", accetti il',
|
||||||
electronic_signature_disclosure: 'Divulgazione della Firma Elettronica',
|
electronic_signature_disclosure: 'Divulgazione della Firma Elettronica',
|
||||||
esignature_disclosure: 'Divulgazione della eFirma',
|
esignature_disclosure: 'Divulgazione della eFirma',
|
||||||
@@ -293,7 +293,7 @@ const it = {
|
|||||||
toggle_multiline_text: 'Attiva Testo Multilinea',
|
toggle_multiline_text: 'Attiva Testo Multilinea',
|
||||||
email_has_been_sent: "L'email è stata inviata",
|
email_has_been_sent: "L'email è stata inviata",
|
||||||
processing: 'Elaborazione',
|
processing: 'Elaborazione',
|
||||||
pay_with_strip: 'Paga con Stripe',
|
pay_with_stripe: 'Paga con Stripe',
|
||||||
reupload: 'Ricarica',
|
reupload: 'Ricarica',
|
||||||
upload: 'Carica',
|
upload: 'Carica',
|
||||||
files: 'File',
|
files: 'File',
|
||||||
@@ -304,64 +304,64 @@ const it = {
|
|||||||
const de = {
|
const de = {
|
||||||
please_upload_an_image_file: 'Bitte laden Sie eine Bilddatei hoch',
|
please_upload_an_image_file: 'Bitte laden Sie eine Bilddatei hoch',
|
||||||
must_be_characters_length: 'Muss {number} Zeichen lang sein',
|
must_be_characters_length: 'Muss {number} Zeichen lang sein',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: 'Vervollständigen Sie alle erforderlichen Felder, um mit der Identitätsverifizierung fortzufahren.',
|
complete_all_required_fields_to_proceed_with_identity_verification: 'Füllen Sie alle Pflichtfelder aus, um mit der Identitätsprüfung fortzufahren.',
|
||||||
verify_id: 'ID überprüfen',
|
verify_id: 'ID überprüfen',
|
||||||
identity_verification: 'Identitätsüberprüfung',
|
identity_verification: 'Identitätsprüfung',
|
||||||
complete: 'Abschließen',
|
complete: 'Abschließen',
|
||||||
fill_all_required_fields_to_complete: 'Alle erforderlichen Felder ausfüllen, um abzuschließen',
|
fill_all_required_fields_to_complete: 'Füllen Sie alle Pflichtfelder aus, um abzuschließen',
|
||||||
sign_and_complete: 'Signieren und Abschließen',
|
sign_and_complete: 'Unterzeichnen und abschließen',
|
||||||
|
text: 'Text',
|
||||||
|
by_clicking_you_agree_to_the: 'Durch Klicken auf "{button}" stimmen Sie dem',
|
||||||
|
electronic_signature_disclosure: 'Hinweis zur E-Signatur',
|
||||||
|
esignature_disclosure: 'Hinweis zur eSignatur',
|
||||||
|
signature: 'Unterschrift',
|
||||||
|
initials: 'Initialen',
|
||||||
|
drawn_signature_on_a_touchscreen_device: 'Auf einem Touchscreen-Gerät gezeichnete Unterschrift',
|
||||||
|
approved: 'Genehmigt',
|
||||||
|
reviewed: 'Geprüft',
|
||||||
|
other: 'Sonstiges',
|
||||||
|
authored_by_me: 'Von mir erstellt',
|
||||||
invite: 'Einladen',
|
invite: 'Einladen',
|
||||||
email: 'E-Mail',
|
email: 'E-Mail',
|
||||||
approved: 'Genehmigt',
|
|
||||||
reviewed: 'Überprüft',
|
|
||||||
other: 'Andere',
|
|
||||||
authored_by_me: 'Von mir verfasst',
|
|
||||||
approved_by: 'Genehmigt von',
|
approved_by: 'Genehmigt von',
|
||||||
reviewed_by: 'Überprüft von',
|
reviewed_by: 'Geprüft von',
|
||||||
authored_by: 'Verfasst von',
|
authored_by: 'Erstellt von',
|
||||||
select_a_reason: 'Grund auswählen',
|
select_a_reason: 'Grund auswählen',
|
||||||
value_is_invalid: 'Wert ist ungültig',
|
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Scannen Sie den QR-Code mit der Kamera-App, um das Formular auf dem Mobilgerät zu öffnen und Ihre Unterschrift zu zeichnen',
|
||||||
verification_code_is_invalid: 'Bestätigungscode ist ungültig',
|
|
||||||
drawn_signature_on_a_touchscreen_device: 'Gezeichnete Unterschrift auf einem Touchscreen-Gerät',
|
|
||||||
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Scannen Sie den QR-Code mit der Kamera-App, um das Formular auf dem Handy zu öffnen und Ihre Unterschrift zu zeichnen',
|
|
||||||
by_clicking_you_agree_to_the: 'Durch Klicken auf "{button}" stimmen Sie zu, dass Sie die',
|
|
||||||
electronic_signature_disclosure: 'Elektronische Unterschriftenoffenlegung',
|
|
||||||
esignature_disclosure: 'eSignatur Offenlegung',
|
|
||||||
already_paid: 'Bereits bezahlt',
|
|
||||||
minimize: 'Minimieren',
|
|
||||||
text: 'Text',
|
|
||||||
signature: 'Unterschrift',
|
|
||||||
digitally_signed_by: 'Digital unterschrieben von',
|
|
||||||
reason: 'Grund',
|
|
||||||
initials: 'Initialen',
|
|
||||||
date: 'Datum',
|
date: 'Datum',
|
||||||
number: 'Nummer',
|
number: 'Zahl',
|
||||||
|
value_is_invalid: 'Wert ist ungültig',
|
||||||
|
verification_code_is_invalid: 'Verifizierungscode ist ungültig',
|
||||||
|
already_paid: 'Bereits bezahlt',
|
||||||
image: 'Bild',
|
image: 'Bild',
|
||||||
pay: 'Bezahlen',
|
pay: 'Bezahlen',
|
||||||
|
take_photo: 'Foto aufnehmen',
|
||||||
|
number_phone_is_invalid: 'Telefonnummer ({number}) ist ungültig',
|
||||||
file: 'Datei',
|
file: 'Datei',
|
||||||
select: 'Auswählen',
|
digitally_signed_by: 'Elektronisch signiert von',
|
||||||
|
reason: 'Grund',
|
||||||
|
select: 'Auswahl',
|
||||||
checkbox: 'Checkbox',
|
checkbox: 'Checkbox',
|
||||||
multiple: 'Mehrere',
|
multiple: 'Mehrfach',
|
||||||
radio: 'Radio',
|
radio: 'Radio',
|
||||||
cells: 'Zellen',
|
cells: 'Zellen',
|
||||||
stamp: 'Stempel',
|
stamp: 'Stempel',
|
||||||
|
minimize: 'Minimieren',
|
||||||
payment: 'Zahlung',
|
payment: 'Zahlung',
|
||||||
number_phone_is_invalid: '{number} Telefonnummer ist ungültig',
|
|
||||||
phone: 'Telefon',
|
phone: 'Telefon',
|
||||||
start_now: 'Jetzt starten',
|
start_now: 'Jetzt starten',
|
||||||
continue: 'Fortsetzen',
|
continue: 'Weiter',
|
||||||
sign_now: 'Jetzt unterschreiben',
|
sign_now: 'Jetzt unterzeichnen',
|
||||||
type_here_: 'Hier eingeben...',
|
type_here_: 'Hier eingeben...',
|
||||||
optional: 'optional',
|
optional: 'optional',
|
||||||
option: 'Option',
|
option: 'Option',
|
||||||
appears_on: 'Erscheint auf',
|
appears_on: 'Erscheint auf',
|
||||||
page: 'Seite',
|
page: 'Seite',
|
||||||
take_photo: 'Foto aufnehmen',
|
select_your_option: 'Option auswählen',
|
||||||
select_your_option: 'Wähle deine Option',
|
complete_hightlighted_checkboxes_and_click: 'Füllen Sie die markierten Kontrollkästchen aus und klicken Sie auf',
|
||||||
complete_hightlighted_checkboxes_and_click: 'Markierte Kontrollkästchen ausfüllen und klicken',
|
submit: 'Einreichen',
|
||||||
submit: 'absenden',
|
next: 'Weiter',
|
||||||
next: 'weiter',
|
click_to_upload: 'Zum Hochladen klicken',
|
||||||
click_to_upload: 'Klicken zum Hochladen',
|
|
||||||
or_drag_and_drop_files: 'oder Dateien hierher ziehen und ablegen',
|
or_drag_and_drop_files: 'oder Dateien hierher ziehen und ablegen',
|
||||||
send_copy_via_email: 'Kopie per E-Mail senden',
|
send_copy_via_email: 'Kopie per E-Mail senden',
|
||||||
download: 'Herunterladen',
|
download: 'Herunterladen',
|
||||||
@@ -370,137 +370,137 @@ const de = {
|
|||||||
draw_initials: 'Initialen zeichnen',
|
draw_initials: 'Initialen zeichnen',
|
||||||
type_signature_here: 'Unterschrift hier eingeben',
|
type_signature_here: 'Unterschrift hier eingeben',
|
||||||
type_initial_here: 'Initialen hier eingeben',
|
type_initial_here: 'Initialen hier eingeben',
|
||||||
form_has_been_completed: 'Formular wurde ausgefüllt!',
|
form_has_been_completed: 'Formular wurde abgeschlossen!',
|
||||||
document_has_been_signed: 'Dokument wurde unterschrieben!',
|
document_has_been_signed: 'Dokument wurde unterzeichnet!',
|
||||||
documents_have_been_signed: 'Dokumente wurden unterschrieben!',
|
documents_have_been_signed: 'Dokumente wurden unterzeichnet!',
|
||||||
create_a_free_account: 'Kostenloses Konto erstellen',
|
create_a_free_account: 'Kostenloses Konto erstellen',
|
||||||
powered_by: 'Bereitgestellt von',
|
powered_by: 'Bereitgestellt von',
|
||||||
please_check_the_box_to_continue: 'Bitte setzen Sie das Häkchen, um fortzufahren.',
|
please_check_the_box_to_continue: 'Bitte aktivieren Sie das Kontrollkästchen, um fortzufahren.',
|
||||||
open_source_documents_software: 'Open-Source-Dokumentensoftware',
|
open_source_documents_software: 'Open-Source-Dokumentensoftware',
|
||||||
verified_phone_number: 'Telefonnummer überprüfen',
|
verified_phone_number: 'Telefonnummer verifizieren',
|
||||||
use_international_format: 'Internationales Format verwenden: +1xxx',
|
use_international_format: 'Internationales Format verwenden: +1xxx',
|
||||||
six_digits_code: '6-stelliger Code',
|
six_digits_code: '6-stelliger Code',
|
||||||
change_phone_number: 'Telefonnummer ändern',
|
change_phone_number: 'Telefonnummer ändern',
|
||||||
sending: 'Senden...',
|
sending: 'Senden...',
|
||||||
resend_code: 'Code erneut senden',
|
resend_code: 'Code erneut senden',
|
||||||
verification_code_has_been_resent: 'Die Verifizierungscode wurde erneut per SMS gesendet',
|
verification_code_has_been_resent: 'Der Verifizierungscode wurde per SMS erneut gesendet',
|
||||||
please_fill_all_required_fields: 'Bitte füllen Sie alle erforderlichen Felder aus',
|
please_fill_all_required_fields: 'Bitte füllen Sie alle Pflichtfelder aus',
|
||||||
set_today: 'Heute einstellen',
|
set_today: 'Heute',
|
||||||
|
toggle_multiline_text: 'Mehrzeiligen Text umschalten',
|
||||||
draw_signature: 'Unterschrift zeichnen',
|
draw_signature: 'Unterschrift zeichnen',
|
||||||
type_initial: 'Initialen eingeben',
|
type_initial: 'Initialen eingeben',
|
||||||
draw: 'Zeichnen',
|
draw: 'Zeichnen',
|
||||||
type: 'Eingeben',
|
type: 'Eingeben',
|
||||||
type_text: 'Text eingeben',
|
type_text: 'Text eingeben',
|
||||||
toggle_multiline_text: 'Mehrzeiligen Text umschalten',
|
email_has_been_sent: 'E-Mail wurde gesendet',
|
||||||
email_has_been_sent: 'Die E-Mail wurde gesendet',
|
|
||||||
processing: 'Verarbeitung',
|
processing: 'Verarbeitung',
|
||||||
pay_with_strip: 'Mit Stripe bezahlen',
|
pay_with_stripe: 'Mit Stripe bezahlen',
|
||||||
reupload: 'Erneut hochladen',
|
reupload: 'Erneut hochladen',
|
||||||
upload: 'Hochladen',
|
upload: 'Hochladen',
|
||||||
files: 'Dateien',
|
files: 'Dateien',
|
||||||
signature_is_too_small_or_simple_please_redraw: 'Die Unterschrift ist zu klein oder zu einfach. Bitte erneut zeichnen.',
|
signature_is_too_small_or_simple_please_redraw: 'Die Unterschrift ist zu klein oder zu einfach. Bitte neu zeichnen.',
|
||||||
wait_countdown_seconds: 'Warte {countdown} Sekunden'
|
wait_countdown_seconds: 'Bitte {countdown} Sekunden warten'
|
||||||
}
|
}
|
||||||
|
|
||||||
const fr = {
|
const fr = {
|
||||||
please_upload_an_image_file: 'Veuillez télécharger un fichier image',
|
please_upload_an_image_file: 'Veuillez téléverser un fichier image',
|
||||||
must_be_characters_length: 'Doit contenir {number} caractères',
|
must_be_characters_length: 'Doit comporter {number} caractères',
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: "Veuillez remplir tous les champs obligatoires pour continuer la vérification de l'identité.",
|
complete_all_required_fields_to_proceed_with_identity_verification: "Veuillez remplir tous les champs obligatoires pour poursuivre la vérification d'identité.",
|
||||||
verify_id: "Vérification de l'ID",
|
verify_id: "Vérifier l'ID",
|
||||||
identity_verification: "Vérification de l'identité",
|
identity_verification: "Vérification d'identité",
|
||||||
complete: 'Terminer',
|
complete: 'Terminer',
|
||||||
fill_all_required_fields_to_complete: 'Veuillez remplir tous les champs obligatoires pour compléter',
|
fill_all_required_fields_to_complete: 'Remplissez tous les champs obligatoires pour terminer',
|
||||||
sign_and_complete: 'Signer et Terminer',
|
sign_and_complete: 'Signer et terminer',
|
||||||
invite: 'Inviter',
|
text: 'Texte',
|
||||||
email: 'Courriel',
|
by_clicking_you_agree_to_the: 'En cliquant sur "{button}", vous acceptez la',
|
||||||
|
electronic_signature_disclosure: 'Déclaration relative à la signature électronique',
|
||||||
|
esignature_disclosure: 'Déclaration eSignature',
|
||||||
|
signature: 'Signature',
|
||||||
|
initials: 'Initiales',
|
||||||
|
drawn_signature_on_a_touchscreen_device: 'Signature dessinée sur un appareil à écran tactile',
|
||||||
approved: 'Approuvé',
|
approved: 'Approuvé',
|
||||||
reviewed: 'Révisé',
|
reviewed: 'Révisé',
|
||||||
other: 'Autre',
|
other: 'Autre',
|
||||||
authored_by_me: 'Rédigé par moi',
|
authored_by_me: 'Rédigé par moi',
|
||||||
|
invite: 'Inviter',
|
||||||
|
email: 'E-mail',
|
||||||
approved_by: 'Approuvé par',
|
approved_by: 'Approuvé par',
|
||||||
reviewed_by: 'Révisé par',
|
reviewed_by: 'Révisé par',
|
||||||
authored_by: 'Rédigé par',
|
authored_by: 'Rédigé par',
|
||||||
select_a_reason: 'Sélectionnez une raison',
|
select_a_reason: 'Sélectionnez une raison',
|
||||||
|
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Scannez le QR avec l’appareil photo pour ouvrir le formulaire et signer sur mobile',
|
||||||
|
date: 'Date',
|
||||||
|
number: 'Nombre',
|
||||||
value_is_invalid: 'La valeur est invalide',
|
value_is_invalid: 'La valeur est invalide',
|
||||||
verification_code_is_invalid: 'Le code de vérification est invalide',
|
verification_code_is_invalid: 'Le code de vérification est invalide',
|
||||||
drawn_signature_on_a_touchscreen_device: 'Signature dessinée sur un appareil à écran tactile',
|
|
||||||
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'Scannez le code QR avec l\'application de l\'appareil photo pour ouvrir le formulaire sur mobile et dessiner votre signature',
|
|
||||||
by_clicking_you_agree_to_the: 'En cliquant sur "{button}", vous acceptez la',
|
|
||||||
electronic_signature_disclosure: 'Divulgation de Signature Électronique',
|
|
||||||
esignature_disclosure: 'Divulgation de la eSignature',
|
|
||||||
minimize: 'Réduire',
|
|
||||||
text: 'Texte',
|
|
||||||
already_paid: 'Déjà payé',
|
already_paid: 'Déjà payé',
|
||||||
signature: 'Signature',
|
|
||||||
initials: 'Initiales',
|
|
||||||
digitally_signed_by: 'Signé numériquement par',
|
|
||||||
reason: 'Raison',
|
|
||||||
pay: 'Payer',
|
|
||||||
date: 'Date',
|
|
||||||
number: 'Numéro',
|
|
||||||
image: 'Image',
|
image: 'Image',
|
||||||
|
pay: 'Payer',
|
||||||
|
take_photo: 'Prendre une photo',
|
||||||
|
number_phone_is_invalid: "Le numéro de téléphone {number} n'est pas valide",
|
||||||
file: 'Fichier',
|
file: 'Fichier',
|
||||||
select: 'Choisir',
|
digitally_signed_by: 'Signé électroniquement par',
|
||||||
|
reason: 'Motif',
|
||||||
|
select: 'Sélectionner',
|
||||||
checkbox: 'Coche',
|
checkbox: 'Coche',
|
||||||
multiple: 'Multiple',
|
multiple: 'Multiple',
|
||||||
radio: 'Radio',
|
radio: 'Radio',
|
||||||
cells: 'Cellules',
|
cells: 'Cellules',
|
||||||
stamp: 'Tampon',
|
stamp: 'Tampon',
|
||||||
|
minimize: 'Réduire',
|
||||||
payment: 'Paiement',
|
payment: 'Paiement',
|
||||||
number_phone_is_invalid: '{number} téléphone est invalide',
|
|
||||||
phone: 'Téléphone',
|
phone: 'Téléphone',
|
||||||
start_now: 'Commencer maintenant',
|
start_now: 'Commencer maintenant',
|
||||||
continue: 'Continuer',
|
continue: 'Continuer',
|
||||||
sign_now: 'Signer maintenant',
|
sign_now: 'Signer maintenant',
|
||||||
type_here_: 'Tapez ici...',
|
type_here_: 'Saisissez ici...',
|
||||||
optional: 'facultatif',
|
optional: 'facultatif',
|
||||||
option: 'Option',
|
option: 'Option',
|
||||||
appears_on: 'Apparaît sur',
|
appears_on: 'Apparaît sur',
|
||||||
page: 'Page',
|
page: 'Page',
|
||||||
take_photo: 'Prendre une photo',
|
|
||||||
select_your_option: 'Sélectionnez votre option',
|
select_your_option: 'Sélectionnez votre option',
|
||||||
complete_hightlighted_checkboxes_and_click: 'Complétez les cases à cocher en surbrillance et cliquez',
|
complete_hightlighted_checkboxes_and_click: 'Cochez les cases mises en évidence puis cliquez',
|
||||||
submit: 'envoyer',
|
submit: 'soumettre',
|
||||||
next: 'suivant',
|
next: 'suivant',
|
||||||
click_to_upload: 'Cliquez pour télécharger',
|
click_to_upload: 'Cliquez pour téléverser',
|
||||||
or_drag_and_drop_files: 'ou faites glisser-déposer les fichiers',
|
or_drag_and_drop_files: 'ou faites glisser et déposez des fichiers',
|
||||||
send_copy_via_email: 'Envoyer une copie par e-mail',
|
send_copy_via_email: 'Envoyer une copie par e-mail',
|
||||||
download: 'Télécharger',
|
download: 'Télécharger',
|
||||||
clear: 'Effacer',
|
clear: 'Effacer',
|
||||||
redraw: 'Redessiner',
|
redraw: 'Redessiner',
|
||||||
draw_initials: 'Dessiner les initiales',
|
draw_initials: 'Dessiner les initiales',
|
||||||
type_signature_here: 'Tapez la signature ici',
|
type_signature_here: 'Saisissez la signature ici',
|
||||||
type_initial_here: 'Tapez les initiales ici',
|
type_initial_here: 'Saisissez les initiales ici',
|
||||||
form_has_been_completed: 'Le formulaire a été complété !',
|
form_has_been_completed: 'Le formulaire a été rempli !',
|
||||||
document_has_been_signed: 'Le document a été signé!',
|
document_has_been_signed: 'Le document a été signé !',
|
||||||
documents_have_been_signed: 'Les documents ont été signés!',
|
documents_have_been_signed: 'Les documents ont été signés !',
|
||||||
create_a_free_account: 'Créer un Compte Gratuit',
|
create_a_free_account: 'Créer un compte gratuit',
|
||||||
powered_by: 'Propulsé par',
|
powered_by: 'Propulsé par',
|
||||||
please_check_the_box_to_continue: 'Veuillez cocher la case pour continuer.',
|
please_check_the_box_to_continue: 'Veuillez cocher la case pour continuer.',
|
||||||
open_source_documents_software: 'logiciel de documents open source',
|
open_source_documents_software: 'logiciel de documents open source',
|
||||||
verified_phone_number: 'Vérifier le numéro de téléphone',
|
verified_phone_number: 'Vérifier le numéro de téléphone',
|
||||||
use_international_format: 'Utiliser le format international : +1xxx',
|
use_international_format: 'Utilisez le format international : +1xxx',
|
||||||
six_digits_code: 'Code à 6 chiffres',
|
six_digits_code: 'Code à 6 chiffres',
|
||||||
change_phone_number: 'Changer le numéro de téléphone',
|
change_phone_number: 'Changer de numéro de téléphone',
|
||||||
sending: 'Envoi en cours...',
|
sending: 'Envoi...',
|
||||||
resend_code: 'Renvoyer le code',
|
resend_code: 'Renvoyer le code',
|
||||||
verification_code_has_been_resent: 'Le code de vérification a été renvoyé par SMS',
|
verification_code_has_been_resent: 'Le code de vérification a été renvoyé par SMS',
|
||||||
please_fill_all_required_fields: 'Veuillez remplir tous les champs obligatoires',
|
please_fill_all_required_fields: 'Veuillez remplir tous les champs obligatoires',
|
||||||
set_today: "Définir Aujourd'hui",
|
set_today: "Définir à aujourd'hui",
|
||||||
draw_signature: 'Dessiner une signature',
|
toggle_multiline_text: 'Basculer le texte multiligne',
|
||||||
|
draw_signature: 'Dessiner la signature',
|
||||||
type_initial: 'Saisir les initiales',
|
type_initial: 'Saisir les initiales',
|
||||||
draw: 'Dessiner',
|
draw: 'Dessiner',
|
||||||
type: 'Saisir',
|
type: 'Saisir',
|
||||||
type_text: 'Saisir du texte',
|
type_text: 'Saisir du texte',
|
||||||
toggle_multiline_text: 'Basculer le Texte Multiligne',
|
email_has_been_sent: "L'e-mail a été envoyé",
|
||||||
email_has_been_sent: "L'email a été envoyé",
|
processing: 'Traitement en cours',
|
||||||
processing: 'Traitement',
|
pay_with_stripe: 'Payer avec Stripe',
|
||||||
pay_with_strip: 'Paiement avec Stripe',
|
reupload: 'Retéléverser',
|
||||||
reupload: 'Recharger',
|
upload: 'Téléverser',
|
||||||
upload: 'Télécharger',
|
|
||||||
files: 'Fichiers',
|
files: 'Fichiers',
|
||||||
signature_is_too_small_or_simple_please_redraw: 'La signature est trop petite ou trop simple. Veuillez la redessiner.',
|
signature_is_too_small_or_simple_please_redraw: 'La signature est trop petite ou trop simple. Veuillez la redessiner.',
|
||||||
wait_countdown_seconds: 'Attendez {countdown} secondes'
|
wait_countdown_seconds: 'Veuillez patienter {countdown} secondes'
|
||||||
}
|
}
|
||||||
|
|
||||||
const pl = {
|
const pl = {
|
||||||
@@ -596,7 +596,7 @@ const pl = {
|
|||||||
toggle_multiline_text: 'Przełącz Tekst Wielolinijkowy',
|
toggle_multiline_text: 'Przełącz Tekst Wielolinijkowy',
|
||||||
email_has_been_sent: 'E-mail został wysłany',
|
email_has_been_sent: 'E-mail został wysłany',
|
||||||
processing: 'Przetwarzanie',
|
processing: 'Przetwarzanie',
|
||||||
pay_with_strip: 'Płatność za pomocą Stripe',
|
pay_with_stripe: 'Płatność za pomocą Stripe',
|
||||||
reupload: 'Ponowne przesłanie',
|
reupload: 'Ponowne przesłanie',
|
||||||
upload: 'Przesyłanie',
|
upload: 'Przesyłanie',
|
||||||
files: 'Pliki',
|
files: 'Pliki',
|
||||||
@@ -798,7 +798,7 @@ const cs = {
|
|||||||
toggle_multiline_text: 'Přepnout Víceřádkový Text',
|
toggle_multiline_text: 'Přepnout Víceřádkový Text',
|
||||||
email_has_been_sent: 'E-mail byl odeslán',
|
email_has_been_sent: 'E-mail byl odeslán',
|
||||||
processing: 'Zpracování',
|
processing: 'Zpracování',
|
||||||
pay_with_strip: 'Zaplacení přes Stripe',
|
pay_with_stripe: 'Zaplacení přes Stripe',
|
||||||
reupload: 'Znovu nahrát',
|
reupload: 'Znovu nahrát',
|
||||||
upload: 'Nahrát',
|
upload: 'Nahrát',
|
||||||
files: 'Soubory',
|
files: 'Soubory',
|
||||||
@@ -899,7 +899,7 @@ const pt = {
|
|||||||
toggle_multiline_text: 'Alternar Texto Multilinha',
|
toggle_multiline_text: 'Alternar Texto Multilinha',
|
||||||
email_has_been_sent: 'Email enviado',
|
email_has_been_sent: 'Email enviado',
|
||||||
processing: 'Processamento',
|
processing: 'Processamento',
|
||||||
pay_with_strip: 'Pagar com Stripe',
|
pay_with_stripe: 'Pagar com Stripe',
|
||||||
reupload: 'Reenviar',
|
reupload: 'Reenviar',
|
||||||
upload: 'Carregar',
|
upload: 'Carregar',
|
||||||
files: 'Arquivos',
|
files: 'Arquivos',
|
||||||
@@ -1000,7 +1000,7 @@ const he = {
|
|||||||
type_text: 'הקלד טקסט',
|
type_text: 'הקלד טקסט',
|
||||||
email_has_been_sent: 'האימייל נשלח',
|
email_has_been_sent: 'האימייל נשלח',
|
||||||
processing: 'מעבד',
|
processing: 'מעבד',
|
||||||
pay_with_strip: 'שלם עם סטרייפ',
|
pay_with_stripe: 'שלם עם סטרייפ',
|
||||||
reupload: 'העלה שוב',
|
reupload: 'העלה שוב',
|
||||||
upload: 'העלאה',
|
upload: 'העלאה',
|
||||||
files: 'קבצים',
|
files: 'קבצים',
|
||||||
@@ -1101,7 +1101,7 @@ const nl = {
|
|||||||
type_text: 'Typ tekst',
|
type_text: 'Typ tekst',
|
||||||
email_has_been_sent: 'E-mail is verzonden',
|
email_has_been_sent: 'E-mail is verzonden',
|
||||||
processing: 'Verwerken',
|
processing: 'Verwerken',
|
||||||
pay_with_strip: 'Betalen met Stripe',
|
pay_with_stripe: 'Betalen met Stripe',
|
||||||
reupload: 'Opnieuw uploaden',
|
reupload: 'Opnieuw uploaden',
|
||||||
upload: 'Uploaden',
|
upload: 'Uploaden',
|
||||||
files: 'Bestanden',
|
files: 'Bestanden',
|
||||||
@@ -1303,7 +1303,7 @@ const ko = {
|
|||||||
type_text: '텍스트 입력',
|
type_text: '텍스트 입력',
|
||||||
email_has_been_sent: '이메일이 전송되었습니다',
|
email_has_been_sent: '이메일이 전송되었습니다',
|
||||||
processing: '처리 중',
|
processing: '처리 중',
|
||||||
pay_with_strip: '스트라이프로 결제',
|
pay_with_stripe: '스트라이프로 결제',
|
||||||
reupload: '다시 업로드',
|
reupload: '다시 업로드',
|
||||||
upload: '업로드',
|
upload: '업로드',
|
||||||
files: '파일',
|
files: '파일',
|
||||||
@@ -1404,7 +1404,7 @@ const ja = {
|
|||||||
type_text: 'テキストを入力',
|
type_text: 'テキストを入力',
|
||||||
email_has_been_sent: 'メールが送信されました',
|
email_has_been_sent: 'メールが送信されました',
|
||||||
processing: '処理中',
|
processing: '処理中',
|
||||||
pay_with_strip: 'Stripeで支払う',
|
pay_with_stripe: 'Stripeで支払う',
|
||||||
reupload: '再アップロード',
|
reupload: '再アップロード',
|
||||||
upload: 'アップロード',
|
upload: 'アップロード',
|
||||||
files: 'ファイル',
|
files: 'ファイル',
|
||||||
|
|||||||
@@ -123,6 +123,11 @@
|
|||||||
v-if="!isDrawInitials"
|
v-if="!isDrawInitials"
|
||||||
class="absolute top-0 right-0 left-0 bottom-0"
|
class="absolute top-0 right-0 left-0 bottom-0"
|
||||||
/>
|
/>
|
||||||
|
<label
|
||||||
|
v-if="!isDrawInitials && !modelValue && !computedPreviousValue"
|
||||||
|
for="initials_text_input"
|
||||||
|
class="absolute top-0 right-0 left-0 bottom-0"
|
||||||
|
/>
|
||||||
<canvas
|
<canvas
|
||||||
v-show="!modelValue && !computedPreviousValue"
|
v-show="!modelValue && !computedPreviousValue"
|
||||||
ref="canvas"
|
ref="canvas"
|
||||||
|
|||||||
@@ -62,7 +62,7 @@
|
|||||||
width="22"
|
width="22"
|
||||||
/>
|
/>
|
||||||
<span>
|
<span>
|
||||||
{{ t('pay_with_strip') }}
|
{{ t('pay_with_stripe') }}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -260,8 +260,12 @@
|
|||||||
:style="{ backgroundColor }"
|
:style="{ backgroundColor }"
|
||||||
>
|
>
|
||||||
<Upload
|
<Upload
|
||||||
v-if="sortedDocuments.length && editable && withUploadButton"
|
v-if="editable && withUploadButton"
|
||||||
|
v-show="sortedDocuments.length"
|
||||||
|
ref="upload"
|
||||||
:accept-file-types="acceptFileTypes"
|
:accept-file-types="acceptFileTypes"
|
||||||
|
:authenticity-token="authenticityToken"
|
||||||
|
:with-google-drive="withGoogleDrive"
|
||||||
:template-id="template.id"
|
:template-id="template.id"
|
||||||
@success="updateFromUpload"
|
@success="updateFromUpload"
|
||||||
/>
|
/>
|
||||||
@@ -297,6 +301,8 @@
|
|||||||
v-if="withUploadButton"
|
v-if="withUploadButton"
|
||||||
:template-id="template.id"
|
:template-id="template.id"
|
||||||
:accept-file-types="acceptFileTypes"
|
:accept-file-types="acceptFileTypes"
|
||||||
|
:with-google-drive="withGoogleDrive"
|
||||||
|
@click-google-drive="$refs.upload.openGoogleDriveModal()"
|
||||||
@success="updateFromUpload"
|
@success="updateFromUpload"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
@@ -368,6 +374,8 @@
|
|||||||
v-if="withUploadButton"
|
v-if="withUploadButton"
|
||||||
:template-id="template.id"
|
:template-id="template.id"
|
||||||
:accept-file-types="acceptFileTypes"
|
:accept-file-types="acceptFileTypes"
|
||||||
|
:authenticity-token="authenticityToken"
|
||||||
|
:with-google-drive="withGoogleDrive"
|
||||||
@success="updateFromUpload"
|
@success="updateFromUpload"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
@@ -441,6 +449,7 @@
|
|||||||
:default-required-fields="defaultRequiredFields"
|
:default-required-fields="defaultRequiredFields"
|
||||||
:field-types="fieldTypes"
|
:field-types="fieldTypes"
|
||||||
:with-sticky-submitters="withStickySubmitters"
|
:with-sticky-submitters="withStickySubmitters"
|
||||||
|
:with-fields-detection="withFieldsDetection"
|
||||||
:with-signature-id="withSignatureId"
|
:with-signature-id="withSignatureId"
|
||||||
:with-prefillable="withPrefillable"
|
:with-prefillable="withPrefillable"
|
||||||
:only-defined-fields="onlyDefinedFields"
|
:only-defined-fields="onlyDefinedFields"
|
||||||
@@ -610,6 +619,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
|
withFieldsDetection: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
withAddPageButton: {
|
withAddPageButton: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -766,6 +780,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
withGoogleDrive: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
onlyDefinedFields: {
|
onlyDefinedFields: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -1757,8 +1776,9 @@ export default {
|
|||||||
},
|
},
|
||||||
onDocumentReplace (data) {
|
onDocumentReplace (data) {
|
||||||
const { replaceSchemaItem, schema, documents } = data
|
const { replaceSchemaItem, schema, documents } = data
|
||||||
|
const { google_drive_file_id, ...cleanedReplaceSchemaItem } = replaceSchemaItem
|
||||||
|
|
||||||
this.template.schema.splice(this.template.schema.indexOf(replaceSchemaItem), 1, { ...replaceSchemaItem, ...schema[0] })
|
this.template.schema.splice(this.template.schema.indexOf(replaceSchemaItem), 1, { ...cleanedReplaceSchemaItem, ...schema[0] })
|
||||||
this.template.documents.push(...documents)
|
this.template.documents.push(...documents)
|
||||||
|
|
||||||
if (data.fields) {
|
if (data.fields) {
|
||||||
|
|||||||
@@ -8,8 +8,8 @@
|
|||||||
dir="auto"
|
dir="auto"
|
||||||
:contenteditable="editable"
|
:contenteditable="editable"
|
||||||
style="min-width: 2px"
|
style="min-width: 2px"
|
||||||
:class="iconInline ? 'inline' : 'block'"
|
:class="[iconInline ? 'inline' : 'block', hideIcon ? 'focus:block' : '']"
|
||||||
class="peer outline-none focus:block"
|
class="peer outline-none"
|
||||||
@paste.prevent="onPaste"
|
@paste.prevent="onPaste"
|
||||||
@keydown.enter.prevent="blurContenteditable"
|
@keydown.enter.prevent="blurContenteditable"
|
||||||
@focus="$emit('focus', $event)"
|
@focus="$emit('focus', $event)"
|
||||||
@@ -18,22 +18,19 @@
|
|||||||
{{ value }}
|
{{ value }}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
v-if="withRequired"
|
class="relative inline"
|
||||||
title="Required"
|
:class="{ 'peer-focus:hidden': hideIcon, 'peer-focus:invisible': !hideIcon }"
|
||||||
class="text-red-500 peer-focus:hidden"
|
|
||||||
@click="focusContenteditable"
|
|
||||||
>
|
>
|
||||||
*
|
<IconPencil
|
||||||
|
class="cursor-pointer flex-none opacity-0 group-hover/contenteditable-container:opacity-100 group-hover/contenteditable:opacity-100 align-middle pl-1"
|
||||||
|
:style="iconInline ? {} : { right: -(1.1 * iconWidth) + 'px' }"
|
||||||
|
:title="t('edit')"
|
||||||
|
:class="{ invisible: !editable, 'absolute top-1/2 -translate-y-1/2': !iconInline || floatIcon, 'inline align-bottom': iconInline, 'left-0': floatIcon }"
|
||||||
|
:width="iconWidth + 4"
|
||||||
|
:stroke-width="iconStrokeWidth"
|
||||||
|
@click="[focusContenteditable(), selectOnEditClick && selectContent()]"
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
<IconPencil
|
|
||||||
class="cursor-pointer flex-none opacity-0 group-hover/contenteditable-container:opacity-100 group-hover/contenteditable:opacity-100 align-middle peer-focus:hidden"
|
|
||||||
:style="iconInline ? {} : { right: -(1.1 * iconWidth) + 'px' }"
|
|
||||||
:title="t('edit')"
|
|
||||||
:class="{ invisible: !editable, 'ml-1': !withRequired, 'absolute': !iconInline, 'inline align-bottom': iconInline }"
|
|
||||||
:width="iconWidth"
|
|
||||||
:stroke-width="iconStrokeWidth"
|
|
||||||
@click="[focusContenteditable(), selectOnEditClick && selectContent()]"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -62,7 +59,12 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: 30
|
default: 30
|
||||||
},
|
},
|
||||||
withRequired: {
|
hideIcon: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
floatIcon: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
|
|||||||
@@ -12,8 +12,8 @@
|
|||||||
:for="inputId"
|
:for="inputId"
|
||||||
:class="{ 'opacity-50': isLoading, 'hover:bg-base-200/50': withHoverClass && !isDragEntering, 'bg-base-200/50 border-base-content/30': isDragEntering }"
|
:class="{ 'opacity-50': isLoading, 'hover:bg-base-200/50': withHoverClass && !isDragEntering, 'bg-base-200/50 border-base-content/30': isDragEntering }"
|
||||||
>
|
>
|
||||||
<div class="absolute top-0 right-0 left-0 bottom-0 flex items-center justify-center pointer-events-none">
|
<div class="absolute top-0 right-0 left-0 bottom-0 flex items-center justify-center">
|
||||||
<div class="flex flex-col items-center">
|
<div class="flex flex-col items-center pointer-events-none">
|
||||||
<IconInnerShadowTop
|
<IconInnerShadowTop
|
||||||
v-if="isLoading"
|
v-if="isLoading"
|
||||||
class="animate-spin"
|
class="animate-spin"
|
||||||
@@ -40,6 +40,15 @@
|
|||||||
>
|
>
|
||||||
<span class="font-medium">{{ t('click_to_upload') }}</span> {{ t('or_drag_and_drop_files') }}
|
<span class="font-medium">{{ t('click_to_upload') }}</span> {{ t('or_drag_and_drop_files') }}
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
|
v-if="withGoogleDrive"
|
||||||
|
class="flex items-center text-sm mt-2 pointer-events-auto"
|
||||||
|
@click.stop.prevent="$emit('click-google-drive')"
|
||||||
|
>
|
||||||
|
<span>{{ t('or_add_from') }}</span>
|
||||||
|
<IconBrandGoogleDrive class="w-4 h-4 inline-block ml-1" />
|
||||||
|
<span class="ml-1 font-medium hover:underline">Google Drive</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<form
|
<form
|
||||||
@@ -62,7 +71,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Upload from './upload'
|
import Upload from './upload'
|
||||||
import { IconCloudUpload, IconFilePlus, IconFileSymlink, IconFiles, IconInnerShadowTop } from '@tabler/icons-vue'
|
import { IconCloudUpload, IconFilePlus, IconFileSymlink, IconFiles, IconInnerShadowTop, IconBrandGoogleDrive } from '@tabler/icons-vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FileDropzone',
|
name: 'FileDropzone',
|
||||||
@@ -71,7 +80,8 @@ export default {
|
|||||||
IconCloudUpload,
|
IconCloudUpload,
|
||||||
IconInnerShadowTop,
|
IconInnerShadowTop,
|
||||||
IconFileSymlink,
|
IconFileSymlink,
|
||||||
IconFiles
|
IconFiles,
|
||||||
|
IconBrandGoogleDrive
|
||||||
},
|
},
|
||||||
inject: ['baseFetch', 't'],
|
inject: ['baseFetch', 't'],
|
||||||
props: {
|
props: {
|
||||||
@@ -99,6 +109,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
|
withGoogleDrive: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
title: {
|
title: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -110,7 +125,7 @@ export default {
|
|||||||
default: 'image/*, application/pdf, application/zip'
|
default: 'image/*, application/pdf, application/zip'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['success', 'error', 'loading'],
|
emits: ['success', 'error', 'loading', 'click-google-drive'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
|
|||||||
@@ -52,7 +52,7 @@
|
|||||||
>
|
>
|
||||||
<span class="py-1 flex items-center">
|
<span class="py-1 flex items-center">
|
||||||
<span
|
<span
|
||||||
class="rounded-full w-3 h-3 ml-1 mr-3"
|
class="rounded-full w-3 h-3 ml-1 mr-3 flex-shrink-0"
|
||||||
:class="colors[index % colors.length]"
|
:class="colors[index % colors.length]"
|
||||||
/>
|
/>
|
||||||
<span>
|
<span>
|
||||||
@@ -100,7 +100,7 @@
|
|||||||
class="cursor-pointer text-base-100 flex h-full items-center justify-center"
|
class="cursor-pointer text-base-100 flex h-full items-center justify-center"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="mx-1 w-3 h-3 rounded-full"
|
class="mx-1 w-3 h-3 rounded-full flex-shrink-0"
|
||||||
:class="colors[submitters.indexOf(selectedSubmitter) % colors.length]"
|
:class="colors[submitters.indexOf(selectedSubmitter) % colors.length]"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
@@ -108,11 +108,11 @@
|
|||||||
v-else
|
v-else
|
||||||
ref="label"
|
ref="label"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
class="group cursor-pointer group/contenteditable-container rounded-md p-2 border border-base-300 hover:border-content w-full flex justify-between"
|
class="group cursor-pointer group/contenteditable-container rounded-md p-2 border border-base-300 hover:border-content w-full flex justify-between items-center"
|
||||||
>
|
>
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-2">
|
||||||
<span
|
<span
|
||||||
class="w-3 h-3 rounded-full"
|
class="w-3 h-3 rounded-full flex-shrink-0"
|
||||||
:class="colors[submitters.indexOf(selectedSubmitter) % colors.length]"
|
:class="colors[submitters.indexOf(selectedSubmitter) % colors.length]"
|
||||||
/>
|
/>
|
||||||
<Contenteditable
|
<Contenteditable
|
||||||
@@ -125,7 +125,7 @@
|
|||||||
@update:model-value="$emit('name-change', selectedSubmitter)"
|
@update:model-value="$emit('name-change', selectedSubmitter)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span class="flex items-center transition-all duration-75 group-hover:border border-base-content/20 border-dashed w-6 h-6 flex justify-center items-center rounded">
|
<span class="flex items-center transition-all duration-75 group-hover:border border-base-content/20 border-dashed w-6 h-6 justify-center rounded flex-shrink-0">
|
||||||
<component
|
<component
|
||||||
:is="editable ? 'IconPlus' : 'IconChevronDown'"
|
:is="editable ? 'IconPlus' : 'IconChevronDown'"
|
||||||
width="18"
|
width="18"
|
||||||
@@ -153,7 +153,7 @@
|
|||||||
>
|
>
|
||||||
<span class="py-1 flex items-center">
|
<span class="py-1 flex items-center">
|
||||||
<span
|
<span
|
||||||
class="rounded-full w-3 h-3 ml-1 mr-3"
|
class="rounded-full w-3 h-3 ml-1 mr-3 flex-shrink-0"
|
||||||
:class="colors[index % colors.length]"
|
:class="colors[index % colors.length]"
|
||||||
/>
|
/>
|
||||||
<span>
|
<span>
|
||||||
@@ -164,7 +164,7 @@
|
|||||||
v-if="!compact && submitters.length > 1 && editable"
|
v-if="!compact && submitters.length > 1 && editable"
|
||||||
class="flex"
|
class="flex"
|
||||||
>
|
>
|
||||||
<div class="flex-col pr-1 hidden group-hover:flex -mt-1 h-0">
|
<div class="flex-col pr-1 flex invisible group-hover:visible -mt-1 h-0">
|
||||||
<button
|
<button
|
||||||
:title="t('up')"
|
:title="t('up')"
|
||||||
class="relative w-2"
|
class="relative w-2"
|
||||||
@@ -184,7 +184,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
v-if="!compact && submitters.length > 1 && editable"
|
v-if="!compact && submitters.length > 1 && editable"
|
||||||
class="hidden group-hover:block px-2"
|
class="invisible group-hover:visible px-2"
|
||||||
@click.prevent.stop="remove(submitter)"
|
@click.prevent.stop="remove(submitter)"
|
||||||
>
|
>
|
||||||
<IconTrashX :width="18" />
|
<IconTrashX :width="18" />
|
||||||
|
|||||||
@@ -208,6 +208,34 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="withFieldsDetection && editable && fields.length < 2"
|
||||||
|
class="my-2"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="btn w-full"
|
||||||
|
:class="{ 'bg-base-300': fieldPagesLoaded !== null }"
|
||||||
|
@click="fieldPagesLoaded !== null ? null : detectFields()"
|
||||||
|
>
|
||||||
|
<template v-if="fieldPagesLoaded !== null">
|
||||||
|
<IconInnerShadowTop
|
||||||
|
width="22"
|
||||||
|
class="animate-spin"
|
||||||
|
/>
|
||||||
|
<span class="hidden md:inline">
|
||||||
|
{{ fieldPagesLoaded }} / {{ numberOfPages }} {{ t('processing_') }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<IconListSearch width="22" />
|
||||||
|
<span
|
||||||
|
class="hidden md:inline"
|
||||||
|
>
|
||||||
|
{{ t('autodetect_fields') }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
v-show="fields.length < 4 && editable && withHelp && showTourStartForm"
|
v-show="fields.length < 4 && editable && withHelp && showTourStartForm"
|
||||||
class="rounded py-2 px-4 w-full border border-dashed border-base-300"
|
class="rounded py-2 px-4 w-full border border-dashed border-base-300"
|
||||||
@@ -231,7 +259,7 @@
|
|||||||
import Field from './field'
|
import Field from './field'
|
||||||
import FieldType from './field_type'
|
import FieldType from './field_type'
|
||||||
import FieldSubmitter from './field_submitter'
|
import FieldSubmitter from './field_submitter'
|
||||||
import { IconLock, IconCirclePlus } from '@tabler/icons-vue'
|
import { IconLock, IconCirclePlus, IconInnerShadowTop, IconListSearch } from '@tabler/icons-vue'
|
||||||
import IconDrag from './icon_drag'
|
import IconDrag from './icon_drag'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -240,11 +268,13 @@ export default {
|
|||||||
Field,
|
Field,
|
||||||
FieldType,
|
FieldType,
|
||||||
IconCirclePlus,
|
IconCirclePlus,
|
||||||
|
IconListSearch,
|
||||||
|
IconInnerShadowTop,
|
||||||
FieldSubmitter,
|
FieldSubmitter,
|
||||||
IconDrag,
|
IconDrag,
|
||||||
IconLock
|
IconLock
|
||||||
},
|
},
|
||||||
inject: ['save', 'backgroundColor', 'withPhone', 'withVerification', 'withPayment', 't', 'fieldsDragFieldRef'],
|
inject: ['save', 'backgroundColor', 'withPhone', 'withVerification', 'withPayment', 't', 'fieldsDragFieldRef', 'baseFetch'],
|
||||||
props: {
|
props: {
|
||||||
fields: {
|
fields: {
|
||||||
type: Array,
|
type: Array,
|
||||||
@@ -255,6 +285,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: null
|
default: null
|
||||||
},
|
},
|
||||||
|
withFieldsDetection: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
withSignatureId: {
|
withSignatureId: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -331,12 +366,18 @@ export default {
|
|||||||
emits: ['add-field', 'set-draw', 'set-draw-type', 'set-drag', 'drag-end', 'scroll-to-area', 'change-submitter', 'set-drag-placeholder'],
|
emits: ['add-field', 'set-draw', 'set-draw-type', 'set-drag', 'drag-end', 'scroll-to-area', 'change-submitter', 'set-drag-placeholder'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
fieldPagesLoaded: null,
|
||||||
defaultFieldsSearch: ''
|
defaultFieldsSearch: ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
fieldNames: FieldType.computed.fieldNames,
|
fieldNames: FieldType.computed.fieldNames,
|
||||||
fieldIcons: FieldType.computed.fieldIcons,
|
fieldIcons: FieldType.computed.fieldIcons,
|
||||||
|
numberOfPages () {
|
||||||
|
return this.template.documents.reduce((acc, doc) => {
|
||||||
|
return acc + doc.metadata?.pdf?.number_of_pages || doc.preview_images.length
|
||||||
|
}, 0)
|
||||||
|
},
|
||||||
isShowFieldSearch () {
|
isShowFieldSearch () {
|
||||||
if (this.withFieldsSearch === false) {
|
if (this.withFieldsSearch === false) {
|
||||||
return false
|
return false
|
||||||
@@ -389,6 +430,68 @@ export default {
|
|||||||
|
|
||||||
this.$emit('set-drag', field)
|
this.$emit('set-drag', field)
|
||||||
},
|
},
|
||||||
|
detectFields () {
|
||||||
|
const fields = []
|
||||||
|
|
||||||
|
this.fieldPagesLoaded = 0
|
||||||
|
|
||||||
|
this.baseFetch(`/templates/${this.template.id}/detect_fields`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
}).then(async (response) => {
|
||||||
|
const reader = response.body.getReader()
|
||||||
|
const decoder = new TextDecoder('utf-8')
|
||||||
|
let buffer = ''
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const { value, done } = await reader.read()
|
||||||
|
|
||||||
|
if (done) break
|
||||||
|
|
||||||
|
buffer += decoder.decode(value, { stream: true })
|
||||||
|
|
||||||
|
const lines = buffer.split('\n\n')
|
||||||
|
|
||||||
|
buffer = lines.pop()
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.startsWith('data: ')) {
|
||||||
|
const jsonStr = line.replace(/^data: /, '')
|
||||||
|
const data = JSON.parse(jsonStr)
|
||||||
|
|
||||||
|
if (data.error) {
|
||||||
|
alert(data.error)
|
||||||
|
|
||||||
|
break
|
||||||
|
} else if (data.completed) {
|
||||||
|
this.fieldPagesLoaded = null
|
||||||
|
this.template.fields = fields
|
||||||
|
this.save()
|
||||||
|
|
||||||
|
break
|
||||||
|
} else if (data.fields) {
|
||||||
|
data.fields.forEach((f) => {
|
||||||
|
if (!f.submitter_uuid) {
|
||||||
|
f.submitter_uuid = this.template.submitters[0].uuid
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.fieldPagesLoaded += 1
|
||||||
|
|
||||||
|
fields.push(...data.fields)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
console.error('Error in streaming message: ', error)
|
||||||
|
}).finally(() => {
|
||||||
|
this.fieldPagesLoaded = null
|
||||||
|
this.isFieldsLoading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
setDragPlaceholder (event) {
|
setDragPlaceholder (event) {
|
||||||
this.$emit('set-drag-placeholder', {
|
this.$emit('set-drag-placeholder', {
|
||||||
offsetX: event.offsetX,
|
offsetX: event.offsetX,
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="dropdown"
|
||||||
|
:class="{ 'dropdown-open': isLoading }"
|
||||||
|
>
|
||||||
|
<label tabindex="0">
|
||||||
|
<IconBrandGoogleDrive
|
||||||
|
width="19"
|
||||||
|
class="inline-block mr-1 cursor-pointer"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<ul
|
||||||
|
tabindex="0"
|
||||||
|
:style="{ backgroundColor }"
|
||||||
|
class="dropdown-content z-[1] shadow menu rounded-box"
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
:href="`https://drive.google.com/file/d/${googleDriveFileId}/view?usp=sharing`"
|
||||||
|
data-turbo="false"
|
||||||
|
target="_blank"
|
||||||
|
class="flex items-center"
|
||||||
|
>
|
||||||
|
<IconExternalLink class="w-4 h-4 flex-shrink-0" />
|
||||||
|
<span>{{ t('view') }}</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
form="sync_form"
|
||||||
|
type="submit"
|
||||||
|
:disabled="isLoading"
|
||||||
|
>
|
||||||
|
<IconRefresh
|
||||||
|
class="w-4 h-4 flex-shrink-0"
|
||||||
|
:class="{ 'animate-spin': isLoading }"
|
||||||
|
/>
|
||||||
|
<span>{{ message }}</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<form
|
||||||
|
id="sync_form"
|
||||||
|
ref="form"
|
||||||
|
class="hidden"
|
||||||
|
@submit.prevent="upload({ path: uploadUrl })"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
:id="inputId"
|
||||||
|
ref="input"
|
||||||
|
:value="googleDriveFileId"
|
||||||
|
name="google_drive_file_ids[]"
|
||||||
|
>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Upload from './upload'
|
||||||
|
import { IconRefresh, IconBrandGoogleDrive, IconExternalLink } from '@tabler/icons-vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'GoogleDriveDocumentSettings',
|
||||||
|
components: {
|
||||||
|
IconRefresh,
|
||||||
|
IconBrandGoogleDrive,
|
||||||
|
IconExternalLink
|
||||||
|
},
|
||||||
|
inject: ['baseFetch', 't', 'backgroundColor'],
|
||||||
|
props: {
|
||||||
|
templateId: {
|
||||||
|
type: [Number, String],
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
googleDriveFileId: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['success'],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
isLoading: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
inputId () {
|
||||||
|
return 'el' + Math.random().toString(32).split('.')[1]
|
||||||
|
},
|
||||||
|
uploadUrl () {
|
||||||
|
return `/templates/${this.templateId}/google_drive_documents`
|
||||||
|
},
|
||||||
|
message () {
|
||||||
|
if (this.isLoading) {
|
||||||
|
return this.t('syncing')
|
||||||
|
} else {
|
||||||
|
return this.t('sync')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
upload: Upload.methods.upload
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
const en = {
|
const en = {
|
||||||
|
view: 'View',
|
||||||
|
autodetect_fields: 'Autodetect fields',
|
||||||
payment_link: 'Payment link',
|
payment_link: 'Payment link',
|
||||||
strikeout: 'Strikeout',
|
strikeout: 'Strikeout',
|
||||||
draw_strikethrough_the_document: 'Draw strikethrough the document',
|
draw_strikethrough_the_document: 'Draw strikethrough the document',
|
||||||
@@ -176,10 +178,14 @@ const en = {
|
|||||||
and: 'and',
|
and: 'and',
|
||||||
or: 'or',
|
or: 'or',
|
||||||
start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: 'Start a quick tour to learn how to create an send your first document',
|
start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: 'Start a quick tour to learn how to create an send your first document',
|
||||||
start_tour: 'Start Tour'
|
start_tour: 'Start Tour',
|
||||||
|
or_add_from: 'Or add from',
|
||||||
|
sync: 'Sync',
|
||||||
|
syncing: 'Syncing...'
|
||||||
}
|
}
|
||||||
|
|
||||||
const es = {
|
const es = {
|
||||||
|
view: 'Vista',
|
||||||
payment_link: 'Enlace de pago',
|
payment_link: 'Enlace de pago',
|
||||||
strikeout: 'Tachar',
|
strikeout: 'Tachar',
|
||||||
draw_strikethrough_the_document: 'Dibujar una línea de tachado en el documento',
|
draw_strikethrough_the_document: 'Dibujar una línea de tachado en el documento',
|
||||||
@@ -357,10 +363,14 @@ const es = {
|
|||||||
and: 'y',
|
and: 'y',
|
||||||
or: 'o',
|
or: 'o',
|
||||||
start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: 'Inicia una guía rápida para aprender a crear y enviar tu primer documento.',
|
start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: 'Inicia una guía rápida para aprender a crear y enviar tu primer documento.',
|
||||||
start_tour: 'Iniciar guía'
|
start_tour: 'Iniciar guía',
|
||||||
|
or_add_from: 'O agregar desde',
|
||||||
|
sync: 'Sincronizar',
|
||||||
|
syncing: 'Sincronizando...'
|
||||||
}
|
}
|
||||||
|
|
||||||
const it = {
|
const it = {
|
||||||
|
view: 'Vista',
|
||||||
payment_link: 'Link di pagamento',
|
payment_link: 'Link di pagamento',
|
||||||
strikeout: 'Barrato',
|
strikeout: 'Barrato',
|
||||||
draw_strikethrough_the_document: 'Disegna una linea barrata sul documento',
|
draw_strikethrough_the_document: 'Disegna una linea barrata sul documento',
|
||||||
@@ -538,10 +548,14 @@ const it = {
|
|||||||
and: 'e',
|
and: 'e',
|
||||||
or: 'o',
|
or: 'o',
|
||||||
start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: 'Inizia un tour rapido per imparare a creare e inviare il tuo primo documento.',
|
start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: 'Inizia un tour rapido per imparare a creare e inviare il tuo primo documento.',
|
||||||
start_tour: 'Inizia il tour'
|
start_tour: 'Inizia il tour',
|
||||||
|
or_add_from: 'O aggiungi da',
|
||||||
|
sync: 'Sincronizza',
|
||||||
|
syncing: 'Sincronizzazione...'
|
||||||
}
|
}
|
||||||
|
|
||||||
const pt = {
|
const pt = {
|
||||||
|
view: 'Visualizar',
|
||||||
payment_link: 'Link de pagamento',
|
payment_link: 'Link de pagamento',
|
||||||
strikeout: 'Tachado',
|
strikeout: 'Tachado',
|
||||||
draw_strikethrough_the_document: 'Desenhe uma linha de tachado no documento',
|
draw_strikethrough_the_document: 'Desenhe uma linha de tachado no documento',
|
||||||
@@ -719,94 +733,104 @@ const pt = {
|
|||||||
and: 'e',
|
and: 'e',
|
||||||
or: 'ou',
|
or: 'ou',
|
||||||
start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: 'Comece um tour rápido para aprender a criar e enviar seu primeiro documento.',
|
start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: 'Comece um tour rápido para aprender a criar e enviar seu primeiro documento.',
|
||||||
start_tour: 'Iniciar tour'
|
start_tour: 'Iniciar tour',
|
||||||
|
or_add_from: 'Ou adicionar de',
|
||||||
|
sync: 'Sincronizar',
|
||||||
|
syncing: 'Sincronizando...'
|
||||||
}
|
}
|
||||||
|
|
||||||
const fr = {
|
const fr = {
|
||||||
|
view: 'Voir',
|
||||||
payment_link: 'Lien de paiement',
|
payment_link: 'Lien de paiement',
|
||||||
strikeout: 'Barrer',
|
strikeout: 'Rature',
|
||||||
draw_strikethrough_the_document: 'Tracer une ligne de suppression sur le document',
|
draw_strikethrough_the_document: 'Tracer une rature sur le document',
|
||||||
quantity: 'Quantité',
|
quantity: 'Quantité',
|
||||||
prefillable: 'Pré-remplissable',
|
prefillable: 'Préremplissable',
|
||||||
signature_id: 'ID de signature',
|
signature_id: 'Identifiant de signature',
|
||||||
error_message: 'Message d\'erreur',
|
error_message: "Message d'erreur",
|
||||||
length: 'Longueur',
|
length: 'Longueur',
|
||||||
min: 'Min',
|
min: 'Min',
|
||||||
max: 'Max',
|
max: 'Max',
|
||||||
date_signed: 'Date actuelle',
|
|
||||||
font: 'Police',
|
font: 'Police',
|
||||||
party: 'Partie',
|
party: 'Partie',
|
||||||
|
date_signed: 'Date de signature',
|
||||||
method: 'Méthode',
|
method: 'Méthode',
|
||||||
reorder_fields: 'Réorganiser les champs',
|
reorder_fields: 'Réorganiser les champs',
|
||||||
verify_id: "Vérifier l'ID",
|
verify_id: "Vérifier l'ID",
|
||||||
obtain_qualified_electronic_signature_with_the_trusted_provider_click_to_learn_more: 'Obtenez une signature électronique qualifiée (QeS) avec le fournisseur de confiance. Cliquez pour en savoir plus.',
|
obtain_qualified_electronic_signature_with_the_trusted_provider_click_to_learn_more: 'Obtenez une signature électronique qualifiée (QeS) auprès du fournisseur de confiance. Cliquez pour en savoir plus.',
|
||||||
|
editable: 'Modifiable',
|
||||||
recurrent: 'Récurrent',
|
recurrent: 'Récurrent',
|
||||||
one_off: 'Ponctuel',
|
one_off: 'Ponctuel',
|
||||||
editable: 'Éditable',
|
|
||||||
search_field: 'Champ de recherche',
|
search_field: 'Champ de recherche',
|
||||||
field_not_found: 'Champ non trouvé',
|
field_not_found: 'Champ introuvable',
|
||||||
clear: 'Effacer',
|
clear: 'Effacer',
|
||||||
type_value: 'Tapez la valeur',
|
|
||||||
add_all_required_fields_to_continue: 'Ajoutez tous les champs obligatoires pour continuer',
|
|
||||||
uploaded_pdf_contains_form_fields_keep_or_remove_them: 'Le PDF téléchargé contient des champs. Les conserver ou les supprimer?',
|
|
||||||
keep: 'Conserver',
|
|
||||||
align: 'Aligner',
|
align: 'Aligner',
|
||||||
|
add_all_required_fields_to_continue: 'Ajoutez tous les champs obligatoires pour continuer',
|
||||||
|
uploaded_pdf_contains_form_fields_keep_or_remove_them: 'Le PDF téléversé contient des champs de formulaire. Les conserver ou les supprimer ?',
|
||||||
|
keep: 'Conserver',
|
||||||
left: 'Gauche',
|
left: 'Gauche',
|
||||||
heading: 'En-tête',
|
heading: 'Titre',
|
||||||
validation: 'Validation',
|
validation: 'Validation',
|
||||||
add_blank_page: 'Ajouter une page blanche',
|
add_blank_page: 'Ajouter une page blanche',
|
||||||
right: 'Droite',
|
right: 'Droite',
|
||||||
add_condition: 'Ajouter une condition',
|
|
||||||
center: 'Centre',
|
center: 'Centre',
|
||||||
with_logo: 'Avec logo',
|
|
||||||
description: 'Description',
|
description: 'Description',
|
||||||
display_title: "Titre d'affichage",
|
display_title: 'Afficher le titre',
|
||||||
unchecked: 'Non coché',
|
with_logo: 'Avec logo',
|
||||||
|
unchecked: 'Décoché',
|
||||||
price: 'Prix',
|
price: 'Prix',
|
||||||
|
type_value: 'Saisir une valeur',
|
||||||
equal: 'Égal',
|
equal: 'Égal',
|
||||||
not_equal: 'Non égal',
|
not_equal: 'Différent',
|
||||||
contains: 'Contient',
|
contains: 'Contient',
|
||||||
signing_date: 'Date de signature',
|
|
||||||
does_not_contain: 'Ne contient pas',
|
does_not_contain: 'Ne contient pas',
|
||||||
not_empty: 'Non vide',
|
not_empty: 'Non vide',
|
||||||
empty: 'Vide',
|
empty: 'Vide',
|
||||||
select_field_: 'Sélectionner un champ...',
|
select_field_: 'Sélectionner un champ...',
|
||||||
select_value_: 'Sélectionner une valeur...',
|
select_value_: 'Sélectionner une valeur...',
|
||||||
remove_condition: 'Supprimer la condition',
|
remove_condition: 'Supprimer la condition',
|
||||||
condition: 'Condition',
|
add_condition: 'Ajouter une condition',
|
||||||
formula: 'Formule',
|
are_you_sure_: 'Êtes-vous sûr ?',
|
||||||
edit: 'Éditer',
|
sign_yourself: 'Signer vous-même',
|
||||||
|
set_signing_date: 'Définir la date de signature',
|
||||||
|
signing_date: 'Date de signature',
|
||||||
|
send: 'Envoyer',
|
||||||
|
remove: 'Supprimer',
|
||||||
|
edit: 'Modifier',
|
||||||
settings: 'Paramètres',
|
settings: 'Paramètres',
|
||||||
up: 'Haut',
|
up: 'Haut',
|
||||||
down: 'Bas',
|
down: 'Bas',
|
||||||
set_signing_date: 'Définir la date de signature',
|
|
||||||
are_you_sure_: 'Êtes-vous sûr?',
|
|
||||||
sign_yourself: 'Signez-vous',
|
|
||||||
checked: 'Coché',
|
checked: 'Coché',
|
||||||
send: 'Envoyer',
|
|
||||||
remove: 'Supprimer',
|
|
||||||
save: 'Enregistrer',
|
save: 'Enregistrer',
|
||||||
cancel: 'Annuler',
|
cancel: 'Annuler',
|
||||||
draw_field_on_the_document: 'Dessiner un champ sur le document',
|
any: "N'importe lequel",
|
||||||
click_to_upload: 'Cliquez pour télécharger',
|
drawn: 'Dessiné',
|
||||||
|
drawn_or_typed: 'Dessiné ou tapé',
|
||||||
|
drawn_or_upload: 'Dessiné ou téléversé',
|
||||||
|
upload: 'Téléverser',
|
||||||
|
formula: 'Formule',
|
||||||
|
typed: 'Tapé',
|
||||||
|
draw_field_on_the_document: 'Dessinez un champ sur le document',
|
||||||
|
click_to_upload: 'Cliquez pour téléverser',
|
||||||
or_drag_and_drop_files: 'ou faites glisser-déposer des fichiers',
|
or_drag_and_drop_files: 'ou faites glisser-déposer des fichiers',
|
||||||
uploading: 'Téléchargement en cours',
|
uploading: 'Téléversement en cours',
|
||||||
processing_: 'Traitement en cours...',
|
processing_: 'Traitement...',
|
||||||
add_pdf_documents_or_images: 'Ajoutez des documents PDF ou des images',
|
add_pdf_documents_or_images: 'Ajouter des documents PDF ou des images',
|
||||||
add_documents_or_images: 'Ajoutez des documents ou des images',
|
add_documents_or_images: 'Ajouter des documents ou des images',
|
||||||
add_a_new_document: 'Adicionar um novo documento',
|
add_a_new_document: 'Ajouter un nouveau document',
|
||||||
replace_existing_document: 'Remplacer le document existant',
|
replace_existing_document: 'Remplacer le document existant',
|
||||||
clone_and_replace_documents: 'Cloner et remplacer les documents',
|
clone_and_replace_documents: 'Cloner et remplacer des documents',
|
||||||
required: 'Requis',
|
required: 'Obligatoire',
|
||||||
default_value: 'Valeur par défaut',
|
default_value: 'Valeur par défaut',
|
||||||
format: 'Format',
|
format: 'Format',
|
||||||
read_only: 'Lecture seule',
|
read_only: 'Lecture seule',
|
||||||
page: 'Page',
|
page: 'Page',
|
||||||
draw_new_area: 'Dessiner une nouvelle zone',
|
draw_new_area: 'Dessiner une zone',
|
||||||
copy_to_all_pages: 'Copier sur toutes les pages',
|
copy_to_all_pages: 'Copier sur toutes les pages',
|
||||||
add_option: 'Ajouter une option',
|
add_option: 'Ajouter une option',
|
||||||
option: 'Option',
|
option: 'Option',
|
||||||
options: 'Options',
|
options: 'Options',
|
||||||
|
condition: 'Condition',
|
||||||
first_party: 'Première partie',
|
first_party: 'Première partie',
|
||||||
second_party: 'Deuxième partie',
|
second_party: 'Deuxième partie',
|
||||||
third_party: 'Troisième partie',
|
third_party: 'Troisième partie',
|
||||||
@@ -817,27 +841,27 @@ const fr = {
|
|||||||
eighth_party: 'Huitième partie',
|
eighth_party: 'Huitième partie',
|
||||||
ninth_party: 'Neuvième partie',
|
ninth_party: 'Neuvième partie',
|
||||||
tenth_party: 'Dixième partie',
|
tenth_party: 'Dixième partie',
|
||||||
eleventh_party: 'Onzième Parti',
|
eleventh_party: 'Onzième partie',
|
||||||
twelfth_party: 'Douzième Parti',
|
twelfth_party: 'Douzième partie',
|
||||||
thirteenth_party: 'Treizième Parti',
|
thirteenth_party: 'Treizième partie',
|
||||||
fourteenth_party: 'Quatorzième Parti',
|
fourteenth_party: 'Quatorzième partie',
|
||||||
fifteenth_party: 'Quinzième Parti',
|
fifteenth_party: 'Quinzième partie',
|
||||||
sixteenth_party: 'Seizième Parti',
|
sixteenth_party: 'Seizième partie',
|
||||||
seventeenth_party: 'Dix-septième Parti',
|
seventeenth_party: 'Dix-septième partie',
|
||||||
eighteenth_party: 'Dix-huitième Parti',
|
eighteenth_party: 'Dix-huitième partie',
|
||||||
nineteenth_party: 'Dix-Neuvième Fête',
|
nineteenth_party: 'Dix-neuvième partie',
|
||||||
twentieth_party: 'Vingtième Fête',
|
twentieth_party: 'Vingtième partie',
|
||||||
draw: 'Dessiner',
|
draw: 'Dessiner',
|
||||||
add: 'Ajouter',
|
add: 'Ajouter',
|
||||||
or_add_field_without_drawing: 'Ou ajoutez un champ sans dessiner',
|
or_add_field_without_drawing: 'Ou ajouter un champ sans le dessiner',
|
||||||
text: 'Texte',
|
text: 'Texte',
|
||||||
|
number: 'Nombre',
|
||||||
signature: 'Signature',
|
signature: 'Signature',
|
||||||
initials: 'Initiales',
|
initials: 'Initiales',
|
||||||
date: 'Date',
|
date: 'Date',
|
||||||
number: 'Numéro',
|
|
||||||
image: 'Image',
|
image: 'Image',
|
||||||
file: 'Fichier',
|
file: 'Fichier',
|
||||||
select: 'Choisir',
|
select: 'Sélection',
|
||||||
checkbox: 'Coche',
|
checkbox: 'Coche',
|
||||||
multiple: 'Multiple',
|
multiple: 'Multiple',
|
||||||
radio: 'Radio',
|
radio: 'Radio',
|
||||||
@@ -845,40 +869,34 @@ const fr = {
|
|||||||
stamp: 'Tampon',
|
stamp: 'Tampon',
|
||||||
payment: 'Paiement',
|
payment: 'Paiement',
|
||||||
phone: 'Téléphone',
|
phone: 'Téléphone',
|
||||||
text_field: 'Champ de Texte',
|
text_field: 'Champ de texte',
|
||||||
signature_field: 'Champ de Signature',
|
signature_field: 'Champ de signature',
|
||||||
initials_field: "Champ d'Initiales",
|
initials_field: "Champ d'initiales",
|
||||||
date_field: 'Champ de Date',
|
date_field: 'Champ de date',
|
||||||
number_field: 'Champ de Numéro',
|
number_field: 'Champ de numéro',
|
||||||
image_field: "Champ d'Image",
|
image_field: "Champ d'image",
|
||||||
file_field: 'Champ de Fichier',
|
file_field: 'Champ de fichier',
|
||||||
select_field: 'Champ de Choix',
|
select_field: 'Champ de choix',
|
||||||
checkbox_field: 'Champ de Coche',
|
checkbox_field: 'Champ de coche',
|
||||||
multiple_field: 'Champ de Choix Multiple',
|
multiple_field: 'Champ de choix multiple',
|
||||||
radio_field: 'Champ de Groupe Radio',
|
radio_field: 'Champ de groupe radio',
|
||||||
cells_field: 'Champ de Cellules',
|
cells_field: 'Champ de cellules',
|
||||||
stamp_field: 'Champ de Tampon',
|
stamp_field: 'Champ de tampon',
|
||||||
payment_field: 'Champ de Paiement',
|
payment_field: 'Champ de paiement',
|
||||||
phone_field: 'Champ de Téléphone',
|
phone_field: 'Champ de téléphone',
|
||||||
draw_a_text_field_on_the_page_with_a_mouse: 'Dessinez un champ texte sur la page avec une souris',
|
draw_a_text_field_on_the_page_with_a_mouse: 'Dessinez un champ de texte sur la page avec la souris',
|
||||||
drag_and_drop_any_other_field_type_on_the_page: 'Glissez et déposez tout autre type de champ sur la page',
|
drag_and_drop_any_other_field_type_on_the_page: 'Glissez-déposez tout autre type de champ sur la page',
|
||||||
click_on_the_field_type_above_to_start_drawing_it: 'Cliquez sur le type de champ ci-dessus pour commencer à le dessiner',
|
click_on_the_field_type_above_to_start_drawing_it: 'Cliquez sur le type de champ ci-dessus pour commencer à le dessiner',
|
||||||
please_draw_fields_to_prepare_the_document: 'Veuillez dessiner les champs pour préparer le document.',
|
please_draw_fields_to_prepare_the_document: 'Veuillez dessiner des champs pour préparer le document.',
|
||||||
only_pdf_and_images_are_supported: 'Seuls les PDF et les images sont pris en charge.',
|
only_pdf_and_images_are_supported: 'Seuls les PDF et les images sont pris en charge.',
|
||||||
unlock_sms_verified_phone_number_field_with_paid_plan_use_text_field_for_phone_numbers_without_verification: 'Débloquez le champ de numéro de téléphone vérifié par SMS avec un plan payant. Utilisez un champ texte pour les numéros de téléphone sans vérification.',
|
unlock_sms_verified_phone_number_field_with_paid_plan_use_text_field_for_phone_numbers_without_verification: 'Débloquez le champ de numéro de téléphone vérifié par SMS avec un forfait payant. Utilisez un champ texte pour les numéros sans vérification.',
|
||||||
available_only_in_pro: 'Disponible uniquement en Pro',
|
available_only_in_pro: 'Disponible uniquement dans la version Pro',
|
||||||
failed_to_download_files: 'Échec du téléchargement des fichiers',
|
failed_to_download_files: 'Impossible de télécharger les fichiers',
|
||||||
please_add_fields_for_the_submitter_name_or_remove_the_submitter_name_if_not_needed: 'Veuillez ajouter des champs pour {submitter_name} ou retirer {submitter_name} si ce n\'est pas nécessaire.',
|
please_add_fields_for_the_submitter_name_or_remove_the_submitter_name_if_not_needed: "Veuillez ajouter des champs pour {submitter_name}. Ou supprimez {submitter_name} si ce n'est pas nécessaire.",
|
||||||
draw_field: 'Dessiner le champ {field}',
|
draw_field: 'Dessiner le champ {field}',
|
||||||
replace: 'Remplacer',
|
replace: 'Remplacer',
|
||||||
uploading_: 'Téléchargement en cours...',
|
uploading_: 'Téléversement...',
|
||||||
add_document: 'Télécharger',
|
add_document: 'Téléverser',
|
||||||
any: 'Tout',
|
|
||||||
drawn: 'Dessiné',
|
|
||||||
drawn_or_typed: 'Dessiné ou Tapé',
|
|
||||||
drawn_or_upload: 'Dessiné ou Téléchargé',
|
|
||||||
upload: 'Téléverser',
|
|
||||||
typed: 'Tapé',
|
|
||||||
none: 'Aucun',
|
none: 'Aucun',
|
||||||
ssn: 'SSN',
|
ssn: 'SSN',
|
||||||
ein: 'EIN',
|
ein: 'EIN',
|
||||||
@@ -889,90 +907,99 @@ const fr = {
|
|||||||
numbers_only: 'Chiffres uniquement',
|
numbers_only: 'Chiffres uniquement',
|
||||||
letters_only: 'Lettres uniquement',
|
letters_only: 'Lettres uniquement',
|
||||||
regexp_validation: 'Validation par expression régulière',
|
regexp_validation: 'Validation par expression régulière',
|
||||||
enter_pdf_password: 'Entrez le mot de passe PDF',
|
enter_pdf_password: 'Saisir le mot de passe du PDF',
|
||||||
wrong_password: 'Mot de passe incorrect',
|
wrong_password: 'Mot de passe incorrect',
|
||||||
currency: 'Devise',
|
currency: 'Devise',
|
||||||
save_and_preview: 'Enregistrer et prévisualiser',
|
save_and_preview: 'Enregistrer et prévisualiser',
|
||||||
preferences: 'Préférences',
|
preferences: 'Préférences',
|
||||||
available_in_pro: 'Disponible en version Pro',
|
available_in_pro: 'Disponible dans la version Pro',
|
||||||
some_fields_are_missing_in_the_formula: 'Certains champs manquent dans la formule.',
|
some_fields_are_missing_in_the_formula: 'Certains champs manquent dans la formule.',
|
||||||
learn_more: 'En savoir plus',
|
learn_more: 'En savoir plus',
|
||||||
and: 'et',
|
and: 'et',
|
||||||
or: 'ou',
|
or: 'ou',
|
||||||
start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: 'Lancez une visite rapide pour apprendre à créer et envoyer votre premier document.',
|
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_tour: 'Démarrer'
|
start_tour: 'Démarrer',
|
||||||
|
or_add_from: 'Ou ajouter depuis',
|
||||||
|
sync: 'Synchroniser',
|
||||||
|
syncing: 'Synchronisation...'
|
||||||
}
|
}
|
||||||
|
|
||||||
const de = {
|
const de = {
|
||||||
|
view: 'Anzeigen',
|
||||||
payment_link: 'Zahlungslink',
|
payment_link: 'Zahlungslink',
|
||||||
strikeout: 'Streichung',
|
strikeout: 'Durchstreichen',
|
||||||
draw_strikethrough_the_document: 'Ziehe eine Streichung auf das Dokument',
|
draw_strikethrough_the_document: 'Durchstreichung im Dokument zeichnen',
|
||||||
quantity: 'Menge',
|
quantity: 'Menge',
|
||||||
prefillable: 'Vorausfüllbar',
|
prefillable: 'Vorausfüllbar',
|
||||||
signature_id: 'Signatur-ID',
|
signature_id: 'Signatur-ID',
|
||||||
error_message: 'Fehlermeldung',
|
error_message: 'Fehlermeldung',
|
||||||
length: 'Länge',
|
length: 'Länge',
|
||||||
min: 'Min',
|
min: 'Min.',
|
||||||
max: 'Max',
|
max: 'Max.',
|
||||||
date_now: 'Akt. Datum',
|
|
||||||
font: 'Schriftart',
|
font: 'Schriftart',
|
||||||
party: 'Partei',
|
party: 'Partei',
|
||||||
method: 'Verfahren',
|
date_signed: 'Unterzeichnungsdatum',
|
||||||
|
method: 'Methode',
|
||||||
reorder_fields: 'Felder neu anordnen',
|
reorder_fields: 'Felder neu anordnen',
|
||||||
verify_id: 'ID überprüfen',
|
verify_id: 'ID überprüfen',
|
||||||
obtain_qualified_electronic_signature_with_the_trusted_provider_click_to_learn_more: 'Erhalten Sie eine qualifizierte elektronische Signatur (QeS) beim vertrauenswürdigen Anbieter. Klicken Sie hier, um mehr zu erfahren.',
|
obtain_qualified_electronic_signature_with_the_trusted_provider_click_to_learn_more: 'Qualifizierte elektronische Signatur (QES) beim vertrauenswürdigen Anbieter einholen. Klicken Sie, um mehr zu erfahren.',
|
||||||
wiederkehrend: 'Wiederkehrend',
|
|
||||||
einmalig: 'Einmalig',
|
|
||||||
editable: 'Bearbeitbar',
|
editable: 'Bearbeitbar',
|
||||||
|
recurrent: 'Wiederkehrend',
|
||||||
|
one_off: 'Einmalig',
|
||||||
search_field: 'Suchfeld',
|
search_field: 'Suchfeld',
|
||||||
field_not_found: 'Feld nicht gefunden',
|
field_not_found: 'Feld nicht gefunden',
|
||||||
clear: 'Löschen',
|
clear: 'Leeren',
|
||||||
type_value: 'Wert eingeben',
|
|
||||||
add_all_required_fields_to_continue: 'Fügen Sie alle erforderlichen Felder hinzu, um fortzufahren',
|
|
||||||
uploaded_pdf_contains_form_fields_keep_or_remove_them: 'Das hochgeladene PDF enthält Formularfelder. Behalten oder entfernen?',
|
|
||||||
keep: 'Behalten',
|
|
||||||
align: 'Ausrichten',
|
align: 'Ausrichten',
|
||||||
|
add_all_required_fields_to_continue: 'Fügen Sie alle erforderlichen Felder hinzu, um fortzufahren',
|
||||||
|
uploaded_pdf_contains_form_fields_keep_or_remove_them: 'Das hochgeladene PDF enthält Formularfelder. Beibehalten oder entfernen?',
|
||||||
|
keep: 'Beibehalten',
|
||||||
left: 'Links',
|
left: 'Links',
|
||||||
heading: 'Überschrift',
|
heading: 'Überschrift',
|
||||||
validation: 'Validierung',
|
validation: 'Validierung',
|
||||||
add_blank_page: 'Leere Seite hinzufügen',
|
add_blank_page: 'Leere Seite hinzufügen',
|
||||||
right: 'Rechts',
|
right: 'Rechts',
|
||||||
add_condition: 'Bedingung hinzufügen',
|
|
||||||
center: 'Zentriert',
|
center: 'Zentriert',
|
||||||
with_logo: 'Mit Logo',
|
|
||||||
description: 'Beschreibung',
|
description: 'Beschreibung',
|
||||||
display_title: 'Anzeigetitel',
|
display_title: 'Anzeigetitel',
|
||||||
unchecked: 'Nicht überprüft',
|
with_logo: 'Mit Logo',
|
||||||
|
unchecked: 'Nicht markiert',
|
||||||
price: 'Preis',
|
price: 'Preis',
|
||||||
|
type_value: 'Wert eingeben',
|
||||||
equal: 'Gleich',
|
equal: 'Gleich',
|
||||||
not_equal: 'Ungleich',
|
not_equal: 'Ungleich',
|
||||||
contains: 'Enthält',
|
contains: 'Enthält',
|
||||||
does_not_contain: 'Enthält nicht',
|
does_not_contain: 'Enthält nicht',
|
||||||
not_empty: 'Nicht leer',
|
not_empty: 'Nicht leer',
|
||||||
empty: 'Leer',
|
empty: 'Leer',
|
||||||
signing_date: 'Signaturdatum',
|
|
||||||
select_field_: 'Feld auswählen...',
|
select_field_: 'Feld auswählen...',
|
||||||
select_value_: 'Wert auswählen...',
|
select_value_: 'Wert auswählen...',
|
||||||
remove_condition: 'Bedingung entfernen',
|
remove_condition: 'Bedingung entfernen',
|
||||||
condition: 'Bedingung',
|
add_condition: 'Bedingung hinzufügen',
|
||||||
formula: 'Formel',
|
are_you_sure_: 'Sind Sie sicher?',
|
||||||
|
sign_yourself: 'Selbst unterschreiben',
|
||||||
|
set_signing_date: 'Unterzeichnungsdatum festlegen',
|
||||||
|
signing_date: 'Unterzeichnungsdatum',
|
||||||
|
send: 'Senden',
|
||||||
|
remove: 'Entfernen',
|
||||||
edit: 'Bearbeiten',
|
edit: 'Bearbeiten',
|
||||||
settings: 'Einstellungen',
|
settings: 'Einstellungen',
|
||||||
up: 'Nach oben',
|
up: 'Nach oben',
|
||||||
down: 'Nach unten',
|
down: 'Nach unten',
|
||||||
set_signing_date: 'Unterschriftdatum festlegen',
|
checked: 'Markiert',
|
||||||
are_you_sure_: 'Bist du sicher?',
|
|
||||||
sign_yourself: 'Unterschreiben Sie selbst',
|
|
||||||
checked: 'Ausgewählt',
|
|
||||||
send: 'Senden',
|
|
||||||
remove: 'Entfernen',
|
|
||||||
save: 'Speichern',
|
save: 'Speichern',
|
||||||
cancel: 'Abbrechen',
|
cancel: 'Abbrechen',
|
||||||
draw_field_on_the_document: 'Ein Feld auf dem Dokument zeichnen',
|
any: 'Beliebig',
|
||||||
click_to_upload: 'Klicken Sie zum Hochladen',
|
drawn: 'Gezeichnet',
|
||||||
or_drag_and_drop_files: 'oder Dateien hierher ziehen',
|
drawn_or_typed: 'Gezeichnet oder getippt',
|
||||||
uploading: 'Hochladen',
|
drawn_or_upload: 'Gezeichnet oder hochgeladen',
|
||||||
processing_: 'Verarbeitung...',
|
upload: 'Hochladen',
|
||||||
|
formula: 'Formel',
|
||||||
|
typed: 'Getippt',
|
||||||
|
draw_field_on_the_document: 'Ein Feld im Dokument zeichnen',
|
||||||
|
click_to_upload: 'Zum Hochladen klicken',
|
||||||
|
or_drag_and_drop_files: 'oder Dateien per Drag & Drop ablegen',
|
||||||
|
uploading: 'Hochladen...',
|
||||||
|
processing_: 'Verarbeiten...',
|
||||||
add_pdf_documents_or_images: 'PDF-Dokumente oder Bilder hinzufügen',
|
add_pdf_documents_or_images: 'PDF-Dokumente oder Bilder hinzufügen',
|
||||||
add_documents_or_images: 'Dokumente oder Bilder hinzufügen',
|
add_documents_or_images: 'Dokumente oder Bilder hinzufügen',
|
||||||
add_a_new_document: 'Neues Dokument hinzufügen',
|
add_a_new_document: 'Neues Dokument hinzufügen',
|
||||||
@@ -983,11 +1010,12 @@ const de = {
|
|||||||
format: 'Format',
|
format: 'Format',
|
||||||
read_only: 'Nur lesen',
|
read_only: 'Nur lesen',
|
||||||
page: 'Seite',
|
page: 'Seite',
|
||||||
draw_new_area: 'Neuen Bereich zeichnen',
|
draw_new_area: 'Bereich zeichnen',
|
||||||
copy_to_all_pages: 'Auf alle Seiten kopieren',
|
copy_to_all_pages: 'Auf alle Seiten kopieren',
|
||||||
add_option: 'Option hinzufügen',
|
add_option: 'Option hinzufügen',
|
||||||
option: 'Option',
|
option: 'Option',
|
||||||
options: 'Options',
|
options: 'Optionen',
|
||||||
|
condition: 'Bedingung',
|
||||||
first_party: 'Erste Partei',
|
first_party: 'Erste Partei',
|
||||||
second_party: 'Zweite Partei',
|
second_party: 'Zweite Partei',
|
||||||
third_party: 'Dritte Partei',
|
third_party: 'Dritte Partei',
|
||||||
@@ -1006,21 +1034,21 @@ const de = {
|
|||||||
sixteenth_party: 'Sechzehnte Partei',
|
sixteenth_party: 'Sechzehnte Partei',
|
||||||
seventeenth_party: 'Siebzehnte Partei',
|
seventeenth_party: 'Siebzehnte Partei',
|
||||||
eighteenth_party: 'Achtzehnte Partei',
|
eighteenth_party: 'Achtzehnte Partei',
|
||||||
nineteenth_party: 'Neunzehnte Party',
|
nineteenth_party: 'Neunzehnte Partei',
|
||||||
twentieth_party: 'Zwanzigste Party',
|
twentieth_party: 'Zwanzigste Partei',
|
||||||
draw: 'Zeichnen',
|
draw: 'Zeichnen',
|
||||||
add: 'Hinzufügen',
|
add: 'Hinzufügen',
|
||||||
or_add_field_without_drawing: 'Oder Feld ohne Zeichnung hinzufügen',
|
or_add_field_without_drawing: 'Oder Feld ohne Zeichnen hinzufügen',
|
||||||
text: 'Text',
|
text: 'Text',
|
||||||
|
number: 'Zahl',
|
||||||
signature: 'Unterschrift',
|
signature: 'Unterschrift',
|
||||||
initials: 'Initialen',
|
initials: 'Initialen',
|
||||||
date: 'Datum',
|
date: 'Datum',
|
||||||
number: 'Nummer',
|
|
||||||
image: 'Bild',
|
image: 'Bild',
|
||||||
file: 'Datei',
|
file: 'Datei',
|
||||||
select: 'Auswählen',
|
select: 'Auswahl',
|
||||||
checkbox: 'Checkbox',
|
checkbox: 'Checkbox',
|
||||||
multiple: 'Mehrere',
|
multiple: 'Mehrfach',
|
||||||
radio: 'Radio',
|
radio: 'Radio',
|
||||||
cells: 'Zellen',
|
cells: 'Zellen',
|
||||||
stamp: 'Stempel',
|
stamp: 'Stempel',
|
||||||
@@ -1034,32 +1062,26 @@ const de = {
|
|||||||
image_field: 'Bildfeld',
|
image_field: 'Bildfeld',
|
||||||
file_field: 'Dateifeld',
|
file_field: 'Dateifeld',
|
||||||
select_field: 'Auswahlfeld',
|
select_field: 'Auswahlfeld',
|
||||||
checkbox_field: 'Checkbox-Feld',
|
checkbox_field: 'Checkboxfeld',
|
||||||
multiple_field: 'Mehrfach-Auswahlfeld',
|
multiple_field: 'Mehrfachauswahlfeld',
|
||||||
radio_field: 'Radio-Gruppenfeld',
|
radio_field: 'Radiogruppe',
|
||||||
cells_field: 'Zellenfeld',
|
cells_field: 'Zellenfeld',
|
||||||
stamp_field: 'Stempelfeld',
|
stamp_field: 'Stempelfeld',
|
||||||
payment_field: 'Zahlungsfeld',
|
payment_field: 'Zahlungsfeld',
|
||||||
phone_field: 'Telefonfeld',
|
phone_field: 'Telefonfeld',
|
||||||
draw_a_text_field_on_the_page_with_a_mouse: 'Textfeld auf der Seite mit der Maus zeichnen',
|
draw_a_text_field_on_the_page_with_a_mouse: 'Ein Textfeld mit der Maus auf der Seite zeichnen',
|
||||||
drag_and_drop_any_other_field_type_on_the_page: 'Ziehe und lasse einen anderen Feldtyp auf die Seite fallen',
|
drag_and_drop_any_other_field_type_on_the_page: 'Jeden anderen Feldtyp auf die Seite ziehen und ablegen',
|
||||||
click_on_the_field_type_above_to_start_drawing_it: 'Klicke auf den Feldtyp oben, um mit dem Zeichnen zu beginnen',
|
click_on_the_field_type_above_to_start_drawing_it: 'Klicken Sie oben auf den Feldtyp, um mit dem Zeichnen zu beginnen',
|
||||||
please_draw_fields_to_prepare_the_document: 'Bitte zeichne Felder, um das Dokument vorzubereiten.',
|
please_draw_fields_to_prepare_the_document: 'Bitte zeichnen Sie Felder, um das Dokument vorzubereiten.',
|
||||||
only_pdf_and_images_are_supported: 'Nur PDFs und Bilder werden unterstützt.',
|
only_pdf_and_images_are_supported: 'Nur PDF und Bilder werden unterstützt.',
|
||||||
unlock_sms_verified_phone_number_field_with_paid_plan_use_text_field_for_phone_numbers_without_verification: 'Schalte das SMS-verifizierte Telefonnummernfeld mit einem kostenpflichtigen Plan frei. Verwende das Textfeld für Telefonnummern ohne Verifizierung.',
|
unlock_sms_verified_phone_number_field_with_paid_plan_use_text_field_for_phone_numbers_without_verification: 'SMS-verifiziertes Telefonnummernfeld mit kostenpflichtigem Plan freischalten. Verwenden Sie für Telefonnummern ohne Verifizierung das Textfeld.',
|
||||||
available_only_in_pro: 'Nur in Pro verfügbar',
|
available_only_in_pro: 'Nur in Pro verfügbar',
|
||||||
failed_to_download_files: 'Fehler beim Herunterladen der Dateien',
|
failed_to_download_files: 'Dateien konnten nicht heruntergeladen werden',
|
||||||
please_add_fields_for_the_submitter_name_or_remove_the_submitter_name_if_not_needed: 'Bitte füge Felder für {submitter_name} hinzu oder entferne {submitter_name}, falls nicht erforderlich.',
|
please_add_fields_for_the_submitter_name_or_remove_the_submitter_name_if_not_needed: 'Bitte fügen Sie Felder für {submitter_name} hinzu oder entfernen Sie {submitter_name}, falls nicht benötigt.',
|
||||||
draw_field: 'Feld {field} zeichnen',
|
draw_field: '{field}-Feld zeichnen',
|
||||||
replace: 'Ersetzen',
|
replace: 'Ersetzen',
|
||||||
uploading_: 'Hochladen...',
|
uploading_: 'Hochladen...',
|
||||||
add_document: 'Hochladen',
|
add_document: 'Hochladen',
|
||||||
any: 'Jede',
|
|
||||||
drawn: 'Gezeichnet',
|
|
||||||
drawn_or_typed: 'Gezeichnet oder getippt',
|
|
||||||
drawn_or_upload: 'Gezeichnet oder hochgeladen',
|
|
||||||
upload: 'Upload',
|
|
||||||
typed: 'Getippt',
|
|
||||||
none: 'Keine',
|
none: 'Keine',
|
||||||
ssn: 'SSN',
|
ssn: 'SSN',
|
||||||
ein: 'EIN',
|
ein: 'EIN',
|
||||||
@@ -1069,19 +1091,207 @@ const de = {
|
|||||||
custom: 'Benutzerdefiniert',
|
custom: 'Benutzerdefiniert',
|
||||||
numbers_only: 'Nur Zahlen',
|
numbers_only: 'Nur Zahlen',
|
||||||
letters_only: 'Nur Buchstaben',
|
letters_only: 'Nur Buchstaben',
|
||||||
regexp_validation: 'Regulärer Ausdruck Überprüfung',
|
regexp_validation: 'RegExp-Validierung',
|
||||||
enter_pdf_password: 'Geben Sie das PDF-Passwort ein',
|
enter_pdf_password: 'PDF-Passwort eingeben',
|
||||||
wrong_password: 'Falsches Passwort',
|
wrong_password: 'Falsches Passwort',
|
||||||
currency: 'Währung',
|
currency: 'Währung',
|
||||||
save_and_preview: 'Speichern und Vorschau',
|
save_and_preview: 'Speichern und Vorschau',
|
||||||
preferences: 'Einstellungen',
|
preferences: 'Voreinstellungen',
|
||||||
available_in_pro: 'In Pro verfügbar',
|
available_in_pro: 'In Pro verfügbar',
|
||||||
some_fields_are_missing_in_the_formula: 'Einige Felder fehlen in der Formel.',
|
some_fields_are_missing_in_the_formula: 'Einige Felder fehlen in der Formel.',
|
||||||
learn_more: 'Erfahren Sie mehr',
|
learn_more: 'Mehr erfahren',
|
||||||
and: 'und',
|
and: 'und',
|
||||||
or: 'oder',
|
or: 'oder',
|
||||||
start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: 'Starte eine kurze Tour, um zu lernen, wie du dein erstes Dokument erstellst und versendest.',
|
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_tour: 'Starten'
|
start_tour: 'Tour starten',
|
||||||
|
or_add_from: 'Oder hinzufügen aus',
|
||||||
|
sync: 'Synchronisieren',
|
||||||
|
syncing: 'Synchronisiere...'
|
||||||
}
|
}
|
||||||
|
|
||||||
export { en, es, it, pt, fr, de }
|
const nl = {
|
||||||
|
view: 'Bekijken',
|
||||||
|
payment_link: 'Betaallink',
|
||||||
|
strikeout: 'Doorhalen',
|
||||||
|
draw_strikethrough_the_document: 'Teken doorhaling in het document',
|
||||||
|
quantity: 'Aantal',
|
||||||
|
prefillable: 'Voorinvulbaar',
|
||||||
|
signature_id: 'Handtekening-ID',
|
||||||
|
error_message: 'Foutmelding',
|
||||||
|
length: 'Lengte',
|
||||||
|
min: 'Min',
|
||||||
|
max: 'Max',
|
||||||
|
font: 'Lettertype',
|
||||||
|
party: 'Partij',
|
||||||
|
date_signed: 'Datum ondertekend',
|
||||||
|
method: 'Methode',
|
||||||
|
reorder_fields: 'Velden herschikken',
|
||||||
|
verify_id: 'ID verifiëren',
|
||||||
|
obtain_qualified_electronic_signature_with_the_trusted_provider_click_to_learn_more: 'Verkrijg een gekwalificeerde elektronische handtekening (QeS) via de vertrouwde provider. Klik voor meer informatie.',
|
||||||
|
editable: 'Bewerkbaar',
|
||||||
|
recurrent: 'Terugkerend',
|
||||||
|
one_off: 'Eenmalig',
|
||||||
|
search_field: 'Zoekveld',
|
||||||
|
field_not_found: 'Veld niet gevonden',
|
||||||
|
clear: 'Wissen',
|
||||||
|
align: 'Uitlijnen',
|
||||||
|
add_all_required_fields_to_continue: 'Voeg alle vereiste velden toe om door te gaan',
|
||||||
|
uploaded_pdf_contains_form_fields_keep_or_remove_them: 'Geüploade PDF bevat formuliervelden. Behouden of verwijderen?',
|
||||||
|
keep: 'Behouden',
|
||||||
|
left: 'Links',
|
||||||
|
heading: 'Koptekst',
|
||||||
|
validation: 'Validatie',
|
||||||
|
add_blank_page: 'Lege pagina toevoegen',
|
||||||
|
right: 'Rechts',
|
||||||
|
center: 'Centreren',
|
||||||
|
description: 'Beschrijving',
|
||||||
|
display_title: 'Titel weergeven',
|
||||||
|
with_logo: 'Met logo',
|
||||||
|
unchecked: 'Niet aangevinkt',
|
||||||
|
price: 'Prijs',
|
||||||
|
type_value: 'Typ waarde',
|
||||||
|
equal: 'Gelijk aan',
|
||||||
|
not_equal: 'Niet gelijk aan',
|
||||||
|
contains: 'Bevat',
|
||||||
|
does_not_contain: 'Bevat niet',
|
||||||
|
not_empty: 'Niet leeg',
|
||||||
|
empty: 'Leeg',
|
||||||
|
select_field_: 'Veld selecteren...',
|
||||||
|
select_value_: 'Waarde selecteren...',
|
||||||
|
remove_condition: 'Voorwaarde verwijderen',
|
||||||
|
add_condition: 'Voorwaarde toevoegen',
|
||||||
|
are_you_sure_: 'Weet u het zeker?',
|
||||||
|
sign_yourself: 'Zelf ondertekenen',
|
||||||
|
set_signing_date: 'Ondertekeningsdatum instellen',
|
||||||
|
signing_date: 'Ondertekeningsdatum',
|
||||||
|
send: 'Verzenden',
|
||||||
|
remove: 'Verwijderen',
|
||||||
|
edit: 'Bewerken',
|
||||||
|
settings: 'Instellingen',
|
||||||
|
up: 'Omhoog',
|
||||||
|
down: 'Omlaag',
|
||||||
|
checked: 'Aangevinkt',
|
||||||
|
save: 'Opslaan',
|
||||||
|
cancel: 'Annuleren',
|
||||||
|
any: 'Elke',
|
||||||
|
drawn: 'Getekend',
|
||||||
|
drawn_or_typed: 'Getekend of getypt',
|
||||||
|
drawn_or_upload: 'Getekend of uploaden',
|
||||||
|
upload: 'Uploaden',
|
||||||
|
formula: 'Formule',
|
||||||
|
typed: 'Getypt',
|
||||||
|
draw_field_on_the_document: 'Teken een veld op het document',
|
||||||
|
click_to_upload: 'Klik om te uploaden',
|
||||||
|
or_drag_and_drop_files: 'of sleep bestanden hierheen',
|
||||||
|
uploading: 'Uploaden',
|
||||||
|
processing_: 'Bezig met verwerken...',
|
||||||
|
add_pdf_documents_or_images: 'PDF-documenten of afbeeldingen toevoegen',
|
||||||
|
add_documents_or_images: 'Documenten of afbeeldingen toevoegen',
|
||||||
|
add_a_new_document: 'Nieuw document toevoegen',
|
||||||
|
replace_existing_document: 'Bestaand document vervangen',
|
||||||
|
clone_and_replace_documents: 'Documenten klonen en vervangen',
|
||||||
|
required: 'Vereist',
|
||||||
|
default_value: 'Standaardwaarde',
|
||||||
|
format: 'Formaat',
|
||||||
|
read_only: 'Alleen-lezen',
|
||||||
|
page: 'Pagina',
|
||||||
|
draw_new_area: 'Nieuw gebied tekenen',
|
||||||
|
copy_to_all_pages: 'Kopieer naar alle pag.',
|
||||||
|
add_option: 'Optie toevoegen',
|
||||||
|
option: 'Optie',
|
||||||
|
options: 'Opties',
|
||||||
|
condition: 'Voorwaarde',
|
||||||
|
first_party: 'Eerste partij',
|
||||||
|
second_party: 'Tweede partij',
|
||||||
|
third_party: 'Derde partij',
|
||||||
|
fourth_party: 'Vierde partij',
|
||||||
|
fifth_party: 'Vijfde partij',
|
||||||
|
sixth_party: 'Zesde partij',
|
||||||
|
seventh_party: 'Zevende partij',
|
||||||
|
eighth_party: 'Achtste partij',
|
||||||
|
ninth_party: 'Negende partij',
|
||||||
|
tenth_party: 'Tiende partij',
|
||||||
|
eleventh_party: 'Elfde partij',
|
||||||
|
twelfth_party: 'Twaalfde partij',
|
||||||
|
thirteenth_party: 'Dertiende partij',
|
||||||
|
fourteenth_party: 'Veertiende partij',
|
||||||
|
fifteenth_party: 'Vijftiende partij',
|
||||||
|
sixteenth_party: 'Zestiende partij',
|
||||||
|
seventeenth_party: 'Zeventiende partij',
|
||||||
|
eighteenth_party: 'Achttiende partij',
|
||||||
|
nineteenth_party: 'Negentiende partij',
|
||||||
|
twentieth_party: 'Twintigste partij',
|
||||||
|
draw: 'Tekenen',
|
||||||
|
add: 'Toevoegen',
|
||||||
|
or_add_field_without_drawing: 'Of voeg een veld toe zonder te tekenen',
|
||||||
|
text: 'Tekst',
|
||||||
|
number: 'Getal',
|
||||||
|
signature: 'Signatuur',
|
||||||
|
initials: 'Initialen',
|
||||||
|
date: 'Datum',
|
||||||
|
image: 'Afbeelding',
|
||||||
|
file: 'Bestand',
|
||||||
|
select: 'Selecteren',
|
||||||
|
checkbox: 'Checkbox',
|
||||||
|
multiple: 'Meerdere',
|
||||||
|
radio: 'Radio',
|
||||||
|
cells: 'Cellen',
|
||||||
|
stamp: 'Stempel',
|
||||||
|
payment: 'Betaling',
|
||||||
|
phone: 'Telefoon',
|
||||||
|
text_field: 'Tekstveld',
|
||||||
|
signature_field: 'Signatuurveld',
|
||||||
|
initials_field: 'Initialenveld',
|
||||||
|
date_field: 'Datumveld',
|
||||||
|
number_field: 'Getalveld',
|
||||||
|
image_field: 'Afbeeldingsveld',
|
||||||
|
file_field: 'Bestandsveld',
|
||||||
|
select_field: 'Selectieveld',
|
||||||
|
checkbox_field: 'Aankruisvakje',
|
||||||
|
multiple_field: 'Meerkeuzeveld',
|
||||||
|
radio_field: 'Radiogroepveld',
|
||||||
|
cells_field: 'Cellenveld',
|
||||||
|
stamp_field: 'Stempelveld',
|
||||||
|
payment_field: 'Betalingsveld',
|
||||||
|
phone_field: 'Telefoonveld',
|
||||||
|
draw_a_text_field_on_the_page_with_a_mouse: 'Teken met de muis een tekstveld op de pagina',
|
||||||
|
drag_and_drop_any_other_field_type_on_the_page: 'Sleep elk ander veldtype naar de pagina',
|
||||||
|
click_on_the_field_type_above_to_start_drawing_it: 'Klik op het bovenstaande veldtype om te beginnen met tekenen',
|
||||||
|
please_draw_fields_to_prepare_the_document: 'Teken velden om het document voor te bereiden.',
|
||||||
|
only_pdf_and_images_are_supported: 'Alleen PDF en afbeeldingen worden ondersteund.',
|
||||||
|
unlock_sms_verified_phone_number_field_with_paid_plan_use_text_field_for_phone_numbers_without_verification: 'Ontgrendel het SMS-geverifieerde veld voor telefoonnummer met een betaald abonnement. Gebruik het tekstveld voor telefoonnummers zonder verificatie.',
|
||||||
|
available_only_in_pro: 'Alleen beschikbaar in Pro',
|
||||||
|
failed_to_download_files: 'Bestanden downloaden mislukt',
|
||||||
|
please_add_fields_for_the_submitter_name_or_remove_the_submitter_name_if_not_needed: 'Voeg velden toe voor de {submitter_name}. Of verwijder de {submitter_name} als deze niet nodig is.',
|
||||||
|
draw_field: 'Teken het veld {field}',
|
||||||
|
replace: 'Vervangen',
|
||||||
|
uploading_: 'Bezig met uploaden...',
|
||||||
|
add_document: 'Doc. toevoegen',
|
||||||
|
none: 'Geen',
|
||||||
|
ssn: 'SSN',
|
||||||
|
ein: 'EIN',
|
||||||
|
email: 'E-mail',
|
||||||
|
url: 'URL',
|
||||||
|
zip: 'Postcode',
|
||||||
|
custom: 'Aangepast',
|
||||||
|
numbers_only: 'Alleen cijfers',
|
||||||
|
letters_only: 'Alleen letters',
|
||||||
|
regexp_validation: 'Regex validatie',
|
||||||
|
enter_pdf_password: 'Voer PDF-wachtwoord in',
|
||||||
|
wrong_password: 'Onjuist wachtwoord',
|
||||||
|
currency: 'Valuta',
|
||||||
|
save_and_preview: 'Opslaan & bekijken',
|
||||||
|
preferences: 'Voorkeuren',
|
||||||
|
available_in_pro: 'Beschikbaar in Pro',
|
||||||
|
some_fields_are_missing_in_the_formula: 'Sommige velden ontbreken in de formule.',
|
||||||
|
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_tour: 'Rondleiding starten',
|
||||||
|
or_add_from: 'Of toevoegen van',
|
||||||
|
sync: 'Synchroniseren',
|
||||||
|
syncing: 'Synchroniseren...'
|
||||||
|
}
|
||||||
|
|
||||||
|
export { en, es, it, pt, fr, de, nl }
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="absolute text-center w-full bottom-0 pr-3 mb-4">
|
<div class="absolute w-full bottom-0 pr-3 mb-4">
|
||||||
<span class="w-full bg-base-200 px-4 py-2 rounded-md inline-flex space-x-2 mx-auto items-center justify-between mb-2 z-20 draw-field-container-mobile">
|
<span class="w-full bg-base-200 px-4 py-2 rounded-md inline-flex space-x-2 mx-auto items-center justify-between mb-2 z-20 draw-field-container-mobile">
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-2">
|
||||||
<component
|
<component
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
loading="lazy"
|
loading="lazy"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="group flex justify-end cursor-pointer top-0 bottom-0 left-0 right-0 absolute p-1 hover:bg-black/10 transition-colors"
|
class="group flex justify-end cursor-pointer top-0 bottom-0 left-0 right-0 absolute p-1 hover:bg-black/10 transition-colors rounded"
|
||||||
@click="$emit('scroll-to', item)"
|
@click="$emit('scroll-to', item)"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
class="btn border-base-200 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors p-0 document-control-button"
|
class="btn border-gray-300 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors p-0 document-control-button"
|
||||||
@click.stop="isShowConditionsModal = true"
|
@click.stop="isShowConditionsModal = true"
|
||||||
>
|
>
|
||||||
<IconRouteAltLeft
|
<IconRouteAltLeft
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
class="btn border-base-200 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors document-control-button"
|
class="btn border-gray-300 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors document-control-button"
|
||||||
style="width: 24px; height: 24px"
|
style="width: 24px; height: 24px"
|
||||||
@click.stop="$emit('remove', item)"
|
@click.stop="$emit('remove', item)"
|
||||||
>
|
>
|
||||||
@@ -63,7 +63,7 @@
|
|||||||
class="tooltip tooltip-left before:text-xs"
|
class="tooltip tooltip-left before:text-xs"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="btn border-base-200 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors p-0 document-control-button"
|
class="btn border-gray-300 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors p-0 document-control-button"
|
||||||
@click.stop="$emit('reorder', item)"
|
@click.stop="$emit('reorder', item)"
|
||||||
>
|
>
|
||||||
<IconSortDescending2
|
<IconSortDescending2
|
||||||
@@ -75,14 +75,14 @@
|
|||||||
</span>
|
</span>
|
||||||
<template v-if="withArrows">
|
<template v-if="withArrows">
|
||||||
<button
|
<button
|
||||||
class="btn border-base-200 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors document-control-button"
|
class="btn border-gray-300 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors document-control-button"
|
||||||
style="width: 24px; height: 24px"
|
style="width: 24px; height: 24px"
|
||||||
@click.stop="$emit('up', item)"
|
@click.stop="$emit('up', item)"
|
||||||
>
|
>
|
||||||
↑
|
↑
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="btn border-base-200 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors document-control-button"
|
class="btn border-gray-300 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors document-control-button"
|
||||||
style="width: 24px; height: 24px"
|
style="width: 24px; height: 24px"
|
||||||
@click.stop="$emit('down', item)"
|
@click.stop="$emit('down', item)"
|
||||||
>
|
>
|
||||||
@@ -94,12 +94,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex pb-2 pt-1.5 document-preview-name">
|
<div class="flex items-center gap-1 pb-2 pt-1.5 document-preview-name">
|
||||||
|
<GoogleDriveDocumentSettings
|
||||||
|
v-if="item.google_drive_file_id"
|
||||||
|
:template-id="template.id"
|
||||||
|
:google-drive-file-id="item.google_drive_file_id"
|
||||||
|
@success="$emit('replace', { replaceSchemaItem: item, ...$event })"
|
||||||
|
/>
|
||||||
<Contenteditable
|
<Contenteditable
|
||||||
:model-value="item.name"
|
:model-value="item.name"
|
||||||
:icon-width="16"
|
:icon-width="16"
|
||||||
|
:icon-inline="true"
|
||||||
|
:float-icon="!item.google_drive_file_id"
|
||||||
|
:hide-icon="!item.google_drive_file_id"
|
||||||
:editable="editable"
|
:editable="editable"
|
||||||
style="max-width: 95%"
|
|
||||||
class="mx-auto"
|
class="mx-auto"
|
||||||
@update:model-value="onUpdateName"
|
@update:model-value="onUpdateName"
|
||||||
/>
|
/>
|
||||||
@@ -123,6 +131,7 @@ import Upload from './upload'
|
|||||||
import { IconRouteAltLeft, IconSortDescending2 } from '@tabler/icons-vue'
|
import { IconRouteAltLeft, IconSortDescending2 } from '@tabler/icons-vue'
|
||||||
import ConditionsModal from './conditions_modal'
|
import ConditionsModal from './conditions_modal'
|
||||||
import ReplaceButton from './replace'
|
import ReplaceButton from './replace'
|
||||||
|
import GoogleDriveDocumentSettings from './google_drive_document_settings'
|
||||||
import Field from './field'
|
import Field from './field'
|
||||||
import FieldType from './field_type'
|
import FieldType from './field_type'
|
||||||
|
|
||||||
@@ -133,6 +142,7 @@ export default {
|
|||||||
IconRouteAltLeft,
|
IconRouteAltLeft,
|
||||||
ConditionsModal,
|
ConditionsModal,
|
||||||
ReplaceButton,
|
ReplaceButton,
|
||||||
|
GoogleDriveDocumentSettings,
|
||||||
IconSortDescending2
|
IconSortDescending2
|
||||||
},
|
},
|
||||||
inject: ['t'],
|
inject: ['t'],
|
||||||
|
|||||||
@@ -3,29 +3,155 @@
|
|||||||
<label
|
<label
|
||||||
id="add_document_button"
|
id="add_document_button"
|
||||||
:for="inputId"
|
:for="inputId"
|
||||||
class="btn btn-outline w-full add-document-button"
|
class="btn btn-outline w-full add-document-button px-0"
|
||||||
:class="{ 'btn-disabled': isLoading }"
|
:class="{ 'btn-disabled': isLoading }"
|
||||||
>
|
>
|
||||||
<IconInnerShadowTop
|
<div
|
||||||
v-if="isLoading"
|
class="flex items-center justify-between w-full h-full"
|
||||||
width="20"
|
>
|
||||||
class="animate-spin"
|
<span
|
||||||
/>
|
class="flex items-center space-x-2 w-full justify-center"
|
||||||
<IconUpload
|
:class="{ 'pl-3': withGoogleDrive }"
|
||||||
v-else
|
>
|
||||||
width="20"
|
<IconInnerShadowTop
|
||||||
/>
|
v-if="isLoading"
|
||||||
<span v-if="isLoading">
|
width="20"
|
||||||
{{ t('uploading_') }}
|
class="animate-spin"
|
||||||
</span>
|
/>
|
||||||
<span v-else>
|
<IconUpload
|
||||||
{{ t('add_document') }}
|
v-else
|
||||||
</span>
|
width="20"
|
||||||
|
/>
|
||||||
|
<span v-if="isLoading">
|
||||||
|
{{ t('uploading_') }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
class="mr-1 whitespace-nowrap truncate"
|
||||||
|
>
|
||||||
|
{{ t('add_document') }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="withGoogleDrive"
|
||||||
|
class="dropdown dropdown-end dropdown-top inline h-full"
|
||||||
|
style="width: 33px"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
tabindex="0"
|
||||||
|
class="flex items-center h-full cursor-pointer"
|
||||||
|
>
|
||||||
|
<IconChevronDown class="w-5 h-5 flex-shrink-0" />
|
||||||
|
</label>
|
||||||
|
<ul
|
||||||
|
tabindex="0"
|
||||||
|
:style="{ backgroundColor }"
|
||||||
|
class="dropdown-content p-2 mt-2 shadow menu text-base mb-1 rounded-box text-right !text-base-content"
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="openGoogleDriveModal"
|
||||||
|
>
|
||||||
|
<IconBrandGoogleDrive class="w-5 h-5 flex-shrink-0" />
|
||||||
|
<span class="whitespace-nowrap text-sm normal-case font-medium">Google Drive</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</label>
|
</label>
|
||||||
|
<Teleport
|
||||||
|
v-if="showGoogleDriveModal"
|
||||||
|
:to="modalContainerEl"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="modal modal-open items-start !animate-none overflow-y-auto"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="absolute top-0 bottom-0 right-0 left-0"
|
||||||
|
@click.prevent="showGoogleDriveModal = false"
|
||||||
|
/>
|
||||||
|
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl">
|
||||||
|
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
|
||||||
|
<span class="modal-title">
|
||||||
|
Google Drive
|
||||||
|
</span>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="text-xl modal-close-button"
|
||||||
|
@click.prevent="showGoogleDriveModal = false"
|
||||||
|
>×</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<form
|
||||||
|
v-if="showGoogleDriveOauthButton"
|
||||||
|
method="post"
|
||||||
|
:action="googleDriveOauthPath"
|
||||||
|
@submit="isConnectGoogleDriveClicked = true"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
name="authenticity_token"
|
||||||
|
:value="authenticityToken"
|
||||||
|
autocomplete="off"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
id="gdrive_oauth_button"
|
||||||
|
class="btn bg-white btn-outline w-full text-base font-medium mt-4"
|
||||||
|
data-turbo="false"
|
||||||
|
type="submit"
|
||||||
|
:disabled="isConnectGoogleDriveClicked"
|
||||||
|
>
|
||||||
|
<span v-if="isConnectGoogleDriveClicked">
|
||||||
|
<span class="flex items-center justify-center space-x-2">
|
||||||
|
<IconInnerShadowTop class="animate-spin" />
|
||||||
|
<span class="">Submitting...</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
>
|
||||||
|
<span class="flex items-center justify-center space-x-2">
|
||||||
|
<IconBrandGoogleDrive />
|
||||||
|
<span>Connect Google Drive</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="relative"
|
||||||
|
>
|
||||||
|
<iframe
|
||||||
|
class="border border-base-300 rounded-lg"
|
||||||
|
style="width: 100%; height: 440px; background: white;"
|
||||||
|
src="/template_google_drive"
|
||||||
|
/>
|
||||||
|
<div v-if="isLoadingGoogleDrive">
|
||||||
|
<div
|
||||||
|
class="bg-white absolute top-0 bottom-0 left-0 right-0 opacity-80 rounded-lg"
|
||||||
|
style="margin: 1px"
|
||||||
|
/>
|
||||||
|
<div class="absolute top-0 bottom-0 left-0 right-0 flex items-center justify-center">
|
||||||
|
<IconInnerShadowTop class="animate-spin" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Teleport>
|
||||||
<form
|
<form
|
||||||
ref="form"
|
ref="form"
|
||||||
class="hidden"
|
class="hidden"
|
||||||
>
|
>
|
||||||
|
<input
|
||||||
|
v-for="file in googleDriveFiles"
|
||||||
|
:key="file.id"
|
||||||
|
name="google_drive_file_ids[]"
|
||||||
|
:value="file.id"
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
:id="inputId"
|
:id="inputId"
|
||||||
ref="input"
|
ref="input"
|
||||||
@@ -40,20 +166,32 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { IconUpload, IconInnerShadowTop } from '@tabler/icons-vue'
|
import { IconUpload, IconInnerShadowTop, IconChevronDown, IconBrandGoogleDrive } from '@tabler/icons-vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'DocumentsUpload',
|
name: 'DocumentsUpload',
|
||||||
components: {
|
components: {
|
||||||
IconUpload,
|
IconUpload,
|
||||||
IconInnerShadowTop
|
IconInnerShadowTop,
|
||||||
|
IconChevronDown,
|
||||||
|
IconBrandGoogleDrive
|
||||||
},
|
},
|
||||||
inject: ['baseFetch', 't'],
|
inject: ['baseFetch', 't', 'backgroundColor'],
|
||||||
props: {
|
props: {
|
||||||
templateId: {
|
templateId: {
|
||||||
type: [Number, String],
|
type: [Number, String],
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
authenticityToken: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
withGoogleDrive: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
acceptFileTypes: {
|
acceptFileTypes: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -63,22 +201,85 @@ export default {
|
|||||||
emits: ['success', 'error'],
|
emits: ['success', 'error'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
isLoading: false
|
isLoading: false,
|
||||||
|
isConnectGoogleDriveClicked: false,
|
||||||
|
isLoadingGoogleDrive: false,
|
||||||
|
googleDriveFiles: [],
|
||||||
|
showGoogleDriveModal: false,
|
||||||
|
showGoogleDriveOauthButton: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
inputId () {
|
inputId () {
|
||||||
return 'el' + Math.random().toString(32).split('.')[1]
|
return 'el' + Math.random().toString(32).split('.')[1]
|
||||||
},
|
},
|
||||||
uploadUrl () {
|
queryParams () {
|
||||||
return `/templates/${this.templateId}/documents`
|
return new URLSearchParams(window.location.search)
|
||||||
|
},
|
||||||
|
googleDriveOauthPath () {
|
||||||
|
const params = {
|
||||||
|
access_type: 'offline',
|
||||||
|
include_granted_scopes: 'true',
|
||||||
|
prompt: 'consent',
|
||||||
|
scope: [
|
||||||
|
'https://www.googleapis.com/auth/userinfo.email',
|
||||||
|
'https://www.googleapis.com/auth/drive.file'
|
||||||
|
].join(' '),
|
||||||
|
state: new URLSearchParams({
|
||||||
|
redir: `/templates/${this.templateId}/edit?google_drive_open=1`
|
||||||
|
}).toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = new URLSearchParams(params).toString()
|
||||||
|
|
||||||
|
return `/auth/google_oauth2?${query}`
|
||||||
|
},
|
||||||
|
modalContainerEl () {
|
||||||
|
return this.$el.getRootNode().querySelector('#docuseal_modal_container')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
mounted () {
|
||||||
|
window.addEventListener('message', this.messageHandler)
|
||||||
|
|
||||||
|
if (this.queryParams.get('google_drive_open') === '1') {
|
||||||
|
this.openGoogleDriveModal()
|
||||||
|
|
||||||
|
window.history.replaceState({}, document.title, window.location.pathname)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeUnmount () {
|
||||||
|
window.removeEventListener('message', this.messageHandler)
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async upload () {
|
openGoogleDriveModal () {
|
||||||
|
this.showGoogleDriveModal = true
|
||||||
|
this.isLoadingGoogleDrive = true
|
||||||
|
},
|
||||||
|
messageHandler (event) {
|
||||||
|
if (event.data.type === 'google-drive-files-picked') {
|
||||||
|
this.googleDriveFiles = event.data.files || []
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.isLoadingGoogleDrive = true
|
||||||
|
|
||||||
|
this.upload({ path: `/templates/${this.templateId}/google_drive_documents` }).then((resp) => {
|
||||||
|
if (resp.ok) {
|
||||||
|
this.showGoogleDriveModal = false
|
||||||
|
}
|
||||||
|
}).finally(() => {
|
||||||
|
this.isLoadingGoogleDrive = false
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} else if (event.data.type === 'google-drive-picker-loaded') {
|
||||||
|
this.isLoadingGoogleDrive = false
|
||||||
|
} else if (event.data.type === 'google-drive-picker-request-oauth') {
|
||||||
|
this.showGoogleDriveOauthButton = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async upload ({ path } = {}) {
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
|
|
||||||
this.baseFetch(this.uploadUrl, {
|
return this.baseFetch(path || `/templates/${this.templateId}/documents`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { Accept: 'application/json' },
|
headers: { Accept: 'application/json' },
|
||||||
body: new FormData(this.$refs.form)
|
body: new FormData(this.$refs.form)
|
||||||
@@ -111,6 +312,10 @@ export default {
|
|||||||
this.isLoading = false
|
this.isLoading = false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
} else if (data.status === 'google_drive_file_missing') {
|
||||||
|
alert(data.error)
|
||||||
|
this.$emit('error', data.error)
|
||||||
|
this.isLoading = false
|
||||||
} else {
|
} else {
|
||||||
this.$emit('error', data.error)
|
this.$emit('error', data.error)
|
||||||
this.isLoading = false
|
this.isLoading = false
|
||||||
@@ -122,6 +327,8 @@ export default {
|
|||||||
this.isLoading = false
|
this.isLoading = false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return resp
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
this.isLoading = false
|
this.isLoading = false
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ class GeneratePreviewImagesJob
|
|||||||
max_page = [attachment.metadata['pdf']['number_of_pages'].to_i - 1,
|
max_page = [attachment.metadata['pdf']['number_of_pages'].to_i - 1,
|
||||||
Templates::ProcessDocument::MAX_NUMBER_OF_PAGES_PROCESSED].min
|
Templates::ProcessDocument::MAX_NUMBER_OF_PAGES_PROCESSED].min
|
||||||
|
|
||||||
Templates::ProcessDocument.generate_document_preview_images(attachment, attachment.download, (1..max_page),
|
Templates::ProcessDocument.generate_document_preview_images(attachment, attachment.download, 1..max_page,
|
||||||
concurrency: 1)
|
concurrency: 1)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -93,18 +93,26 @@ class ProcessSubmitterCompletionJob
|
|||||||
|
|
||||||
def enqueue_completed_emails(submitter)
|
def enqueue_completed_emails(submitter)
|
||||||
submission = submitter.submission
|
submission = submitter.submission
|
||||||
|
template = submitter.template
|
||||||
|
|
||||||
user = submission.created_by_user || submitter.template.author
|
user = submission.created_by_user || template.author
|
||||||
|
|
||||||
if submitter.account.users.exists?(id: user.id) && submission.preferences['send_email'] != false &&
|
if submitter.account.users.exists?(id: user.id) && submission.preferences['send_email'] != false &&
|
||||||
submitter.template&.preferences&.dig('completed_notification_email_enabled') != false
|
(!template || template.preferences['completed_notification_email_enabled'] != false)
|
||||||
if submission.submitters.map(&:email).exclude?(user.email) &&
|
user_submitter = submission.submitters.find { |s| s.email == user.email }
|
||||||
user.user_configs.find_by(key: UserConfig::RECEIVE_COMPLETED_EMAIL)&.value != false &&
|
|
||||||
user.role != 'integration'
|
is_sent_to_user =
|
||||||
SubmitterMailer.completed_email(submitter, user).deliver_later!
|
if user.role != 'integration' &&
|
||||||
end
|
(!user_submitter || user_submitter.preferences['send_email'] == false) &&
|
||||||
|
user.user_configs.find_by(key: UserConfig::RECEIVE_COMPLETED_EMAIL)&.value != false
|
||||||
|
SubmitterMailer.completed_email(submitter, user).deliver_later!
|
||||||
|
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
build_bcc_addresses(submission).each do |to|
|
build_bcc_addresses(submission).each do |to|
|
||||||
|
next if is_sent_to_user && to == user.email
|
||||||
|
|
||||||
SubmitterMailer.completed_email(submitter, user, to:).deliver_later!
|
SubmitterMailer.completed_email(submitter, user, to:).deliver_later!
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
+1
-1
@@ -50,7 +50,7 @@ class User < ApplicationRecord
|
|||||||
EMAIL_REGEXP = /[^@;,<>\s]+@[^@;,<>\s]+/
|
EMAIL_REGEXP = /[^@;,<>\s]+@[^@;,<>\s]+/
|
||||||
|
|
||||||
FULL_EMAIL_REGEXP =
|
FULL_EMAIL_REGEXP =
|
||||||
/\A[a-z0-9][\.']?(?:(?:[a-z0-9_-]+[\.\+'])*[a-z0-9_-]+)*@(?:[a-z0-9]+[\.-])*[a-z0-9]+\.[a-z]{2,}\z/i
|
/\A[a-z0-9][.']?(?:(?:[a-z0-9_-]+[.+'])*[a-z0-9_-]+)*@(?:[a-z0-9]+[.-])*[a-z0-9]+\.[a-z]{2,}\z/i
|
||||||
|
|
||||||
has_one_attached :signature
|
has_one_attached :signature
|
||||||
has_one_attached :initials
|
has_one_attached :initials
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
<%= hidden_field_tag :redir, params[:redir] %>
|
<%= hidden_field_tag :redir, params[:redir] %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<submit-form data-on="change">
|
<submit-form data-on="change">
|
||||||
<%= select_tag :lang, options_for_select((I18n.available_locales - %i[en pt-PT de-DE fr-FR it-IT es-ES]).map { |code| [t("language_#{code}"), code] }, I18n.locale), class: 'select select-sm border-base-content/30 text-base' %>
|
<%= select_tag :lang, options_for_select((I18n.available_locales - %i[en pt-PT de-DE fr-FR it-IT es-ES nl-NL]).map { |code| [t("language_#{code}"), code] }, I18n.locale), class: 'select select-sm border-base-content/30 text-base' %>
|
||||||
</submit-form>
|
</submit-form>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg class="<%= local_assigns[:class] %>" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-brand-google-drive"><path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M12 10l-6 10l-3 -5l6 -10z" /><path d="M9 15h12l-3 5h-12" /><path d="M15 15l-6 -10h6l6 10z" /></svg>
|
||||||
|
After Width: | Height: | Size: 458 B |
@@ -19,6 +19,12 @@
|
|||||||
<%= stylesheet_pack_tag 'application', media: 'all' %>
|
<%= stylesheet_pack_tag 'application', media: 'all' %>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<% if params[:modal].present? %>
|
||||||
|
<% url_params = Rails.application.routes.recognize_path(params[:modal], method: :get) %>
|
||||||
|
<% if url_params[:action] == 'new' %>
|
||||||
|
<open-modal src="<%= params[:modal] %>"></open-modal>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
<turbo-frame id="modal"></turbo-frame>
|
<turbo-frame id="modal"></turbo-frame>
|
||||||
<turbo-frame id="drawer"></turbo-frame>
|
<turbo-frame id="drawer"></turbo-frame>
|
||||||
<%= render 'shared/navbar' %>
|
<%= render 'shared/navbar' %>
|
||||||
|
|||||||
@@ -9,6 +9,8 @@
|
|||||||
<span class="disabled">
|
<span class="disabled">
|
||||||
<span class="flex items-center justify-center space-x-2">
|
<span class="flex items-center justify-center space-x-2">
|
||||||
<%= local_assigns[:icon_disabled] || svg_icon('loader', class: 'w-5 h-5 animate-spin') %>
|
<%= local_assigns[:icon_disabled] || svg_icon('loader', class: 'w-5 h-5 animate-spin') %>
|
||||||
<span class="<%= local_assigns[:title_class] %>"><%= disabled_with %>...</span>
|
<% if disabled_with %>
|
||||||
|
<span class="<%= local_assigns[:title_class] %>"><%= disabled_with %>...</span>
|
||||||
|
<% end %>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
<% submitter_preferences_index = template&.preferences&.dig('submitters').to_a.index_by { |e| e['uuid'] } %>
|
<% submitter_preferences_index = template&.preferences&.dig('submitters').to_a.index_by { |e| e['uuid'] } %>
|
||||||
<% template_submitters = local_assigns[:submitter]&.submission&.template_submitters || template.submitters %>
|
<% template_submitters = local_assigns[:submitter]&.submission&.template_submitters || template.submitters %>
|
||||||
|
<% message_field_id = "message_field_#{SecureRandom.hex(3)}" %>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<% can_send_emails = Accounts.can_send_emails?(current_account) %>
|
<% can_send_emails = Accounts.can_send_emails?(current_account) %>
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
@@ -11,7 +12,7 @@
|
|||||||
<% if can_send_emails %>
|
<% if can_send_emails %>
|
||||||
<%= render 'submissions/email_stats' %>
|
<%= render 'submissions/email_stats' %>
|
||||||
<%= content_for(:edit_button) || capture do %>
|
<%= content_for(:edit_button) || capture do %>
|
||||||
<toggle-visible data-element-ids="<%= %w[message_field].to_json %>" class="flex">
|
<toggle-visible data-element-ids="<%= [message_field_id].to_json %>" class="flex">
|
||||||
<label>
|
<label>
|
||||||
<%= f.check_box :is_custom_message, checked: false, class: 'hidden peer', data: { action: 'change:toggle-visible#trigger', type: 'checkbox' } %>
|
<%= f.check_box :is_custom_message, checked: false, class: 'hidden peer', data: { action: 'change:toggle-visible#trigger', type: 'checkbox' } %>
|
||||||
<span class="link peer-checked:hidden"><%= t('edit_message') %></span>
|
<span class="link peer-checked:hidden"><%= t('edit_message') %></span>
|
||||||
@@ -38,7 +39,7 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% config = AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY) %>
|
<% config = AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY) %>
|
||||||
<div id="message_field" class="card card-compact bg-base-300/40 hidden">
|
<div id="<%= message_field_id %>" class="card card-compact bg-base-300/40 hidden">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<%= tag.input id: toggle_uuid = SecureRandom.uuid, value: '1', name: 'request_email_per_submitter', class: 'peer', type: 'checkbox', hidden: true, checked: local_assigns[:message_per_submitter] != false && template&.preferences&.dig('submitters').to_a.size > 1 %>
|
<%= tag.input id: toggle_uuid = SecureRandom.uuid, value: '1', name: 'request_email_per_submitter', class: 'peer', type: 'checkbox', hidden: true, checked: local_assigns[:message_per_submitter] != false && template&.preferences&.dig('submitters').to_a.size > 1 %>
|
||||||
<div class="peer-checked:hidden form-control space-y-2">
|
<div class="peer-checked:hidden form-control space-y-2">
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
<% align = field.dig('preferences', 'align') %>
|
<% align = field.dig('preferences', 'align') %>
|
||||||
<% valign = field.dig('preferences', 'valign') %>
|
<% valign = field.dig('preferences', 'valign') %>
|
||||||
<% color = field.dig('preferences', 'color') %>
|
<% color = field.dig('preferences', 'color') %>
|
||||||
|
<% bg_color = field.dig('preferences', 'background') %>
|
||||||
<% font = field.dig('preferences', 'font') %>
|
<% font = field.dig('preferences', 'font') %>
|
||||||
<% font_type = field.dig('preferences', 'font_type') %>
|
<% font_type = field.dig('preferences', 'font_type') %>
|
||||||
<% font_size_px = (field.dig('preferences', 'font_size').presence || Submissions::GenerateResultAttachments::FONT_SIZE).to_i * local_assigns.fetch(:font_scale) { 1000.0 / PdfUtils::US_LETTER_W } %>
|
<% font_size_px = (field.dig('preferences', 'font_size').presence || Submissions::GenerateResultAttachments::FONT_SIZE).to_i * local_assigns.fetch(:font_scale) { 1000.0 / PdfUtils::US_LETTER_W } %>
|
||||||
<field-value dir="auto" class="flex absolute <%= 'font-courier' if font == 'Courier' %> <%= 'font-times' if font == 'Times' %> <%= 'font-bold' if font_type == 'bold' || font_type == 'bold_italic' %> <%= 'italic' if font_type == 'italic' || font_type == 'bold_italic' %> <%= align == 'right' ? 'text-right' : (align == 'center' ? 'text-center' : '') %>" style="<%= "color: #{color}; " if color.present? %>width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%; font-size: <%= fs = "clamp(1pt, #{font_size_px / 10}vw, #{font_size_px}px)" %>; line-height: calc(<%= fs %> * 1.3); font-size: <%= fs = "#{font_size_px / 10}cqmin" %>; line-height: calc(<%= fs %> * 1.3)">
|
<field-value dir="auto" class="flex absolute <%= 'font-courier' if font == 'Courier' %> <%= 'font-times' if font == 'Times' %> <%= 'font-bold' if font_type == 'bold' || font_type == 'bold_italic' %> <%= 'italic' if font_type == 'italic' || font_type == 'bold_italic' %> <%= align == 'right' ? 'text-right' : (align == 'center' ? 'text-center' : '') %>" style="<%= "color: #{color}; " if color.present? %><%= "background: #{bg_color}; " if bg_color.present? %>width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%; font-size: <%= fs = "clamp(1pt, #{font_size_px / 10}vw, #{font_size_px}px)" %>; line-height: calc(<%= fs %> * 1.3); font-size: <%= fs = "#{font_size_px / 10}cqmin" %>; line-height: calc(<%= fs %> * 1.3)">
|
||||||
<% if field['type'] == 'signature' %>
|
<% if field['type'] == 'signature' %>
|
||||||
<% is_narrow = area['h']&.positive? && (area['w'].to_f / area['h']) > 6 %>
|
<% is_narrow = area['h']&.positive? && (area['w'].to_f / area['h']) > 6 %>
|
||||||
<div class="flex justify-between w-full h-full gap-1 <%= is_narrow && (local_assigns[:with_signature_id] || field.dig('preferences', 'reason_field_uuid').present?) ? 'flex-row' : 'flex-col' %>">
|
<div class="flex justify-between w-full h-full gap-1 <%= is_narrow && (local_assigns[:with_signature_id] || field.dig('preferences', 'reason_field_uuid').present?) ? 'flex-row' : 'flex-col' %>">
|
||||||
|
|||||||
@@ -38,14 +38,6 @@
|
|||||||
<%= f.button button_title(title: t('send_copy_to_email'), disabled_with: t('starting')), class: 'base-button' %>
|
<%= f.button button_title(title: t('send_copy_to_email'), disabled_with: t('starting')), class: 'base-button' %>
|
||||||
</toggle-submit>
|
</toggle-submit>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if Docuseal.multitenant? %>
|
|
||||||
<div class="divider uppercase"><%= t('or') %></div>
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
|
||||||
<% if Docuseal.multitenant? %>
|
|
||||||
<div>
|
|
||||||
<%= link_to t('create_free_account'), registration_path, class: 'white-button w-full' %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<%= svg_icon('cloud_upload', class: 'w-9 h-9') %>
|
<%= svg_icon('cloud_upload', class: 'w-9 h-9') %>
|
||||||
</span>
|
</span>
|
||||||
<div class="font-medium mb-1">
|
<div class="font-medium mb-1">
|
||||||
<%= t('upload_new_document') %>
|
<%= t('upload_a_new_document') %>
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
<span class="flex flex-col items-center hidden" data-target="dashboard-dropzone.fileDropzoneLoading">
|
<span class="flex flex-col items-center hidden" data-target="dashboard-dropzone.fileDropzoneLoading">
|
||||||
|
|||||||
@@ -5,16 +5,23 @@
|
|||||||
<label for="file_dropzone_input" class="w-full block h-52 relative hover:bg-base-200/30 rounded-xl border-2 border-base-300 border-dashed" data-target="file-dropzone.area">
|
<label for="file_dropzone_input" class="w-full block h-52 relative hover:bg-base-200/30 rounded-xl border-2 border-base-300 border-dashed" data-target="file-dropzone.area">
|
||||||
<div class="absolute top-0 right-0 left-0 bottom-0 flex items-center justify-center p-2 pointer-events-none">
|
<div class="absolute top-0 right-0 left-0 bottom-0 flex items-center justify-center p-2 pointer-events-none">
|
||||||
<div class="flex flex-col items-center text-center">
|
<div class="flex flex-col items-center text-center">
|
||||||
<span data-target="file-dropzone.icon" class="flex flex-col items-center">
|
<span data-target="file-dropzone.icon" class="flex flex-col items-center <%= 'mb-5' if Docuseal.multitenant? %>">
|
||||||
<span>
|
<span>
|
||||||
<%= svg_icon('cloud_upload', class: 'w-10 h-10') %>
|
<%= svg_icon('cloud_upload', class: 'w-10 h-10') %>
|
||||||
</span>
|
</span>
|
||||||
<div class="font-medium mb-1">
|
<div class="font-medium mb-1">
|
||||||
<%= t('upload_new_document') %>
|
<%= t('upload_a_new_document') %>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs">
|
<div class="text-sm">
|
||||||
<%= t('click_to_upload_or_drag_and_drop_html') %>
|
<%= t('click_to_upload_or_drag_and_drop_html') %>
|
||||||
</div>
|
</div>
|
||||||
|
<% if Docuseal.multitenant? %>
|
||||||
|
<a class="flex absolute bottom-4 items-center text-sm mt-2 pointer-events-auto" href="<%= new_template_path(modal_tab: 'google_drive') %>" data-turbo-frame="modal">
|
||||||
|
<span><%= t('or_add_from') %></span>
|
||||||
|
<%= svg_icon('brand_gdrive', class: 'w-4 h-4 ml-1') %>
|
||||||
|
<span class="ml-1 font-medium hover:underline">Google Drive</span>
|
||||||
|
</a>
|
||||||
|
<% end %>
|
||||||
</span>
|
</span>
|
||||||
<span data-target="file-dropzone.loading" class="flex flex-col items-center hidden">
|
<span data-target="file-dropzone.loading" class="flex flex-col items-center hidden">
|
||||||
<%= svg_icon('loader', class: 'w-10 h-10 animate-spin') %>
|
<%= svg_icon('loader', class: 'w-10 h-10 animate-spin') %>
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<%= form_for @template, data: { turbo_frame: :_top }, html: { autocomplete: :off } do |f| %>
|
||||||
|
<% if @base_template %>
|
||||||
|
<%= hidden_field_tag :base_template_id, @base_template.id %>
|
||||||
|
<% end %>
|
||||||
|
<% if @base_template && (can?(:manage, :tenants) || true_user != current_user) && true_user.account.linked_accounts.active.accessible_by(current_ability).exists? %>
|
||||||
|
<div class="form-control -mb-2 mt-2">
|
||||||
|
<%= select_tag :account_id, options_for_select([true_user.account, *true_user.account.linked_accounts.active.accessible_by(current_ability)].uniq.map { |e| [e.name, e.id] }, current_account.id), required: true, class: 'base-select' %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<div class="form-control mt-4">
|
||||||
|
<%= f.text_field :name, required: true, placeholder: t('document_name'), class: 'base-input', dir: 'auto' %>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 mb-4 flex items-center justify-between">
|
||||||
|
<label for="folder_name" class="cursor-pointer">
|
||||||
|
<%= svg_icon('folder', class: 'w-6 h-6') %>
|
||||||
|
</label>
|
||||||
|
<folder-autocomplete class="flex justify-between w-full">
|
||||||
|
<set-value data-on="blur" data-value="<%= TemplateFolder::DEFAULT_NAME %>" data-empty-only="true" class="peer w-full whitespace-nowrap">
|
||||||
|
<input id="folder_name" placeholder="<%= t('folder_name') %>" type="text" class="w-full outline-none border-transparent focus:border-transparent focus:ring-0 bg-base-100 px-1" name="folder_name" value="<%= params[:folder_name].presence || @base_template&.folder&.full_name || TemplateFolder::DEFAULT_NAME %>" autocomplete="off">
|
||||||
|
</set-value>
|
||||||
|
<set-value data-on="click" data-value="" data-input-id="folder_name" class="peer-focus-within:hidden whitespace-nowrap">
|
||||||
|
<label for="folder_name" data-clear-on-focus="true" class="shrink-0 link mr-1.5 cursor-pointer">
|
||||||
|
<%= t('change_folder') %>
|
||||||
|
</label>
|
||||||
|
</set-value>
|
||||||
|
</folder-autocomplete>
|
||||||
|
</div>
|
||||||
|
<div class="form-control">
|
||||||
|
<%= f.button button_title(title: @base_template ? t('submit') : t('create'), disabled_with: t('creating')), class: 'base-button' %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
@@ -103,12 +103,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<% if !submission.archived_at? && !template&.archived_at? && can?(:destroy, submission) %>
|
<% if !submission.archived_at? && !template&.archived_at? && can?(:destroy, submission) %>
|
||||||
<span data-tip="<%= t('archive') %>" class="sm:tooltip tooltip-top">
|
<span data-tip="<%= t('archive') %>" class="sm:tooltip tooltip-top">
|
||||||
<%= button_to button_title(title: nil, disabled_with: t(:archive).first(4), icon: svg_icon('archive', class: 'w-6 h-6')), submission_path(submission), class: 'btn btn-outline btn-sm w-full md:w-fit', form: { class: 'flex' }, title: t('archive'), method: :delete %>
|
<%= button_to button_title(title: nil, disabled_with: nil, icon: svg_icon('archive', class: 'w-6 h-6'), icon_disabled: svg_icon('loader', class: 'w-6 h-6 animate-spin')), submission_path(submission), class: 'btn btn-outline btn-sm w-full md:w-fit', form: { class: 'flex' }, title: t('archive'), method: :delete %>
|
||||||
</span>
|
</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if local_assigns[:archived] && can?(:destroy, submission) %>
|
<% if local_assigns[:archived] && can?(:destroy, submission) %>
|
||||||
<span data-tip="<%= t('remove') %>" class="sm:tooltip tooltip-top">
|
<span data-tip="<%= t('remove') %>" class="sm:tooltip tooltip-top">
|
||||||
<%= button_to button_title(title: nil, disabled_with: t(:remove).first(3), icon: svg_icon('trash', class: 'w-6 h-6')), submission_path(submission, permanently: true), class: 'btn btn-outline btn-sm w-full md:w-fit', form: { class: 'flex' }, title: t('remove'), method: :delete, data: { turbo_confirm: t('submission_deletion_is_irreversible_and_will_permanently_remove_all_associated_signed_documents_with_it_are_you_sure_') } %>
|
<%= button_to button_title(title: nil, disabled_with: nil, icon: svg_icon('trash', class: 'w-6 h-6'), icon_disabled: svg_icon('loader', class: 'w-6 h-6 animate-spin')), submission_path(submission, permanently: true), class: 'btn btn-outline btn-sm w-full md:w-fit', form: { class: 'flex' }, title: t('remove'), method: :delete, data: { turbo_confirm: t('submission_deletion_is_irreversible_and_will_permanently_remove_all_associated_signed_documents_with_it_are_you_sure_') } %>
|
||||||
</span>
|
</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
@@ -210,12 +210,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<% if !submission.archived_at? && !template&.archived_at? %>
|
<% if !submission.archived_at? && !template&.archived_at? %>
|
||||||
<span data-tip="<%= t('archive') %>" class="sm:tooltip tooltip-top">
|
<span data-tip="<%= t('archive') %>" class="sm:tooltip tooltip-top">
|
||||||
<%= button_to button_title(title: nil, disabled_with: t(:archive).first(4), icon: svg_icon('archive', class: 'w-6 h-6')), submission_path(submission), class: 'btn btn-outline btn-sm w-full md:w-fit', form: { class: 'flex' }, title: t('archive'), method: :delete %>
|
<%= button_to button_title(title: nil, disabled_with: nil, icon: svg_icon('archive', class: 'w-6 h-6'), icon_disabled: svg_icon('loader', class: 'w-6 h-6 animate-spin')), submission_path(submission), class: 'btn btn-outline btn-sm w-full md:w-fit', form: { class: 'flex' }, title: t('archive'), method: :delete %>
|
||||||
</span>
|
</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if local_assigns[:archived] && can?(:destroy, submission) %>
|
<% if local_assigns[:archived] && can?(:destroy, submission) %>
|
||||||
<span data-tip="<%= t('remove') %>" class="sm:tooltip tooltip-top">
|
<span data-tip="<%= t('remove') %>" class="sm:tooltip tooltip-top">
|
||||||
<%= button_to button_title(title: nil, disabled_with: t(:remove).first(3), icon: svg_icon('trash', class: 'w-6 h-6')), submission_path(submission, permanently: true), class: 'btn btn-outline btn-sm w-full md:w-fit', form: { class: 'flex' }, title: t('remove'), method: :delete, data: { turbo_confirm: t('submission_deletion_is_irreversible_and_will_permanently_remove_all_associated_signed_documents_with_it_are_you_sure_') } %>
|
<%= button_to button_title(title: nil, disabled_with: nil, icon: svg_icon('trash', class: 'w-6 h-6'), icon_disabled: svg_icon('loader', class: 'w-6 h-6 animate-spin')), submission_path(submission, permanently: true), class: 'btn btn-outline btn-sm w-full md:w-fit', form: { class: 'flex' }, title: t('remove'), method: :delete, data: { turbo_confirm: t('submission_deletion_is_irreversible_and_will_permanently_remove_all_associated_signed_documents_with_it_are_you_sure_') } %>
|
||||||
</span>
|
</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,4 +6,4 @@
|
|||||||
<%= button_to nil, user_configs_path, method: :post, params: { user_config: { key: UserConfig::SHOW_APP_TOUR, value: true } }, class: 'hidden', id: 'start_tour_button' %>
|
<%= button_to nil, user_configs_path, method: :post, params: { user_config: { key: UserConfig::SHOW_APP_TOUR, value: true } }, class: 'hidden', id: 'start_tour_button' %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<template-builder class="grid" data-template="<%= @template_data %>" data-with-sign-yourself-button="<%= !@template.archived_at? %>" 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 %>" 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>
|
||||||
@@ -1,33 +1,3 @@
|
|||||||
<%= render 'shared/turbo_modal', title: @base_template ? t('clone_template') : t('new_document_template') do %>
|
<%= render 'shared/turbo_modal', title: @base_template ? t('clone_template') : t('new_document_template') do %>
|
||||||
<%= form_for @template, data: { turbo_frame: :_top }, html: { autocomplete: :off } do |f| %>
|
<%= render 'templates/file_form' %>
|
||||||
<% if @base_template %>
|
|
||||||
<%= hidden_field_tag :base_template_id, @base_template.id %>
|
|
||||||
<% end %>
|
|
||||||
<% if @base_template && (can?(:manage, :tenants) || true_user != current_user) && true_user.account.linked_accounts.active.accessible_by(current_ability).exists? %>
|
|
||||||
<div class="form-control -mb-2 mt-2">
|
|
||||||
<%= select_tag :account_id, options_for_select([true_user.account, *true_user.account.linked_accounts.active.accessible_by(current_ability)].uniq.map { |e| [e.name, e.id] }, current_account.id), required: true, class: 'base-select' %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<div class="form-control mt-6">
|
|
||||||
<%= f.text_field :name, required: true, placeholder: t('document_name'), class: 'base-input', dir: 'auto' %>
|
|
||||||
</div>
|
|
||||||
<div class="mt-3 mb-4 flex items-center justify-between">
|
|
||||||
<label for="folder_name" class="cursor-pointer">
|
|
||||||
<%= svg_icon('folder', class: 'w-6 h-6') %>
|
|
||||||
</label>
|
|
||||||
<folder-autocomplete class="flex justify-between w-full">
|
|
||||||
<set-value data-on="blur" data-value="<%= TemplateFolder::DEFAULT_NAME %>" data-empty-only="true" class="peer w-full whitespace-nowrap">
|
|
||||||
<input id="folder_name" placeholder="<%= t('folder_name') %>" type="text" class="w-full outline-none border-transparent focus:border-transparent focus:ring-0 bg-base-100 px-1" name="folder_name" value="<%= params[:folder_name].presence || @base_template&.folder&.full_name || TemplateFolder::DEFAULT_NAME %>" autocomplete="off">
|
|
||||||
</set-value>
|
|
||||||
<set-value data-on="click" data-value="" data-input-id="folder_name" class="peer-focus-within:hidden whitespace-nowrap">
|
|
||||||
<label for="folder_name" data-clear-on-focus="true" class="shrink-0 link mr-1.5 cursor-pointer">
|
|
||||||
<%= t('change_folder') %>
|
|
||||||
</label>
|
|
||||||
</set-value>
|
|
||||||
</folder-autocomplete>
|
|
||||||
</div>
|
|
||||||
<div class="form-control">
|
|
||||||
<%= f.button button_title(title: @base_template ? t('submit') : t('create'), disabled_with: t('creating')), class: 'base-button' %>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
@@ -25,8 +25,8 @@ module DocuSeal
|
|||||||
|
|
||||||
config.active_storage.draw_routes = ENV['MULTITENANT'] != 'true'
|
config.active_storage.draw_routes = ENV['MULTITENANT'] != 'true'
|
||||||
|
|
||||||
config.i18n.available_locales = %i[en en-US en-GB es-ES fr-FR pt-PT de-DE it-IT
|
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 pl uk cs pt he nl ar ko ja]
|
es it de fr nl pl uk cs pt he ar ko ja]
|
||||||
config.i18n.fallbacks = [:en]
|
config.i18n.fallbacks = [:en]
|
||||||
|
|
||||||
config.exceptions_app = ->(env) { ErrorsController.action(:show).call(env) }
|
config.exceptions_app = ->(env) { ErrorsController.action(:show).call(env) }
|
||||||
|
|||||||
+1686
-815
File diff suppressed because it is too large.
Load diff
@@ -100,6 +100,9 @@ Rails.application.routes.draw do
|
|||||||
resource :debug, only: %i[show], controller: 'templates_debug' if Rails.env.development?
|
resource :debug, only: %i[show], controller: 'templates_debug' if Rails.env.development?
|
||||||
resources :documents, only: %i[create], controller: 'template_documents'
|
resources :documents, only: %i[create], controller: 'template_documents'
|
||||||
resources :clone_and_replace, only: %i[create], controller: 'templates_clone_and_replace'
|
resources :clone_and_replace, only: %i[create], controller: 'templates_clone_and_replace'
|
||||||
|
if !Docuseal.multitenant? || Docuseal.demo?
|
||||||
|
resources :detect_fields, only: %i[create], controller: 'templates_detect_fields'
|
||||||
|
end
|
||||||
resources :restore, only: %i[create], controller: 'templates_restore'
|
resources :restore, only: %i[create], controller: 'templates_restore'
|
||||||
resources :archived, only: %i[index], controller: 'templates_archived_submissions'
|
resources :archived, only: %i[index], controller: 'templates_archived_submissions'
|
||||||
resources :submissions, only: %i[new create]
|
resources :submissions, only: %i[new create]
|
||||||
|
|||||||
+1
-1
@@ -78,7 +78,7 @@ module Docuseal
|
|||||||
|
|
||||||
@fulltext_search =
|
@fulltext_search =
|
||||||
if SearchEntry.table_exists?
|
if SearchEntry.table_exists?
|
||||||
Docuseal.multitenant? ? true : AccountConfig.exists?(key: :fulltext_search, value: true)
|
Docuseal.multitenant? || AccountConfig.exists?(key: :fulltext_search, value: true)
|
||||||
else
|
else
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
|
|||||||
+160
@@ -25,6 +25,8 @@ class Pdfium
|
|||||||
typedef :pointer, :FPDF_BITMAP
|
typedef :pointer, :FPDF_BITMAP
|
||||||
typedef :pointer, :FPDF_FORMHANDLE
|
typedef :pointer, :FPDF_FORMHANDLE
|
||||||
typedef :pointer, :FPDF_TEXTPAGE
|
typedef :pointer, :FPDF_TEXTPAGE
|
||||||
|
typedef :pointer, :FPDF_PAGEOBJECT
|
||||||
|
typedef :pointer, :FPDF_PATHSEGMENT
|
||||||
|
|
||||||
MAX_SIZE = 32_767
|
MAX_SIZE = 32_767
|
||||||
|
|
||||||
@@ -37,6 +39,9 @@ class Pdfium
|
|||||||
FPDF_RENDER_FORCEHALFTONE = 0x400
|
FPDF_RENDER_FORCEHALFTONE = 0x400
|
||||||
FPDF_PRINTING = 0x800
|
FPDF_PRINTING = 0x800
|
||||||
|
|
||||||
|
TextNode = Struct.new(:content, :x, :y, :w, :h, keyword_init: true)
|
||||||
|
LineNode = Struct.new(:x, :y, :w, :h, :tilt, keyword_init: true)
|
||||||
|
|
||||||
# rubocop:disable Naming/ClassAndModuleCamelCase
|
# rubocop:disable Naming/ClassAndModuleCamelCase
|
||||||
class FPDF_LIBRARY_CONFIG < FFI::Struct
|
class FPDF_LIBRARY_CONFIG < FFI::Struct
|
||||||
layout :version, :int,
|
layout :version, :int,
|
||||||
@@ -77,6 +82,37 @@ class Pdfium
|
|||||||
attach_function :FPDFText_ClosePage, [:FPDF_TEXTPAGE], :void
|
attach_function :FPDFText_ClosePage, [:FPDF_TEXTPAGE], :void
|
||||||
attach_function :FPDFText_CountChars, [:FPDF_TEXTPAGE], :int
|
attach_function :FPDFText_CountChars, [:FPDF_TEXTPAGE], :int
|
||||||
attach_function :FPDFText_GetText, %i[FPDF_TEXTPAGE int int pointer], :int
|
attach_function :FPDFText_GetText, %i[FPDF_TEXTPAGE int int pointer], :int
|
||||||
|
attach_function :FPDFText_GetUnicode, %i[FPDF_TEXTPAGE int], :uint
|
||||||
|
attach_function :FPDFText_GetCharBox, %i[FPDF_TEXTPAGE int pointer pointer pointer pointer], :int
|
||||||
|
attach_function :FPDFText_GetCharOrigin, %i[FPDF_TEXTPAGE int pointer pointer], :int
|
||||||
|
attach_function :FPDFText_GetCharIndexAtPos, %i[FPDF_TEXTPAGE double double double double], :int
|
||||||
|
attach_function :FPDFText_CountRects, %i[FPDF_TEXTPAGE int int], :int
|
||||||
|
attach_function :FPDFText_GetRect, %i[FPDF_TEXTPAGE int pointer pointer pointer pointer], :int
|
||||||
|
attach_function :FPDFText_GetFontSize, %i[FPDF_TEXTPAGE int], :double
|
||||||
|
|
||||||
|
# Page object functions for extracting paths/lines
|
||||||
|
attach_function :FPDFPage_CountObjects, [:FPDF_PAGE], :int
|
||||||
|
attach_function :FPDFPage_GetObject, %i[FPDF_PAGE int], :FPDF_PAGEOBJECT
|
||||||
|
attach_function :FPDFPageObj_GetType, [:FPDF_PAGEOBJECT], :int
|
||||||
|
attach_function :FPDFPageObj_GetBounds, %i[FPDF_PAGEOBJECT pointer pointer pointer pointer], :int
|
||||||
|
attach_function :FPDFPath_CountSegments, [:FPDF_PAGEOBJECT], :int
|
||||||
|
attach_function :FPDFPath_GetPathSegment, %i[FPDF_PAGEOBJECT int], :FPDF_PATHSEGMENT
|
||||||
|
attach_function :FPDFPathSegment_GetType, [:FPDF_PATHSEGMENT], :int
|
||||||
|
attach_function :FPDFPathSegment_GetPoint, %i[FPDF_PATHSEGMENT pointer pointer], :int
|
||||||
|
|
||||||
|
# Page object types
|
||||||
|
FPDF_PAGEOBJ_UNKNOWN = 0
|
||||||
|
FPDF_PAGEOBJ_TEXT = 1
|
||||||
|
FPDF_PAGEOBJ_PATH = 2
|
||||||
|
FPDF_PAGEOBJ_IMAGE = 3
|
||||||
|
FPDF_PAGEOBJ_SHADING = 4
|
||||||
|
FPDF_PAGEOBJ_FORM = 5
|
||||||
|
|
||||||
|
# Path segment types
|
||||||
|
FPDF_SEGMENT_UNKNOWN = -1
|
||||||
|
FPDF_SEGMENT_LINETO = 0
|
||||||
|
FPDF_SEGMENT_BEZIERTO = 1
|
||||||
|
FPDF_SEGMENT_MOVETO = 2
|
||||||
|
|
||||||
typedef :int, :FPDF_BOOL
|
typedef :int, :FPDF_BOOL
|
||||||
typedef :pointer, :IPDF_JSPLATFORM
|
typedef :pointer, :IPDF_JSPLATFORM
|
||||||
@@ -157,6 +193,7 @@ class Pdfium
|
|||||||
raise PdfiumError, "#{context_message}: #{error_message(error_code)} (Code: #{error_code})"
|
raise PdfiumError, "#{context_message}: #{error_message(error_code)} (Code: #{error_code})"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# rubocop:disable Metrics
|
||||||
class Document
|
class Document
|
||||||
attr_reader :document_ptr, :form_handle
|
attr_reader :document_ptr, :form_handle
|
||||||
|
|
||||||
@@ -386,6 +423,128 @@ class Pdfium
|
|||||||
Pdfium.FPDFText_ClosePage(text_page) if text_page && !text_page.null?
|
Pdfium.FPDFText_ClosePage(text_page) if text_page && !text_page.null?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def text_nodes
|
||||||
|
return @text_nodes if @text_nodes
|
||||||
|
|
||||||
|
text_page = Pdfium.FPDFText_LoadPage(page_ptr)
|
||||||
|
char_count = Pdfium.FPDFText_CountChars(text_page)
|
||||||
|
|
||||||
|
@text_nodes = []
|
||||||
|
|
||||||
|
return @text_nodes if char_count.zero?
|
||||||
|
|
||||||
|
char_count.times do |i|
|
||||||
|
unicode = Pdfium.FPDFText_GetUnicode(text_page, i)
|
||||||
|
|
||||||
|
char = [unicode].pack('U*')
|
||||||
|
|
||||||
|
left_ptr = FFI::MemoryPointer.new(:double)
|
||||||
|
right_ptr = FFI::MemoryPointer.new(:double)
|
||||||
|
bottom_ptr = FFI::MemoryPointer.new(:double)
|
||||||
|
top_ptr = FFI::MemoryPointer.new(:double)
|
||||||
|
|
||||||
|
result = Pdfium.FPDFText_GetCharBox(text_page, i, left_ptr, right_ptr, bottom_ptr, top_ptr)
|
||||||
|
|
||||||
|
next if result.zero?
|
||||||
|
|
||||||
|
left = left_ptr.read_double
|
||||||
|
right = right_ptr.read_double
|
||||||
|
|
||||||
|
origin_x_ptr = FFI::MemoryPointer.new(:double)
|
||||||
|
origin_y_ptr = FFI::MemoryPointer.new(:double)
|
||||||
|
|
||||||
|
Pdfium.FPDFText_GetCharOrigin(text_page, i, origin_x_ptr, origin_y_ptr)
|
||||||
|
|
||||||
|
origin_y = origin_y_ptr.read_double
|
||||||
|
|
||||||
|
font_size = Pdfium.FPDFText_GetFontSize(text_page, i)
|
||||||
|
font_size = 8 if font_size == 1
|
||||||
|
|
||||||
|
abs_x = left
|
||||||
|
abs_y = height - origin_y - (font_size * 0.8)
|
||||||
|
abs_width = right - left
|
||||||
|
abs_height = font_size
|
||||||
|
|
||||||
|
x = abs_x / width
|
||||||
|
y = abs_y / height
|
||||||
|
node_width = abs_width / width
|
||||||
|
node_height = abs_height / height
|
||||||
|
|
||||||
|
@text_nodes << TextNode.new(content: char, x: x, y: y, w: node_width, h: node_height)
|
||||||
|
end
|
||||||
|
|
||||||
|
@text_nodes = @text_nodes.sort { |a, b| a.y == b.y ? a.x <=> b.x : a.y <=> b.y }
|
||||||
|
ensure
|
||||||
|
Pdfium.FPDFText_ClosePage(text_page) if text_page && !text_page.null?
|
||||||
|
end
|
||||||
|
|
||||||
|
def line_nodes
|
||||||
|
return @line_nodes if @line_nodes
|
||||||
|
|
||||||
|
ensure_not_closed!
|
||||||
|
|
||||||
|
@line_nodes = []
|
||||||
|
|
||||||
|
object_count = Pdfium.FPDFPage_CountObjects(page_ptr)
|
||||||
|
|
||||||
|
return @line_nodes if object_count.zero?
|
||||||
|
|
||||||
|
object_count.times do |i|
|
||||||
|
page_object = Pdfium.FPDFPage_GetObject(page_ptr, i)
|
||||||
|
|
||||||
|
next if page_object.null?
|
||||||
|
|
||||||
|
obj_type = Pdfium.FPDFPageObj_GetType(page_object)
|
||||||
|
|
||||||
|
next unless obj_type == Pdfium::FPDF_PAGEOBJ_PATH
|
||||||
|
|
||||||
|
left_ptr = FFI::MemoryPointer.new(:float)
|
||||||
|
bottom_ptr = FFI::MemoryPointer.new(:float)
|
||||||
|
right_ptr = FFI::MemoryPointer.new(:float)
|
||||||
|
top_ptr = FFI::MemoryPointer.new(:float)
|
||||||
|
|
||||||
|
Pdfium.FPDFPageObj_GetBounds(page_object, left_ptr, bottom_ptr, right_ptr, top_ptr)
|
||||||
|
|
||||||
|
obj_left = left_ptr.read_float
|
||||||
|
obj_bottom = bottom_ptr.read_float
|
||||||
|
obj_right = right_ptr.read_float
|
||||||
|
obj_top = top_ptr.read_float
|
||||||
|
|
||||||
|
obj_width = obj_right - obj_left
|
||||||
|
obj_height = obj_top - obj_bottom
|
||||||
|
|
||||||
|
next if obj_width < 1 && obj_height < 1
|
||||||
|
|
||||||
|
segment_count = Pdfium.FPDFPath_CountSegments(page_object)
|
||||||
|
|
||||||
|
next if segment_count < 2
|
||||||
|
|
||||||
|
next unless segment_count <= 10 && (obj_height < 10 || obj_width < 10)
|
||||||
|
|
||||||
|
if obj_width > obj_height && obj_height < 10
|
||||||
|
tilt = 0
|
||||||
|
elsif obj_height > obj_width && obj_width < 10
|
||||||
|
tilt = 90
|
||||||
|
else
|
||||||
|
next
|
||||||
|
end
|
||||||
|
|
||||||
|
x = obj_left
|
||||||
|
y = obj_bottom
|
||||||
|
w = obj_width
|
||||||
|
h = obj_height
|
||||||
|
|
||||||
|
norm_x = x / width
|
||||||
|
norm_y = (height - y - h) / height
|
||||||
|
norm_w = w / width
|
||||||
|
norm_h = h / height
|
||||||
|
|
||||||
|
@line_nodes << LineNode.new(x: norm_x, y: norm_y, w: norm_w, h: norm_h, tilt: tilt)
|
||||||
|
end
|
||||||
|
|
||||||
|
@line_nodes = @line_nodes.sort { |a, b| a.y == b.y ? a.x <=> b.x : a.y <=> b.y }
|
||||||
|
end
|
||||||
|
|
||||||
def close
|
def close
|
||||||
return if closed?
|
return if closed?
|
||||||
|
|
||||||
@@ -445,4 +604,5 @@ class Pdfium
|
|||||||
at_exit do
|
at_exit do
|
||||||
cleanup_library
|
cleanup_library
|
||||||
end
|
end
|
||||||
|
# rubocop:enable Metrics
|
||||||
end
|
end
|
||||||
@@ -56,7 +56,7 @@ module SearchEntries
|
|||||||
end
|
end
|
||||||
|
|
||||||
[sql, number, number.length > 1 ? number.delete_prefix('0') : number, keyword]
|
[sql, number, number.length > 1 ? number.delete_prefix('0') : number, keyword]
|
||||||
elsif keyword.match?(/[^\p{L}\d&@.\-]/) || keyword.match?(/\A['"].*['"]\z/) || keyword.match?(/[.\-]{2,}/)
|
elsif keyword.match?(/[^\p{L}\d&@.-]/) || keyword.match?(/\A['"].*['"]\z/) || keyword.match?(/[.-]{2,}/)
|
||||||
['tsvector @@ plainto_tsquery(?)', TextUtils.transliterate(keyword.downcase)]
|
['tsvector @@ plainto_tsquery(?)', TextUtils.transliterate(keyword.downcase)]
|
||||||
else
|
else
|
||||||
keyword = TextUtils.transliterate(keyword.downcase).squish
|
keyword = TextUtils.transliterate(keyword.downcase).squish
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ module SendWebhookRequest
|
|||||||
LOCALHOSTS = %w[0.0.0.0 127.0.0.1 localhost].freeze
|
LOCALHOSTS = %w[0.0.0.0 127.0.0.1 localhost].freeze
|
||||||
|
|
||||||
MANUAL_ATTEMPT = 99_999
|
MANUAL_ATTEMPT = 99_999
|
||||||
AUTOMATED_RETRY_RANGE = 1..MANUAL_ATTEMPT - 1
|
AUTOMATED_RETRY_RANGE = 1..(MANUAL_ATTEMPT - 1)
|
||||||
|
|
||||||
HttpsError = Class.new(StandardError)
|
HttpsError = Class.new(StandardError)
|
||||||
LocalhostError = Class.new(StandardError)
|
LocalhostError = Class.new(StandardError)
|
||||||
|
|||||||
@@ -481,7 +481,7 @@ module Submissions
|
|||||||
|
|
||||||
def select_attachments(submitter)
|
def select_attachments(submitter)
|
||||||
original_documents = submitter.submission.schema_documents.preload(:blob)
|
original_documents = submitter.submission.schema_documents.preload(:blob)
|
||||||
is_more_than_two_images = original_documents.count(&:image?) > 1
|
is_more_than_two_images = original_documents.many?(&:image?)
|
||||||
|
|
||||||
submitter.documents.preload(:blob).reject do |attachment|
|
submitter.documents.preload(:blob).reject do |attachment|
|
||||||
is_more_than_two_images &&
|
is_more_than_two_images &&
|
||||||
|
|||||||
@@ -244,6 +244,7 @@ module Submissions
|
|||||||
font_size ||= (([page.box.width, page.box.height].min / A4_SIZE[0].to_f) * FONT_SIZE).to_i
|
font_size ||= (([page.box.width, page.box.height].min / A4_SIZE[0].to_f) * FONT_SIZE).to_i
|
||||||
|
|
||||||
fill_color = field.dig('preferences', 'color').to_s.delete_prefix('#').presence
|
fill_color = field.dig('preferences', 'color').to_s.delete_prefix('#').presence
|
||||||
|
bg_color = field.dig('preferences', 'background').to_s.delete_prefix('#').presence
|
||||||
|
|
||||||
font_name = field.dig('preferences', 'font')
|
font_name = field.dig('preferences', 'font')
|
||||||
font_variant = (field.dig('preferences', 'font_type').presence || 'none').to_sym
|
font_variant = (field.dig('preferences', 'font_type').presence || 'none').to_sym
|
||||||
@@ -289,6 +290,13 @@ module Submissions
|
|||||||
with_signature_id = field['preferences']['with_signature_id']
|
with_signature_id = field['preferences']['with_signature_id']
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if bg_color.present?
|
||||||
|
canvas.fill_color(bg_color)
|
||||||
|
.rectangle(area['x'] * width, height - (area['y'] * height) - (area['h'] * height),
|
||||||
|
area['w'] * width, area['h'] * height)
|
||||||
|
.fill
|
||||||
|
end
|
||||||
|
|
||||||
case field_type
|
case field_type
|
||||||
when ->(type) { type == 'signature' && (with_signature_id || field.dig('preferences', 'reason_field_uuid')) }
|
when ->(type) { type == 'signature' && (with_signature_id || field.dig('preferences', 'reason_field_uuid')) }
|
||||||
attachment = submitter.attachments.find { |a| a.uuid == value }
|
attachment = submitter.attachments.find { |a| a.uuid == value }
|
||||||
@@ -311,8 +319,8 @@ module Submissions
|
|||||||
timezone = submitter.timezone || submitter.account.timezone if with_submitter_timezone
|
timezone = submitter.timezone || submitter.account.timezone if with_submitter_timezone
|
||||||
|
|
||||||
if with_signature_id_reason
|
if with_signature_id_reason
|
||||||
"#{reason_value ? "#{I18n.t('reason')}: " : ''}#{reason_value || I18n.t('digitally_signed_by')} " \
|
"#{"#{I18n.t('reason')}: " if reason_value}#{reason_value || I18n.t('digitally_signed_by')} " \
|
||||||
"#{submitter.name}#{submitter.email.present? ? " <#{submitter.email}>" : ''}\n" \
|
"#{submitter.name}#{" <#{submitter.email}>" if submitter.email.present?}\n" \
|
||||||
"#{I18n.l(attachment.created_at.in_time_zone(timezone), format: :long)} " \
|
"#{I18n.l(attachment.created_at.in_time_zone(timezone), format: :long)} " \
|
||||||
"#{TimeUtils.timezone_abbr(timezone, attachment.created_at)}"
|
"#{TimeUtils.timezone_abbr(timezone, attachment.created_at)}"
|
||||||
else
|
else
|
||||||
@@ -321,9 +329,9 @@ module Submissions
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
reason_text = HexaPDF::Layout::TextFragment.create(reason_string,
|
base_font_size = (font_size / 1.8).to_i
|
||||||
font:,
|
|
||||||
font_size: (font_size / 1.8).to_i)
|
result = nil
|
||||||
|
|
||||||
if area['h']&.positive? && (area['w'].to_f / area['h']) > 6
|
if area['h']&.positive? && (area['w'].to_f / area['h']) > 6
|
||||||
area_x = area['x'] * width
|
area_x = area['x'] * width
|
||||||
@@ -344,12 +352,10 @@ module Submissions
|
|||||||
|
|
||||||
id_string = "ID: #{attachment.uuid}".upcase
|
id_string = "ID: #{attachment.uuid}".upcase
|
||||||
|
|
||||||
while true
|
loop do
|
||||||
text = HexaPDF::Layout::TextFragment.create(id_string,
|
text = HexaPDF::Layout::TextFragment.create(id_string, font:, font_size: base_font_size)
|
||||||
font:,
|
|
||||||
font_size: (font_size / 1.8).to_i)
|
|
||||||
|
|
||||||
result = layouter.fit([text], half_width, (font_size / 1.8) / 0.65)
|
result = layouter.fit([text], half_width, base_font_size / 0.65)
|
||||||
|
|
||||||
break if result.status == :success
|
break if result.status == :success
|
||||||
|
|
||||||
@@ -358,25 +364,39 @@ module Submissions
|
|||||||
break if id_string.length < 8
|
break if id_string.length < 8
|
||||||
end
|
end
|
||||||
|
|
||||||
|
string = [id_string, reason_string].join("\n")
|
||||||
|
|
||||||
|
loop do
|
||||||
|
text = HexaPDF::Layout::TextFragment.create(string, font:, font_size: base_font_size)
|
||||||
|
|
||||||
|
result = layouter.fit([text], half_width, area_h)
|
||||||
|
|
||||||
|
break if result.status == :success
|
||||||
|
|
||||||
|
base_font_size *= 0.9
|
||||||
|
|
||||||
|
break if base_font_size < 2
|
||||||
|
end
|
||||||
|
|
||||||
|
text = HexaPDF::Layout::TextFragment.create(string, font:, font_size: base_font_size)
|
||||||
|
|
||||||
text_x = area_x + half_width
|
text_x = area_x + half_width
|
||||||
text_y = height - area_y
|
text_y = height - area_y
|
||||||
|
|
||||||
reason_result = layouter.fit([reason_text], half_width, height)
|
layouter.fit([text], half_width, area_h).draw(canvas, text_x + TEXT_LEFT_MARGIN, text_y)
|
||||||
|
|
||||||
layouter.fit([text], half_width, (font_size / 1.8) / 0.65)
|
|
||||||
.draw(canvas, text_x + TEXT_LEFT_MARGIN, text_y)
|
|
||||||
|
|
||||||
layouter.fit([reason_text], half_width, reason_result.lines.sum(&:height))
|
|
||||||
.draw(canvas, text_x + TEXT_LEFT_MARGIN, text_y - TEXT_TOP_MARGIN - result.lines.sum(&:height))
|
|
||||||
else
|
else
|
||||||
|
reason_text = HexaPDF::Layout::TextFragment.create(reason_string,
|
||||||
|
font:,
|
||||||
|
font_size: base_font_size)
|
||||||
|
|
||||||
id_string = "ID: #{attachment.uuid}".upcase
|
id_string = "ID: #{attachment.uuid}".upcase
|
||||||
|
|
||||||
loop do
|
loop do
|
||||||
text = HexaPDF::Layout::TextFragment.create(id_string,
|
text = HexaPDF::Layout::TextFragment.create(id_string,
|
||||||
font:,
|
font:,
|
||||||
font_size: (font_size / 1.8).to_i)
|
font_size: base_font_size)
|
||||||
|
|
||||||
result = layouter.fit([text], area['w'] * width, (font_size / 1.8) / 0.65)
|
result = layouter.fit([text], area['w'] * width, base_font_size / 0.65)
|
||||||
|
|
||||||
break if result.status == :success
|
break if result.status == :success
|
||||||
|
|
||||||
@@ -395,7 +415,7 @@ module Submissions
|
|||||||
|
|
||||||
io = StringIO.new(image.resize([scale * 4, 1].select(&:positive?).min).write_to_buffer('.png'))
|
io = StringIO.new(image.resize([scale * 4, 1].select(&:positive?).min).write_to_buffer('.png'))
|
||||||
|
|
||||||
layouter.fit([text], area['w'] * width, (font_size / 1.8) / 0.65)
|
layouter.fit([text], area['w'] * width, base_font_size / 0.65)
|
||||||
.draw(canvas, (area['x'] * width) + TEXT_LEFT_MARGIN,
|
.draw(canvas, (area['x'] * width) + TEXT_LEFT_MARGIN,
|
||||||
height - (area['y'] * height) - TEXT_TOP_MARGIN - image_height)
|
height - (area['y'] * height) - TEXT_TOP_MARGIN - image_height)
|
||||||
|
|
||||||
@@ -833,7 +853,7 @@ module Submissions
|
|||||||
def find_last_submitter(submission, submitter: nil)
|
def find_last_submitter(submission, submitter: nil)
|
||||||
submission.submitters
|
submission.submitters
|
||||||
.select(&:completed_at?)
|
.select(&:completed_at?)
|
||||||
.select { |e| submitter.nil? ? true : e.id != submitter.id && e.completed_at <= submitter.completed_at }
|
.select { |e| submitter.nil? || (e.id != submitter.id && e.completed_at <= submitter.completed_at) }
|
||||||
.max_by(&:completed_at)
|
.max_by(&:completed_at)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
+19
-2
@@ -13,6 +13,17 @@ module Submitters
|
|||||||
|
|
||||||
UnableToSendCode = Class.new(StandardError)
|
UnableToSendCode = Class.new(StandardError)
|
||||||
InvalidOtp = Class.new(StandardError)
|
InvalidOtp = Class.new(StandardError)
|
||||||
|
MaliciousFileExtension = Class.new(StandardError)
|
||||||
|
|
||||||
|
DANGEROUS_EXTENSIONS = Set.new(%w[
|
||||||
|
exe com bat cmd scr pif vbs vbe js jse wsf wsh msi msp
|
||||||
|
hta cpl jar app deb rpm dmg pkg mpkg dll so dylib sys
|
||||||
|
inf reg ps1 psm1 psd1 ps1xml psc1 pssc bat cmd vb vba
|
||||||
|
sh bash zsh fish run out bin elf gadget workflow lnk scf
|
||||||
|
url desktop application action workflow apk ipa xap appx
|
||||||
|
appxbundle msix msixbundle diagcab diagpkg cpl msc ocx
|
||||||
|
drv scr ins isp mst paf prf shb shs slk ws wsc inf1 inf2
|
||||||
|
].freeze)
|
||||||
|
|
||||||
module_function
|
module_function
|
||||||
|
|
||||||
@@ -56,7 +67,7 @@ module Submitters
|
|||||||
end
|
end
|
||||||
|
|
||||||
[sql, number, weight, number.length > 1 ? number.delete_prefix('0') : number, weight]
|
[sql, number, weight, number.length > 1 ? number.delete_prefix('0') : number, weight]
|
||||||
elsif keyword.match?(/[^\p{L}\d&@.\-]/) || keyword.match?(/[.\-]{2,}/)
|
elsif keyword.match?(/[^\p{L}\d&@.-]/) || keyword.match?(/[.-]{2,}/)
|
||||||
terms = TextUtils.transliterate(keyword.downcase).split(/\b/).map(&:squish).compact_blank.uniq
|
terms = TextUtils.transliterate(keyword.downcase).split(/\b/).map(&:squish).compact_blank.uniq
|
||||||
|
|
||||||
if terms.size > 1
|
if terms.size > 1
|
||||||
@@ -100,7 +111,7 @@ module Submitters
|
|||||||
end
|
end
|
||||||
|
|
||||||
original_documents = submitter.submission.schema_documents.preload(:blob)
|
original_documents = submitter.submission.schema_documents.preload(:blob)
|
||||||
is_more_than_two_images = original_documents.count(&:image?) > 1
|
is_more_than_two_images = original_documents.many?(&:image?)
|
||||||
|
|
||||||
submitter.documents.preload(:blob).reject do |attachment|
|
submitter.documents.preload(:blob).reject do |attachment|
|
||||||
is_more_than_two_images &&
|
is_more_than_two_images &&
|
||||||
@@ -111,6 +122,12 @@ module Submitters
|
|||||||
def create_attachment!(submitter, params)
|
def create_attachment!(submitter, params)
|
||||||
blob =
|
blob =
|
||||||
if (file = params[:file])
|
if (file = params[:file])
|
||||||
|
extension = File.extname(file.original_filename).delete_prefix('.').downcase
|
||||||
|
|
||||||
|
if DANGEROUS_EXTENSIONS.include?(extension)
|
||||||
|
raise MaliciousFileExtension, "File type '.#{extension}' is not allowed."
|
||||||
|
end
|
||||||
|
|
||||||
ActiveStorage::Blob.create_and_upload!(io: file.open,
|
ActiveStorage::Blob.create_and_upload!(io: file.open,
|
||||||
filename: file.original_filename,
|
filename: file.original_filename,
|
||||||
content_type: file.content_type)
|
content_type: file.content_type)
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ module Submitters
|
|||||||
end
|
end
|
||||||
|
|
||||||
def fetch_fields(template, submitter_name: nil, for_submitter: nil)
|
def fetch_fields(template, submitter_name: nil, for_submitter: nil)
|
||||||
if submitter_name
|
if submitter_name && !for_submitter
|
||||||
submitter =
|
submitter =
|
||||||
template.submitters.find { |e| e['name'] == submitter_name } ||
|
template.submitters.find { |e| e['name'] == submitter_name } ||
|
||||||
raise(UnknownSubmitterName,
|
raise(UnknownSubmitterName,
|
||||||
|
|||||||
Executable
+270
@@ -0,0 +1,270 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Templates
|
||||||
|
module DetectFields
|
||||||
|
module_function
|
||||||
|
|
||||||
|
TextFieldBox = Struct.new(:x, :y, :w, :h, keyword_init: true)
|
||||||
|
|
||||||
|
# rubocop:disable Metrics
|
||||||
|
def call(io, attachment: nil, confidence: 0.3, temperature: 1, inference: Templates::ImageToFields,
|
||||||
|
nms: 0.1, split_page: false, aspect_ratio: true, padding: 20, &)
|
||||||
|
if attachment&.image?
|
||||||
|
process_image_attachment(io, attachment:, confidence:, nms:, split_page:, inference:,
|
||||||
|
temperature:, aspect_ratio:, padding:, &)
|
||||||
|
else
|
||||||
|
process_pdf_attachment(io, attachment:, confidence:, nms:, split_page:, inference:,
|
||||||
|
temperature:, aspect_ratio:, padding:, &)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def process_image_attachment(io, attachment:, confidence:, nms:, temperature:, inference:,
|
||||||
|
split_page: false, aspect_ratio: false, padding: nil)
|
||||||
|
image = Vips::Image.new_from_buffer(io.read, '')
|
||||||
|
|
||||||
|
fields = inference.call(image, confidence:, nms:, split_page:,
|
||||||
|
temperature:, aspect_ratio:, padding:)
|
||||||
|
|
||||||
|
fields = fields.map do |f|
|
||||||
|
{
|
||||||
|
uuid: SecureRandom.uuid,
|
||||||
|
type: f.type,
|
||||||
|
required: true,
|
||||||
|
preferences: {},
|
||||||
|
areas: [{
|
||||||
|
x: f.x,
|
||||||
|
y: f.y,
|
||||||
|
w: f.w,
|
||||||
|
h: f.h,
|
||||||
|
page: 0,
|
||||||
|
attachment_uuid: attachment&.uuid
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
yield [attachment&.uuid, 0, fields] if block_given?
|
||||||
|
|
||||||
|
fields
|
||||||
|
end
|
||||||
|
|
||||||
|
def process_pdf_attachment(io, attachment:, confidence:, nms:, temperature:, inference:,
|
||||||
|
split_page: false, aspect_ratio: false, padding: nil)
|
||||||
|
doc = Pdfium::Document.open_bytes(io.read)
|
||||||
|
|
||||||
|
doc.page_count.times.flat_map do |page_number|
|
||||||
|
page = doc.get_page(page_number)
|
||||||
|
|
||||||
|
data, width, height = page.render_to_bitmap(width: inference::RESOLUTION * 1.5)
|
||||||
|
|
||||||
|
image = Vips::Image.new_from_memory(data, width, height, 4, :uchar)
|
||||||
|
|
||||||
|
fields = inference.call(image, confidence: 0.05, nms:, split_page:,
|
||||||
|
temperature:, aspect_ratio:, padding:)
|
||||||
|
|
||||||
|
text_fields = extract_text_fields_from_page(page)
|
||||||
|
line_fields = extract_line_fields_from_page(page)
|
||||||
|
|
||||||
|
fields = increase_confidence_for_overlapping_fields(fields, text_fields)
|
||||||
|
fields = increase_confidence_for_overlapping_fields(fields, line_fields)
|
||||||
|
|
||||||
|
fields = fields.filter_map do |f|
|
||||||
|
next if f.confidence < confidence
|
||||||
|
|
||||||
|
{
|
||||||
|
uuid: SecureRandom.uuid,
|
||||||
|
type: f.type,
|
||||||
|
required: true,
|
||||||
|
preferences: {},
|
||||||
|
areas: [{
|
||||||
|
x: f.x, y: f.y,
|
||||||
|
w: f.w, h: f.h,
|
||||||
|
page: page_number,
|
||||||
|
attachment_uuid: attachment&.uuid
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
yield [attachment&.uuid, page_number, fields] if block_given?
|
||||||
|
|
||||||
|
fields
|
||||||
|
ensure
|
||||||
|
page.close
|
||||||
|
end
|
||||||
|
ensure
|
||||||
|
doc.close
|
||||||
|
end
|
||||||
|
|
||||||
|
def extract_line_fields_from_page(page)
|
||||||
|
line_thickness = 5.0 / page.height
|
||||||
|
|
||||||
|
vertical_lines, all_horizontal_lines = page.line_nodes.partition { |line| line.tilt == 90 }
|
||||||
|
|
||||||
|
horizontal_lines = all_horizontal_lines.reject do |h_line|
|
||||||
|
next true if h_line.w > 0.7 && (h_line.h < 0.1 || h_line.h < 0.9)
|
||||||
|
|
||||||
|
next false if vertical_lines.blank?
|
||||||
|
|
||||||
|
h_x_min = h_line.x
|
||||||
|
h_x_max = h_line.x + h_line.w
|
||||||
|
h_y_avg = h_line.y + (h_line.h / 2)
|
||||||
|
|
||||||
|
vertical_lines.any? do |v_line|
|
||||||
|
v_x_avg = v_line.x + (v_line.w / 2)
|
||||||
|
v_y_min = v_line.y
|
||||||
|
v_y_max = v_line.y + v_line.h
|
||||||
|
|
||||||
|
h_x_min_expanded = h_x_min - line_thickness
|
||||||
|
h_x_max_expanded = h_x_max + line_thickness
|
||||||
|
h_y_min_expanded = h_y_avg - line_thickness
|
||||||
|
h_y_max_expanded = h_y_avg + line_thickness
|
||||||
|
|
||||||
|
v_x_min_expanded = v_x_avg - line_thickness
|
||||||
|
v_x_max_expanded = v_x_avg + line_thickness
|
||||||
|
v_y_min_expanded = v_y_min - line_thickness
|
||||||
|
v_y_max_expanded = v_y_max + line_thickness
|
||||||
|
|
||||||
|
x_overlap = v_x_min_expanded <= h_x_max_expanded && v_x_max_expanded >= h_x_min_expanded
|
||||||
|
y_overlap = h_y_min_expanded <= v_y_max_expanded && h_y_max_expanded >= v_y_min_expanded
|
||||||
|
|
||||||
|
x_overlap && y_overlap
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
node_index = 0
|
||||||
|
|
||||||
|
horizontal_lines = horizontal_lines.reject do |line|
|
||||||
|
nodes = []
|
||||||
|
|
||||||
|
loop do
|
||||||
|
node = page.text_nodes[node_index += 1]
|
||||||
|
|
||||||
|
break unless node
|
||||||
|
|
||||||
|
break if node.y > line.y
|
||||||
|
|
||||||
|
next if node.x + node.w < line.x || line.x + line.w < node.x ||
|
||||||
|
node.y + node.h < line.y - node.h || line.y < node.y
|
||||||
|
|
||||||
|
nodes << node
|
||||||
|
|
||||||
|
next if nodes.blank?
|
||||||
|
|
||||||
|
next_node = page.text_nodes[node_index + 1]
|
||||||
|
|
||||||
|
break unless next_node
|
||||||
|
|
||||||
|
break if next_node.x + next_node.w < line.x || line.x + line.w < next_node.x ||
|
||||||
|
next_node.y + next_node.h < line.y - next_node.h || line.y < next_node.y
|
||||||
|
end
|
||||||
|
|
||||||
|
next if nodes.blank?
|
||||||
|
|
||||||
|
width = nodes.last.x + nodes.last.w - nodes.first.x
|
||||||
|
|
||||||
|
next true if width > line.w / 2.0
|
||||||
|
end
|
||||||
|
|
||||||
|
horizontal_lines.each do |line|
|
||||||
|
line.h += 4 * line_thickness
|
||||||
|
line.y -= 4 * line_thickness
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def extract_text_fields_from_page(page)
|
||||||
|
text_nodes = page.text_nodes
|
||||||
|
|
||||||
|
field_boxes = []
|
||||||
|
|
||||||
|
i = 0
|
||||||
|
|
||||||
|
while i < text_nodes.length
|
||||||
|
node = text_nodes[i]
|
||||||
|
|
||||||
|
next i += 1 if node.content != '_'
|
||||||
|
|
||||||
|
x1 = node.x
|
||||||
|
y1 = node.y
|
||||||
|
x2 = node.x + node.w
|
||||||
|
y2 = node.y + node.h
|
||||||
|
|
||||||
|
underscore_count = 1
|
||||||
|
|
||||||
|
j = i + 1
|
||||||
|
|
||||||
|
while j < text_nodes.length
|
||||||
|
next_node = text_nodes[j]
|
||||||
|
|
||||||
|
break unless next_node.content == '_'
|
||||||
|
|
||||||
|
distance = next_node.x - x2
|
||||||
|
height_diff = (next_node.y - y1).abs
|
||||||
|
|
||||||
|
break if distance > 0.02 || height_diff > node.h * 0.5
|
||||||
|
|
||||||
|
underscore_count += 1
|
||||||
|
next_x2 = next_node.x + next_node.w
|
||||||
|
next_y2 = next_node.y + next_node.h
|
||||||
|
|
||||||
|
x2 = next_x2
|
||||||
|
y2 = [y2, next_y2].max
|
||||||
|
y1 = [y1, next_node.y].min
|
||||||
|
|
||||||
|
j += 1
|
||||||
|
end
|
||||||
|
|
||||||
|
field_boxes << TextFieldBox.new(x: x1, y: y1, w: x2 - x1, h: y2 - y1) if underscore_count >= 2
|
||||||
|
|
||||||
|
i = j
|
||||||
|
end
|
||||||
|
|
||||||
|
field_boxes
|
||||||
|
end
|
||||||
|
|
||||||
|
def calculate_iou(box1, box2)
|
||||||
|
x1 = [box1.x, box2.x].max
|
||||||
|
y1 = [box1.y, box2.y].max
|
||||||
|
x2 = [box1.x + box1.w, box2.x + box2.w].min
|
||||||
|
y2 = [box1.y + box1.h, box2.y + box2.h].min
|
||||||
|
|
||||||
|
intersection_width = [0, x2 - x1].max
|
||||||
|
intersection_height = [0, y2 - y1].max
|
||||||
|
intersection_area = intersection_width * intersection_height
|
||||||
|
|
||||||
|
return 0.0 if intersection_area.zero?
|
||||||
|
|
||||||
|
box1_area = box1.w * box1.h
|
||||||
|
box2_area = box2.w * box2.h
|
||||||
|
union_area = box1_area + box2_area - intersection_area
|
||||||
|
|
||||||
|
intersection_area / union_area
|
||||||
|
end
|
||||||
|
|
||||||
|
def boxes_overlap?(box1, box2)
|
||||||
|
!(box1.x + box1.w < box2.x || box2.x + box2.w < box1.x ||
|
||||||
|
box1.y + box1.h < box2.y || box2.y + box2.h < box1.y)
|
||||||
|
end
|
||||||
|
|
||||||
|
def increase_confidence_for_overlapping_fields(image_fields, text_fields, by: 1.0)
|
||||||
|
return image_fields if text_fields.blank?
|
||||||
|
|
||||||
|
image_fields.map do |image_field|
|
||||||
|
next if image_field.type != 'text'
|
||||||
|
|
||||||
|
field_bottom = image_field.y + image_field.h
|
||||||
|
|
||||||
|
text_fields.each do |text_field|
|
||||||
|
break if text_field.y > field_bottom
|
||||||
|
|
||||||
|
next if text_field.y + text_field.h < image_field.y
|
||||||
|
|
||||||
|
next unless boxes_overlap?(image_field, text_field) && calculate_iou(image_field, text_field) > 0.5
|
||||||
|
|
||||||
|
break image_field.confidence += by
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
image_fields
|
||||||
|
end
|
||||||
|
# rubocop:enable Metrics
|
||||||
|
end
|
||||||
|
end
|
||||||
Executable
+340
@@ -0,0 +1,340 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Templates
|
||||||
|
module ImageToFields
|
||||||
|
module_function
|
||||||
|
|
||||||
|
Field = Struct.new(:type, :x, :y, :w, :h, :confidence, keyword_init: true)
|
||||||
|
|
||||||
|
MODEL_PATH = Rails.root.join('tmp/model.onnx')
|
||||||
|
|
||||||
|
RESOLUTION = 704
|
||||||
|
|
||||||
|
ID_TO_CLASS = %w[text checkbox].freeze
|
||||||
|
|
||||||
|
MEAN = [0.485, 0.456, 0.406].freeze
|
||||||
|
STD = [0.229, 0.224, 0.225].freeze
|
||||||
|
|
||||||
|
CPU_THREADS = Etc.nprocessors
|
||||||
|
|
||||||
|
# rubocop:disable Metrics
|
||||||
|
def call(image, confidence: 0.3, nms: 0.1, temperature: 1,
|
||||||
|
split_page: false, aspect_ratio: true, padding: nil, resolution: RESOLUTION)
|
||||||
|
base_image = image.extract_band(0, n: 3)
|
||||||
|
|
||||||
|
trimmed_base, base_offset_x, base_offset_y = trim_image_with_padding(base_image, padding)
|
||||||
|
|
||||||
|
if split_page && image.height > image.width
|
||||||
|
regions = build_split_image_regions(trimmed_base)
|
||||||
|
|
||||||
|
detections = { xyxy: Numo::SFloat[], confidence: Numo::SFloat[], class_id: Numo::Int32[] }
|
||||||
|
|
||||||
|
detections = regions.reduce(detections) do |acc, r|
|
||||||
|
next detections if r[:img].height <= 0 || r[:img].width <= 0
|
||||||
|
|
||||||
|
input_tensor, transform_info = preprocess_image(r[:img], resolution, aspect_ratio:)
|
||||||
|
|
||||||
|
transform_info[:trim_offset_x] = base_offset_x
|
||||||
|
transform_info[:trim_offset_y] = base_offset_y + r[:offset_y]
|
||||||
|
|
||||||
|
outputs = model.predict({ 'input' => input_tensor })
|
||||||
|
|
||||||
|
boxes = Numo::SFloat.cast(outputs['dets'])[0, true, true]
|
||||||
|
logits = Numo::SFloat.cast(outputs['labels'])[0, true, true]
|
||||||
|
|
||||||
|
postprocess_outputs(boxes, logits, transform_info, acc, confidence:, temperature:, resolution:)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
input_tensor, transform_info = preprocess_image(trimmed_base, resolution, aspect_ratio:)
|
||||||
|
|
||||||
|
transform_info[:trim_offset_x] = base_offset_x
|
||||||
|
transform_info[:trim_offset_y] = base_offset_y
|
||||||
|
|
||||||
|
outputs = model.predict({ 'input' => input_tensor })
|
||||||
|
|
||||||
|
boxes = Numo::SFloat.cast(outputs['dets'])[0, true, true]
|
||||||
|
logits = Numo::SFloat.cast(outputs['labels'])[0, true, true]
|
||||||
|
|
||||||
|
detections = postprocess_outputs(boxes, logits, transform_info, confidence:, temperature:, resolution:)
|
||||||
|
end
|
||||||
|
|
||||||
|
detections = apply_nms(detections, nms)
|
||||||
|
|
||||||
|
fields = build_fields_from_detections(detections, image)
|
||||||
|
|
||||||
|
sort_fields(fields, y_threshold: 10.0 / image.height)
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_split_image_regions(image)
|
||||||
|
half_h = image.height / 2
|
||||||
|
top_h = half_h
|
||||||
|
bottom_h = image.height - half_h
|
||||||
|
|
||||||
|
[
|
||||||
|
{ img: image.crop(0, 0, image.width, top_h), offset_y: 0 },
|
||||||
|
{ img: image.crop(0, top_h, image.width, bottom_h), offset_y: top_h }
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_fields_from_detections(detections, image)
|
||||||
|
Array.new(detections[:xyxy].shape[0]) do |i|
|
||||||
|
x1 = detections[:xyxy][i, 0]
|
||||||
|
y1 = detections[:xyxy][i, 1]
|
||||||
|
x2 = detections[:xyxy][i, 2]
|
||||||
|
y2 = detections[:xyxy][i, 3]
|
||||||
|
|
||||||
|
class_id = detections[:class_id][i].to_i
|
||||||
|
|
||||||
|
confidence = detections[:confidence][i]
|
||||||
|
|
||||||
|
x0_norm = x1 / image.width.to_f
|
||||||
|
y0_norm = y1 / image.height.to_f
|
||||||
|
x1_norm = x2 / image.width.to_f
|
||||||
|
y1_norm = y2 / image.height.to_f
|
||||||
|
|
||||||
|
type_name = ID_TO_CLASS[class_id]
|
||||||
|
|
||||||
|
Field.new(
|
||||||
|
type: type_name,
|
||||||
|
x: x0_norm,
|
||||||
|
y: y0_norm,
|
||||||
|
w: (x1_norm - x0_norm),
|
||||||
|
h: (y1_norm - y0_norm),
|
||||||
|
confidence:
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def trim_image_with_padding(image, padding = 0)
|
||||||
|
return [image, 0, 0] if padding.nil?
|
||||||
|
|
||||||
|
left, top, trim_width, trim_height = image.find_trim(threshold: 10, background: [255, 255, 255])
|
||||||
|
|
||||||
|
padded_left = [left - padding, 0].max
|
||||||
|
padded_top = [top - padding, 0].max
|
||||||
|
padded_right = [left + trim_width + padding, image.width].min
|
||||||
|
padded_bottom = [top + trim_height + padding, image.height].min
|
||||||
|
|
||||||
|
width = padded_right - padded_left
|
||||||
|
height = padded_bottom - padded_top
|
||||||
|
|
||||||
|
trimmed_image = image.crop(padded_left, padded_top, width, height)
|
||||||
|
|
||||||
|
[trimmed_image, padded_left, padded_top]
|
||||||
|
end
|
||||||
|
|
||||||
|
def preprocess_image(image, resolution, aspect_ratio: false)
|
||||||
|
scale_x = resolution.to_f / image.width
|
||||||
|
scale_y = resolution.to_f / image.height
|
||||||
|
|
||||||
|
if aspect_ratio
|
||||||
|
scale = [scale_x, scale_y].min
|
||||||
|
|
||||||
|
new_width = (image.width * scale).round
|
||||||
|
new_height = (image.height * scale).round
|
||||||
|
|
||||||
|
resized = image.resize(scale, vscale: scale, kernel: :lanczos3)
|
||||||
|
|
||||||
|
pad_x = ((resolution - new_width) / 2.0).round
|
||||||
|
pad_y = ((resolution - new_height) / 2.0).round
|
||||||
|
|
||||||
|
image = resized.embed(pad_x, pad_y, resolution, resolution, background: [255, 255, 255])
|
||||||
|
|
||||||
|
transform_info = { scale_x: scale, scale_y: scale, pad_x: pad_x, pad_y: pad_y }
|
||||||
|
else
|
||||||
|
image = image.resize(scale_x, vscale: scale_y, kernel: :lanczos3)
|
||||||
|
|
||||||
|
transform_info = { scale_x: scale_x, scale_y: scale_y, pad_x: 0, pad_y: 0 }
|
||||||
|
end
|
||||||
|
|
||||||
|
image /= 255.0
|
||||||
|
|
||||||
|
image = (image - MEAN) / STD
|
||||||
|
|
||||||
|
pixel_data = image.write_to_memory
|
||||||
|
|
||||||
|
img_array = Numo::SFloat.from_binary(pixel_data, [resolution, resolution, 3])
|
||||||
|
|
||||||
|
img_array = img_array.transpose(2, 0, 1)
|
||||||
|
|
||||||
|
[img_array.reshape(1, 3, resolution, resolution), transform_info]
|
||||||
|
end
|
||||||
|
|
||||||
|
def nms(boxes, scores, iou_threshold = 0.5)
|
||||||
|
return Numo::Int32[] if boxes.shape[0].zero?
|
||||||
|
|
||||||
|
x1 = boxes[true, 0]
|
||||||
|
y1 = boxes[true, 1]
|
||||||
|
x2 = boxes[true, 2]
|
||||||
|
y2 = boxes[true, 3]
|
||||||
|
|
||||||
|
areas = (x2 - x1) * (y2 - y1)
|
||||||
|
order = scores.sort_index.reverse
|
||||||
|
|
||||||
|
keep = []
|
||||||
|
|
||||||
|
while order.size.positive?
|
||||||
|
i = order[0]
|
||||||
|
keep << i
|
||||||
|
|
||||||
|
break if order.size == 1
|
||||||
|
|
||||||
|
xx1 = Numo::SFloat.maximum(x1[i], x1[order[1..]])
|
||||||
|
yy1 = Numo::SFloat.maximum(y1[i], y1[order[1..]])
|
||||||
|
xx2 = Numo::SFloat.minimum(x2[i], x2[order[1..]])
|
||||||
|
yy2 = Numo::SFloat.minimum(y2[i], y2[order[1..]])
|
||||||
|
|
||||||
|
w = Numo::SFloat.maximum(0.0, xx2 - xx1)
|
||||||
|
h = Numo::SFloat.maximum(0.0, yy2 - yy1)
|
||||||
|
|
||||||
|
intersection = w * h
|
||||||
|
|
||||||
|
iou = intersection / (areas[i] + areas[order[1..]] - intersection)
|
||||||
|
|
||||||
|
inds = iou.le(iou_threshold).where
|
||||||
|
|
||||||
|
order = order[inds + 1]
|
||||||
|
end
|
||||||
|
|
||||||
|
Numo::Int32.cast(keep)
|
||||||
|
end
|
||||||
|
|
||||||
|
def postprocess_outputs(boxes, logits, transform_info, detections = nil, confidence: 0.3, temperature: 1,
|
||||||
|
resolution: RESOLUTION)
|
||||||
|
scaled_logits = logits / temperature
|
||||||
|
|
||||||
|
probs = 1.0 / (1.0 + Numo::NMath.exp(-scaled_logits))
|
||||||
|
|
||||||
|
scores = probs.max(axis: 1)
|
||||||
|
labels = probs.argmax(axis: 1)
|
||||||
|
|
||||||
|
cx = boxes[true, 0]
|
||||||
|
cy = boxes[true, 1]
|
||||||
|
w = boxes[true, 2]
|
||||||
|
h = boxes[true, 3]
|
||||||
|
|
||||||
|
x1 = cx - (w / 2.0)
|
||||||
|
y1 = cy - (h / 2.0)
|
||||||
|
x2 = cx + (w / 2.0)
|
||||||
|
y2 = cy + (h / 2.0)
|
||||||
|
|
||||||
|
boxes_xyxy = Numo::SFloat.zeros(boxes.shape[0], 4)
|
||||||
|
boxes_xyxy[true, 0] = x1
|
||||||
|
boxes_xyxy[true, 1] = y1
|
||||||
|
boxes_xyxy[true, 2] = x2
|
||||||
|
boxes_xyxy[true, 3] = y2
|
||||||
|
|
||||||
|
boxes_xyxy *= resolution
|
||||||
|
|
||||||
|
pad_x = transform_info[:pad_x]
|
||||||
|
pad_y = transform_info[:pad_y]
|
||||||
|
boxes_xyxy[true, 0] -= pad_x
|
||||||
|
boxes_xyxy[true, 1] -= pad_y
|
||||||
|
boxes_xyxy[true, 2] -= pad_x
|
||||||
|
boxes_xyxy[true, 3] -= pad_y
|
||||||
|
|
||||||
|
scale_x = transform_info[:scale_x]
|
||||||
|
scale_y = transform_info[:scale_y]
|
||||||
|
boxes_xyxy[true, 0] /= scale_x
|
||||||
|
boxes_xyxy[true, 1] /= scale_y
|
||||||
|
boxes_xyxy[true, 2] /= scale_x
|
||||||
|
boxes_xyxy[true, 3] /= scale_y
|
||||||
|
|
||||||
|
trim_offset_x = transform_info[:trim_offset_x]
|
||||||
|
trim_offset_y = transform_info[:trim_offset_y]
|
||||||
|
boxes_xyxy[true, 0] += trim_offset_x
|
||||||
|
boxes_xyxy[true, 1] += trim_offset_y
|
||||||
|
boxes_xyxy[true, 2] += trim_offset_x
|
||||||
|
boxes_xyxy[true, 3] += trim_offset_y
|
||||||
|
|
||||||
|
keep_mask = scores.gt(confidence)
|
||||||
|
|
||||||
|
keep_indices = keep_mask.where
|
||||||
|
|
||||||
|
if keep_indices.empty?
|
||||||
|
detections || {
|
||||||
|
xyxy: Numo::SFloat[],
|
||||||
|
confidence: Numo::SFloat[],
|
||||||
|
class_id: Numo::Int32[]
|
||||||
|
}
|
||||||
|
else
|
||||||
|
scores = scores[keep_indices]
|
||||||
|
labels = labels[keep_indices]
|
||||||
|
boxes_xyxy = boxes_xyxy[keep_indices, true]
|
||||||
|
|
||||||
|
if detections
|
||||||
|
existing_n = detections[:xyxy].shape[0]
|
||||||
|
new_n = boxes_xyxy.shape[0]
|
||||||
|
total = existing_n + new_n
|
||||||
|
|
||||||
|
xyxy = Numo::SFloat.zeros(total, 4)
|
||||||
|
conf = Numo::SFloat.zeros(total)
|
||||||
|
cls = Numo::Int32.zeros(total)
|
||||||
|
|
||||||
|
if existing_n.positive?
|
||||||
|
xyxy[0...existing_n, true] = detections[:xyxy]
|
||||||
|
conf[0...existing_n] = detections[:confidence]
|
||||||
|
cls[0...existing_n] = detections[:class_id]
|
||||||
|
end
|
||||||
|
|
||||||
|
xyxy[existing_n...(existing_n + new_n), true] = boxes_xyxy
|
||||||
|
conf[existing_n...(existing_n + new_n)] = scores
|
||||||
|
cls[existing_n...(existing_n + new_n)] = Numo::Int32.cast(labels)
|
||||||
|
|
||||||
|
{ xyxy: xyxy, confidence: conf, class_id: cls }
|
||||||
|
else
|
||||||
|
{
|
||||||
|
xyxy: boxes_xyxy,
|
||||||
|
confidence: scores,
|
||||||
|
class_id: Numo::Int32.cast(labels)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def sort_fields(fields, y_threshold: 0.01)
|
||||||
|
sorted_fields = fields.sort { |a, b| a.y == b.y ? a.x <=> b.x : a.y <=> b.y }
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
current_line = []
|
||||||
|
|
||||||
|
sorted_fields.each do |field|
|
||||||
|
if current_line.blank? || (field.y - current_line.first.y).abs < y_threshold
|
||||||
|
current_line << field
|
||||||
|
else
|
||||||
|
lines << current_line.sort_by(&:x)
|
||||||
|
|
||||||
|
current_line = [field]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
lines << current_line.sort_by(&:x) if current_line.present?
|
||||||
|
|
||||||
|
lines.flatten
|
||||||
|
end
|
||||||
|
|
||||||
|
def apply_nms(detections, threshold = 0.5)
|
||||||
|
return detections if detections[:xyxy].shape[0].zero?
|
||||||
|
|
||||||
|
keep_indices = nms(detections[:xyxy], detections[:confidence], threshold)
|
||||||
|
|
||||||
|
{
|
||||||
|
xyxy: detections[:xyxy][keep_indices, true],
|
||||||
|
confidence: detections[:confidence][keep_indices],
|
||||||
|
class_id: detections[:class_id][keep_indices]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def model
|
||||||
|
@model ||= OnnxRuntime::Model.new(
|
||||||
|
MODEL_PATH.to_s,
|
||||||
|
inter_op_num_threads: CPU_THREADS,
|
||||||
|
intra_op_num_threads: CPU_THREADS,
|
||||||
|
enable_mem_pattern: false,
|
||||||
|
enable_cpu_mem_arena: false,
|
||||||
|
providers: ['CPUExecutionProvider']
|
||||||
|
)
|
||||||
|
end
|
||||||
|
# rubocop:enable Metrics
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -99,7 +99,7 @@ module Templates
|
|||||||
|
|
||||||
max_pages_to_process = data.size < GENERATE_PREVIEW_SIZE_LIMIT ? max_pages : 1
|
max_pages_to_process = data.size < GENERATE_PREVIEW_SIZE_LIMIT ? max_pages : 1
|
||||||
|
|
||||||
generate_document_preview_images(attachment, data, (0..[number_of_pages - 1, max_pages_to_process].min))
|
generate_document_preview_images(attachment, data, 0..[number_of_pages - 1, max_pages_to_process].min)
|
||||||
end
|
end
|
||||||
|
|
||||||
def generate_document_preview_images(attachment, data, range, concurrency: CONCURRENCY)
|
def generate_document_preview_images(attachment, data, range, concurrency: CONCURRENCY)
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
module TextUtils
|
module TextUtils
|
||||||
RTL_REGEXP = /[\p{Hebrew}\p{Arabic}]/
|
RTL_REGEXP = /[\p{Hebrew}\p{Arabic}]/
|
||||||
MASK_REGEXP = /[^\s\-_\[\]\(\)\+\?\.\,]/
|
MASK_REGEXP = /[^\s\-_\[\]()+?.,]/
|
||||||
MASK_SYMBOL = 'X'
|
MASK_SYMBOL = 'X'
|
||||||
|
|
||||||
TRANSLITERATIONS =
|
TRANSLITERATIONS =
|
||||||
|
|||||||
Reference in new issue
Block a user