Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
33ca930055 | ||
|
|
3b396f8421 | ||
|
|
aa77e3a8d3 | ||
|
|
75316d8d87 | ||
|
|
a999109a5c | ||
|
|
bdd33c7d6b | ||
|
|
61c5ee22a0 | ||
|
|
12c5b909e0 | ||
|
|
871ef6dda6 | ||
|
|
40052a2d7c | ||
|
|
347be0137d | ||
|
|
fe6baba8bf | ||
|
|
ed8c313bd4 | ||
|
|
ca0acb34d6 | ||
|
|
34ea639c25 | ||
|
|
680ab9dbed | ||
|
|
9377766e52 | ||
|
|
7a2c37454e | ||
|
|
8f6f418f54 | ||
|
|
9b7745c565 | ||
|
|
a389e8cc38 | ||
|
|
2056301f70 | ||
|
|
93babe3660 | ||
|
|
4aef2d8eb0 | ||
|
|
c5ec2ed230 | ||
|
|
a156d5f6cc | ||
|
|
f84674cddd | ||
|
|
b7b62c197a | ||
|
|
f5350eac36 | ||
|
|
ee6228db15 | ||
|
|
3e434c0878 | ||
|
|
7c5372a8e2 | ||
|
|
6ab68cc36b | ||
|
|
ee2400a5c5 | ||
|
|
80be5d2cf5 | ||
|
|
76cf44a31b | ||
|
|
eb63113227 | ||
|
|
b7045fb3d9 | ||
|
|
11a5b5842d | ||
|
|
362994afd5 | ||
|
|
d7682f4ebd | ||
|
|
56d4fe7fa9 | ||
|
|
93a852ac14 | ||
|
|
964db0ab08 | ||
|
|
8547295da7 | ||
|
|
c75050fa46 | ||
|
|
65372c52aa | ||
|
|
12c767b7d3 | ||
|
|
2fc5e9cfb3 | ||
|
|
bccc78ee65 | ||
|
|
c0793f3ce7 | ||
|
|
a8dd8699a3 | ||
|
|
91bb25e8b6 | ||
|
|
c3f4480cdc | ||
|
|
33e226861e | ||
|
|
4f5acbec9d | ||
|
|
d2c9bbf6ab | ||
|
|
b5e57e1f2f | ||
|
|
c82c8b2272 | ||
|
|
43f1b598a7 | ||
|
|
5081e387d0 | ||
|
|
1e5f55c1d6 |
No files matched your search
@@ -51,7 +51,7 @@ ENV OPENSSL_CONF=/etc/openssl_legacy.cnf
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN apk add --no-cache sqlite-dev libpq-dev mariadb-dev vips-dev yaml-dev redis libheif vips-heif gcompat ttf-freefont onnxruntime && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf
|
RUN apk add --no-cache sqlite-dev libpq-dev vips-dev yaml-dev redis libheif vips-heif gcompat ttf-freefont onnxruntime && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf
|
||||||
|
|
||||||
RUN addgroup -g 2000 docuseal && adduser -u 2000 -G docuseal -s /bin/sh -D -h /home/docuseal docuseal
|
RUN addgroup -g 2000 docuseal && adduser -u 2000 -G docuseal -s /bin/sh -D -h /home/docuseal docuseal
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ activate = 1' >> /etc/openssl_legacy.cnf
|
|||||||
|
|
||||||
COPY --chown=docuseal:docuseal ./Gemfile ./Gemfile.lock ./
|
COPY --chown=docuseal:docuseal ./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 && ln -sf /usr/lib/libonnxruntime.so.1 $(ruby -e "print Dir[Gem::Specification.find_by_name('onnxruntime').gem_dir + '/vendor/*.so'].first")
|
RUN apk add --no-cache build-base git && bundle install && apk del --no-cache build-base git && 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")
|
||||||
|
|
||||||
COPY --chown=docuseal:docuseal ./bin ./bin
|
COPY --chown=docuseal:docuseal ./bin ./bin
|
||||||
COPY --chown=docuseal:docuseal ./app ./app
|
COPY --chown=docuseal:docuseal ./app ./app
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ gem 'hexapdf'
|
|||||||
gem 'image_processing'
|
gem 'image_processing'
|
||||||
gem 'jwt', require: false
|
gem 'jwt', require: false
|
||||||
gem 'lograge'
|
gem 'lograge'
|
||||||
gem 'mysql2', require: false
|
|
||||||
gem 'numo-narray-alt', require: false
|
gem 'numo-narray-alt', require: false
|
||||||
gem 'oj'
|
gem 'oj'
|
||||||
gem 'onnxruntime', require: false
|
gem 'onnxruntime', require: false
|
||||||
@@ -45,6 +44,7 @@ gem 'shakapacker'
|
|||||||
gem 'sidekiq'
|
gem 'sidekiq'
|
||||||
gem 'sqlite3', require: false
|
gem 'sqlite3', require: false
|
||||||
gem 'strip_attributes'
|
gem 'strip_attributes'
|
||||||
|
gem 'trilogy', github: 'trilogy-libraries/trilogy', glob: 'contrib/ruby/*.gemspec', require: false
|
||||||
gem 'turbo-rails'
|
gem 'turbo-rails'
|
||||||
gem 'twitter_cldr', require: false
|
gem 'twitter_cldr', require: false
|
||||||
gem 'tzinfo-data'
|
gem 'tzinfo-data'
|
||||||
|
|||||||
@@ -1,3 +1,11 @@
|
|||||||
|
GIT
|
||||||
|
remote: https://github.com/trilogy-libraries/trilogy.git
|
||||||
|
revision: 3963d490459df7a2b5bedb42424c3285f25eab22
|
||||||
|
glob: contrib/ruby/*.gemspec
|
||||||
|
specs:
|
||||||
|
trilogy (2.10.0)
|
||||||
|
bigdecimal
|
||||||
|
|
||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
@@ -197,7 +205,7 @@ GEM
|
|||||||
railties (>= 6.1.0)
|
railties (>= 6.1.0)
|
||||||
faker (3.6.0)
|
faker (3.6.0)
|
||||||
i18n (>= 1.8.11, < 2)
|
i18n (>= 1.8.11, < 2)
|
||||||
faraday (2.14.0)
|
faraday (2.14.1)
|
||||||
faraday-net_http (>= 2.0, < 3.5)
|
faraday-net_http (>= 2.0, < 3.5)
|
||||||
json
|
json
|
||||||
logger
|
logger
|
||||||
@@ -277,7 +285,7 @@ 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.18.0)
|
json (2.18.1)
|
||||||
jwt (3.1.2)
|
jwt (3.1.2)
|
||||||
base64
|
base64
|
||||||
language_server-protocol (3.17.0.5)
|
language_server-protocol (3.17.0.5)
|
||||||
@@ -318,8 +326,6 @@ GEM
|
|||||||
prism (~> 1.5)
|
prism (~> 1.5)
|
||||||
msgpack (1.8.0)
|
msgpack (1.8.0)
|
||||||
multi_json (1.19.1)
|
multi_json (1.19.1)
|
||||||
mysql2 (0.5.7)
|
|
||||||
bigdecimal
|
|
||||||
net-http (0.9.1)
|
net-http (0.9.1)
|
||||||
uri (>= 0.11.1)
|
uri (>= 0.11.1)
|
||||||
net-imap (0.6.2)
|
net-imap (0.6.2)
|
||||||
@@ -644,7 +650,6 @@ DEPENDENCIES
|
|||||||
jwt
|
jwt
|
||||||
letter_opener_web
|
letter_opener_web
|
||||||
lograge
|
lograge
|
||||||
mysql2
|
|
||||||
numo-narray-alt
|
numo-narray-alt
|
||||||
oj
|
oj
|
||||||
onnxruntime
|
onnxruntime
|
||||||
@@ -673,6 +678,7 @@ DEPENDENCIES
|
|||||||
simplecov
|
simplecov
|
||||||
sqlite3
|
sqlite3
|
||||||
strip_attributes
|
strip_attributes
|
||||||
|
trilogy!
|
||||||
turbo-rails
|
turbo-rails
|
||||||
twitter_cldr
|
twitter_cldr
|
||||||
tzinfo-data
|
tzinfo-data
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<h1 align="center" style="border-bottom: none">
|
<h1 align="center" style="border-bottom: none">
|
||||||
<div>
|
<div>
|
||||||
<a href="https://www.docuseal.com">
|
<a href="https://www.docuseal.com">
|
||||||
<img alt="DocuSeal" src="https://github.com/docusealco/docuseal/assets/5418788/c12cd051-81cd-4402-bc3a-92f2cfdc1b06" width="80" />
|
<img alt="DocuSeal" src="https://github.com/user-attachments/assets/38b45682-ffa4-4919-abde-d2d422325c44" width="80" />
|
||||||
<br>
|
<br>
|
||||||
</a>
|
</a>
|
||||||
DocuSeal
|
DocuSeal
|
||||||
|
|||||||
@@ -188,7 +188,7 @@ module Api
|
|||||||
message: %i[subject body],
|
message: %i[subject body],
|
||||||
submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role,
|
submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role,
|
||||||
:completed, :phone, :application_key, :external_id, :reply_to, :go_to_last,
|
:completed, :phone, :application_key, :external_id, :reply_to, :go_to_last,
|
||||||
:require_phone_2fa, :require_email_2fa, :order, :invite_by,
|
:require_phone_2fa, :require_email_2fa, :order, :index, :invite_by,
|
||||||
{ metadata: {}, values: {}, roles: [], readonly_fields: [], message: %i[subject body],
|
{ metadata: {}, values: {}, roles: [], readonly_fields: [], message: %i[subject body],
|
||||||
fields: [:name, :uuid, :default_value, :value, :title, :description,
|
fields: [:name, :uuid, :default_value, :value, :title, :description,
|
||||||
:readonly, :required, :validation_pattern, :invalid_message,
|
:readonly, :required, :validation_pattern, :invalid_message,
|
||||||
|
|||||||
@@ -27,20 +27,18 @@ class SubmissionsDownloadController < ApplicationController
|
|||||||
|
|
||||||
Submissions::EnsureResultGenerated.call(last_submitter)
|
Submissions::EnsureResultGenerated.call(last_submitter)
|
||||||
|
|
||||||
if last_submitter.completed_at < TTL.ago && !signature_valid && !current_user_submitter?(last_submitter)
|
if !signature_valid && !current_user_submitter?(last_submitter)
|
||||||
Rollbar.info("TTL: #{last_submitter.id}") if defined?(Rollbar)
|
return head :not_found unless Submitters::AuthorizedForForm.call(@submitter, current_user, request)
|
||||||
|
|
||||||
return head :not_found
|
if last_submitter.completed_at < TTL.ago
|
||||||
|
Rollbar.info("TTL: #{last_submitter.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
|
return head :not_found
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if params[:combined] == 'true'
|
if params[:combined] == 'true'
|
||||||
url = build_combined_url(@submitter)
|
respond_with_combined(last_submitter)
|
||||||
|
|
||||||
if url
|
|
||||||
render json: [url]
|
|
||||||
else
|
|
||||||
head :not_found
|
|
||||||
end
|
|
||||||
else
|
else
|
||||||
render json: build_urls(last_submitter)
|
render json: build_urls(last_submitter)
|
||||||
end
|
end
|
||||||
@@ -48,8 +46,18 @@ class SubmissionsDownloadController < ApplicationController
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def respond_with_combined(submitter)
|
||||||
|
url = build_combined_url(submitter)
|
||||||
|
|
||||||
|
if url
|
||||||
|
render json: [url]
|
||||||
|
else
|
||||||
|
head :not_found
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def current_user_submitter?(submitter)
|
def current_user_submitter?(submitter)
|
||||||
current_user && current_user.account.submitters.exists?(id: submitter.id)
|
current_user && current_ability.can?(:read, submitter)
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_urls(submitter)
|
def build_urls(submitter)
|
||||||
|
|||||||
@@ -9,15 +9,15 @@ class SubmitFormController < ApplicationController
|
|||||||
|
|
||||||
before_action :load_submitter, only: %i[show update completed]
|
before_action :load_submitter, only: %i[show update completed]
|
||||||
before_action :maybe_render_locked_page, only: :show
|
before_action :maybe_render_locked_page, only: :show
|
||||||
before_action :maybe_require_link_2fa, only: %i[show update]
|
before_action :maybe_require_link_2fa, only: %i[show]
|
||||||
|
|
||||||
CONFIG_KEYS = [].freeze
|
CONFIG_KEYS = [].freeze
|
||||||
|
|
||||||
def show
|
def show
|
||||||
submission = @submitter.submission
|
submission = @submitter.submission
|
||||||
|
|
||||||
|
return render :email_2fa unless Submitters::AuthorizedForForm.pass_email_2fa?(@submitter, request)
|
||||||
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
|
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
|
||||||
return render :email_2fa if require_email_2fa?(@submitter)
|
|
||||||
|
|
||||||
@form_configs = Submitters::FormConfigs.call(@submitter, CONFIG_KEYS)
|
@form_configs = Submitters::FormConfigs.call(@submitter, CONFIG_KEYS)
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ class SubmitFormController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
if require_email_2fa?(@submitter)
|
unless Submitters::AuthorizedForForm.call(@submitter, current_user, request)
|
||||||
return render json: { error: I18n.t('verification_required_refresh_the_page_and_pass_2fa') },
|
return render json: { error: I18n.t('verification_required_refresh_the_page_and_pass_2fa') },
|
||||||
status: :unprocessable_content
|
status: :unprocessable_content
|
||||||
end
|
end
|
||||||
@@ -84,7 +84,9 @@ class SubmitFormController < ApplicationController
|
|||||||
def completed
|
def completed
|
||||||
raise ActionController::RoutingError, I18n.t('not_found') if @submitter.account.archived_at?
|
raise ActionController::RoutingError, I18n.t('not_found') if @submitter.account.archived_at?
|
||||||
|
|
||||||
redirect_to submit_form_path(params[:submit_form_slug]) if require_email_2fa?(@submitter)
|
return if Submitters::AuthorizedForForm.call(@submitter, current_user, request)
|
||||||
|
|
||||||
|
redirect_to submit_form_path(params[:submit_form_slug])
|
||||||
end
|
end
|
||||||
|
|
||||||
def success; end
|
def success; end
|
||||||
@@ -92,10 +94,7 @@ class SubmitFormController < ApplicationController
|
|||||||
private
|
private
|
||||||
|
|
||||||
def maybe_require_link_2fa
|
def maybe_require_link_2fa
|
||||||
return if @submitter.submission.source != 'link'
|
return if Submitters::AuthorizedForForm.pass_link_2fa?(@submitter, current_user, request)
|
||||||
return unless @submitter.submission.template&.preferences&.dig('shared_link_2fa') == true
|
|
||||||
return if cookies.encrypted[:email_2fa_slug] == @submitter.slug
|
|
||||||
return if @submitter.email == current_user&.email && current_user&.account_id == @submitter.account_id
|
|
||||||
|
|
||||||
redirect_to start_form_path(@submitter.submission.template.slug)
|
redirect_to start_form_path(@submitter.submission.template.slug)
|
||||||
end
|
end
|
||||||
@@ -117,12 +116,4 @@ class SubmitFormController < ApplicationController
|
|||||||
ActiveStorage::Attachment.where(record: submission.submitters, name: :attachments)
|
ActiveStorage::Attachment.where(record: submission.submitters, name: :attachments)
|
||||||
.preload(:blob).index_by(&:uuid)
|
.preload(:blob).index_by(&:uuid)
|
||||||
end
|
end
|
||||||
|
|
||||||
def require_email_2fa?(submitter)
|
|
||||||
return false if submitter.submission.template&.preferences&.dig('require_email_2fa') != true &&
|
|
||||||
submitter.preferences['require_email_2fa'] != true
|
|
||||||
return false if cookies.encrypted[:email_2fa_slug] == submitter.slug
|
|
||||||
|
|
||||||
true
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
@@ -11,7 +11,9 @@ class SubmitFormDeclineController < ApplicationController
|
|||||||
submitter.completed_at? ||
|
submitter.completed_at? ||
|
||||||
submitter.submission.archived_at? ||
|
submitter.submission.archived_at? ||
|
||||||
submitter.submission.expired? ||
|
submitter.submission.expired? ||
|
||||||
submitter.submission.template&.archived_at?
|
submitter.submission.template&.archived_at? ||
|
||||||
|
!Submitters::AuthorizedForForm.call(submitter, current_user,
|
||||||
|
request)
|
||||||
|
|
||||||
ApplicationRecord.transaction do
|
ApplicationRecord.transaction do
|
||||||
submitter.update!(declined_at: Time.current)
|
submitter.update!(declined_at: Time.current)
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ class SubmitFormDownloadController < ApplicationController
|
|||||||
@submitter.submission.template&.archived_at? ||
|
@submitter.submission.template&.archived_at? ||
|
||||||
AccountConfig.exists?(account_id: @submitter.account_id,
|
AccountConfig.exists?(account_id: @submitter.account_id,
|
||||||
key: AccountConfig::ALLOW_TO_PARTIAL_DOWNLOAD_KEY,
|
key: AccountConfig::ALLOW_TO_PARTIAL_DOWNLOAD_KEY,
|
||||||
value: false)
|
value: false) ||
|
||||||
|
!Submitters::AuthorizedForForm.call(@submitter, current_user, request)
|
||||||
|
|
||||||
last_completed_submitter = @submitter.submission.submitters
|
last_completed_submitter = @submitter.submission.submitters
|
||||||
.where.not(id: @submitter.id)
|
.where.not(id: @submitter.id)
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ class SubmitFormDrawSignatureController < ApplicationController
|
|||||||
|
|
||||||
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
|
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
|
||||||
|
|
||||||
if @submitter.submission.template&.archived_at? || @submitter.submission.archived_at?
|
if @submitter.submission.template&.archived_at? || @submitter.submission.archived_at? ||
|
||||||
|
!Submitters::AuthorizedForForm.call(@submitter, current_user, request)
|
||||||
return redirect_to submit_form_path(@submitter.slug)
|
return redirect_to submit_form_path(@submitter.slug)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,8 @@ class SubmitFormInviteController < ApplicationController
|
|||||||
!submitter.completed_at? &&
|
!submitter.completed_at? &&
|
||||||
!submitter.submission.archived_at? &&
|
!submitter.submission.archived_at? &&
|
||||||
!submitter.submission.expired? &&
|
!submitter.submission.expired? &&
|
||||||
!submitter.submission.template&.archived_at?
|
!submitter.submission.template&.archived_at? &&
|
||||||
|
Submitters::AuthorizedForForm.call(submitter, current_user, request)
|
||||||
end
|
end
|
||||||
|
|
||||||
def filter_invite_submitters(submitter, key = 'invite_by_uuid')
|
def filter_invite_submitters(submitter, key = 'invite_by_uuid')
|
||||||
|
|||||||
@@ -7,10 +7,12 @@ class SubmitFormValuesController < ApplicationController
|
|||||||
def index
|
def index
|
||||||
submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||||
|
|
||||||
return render json: {} if submitter.completed_at? || submitter.declined_at?
|
return render json: {} if submitter.completed_at? ||
|
||||||
return render json: {} if submitter.submission.template&.archived_at? ||
|
submitter.declined_at? ||
|
||||||
|
submitter.submission.template&.archived_at? ||
|
||||||
submitter.submission.archived_at? ||
|
submitter.submission.archived_at? ||
|
||||||
submitter.submission.expired?
|
submitter.submission.expired? ||
|
||||||
|
!Submitters::AuthorizedForForm.call(submitter, current_user, request)
|
||||||
|
|
||||||
value = submitter.values[params['field_uuid']]
|
value = submitter.values[params['field_uuid']]
|
||||||
attachment = submitter.attachments.where(created_at: params[:after]..).find_by(uuid: value) if value.present?
|
attachment = submitter.attachments.where(created_at: params[:after]..).find_by(uuid: value) if value.present?
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ class TemplatesUploadsController < ApplicationController
|
|||||||
def create_file_params_from_url
|
def create_file_params_from_url
|
||||||
tempfile = Tempfile.new
|
tempfile = Tempfile.new
|
||||||
tempfile.binmode
|
tempfile.binmode
|
||||||
tempfile.write(DownloadUtils.call(params[:url]).body)
|
tempfile.write(DownloadUtils.call(params[:url], validate: true).body)
|
||||||
tempfile.rewind
|
tempfile.rewind
|
||||||
|
|
||||||
filename = URI.decode_www_form_component(params[:filename]) if params[:filename].present?
|
filename = URI.decode_www_form_component(params[:filename]) if params[:filename].present?
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ window.customElements.define('draw-signature', class extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
redrawCanvas (oldWidth, oldHeight) {
|
redrawCanvas (oldWidth, oldHeight) {
|
||||||
if (this.pad && !this.pad.isEmpty()) {
|
if (this.pad && !this.pad.isEmpty() && oldWidth > 0 && oldHeight > 0) {
|
||||||
const sx = this.canvas.width / oldWidth
|
const sx = this.canvas.width / oldWidth
|
||||||
const sy = this.canvas.height / oldHeight
|
const sy = this.canvas.height / oldHeight
|
||||||
|
|
||||||
|
|||||||
@@ -64,6 +64,22 @@ export default class extends HTMLElement {
|
|||||||
if (action === 'empty' || action === 'unchecked') return this.isEmpty(actual)
|
if (action === 'empty' || action === 'unchecked') return this.isEmpty(actual)
|
||||||
if (action === 'not_empty' || action === 'checked') return !this.isEmpty(actual)
|
if (action === 'not_empty' || action === 'checked') return !this.isEmpty(actual)
|
||||||
|
|
||||||
|
if (['equal', 'not_equal', 'greater_than', 'less_than'].includes(action) && this.sourceEl?.getAttribute('type') === 'number') {
|
||||||
|
if (this.isEmpty(actual) || this.isEmpty(expected)) return false
|
||||||
|
|
||||||
|
const actualNumber = parseFloat(actual)
|
||||||
|
const expectedNumber = parseFloat(expected)
|
||||||
|
|
||||||
|
if (Number.isNaN(actualNumber) || Number.isNaN(expectedNumber)) return false
|
||||||
|
|
||||||
|
if (action === 'equal') return Math.abs(actualNumber - expectedNumber) < Number.EPSILON
|
||||||
|
if (action === 'not_equal') return Math.abs(actualNumber - expectedNumber) > Number.EPSILON
|
||||||
|
if (action === 'greater_than') return actualNumber > expectedNumber
|
||||||
|
if (action === 'less_than') return actualNumber < expectedNumber
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
if (action === 'equal') {
|
if (action === 'equal') {
|
||||||
const list = Array.isArray(actual) ? actual : [actual]
|
const list = Array.isArray(actual) ? actual : [actual]
|
||||||
return list.filter((v) => v !== null && v !== undefined).map(String).includes(String(expected))
|
return list.filter((v) => v !== null && v !== undefined).map(String).includes(String(expected))
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ export default targetable(class extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
redrawCanvas (oldWidth, oldHeight) {
|
redrawCanvas (oldWidth, oldHeight) {
|
||||||
if (this.pad && !this.pad.isEmpty()) {
|
if (this.pad && !this.pad.isEmpty() && oldWidth > 0 && oldHeight > 0) {
|
||||||
const sx = this.canvas.width / oldWidth
|
const sx = this.canvas.width / oldWidth
|
||||||
const sy = this.canvas.height / oldHeight
|
const sy = this.canvas.height / oldHeight
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,6 @@ export default class extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get button () {
|
get button () {
|
||||||
return this.querySelector('button')
|
return this.querySelector('button, label')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -182,8 +182,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else-if="field.type === 'cells'"
|
v-else-if="field.type === 'cells'"
|
||||||
class="w-full flex items-center"
|
class="w-full flex"
|
||||||
:class="{ 'justify-end': field.preferences?.align === 'right', ...fontClasses }"
|
:class="{ 'justify-end': field.preferences?.align === 'right', ...alignClasses, ...fontClasses }"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="(char, index) in modelValue"
|
v-for="(char, index) in modelValue"
|
||||||
|
|||||||
@@ -161,6 +161,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
fetchOptions: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
completedButton: {
|
completedButton: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -214,7 +219,10 @@ export default {
|
|||||||
download () {
|
download () {
|
||||||
this.isDownloading = true
|
this.isDownloading = true
|
||||||
|
|
||||||
fetch(this.baseUrl + `/submitters/${this.submitterSlug}/download`).then(async (response) => {
|
fetch(this.baseUrl + `/submitters/${this.submitterSlug}/download`, {
|
||||||
|
method: 'GET',
|
||||||
|
...this.fetchOptions
|
||||||
|
}).then(async (response) => {
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const urls = await response.json()
|
const urls = await response.json()
|
||||||
const isMobileSafariIos = 'ontouchstart' in window && navigator.maxTouchPoints > 0 && /AppleWebKit/i.test(navigator.userAgent)
|
const isMobileSafariIos = 'ontouchstart' in window && navigator.maxTouchPoints > 0 && /AppleWebKit/i.test(navigator.userAgent)
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
<MarkdownContent :string="field.description" />
|
<MarkdownContent :string="field.description" />
|
||||||
</div>
|
</div>
|
||||||
<AppearsOn :field="field" />
|
<AppearsOn :field="field" />
|
||||||
<div class="text-center">
|
<div class="text-center flex">
|
||||||
<input
|
<input
|
||||||
:id="field.uuid"
|
:id="field.uuid"
|
||||||
ref="input"
|
ref="input"
|
||||||
|
|||||||
@@ -395,7 +395,9 @@
|
|||||||
v-model="values[currentField.uuid]"
|
v-model="values[currentField.uuid]"
|
||||||
:reason="values[currentField.preferences?.reason_field_uuid]"
|
:reason="values[currentField.preferences?.reason_field_uuid]"
|
||||||
:field="currentField"
|
:field="currentField"
|
||||||
|
:values="values"
|
||||||
:previous-value="previousSignatureValueFor(currentField) || previousSignatureValue"
|
:previous-value="previousSignatureValueFor(currentField) || previousSignatureValue"
|
||||||
|
:touch-attachment-uuid="previousSignatureValue"
|
||||||
:with-typed-signature="withTypedSignature"
|
:with-typed-signature="withTypedSignature"
|
||||||
:remember-signature="rememberSignature"
|
:remember-signature="rememberSignature"
|
||||||
:attachments-index="attachmentsIndex"
|
:attachments-index="attachmentsIndex"
|
||||||
@@ -407,6 +409,7 @@
|
|||||||
:submitter="submitter"
|
:submitter="submitter"
|
||||||
:show-field-names="showFieldNames"
|
:show-field-names="showFieldNames"
|
||||||
@update:reason="values[currentField.preferences?.reason_field_uuid] = $event"
|
@update:reason="values[currentField.preferences?.reason_field_uuid] = $event"
|
||||||
|
@touch-attachment="attachmentsIndex[previousSignatureValue] ? attachmentsIndex[previousSignatureValue].created_at = new Date() : null"
|
||||||
@attached="attachments.push($event)"
|
@attached="attachments.push($event)"
|
||||||
@start="scrollIntoField(currentField)"
|
@start="scrollIntoField(currentField)"
|
||||||
@minimize="minimizeForm"
|
@minimize="minimizeForm"
|
||||||
@@ -418,6 +421,7 @@
|
|||||||
v-model="values[currentField.uuid]"
|
v-model="values[currentField.uuid]"
|
||||||
:field="currentField"
|
:field="currentField"
|
||||||
:dry-run="dryRun"
|
:dry-run="dryRun"
|
||||||
|
:submitter="submitter"
|
||||||
:previous-value="previousInitialsValue"
|
:previous-value="previousInitialsValue"
|
||||||
:attachments-index="attachmentsIndex"
|
:attachments-index="attachmentsIndex"
|
||||||
:show-field-names="showFieldNames"
|
:show-field-names="showFieldNames"
|
||||||
@@ -526,6 +530,7 @@
|
|||||||
v-else-if="isInvite"
|
v-else-if="isInvite"
|
||||||
:submitters="inviteSubmitters"
|
:submitters="inviteSubmitters"
|
||||||
:optional-submitters="optionalInviteSubmitters"
|
:optional-submitters="optionalInviteSubmitters"
|
||||||
|
:fetch-options="fetchOptions"
|
||||||
:submitter-slug="submitterSlug"
|
:submitter-slug="submitterSlug"
|
||||||
:authenticity-token="authenticityToken"
|
:authenticity-token="authenticityToken"
|
||||||
:url="baseUrl + submitPath + '/invite'"
|
:url="baseUrl + submitPath + '/invite'"
|
||||||
@@ -539,6 +544,7 @@
|
|||||||
:has-signature-fields="stepFields.some((fields) => fields.some((f) => ['signature', 'initials'].includes(f.type)))"
|
:has-signature-fields="stepFields.some((fields) => fields.some((f) => ['signature', 'initials'].includes(f.type)))"
|
||||||
:has-multiple-documents="hasMultipleDocuments"
|
:has-multiple-documents="hasMultipleDocuments"
|
||||||
:completed-button="completedRedirectUrl ? {} : completedButton"
|
:completed-button="completedRedirectUrl ? {} : completedButton"
|
||||||
|
:fetch-options="fetchOptions"
|
||||||
:completed-message="completedRedirectUrl ? {} : completedMessage"
|
:completed-message="completedRedirectUrl ? {} : completedMessage"
|
||||||
:with-send-copy-button="withSendCopyButton && !completedRedirectUrl"
|
:with-send-copy-button="withSendCopyButton && !completedRedirectUrl"
|
||||||
:with-download-button="withDownloadButton && !completedRedirectUrl && !dryRun"
|
:with-download-button="withDownloadButton && !completedRedirectUrl && !dryRun"
|
||||||
@@ -551,14 +557,18 @@
|
|||||||
class="flex justify-center mt-3 sm:mt-4 mb-0 sm:mb-1 select-none"
|
class="flex justify-center mt-3 sm:mt-4 mb-0 sm:mb-1 select-none"
|
||||||
>
|
>
|
||||||
<div class="flex items-center flex-wrap steps-progress">
|
<div class="flex items-center flex-wrap steps-progress">
|
||||||
<a
|
<template
|
||||||
v-for="(step, index) in stepFields"
|
v-for="(step, index) in stepFields"
|
||||||
:key="step[0].uuid"
|
:key="step[0].uuid"
|
||||||
href="#"
|
>
|
||||||
class="inline border border-base-300 h-3 w-3 rounded-full mx-1 mt-1"
|
<a
|
||||||
:class="{ 'bg-base-300 steps-progress-current': index === currentStep, 'bg-base-content': (index < currentStep && stepFields[index].every((f) => !f.required || ![null, undefined, ''].includes(values[f.uuid]))) || isCompleted, 'bg-white': index > currentStep }"
|
v-if="!onlyRequiredFields || step.some((f) => f.required)"
|
||||||
@click.prevent="isCompleted ? '' : [saveStep(), goToStep(index, true)]"
|
href="#"
|
||||||
/>
|
class="inline border border-base-300 h-3 w-3 rounded-full mx-1 mt-1"
|
||||||
|
:class="{ 'bg-base-300 steps-progress-current': index === currentStep, 'bg-base-content': (index < currentStep && stepFields[index].every((f) => !f.required || ![null, undefined, ''].includes(values[f.uuid]))) || isCompleted, 'bg-white': index > currentStep }"
|
||||||
|
@click.prevent="isCompleted ? '' : [saveStep(), goToStep(index, true)]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -670,6 +680,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
|
fetchOptions: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
optionalInviteSubmitters: {
|
optionalInviteSubmitters: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -690,6 +705,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
onlyRequiredFields: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
requireSigningReason: {
|
requireSigningReason: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -926,10 +946,12 @@ export default {
|
|||||||
}, {})
|
}, {})
|
||||||
},
|
},
|
||||||
attachmentConditionsIndex () {
|
attachmentConditionsIndex () {
|
||||||
|
const cache = {}
|
||||||
|
|
||||||
return this.schema.reduce((acc, item) => {
|
return this.schema.reduce((acc, item) => {
|
||||||
if (item.conditions?.length) {
|
if (item.conditions?.length) {
|
||||||
if (item.conditions.every((c) => this.fieldsUuidIndex[c.field_uuid])) {
|
if (item.conditions.every((c) => this.fieldsUuidIndex[c.field_uuid])) {
|
||||||
acc[item.attachment_uuid] = this.checkFieldConditions(item)
|
acc[item.attachment_uuid] = this.checkFieldConditions(item, cache)
|
||||||
} else {
|
} else {
|
||||||
acc[item.attachment_uuid] = true
|
acc[item.attachment_uuid] = true
|
||||||
}
|
}
|
||||||
@@ -950,7 +972,13 @@ export default {
|
|||||||
submitButtonText () {
|
submitButtonText () {
|
||||||
if (this.alwaysMinimize) {
|
if (this.alwaysMinimize) {
|
||||||
return this.t('submit')
|
return this.t('submit')
|
||||||
} else if (this.stepFields.length === this.currentStep + 1) {
|
} else if (!this.onlyRequiredFields && this.stepFields.length === this.currentStep + 1) {
|
||||||
|
if (this.currentField.type === 'signature') {
|
||||||
|
return this.t('sign_and_complete')
|
||||||
|
} else {
|
||||||
|
return this.t('complete')
|
||||||
|
}
|
||||||
|
} else if (this.onlyRequiredFields && !this.findNextStep(this.currentStep)) {
|
||||||
if (this.currentField.type === 'signature') {
|
if (this.currentField.type === 'signature') {
|
||||||
return this.t('sign_and_complete')
|
return this.t('sign_and_complete')
|
||||||
} else {
|
} else {
|
||||||
@@ -996,7 +1024,9 @@ export default {
|
|||||||
},
|
},
|
||||||
previousInitialsValue () {
|
previousInitialsValue () {
|
||||||
if (this.reuseSignature !== false) {
|
if (this.reuseSignature !== false) {
|
||||||
const initialsField = [...this.fields].reverse().find((field) => field.type === 'initials' && !!this.values[field.uuid])
|
const initialsField = this.fields.findLast
|
||||||
|
? this.fields.findLast((field) => field.type === 'initials' && !!this.values[field.uuid])
|
||||||
|
: [...this.fields].reverse().find((field) => field.type === 'initials' && !!this.values[field.uuid])
|
||||||
|
|
||||||
return this.values[initialsField?.uuid]
|
return this.values[initialsField?.uuid]
|
||||||
} else {
|
} else {
|
||||||
@@ -1023,7 +1053,9 @@ export default {
|
|||||||
return this.readonlyFields.filter((f) => f.conditions?.length)
|
return this.readonlyFields.filter((f) => f.conditions?.length)
|
||||||
},
|
},
|
||||||
readonlyFields () {
|
readonlyFields () {
|
||||||
return this.fields.filter((f) => f.readonly && this.checkFieldConditions(f) && this.checkFieldDocumentsConditions(f))
|
const cache = {}
|
||||||
|
|
||||||
|
return this.fields.filter((f) => f.readonly && this.checkFieldConditions(f, cache) && this.checkFieldDocumentsConditions(f))
|
||||||
},
|
},
|
||||||
stepFields () {
|
stepFields () {
|
||||||
const verificationFields = []
|
const verificationFields = []
|
||||||
@@ -1078,10 +1110,12 @@ export default {
|
|||||||
sortedFields.push(verificationFields.pop())
|
sortedFields.push(verificationFields.pop())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cache = {}
|
||||||
|
|
||||||
return sortedFields.reduce((acc, f) => {
|
return sortedFields.reduce((acc, f) => {
|
||||||
const prevStep = acc[acc.length - 1]
|
const prevStep = acc[acc.length - 1]
|
||||||
|
|
||||||
if (this.checkFieldConditions(f) && this.checkFieldDocumentsConditions(f)) {
|
if (this.checkFieldConditions(f, cache) && this.checkFieldDocumentsConditions(f)) {
|
||||||
if (f.type === 'checkbox' && Array.isArray(prevStep) && prevStep[0].type === 'checkbox' && !f.description) {
|
if (f.type === 'checkbox' && Array.isArray(prevStep) && prevStep[0].type === 'checkbox' && !f.description) {
|
||||||
prevStep.push(f)
|
prevStep.push(f)
|
||||||
} else {
|
} else {
|
||||||
@@ -1093,7 +1127,9 @@ export default {
|
|||||||
}, [])
|
}, [])
|
||||||
},
|
},
|
||||||
formulaFields () {
|
formulaFields () {
|
||||||
return this.fields.filter((f) => f.preferences?.formula && f.type !== 'payment' && this.checkFieldConditions(f) && this.checkFieldDocumentsConditions(f))
|
const cache = {}
|
||||||
|
|
||||||
|
return this.fields.filter((f) => f.preferences?.formula && f.type !== 'payment' && this.checkFieldConditions(f, cache) && this.checkFieldDocumentsConditions(f))
|
||||||
},
|
},
|
||||||
attachmentsIndex () {
|
attachmentsIndex () {
|
||||||
return this.attachments.reduce((acc, a) => {
|
return this.attachments.reduce((acc, a) => {
|
||||||
@@ -1155,15 +1191,17 @@ export default {
|
|||||||
this.currentStep = Math.max(stepIndex, 0)
|
this.currentStep = Math.max(stepIndex, 0)
|
||||||
} else if (this.goToLast) {
|
} else if (this.goToLast) {
|
||||||
const requiredEmptyStepIndex = this.stepFields.indexOf(this.stepFields.find((fields) => fields.some((f) => f.required && !this.submittedValues[f.uuid])))
|
const requiredEmptyStepIndex = this.stepFields.indexOf(this.stepFields.find((fields) => fields.some((f) => f.required && !this.submittedValues[f.uuid])))
|
||||||
const lastFilledStepIndex = this.stepFields.indexOf([...this.stepFields].reverse().find((fields) => fields.some((f) => !!this.submittedValues[f.uuid]))) + 1
|
const lastFilledStepIndex = this.stepFields.indexOf(this.stepFields.findLast
|
||||||
|
? this.stepFields.findLast((fields) => fields.some((f) => !!this.submittedValues[f.uuid]))
|
||||||
|
: [...this.stepFields].reverse().find((fields) => fields.some((f) => !!this.submittedValues[f.uuid]))) + 1
|
||||||
|
|
||||||
const indexesList = [this.stepFields.length - 1]
|
const indexesList = [this.stepFields.length - 1]
|
||||||
|
|
||||||
if (requiredEmptyStepIndex !== -1) {
|
if (requiredEmptyStepIndex !== -1 && (!this.onlyRequiredFields || this.stepFields[requiredEmptyStepIndex].some((f) => f.required))) {
|
||||||
indexesList.push(requiredEmptyStepIndex)
|
indexesList.push(requiredEmptyStepIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lastFilledStepIndex !== -1) {
|
if (lastFilledStepIndex !== -1 && (!this.onlyRequiredFields || this.stepFields[lastFilledStepIndex].some((f) => f.required))) {
|
||||||
indexesList.push(lastFilledStepIndex)
|
indexesList.push(lastFilledStepIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1223,27 +1261,35 @@ export default {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
checkFieldConditions (field) {
|
checkFieldConditions (field, cache = {}) {
|
||||||
|
const cacheKey = field.uuid || field.attachment_uuid
|
||||||
|
|
||||||
|
if (cache[cacheKey] !== undefined) {
|
||||||
|
return cache[cacheKey]
|
||||||
|
}
|
||||||
|
|
||||||
if (field.conditions?.length) {
|
if (field.conditions?.length) {
|
||||||
const result = field.conditions.reduce((acc, cond) => {
|
const result = field.conditions.reduce((acc, cond) => {
|
||||||
if (cond.operation === 'or') {
|
if (cond.operation === 'or') {
|
||||||
acc.push(acc.pop() || this.checkFieldCondition(cond))
|
acc.push(acc.pop() || this.checkFieldCondition(cond, cache))
|
||||||
} else {
|
} else {
|
||||||
acc.push(this.checkFieldCondition(cond))
|
acc.push(this.checkFieldCondition(cond, cache))
|
||||||
}
|
}
|
||||||
|
|
||||||
return acc
|
return acc
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return !result.includes(false)
|
cache[cacheKey] = !result.includes(false)
|
||||||
} else {
|
} else {
|
||||||
return true
|
cache[cacheKey] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return cache[cacheKey]
|
||||||
},
|
},
|
||||||
checkFieldCondition (condition) {
|
checkFieldCondition (condition, cache = {}) {
|
||||||
const field = this.fieldsUuidIndex[condition.field_uuid]
|
const field = this.fieldsUuidIndex[condition.field_uuid]
|
||||||
|
|
||||||
if (['not_empty', 'checked', 'equal', 'contains'].includes(condition.action) && field && !this.checkFieldConditions(field)) {
|
if (['not_empty', 'checked', 'equal', 'contains', 'greater_than', 'less_than'].includes(condition.action) && field && !this.checkFieldConditions(field, cache)) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1253,6 +1299,22 @@ export default {
|
|||||||
return isEmpty(this.values[condition.field_uuid] ?? defaultValue)
|
return isEmpty(this.values[condition.field_uuid] ?? defaultValue)
|
||||||
} else if (['not_empty', 'checked'].includes(condition.action)) {
|
} else if (['not_empty', 'checked'].includes(condition.action)) {
|
||||||
return !isEmpty(this.values[condition.field_uuid] ?? defaultValue)
|
return !isEmpty(this.values[condition.field_uuid] ?? defaultValue)
|
||||||
|
} else if (field?.type === 'number' && ['equal', 'not_equal', 'greater_than', 'less_than'].includes(condition.action)) {
|
||||||
|
const value = this.values[condition.field_uuid] ?? defaultValue
|
||||||
|
|
||||||
|
if (isEmpty(value) || isEmpty(condition.value)) return false
|
||||||
|
|
||||||
|
const actual = parseFloat(value)
|
||||||
|
const expected = parseFloat(condition.value)
|
||||||
|
|
||||||
|
if (Number.isNaN(actual) || Number.isNaN(expected)) return false
|
||||||
|
|
||||||
|
if (condition.action === 'equal') return Math.abs(actual - expected) < Number.EPSILON
|
||||||
|
if (condition.action === 'not_equal') return Math.abs(actual - expected) > Number.EPSILON
|
||||||
|
if (condition.action === 'greater_than') return actual > expected
|
||||||
|
if (condition.action === 'less_than') return actual < expected
|
||||||
|
|
||||||
|
return false
|
||||||
} else if (['equal', 'contains'].includes(condition.action) && field) {
|
} else if (['equal', 'contains'].includes(condition.action) && field) {
|
||||||
if (field.options) {
|
if (field.options) {
|
||||||
const option = field.options.find((o) => o.uuid === condition.value)
|
const option = field.options.find((o) => o.uuid === condition.value)
|
||||||
@@ -1292,6 +1354,13 @@ export default {
|
|||||||
return `${this.t('option')} ${index + 1}`
|
return `${this.t('option')} ${index + 1}`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
findNextStep (currentStepIndex) {
|
||||||
|
if (this.onlyRequiredFields) {
|
||||||
|
return this.stepFields.find((step, index) => index > currentStepIndex && step.some((f) => f.required))
|
||||||
|
} else {
|
||||||
|
return this.stepFields[currentStepIndex + 1]
|
||||||
|
}
|
||||||
|
},
|
||||||
maybeTrackEmailClick () {
|
maybeTrackEmailClick () {
|
||||||
const { queryParams } = this
|
const { queryParams } = this
|
||||||
|
|
||||||
@@ -1353,9 +1422,9 @@ export default {
|
|||||||
},
|
},
|
||||||
previousSignatureValueFor (field) {
|
previousSignatureValueFor (field) {
|
||||||
if (this.reuseSignature !== false) {
|
if (this.reuseSignature !== false) {
|
||||||
const signatureField = [...this.fields].reverse().find((f) =>
|
const signatureField = this.fields.findLast
|
||||||
f.type === 'signature' && field.preferences?.format === f.preferences?.format && !!this.values[f.uuid]
|
? this.fields.findLast((f) => f.type === 'signature' && field.preferences?.format === f.preferences?.format && !!this.values[f.uuid])
|
||||||
)
|
: [...this.fields].reverse().find((f) => f.type === 'signature' && field.preferences?.format === f.preferences?.format && !!this.values[f.uuid])
|
||||||
|
|
||||||
return this.values[signatureField?.uuid]
|
return this.values[signatureField?.uuid]
|
||||||
} else {
|
} else {
|
||||||
@@ -1405,7 +1474,8 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
return fetch(this.baseUrl + this.submitPath, {
|
return fetch(this.baseUrl + this.submitPath, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData || new FormData(this.$refs.form)
|
body: formData || new FormData(this.$refs.form),
|
||||||
|
...this.fetchOptions
|
||||||
}).then((response) => {
|
}).then((response) => {
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
currentFieldUuids.forEach((fieldUuid) => {
|
currentFieldUuids.forEach((fieldUuid) => {
|
||||||
@@ -1438,7 +1508,7 @@ export default {
|
|||||||
this.isSubmittingComplete = true
|
this.isSubmittingComplete = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const submitStep = this.currentStep
|
const submitStepIndex = this.currentStep
|
||||||
|
|
||||||
const stepPromise = ['signature', 'phone', 'initials', 'payment', 'verification', 'kba'].includes(this.currentField.type)
|
const stepPromise = ['signature', 'phone', 'initials', 'payment', 'verification', 'kba'].includes(this.currentField.type)
|
||||||
? this.$refs.currentStep.submit
|
? this.$refs.currentStep.submit
|
||||||
@@ -1446,7 +1516,7 @@ export default {
|
|||||||
|
|
||||||
stepPromise().then(async () => {
|
stepPromise().then(async () => {
|
||||||
const emptyRequiredField = this.stepFields.find((fields, index) => {
|
const emptyRequiredField = this.stepFields.find((fields, index) => {
|
||||||
if (forceComplete ? index === submitStep : index >= submitStep) {
|
if (forceComplete ? index === submitStepIndex : index >= submitStepIndex) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1456,7 +1526,7 @@ export default {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const formData = new FormData(this.$refs.form)
|
const formData = new FormData(this.$refs.form)
|
||||||
const isLastStep = (submitStep === this.stepFields.length - 1) || forceComplete
|
const isLastStep = (this.onlyRequiredFields ? !this.findNextStep(submitStepIndex) : (submitStepIndex === this.stepFields.length - 1)) || forceComplete
|
||||||
|
|
||||||
if (isLastStep && !emptyRequiredField && !this.inviteSubmitters.length && !this.optionalInviteSubmitters.length) {
|
if (isLastStep && !emptyRequiredField && !this.inviteSubmitters.length && !this.optionalInviteSubmitters.length) {
|
||||||
formData.append('completed', 'true')
|
formData.append('completed', 'true')
|
||||||
@@ -1500,7 +1570,7 @@ export default {
|
|||||||
return Promise.reject(new Error(data.error))
|
return Promise.reject(new Error(data.error))
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextStep = (isLastStep && emptyRequiredField) || (forceComplete ? null : this.stepFields[submitStep + 1])
|
const nextStep = (isLastStep && emptyRequiredField) || (forceComplete ? null : this.findNextStep(submitStepIndex))
|
||||||
|
|
||||||
if (nextStep) {
|
if (nextStep) {
|
||||||
if (this.alwaysMinimize) {
|
if (this.alwaysMinimize) {
|
||||||
|
|||||||
@@ -175,6 +175,10 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
submitter: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
dryRun: {
|
dryRun: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -257,6 +261,14 @@ export default {
|
|||||||
|
|
||||||
this.$refs.canvas.getContext('2d').scale(scale, scale)
|
this.$refs.canvas.getContext('2d').scale(scale, scale)
|
||||||
|
|
||||||
|
if (!this.isDrawInitials) {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (this.$refs.textInput) {
|
||||||
|
this.initTextInitial()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
this.intersectionObserver?.disconnect()
|
this.intersectionObserver?.disconnect()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -332,12 +344,27 @@ export default {
|
|||||||
|
|
||||||
if (!this.isDrawInitials) {
|
if (!this.isDrawInitials) {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.$refs.textInput.focus()
|
if (this.$refs.textInput) {
|
||||||
|
if (!this.submitter.name) {
|
||||||
|
this.$refs.textInput.focus()
|
||||||
|
}
|
||||||
|
|
||||||
this.$emit('start')
|
this.initTextInitial()
|
||||||
|
|
||||||
|
this.$emit('start')
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
initTextInitial () {
|
||||||
|
if (this.submitter.name) {
|
||||||
|
this.$refs.textInput.value = this.submitter.name.trim().split(/\s+/).filter(Boolean).slice(0, 2).map((part) => part[0]?.toUpperCase() || '').join('')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.$refs.textInput.value) {
|
||||||
|
this.updateWrittenInitials({ target: this.$refs.textInput })
|
||||||
|
}
|
||||||
|
},
|
||||||
async submit () {
|
async submit () {
|
||||||
if (this.modelValue || this.computedPreviousValue) {
|
if (this.modelValue || this.computedPreviousValue) {
|
||||||
if (this.computedPreviousValue) {
|
if (this.computedPreviousValue) {
|
||||||
|
|||||||
@@ -78,6 +78,11 @@ export default {
|
|||||||
type: Array,
|
type: Array,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
fetchOptions: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
optionalSubmitters: {
|
optionalSubmitters: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -108,7 +113,8 @@ export default {
|
|||||||
|
|
||||||
return fetch(this.url, {
|
return fetch(this.url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: new FormData(this.$refs.form)
|
body: new FormData(this.$refs.form),
|
||||||
|
...this.fetchOptions
|
||||||
}).then((response) => {
|
}).then((response) => {
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
this.$emit('success')
|
this.$emit('success')
|
||||||
|
|||||||
@@ -497,10 +497,10 @@ export default {
|
|||||||
body: JSON.stringify(payload)
|
body: JSON.stringify(payload)
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!resp.ok) throw new Error('Failed to start KBA')
|
|
||||||
|
|
||||||
const data = await resp.json()
|
const data = await resp.json()
|
||||||
|
|
||||||
|
if (!resp.ok) throw new Error(data.error || 'Failed to start KBA')
|
||||||
|
|
||||||
if (data.result && data.result.action === 'FAIL') {
|
if (data.result && data.result.action === 'FAIL') {
|
||||||
if (data.result.detail === 'NO MATCH') {
|
if (data.result.detail === 'NO MATCH') {
|
||||||
throw new Error('Unfortunately, we were unable to start Knowledge Based Authentication with the details provided. Please review and confirm that all your personal details are correct.')
|
throw new Error('Unfortunately, we were unable to start Knowledge Based Authentication with the details provided. Please review and confirm that all your personal details are correct.')
|
||||||
@@ -555,7 +555,11 @@ export default {
|
|||||||
const data = await resp.json()
|
const data = await resp.json()
|
||||||
|
|
||||||
if (data.result?.action !== 'PASS') {
|
if (data.result?.action !== 'PASS') {
|
||||||
this.error = 'Knowledge Based Authentication Failed - make sure you provide correct answers for the Knowledge Based authentication.'
|
if (data.result?.issues?.length) {
|
||||||
|
this.error = `Knowledge Based Authentication Failed - make sure you provide correct details for the Knowledge Based authentication: ${data.result.issues.join(', ')}`
|
||||||
|
} else {
|
||||||
|
this.error = 'Knowledge Based Authentication Failed - make sure you provide correct answers for the Knowledge Based authentication.'
|
||||||
|
}
|
||||||
|
|
||||||
throw new Error('Knowledge Based Authentication Failed')
|
throw new Error('Knowledge Based Authentication Failed')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,6 +127,12 @@
|
|||||||
type="hidden"
|
type="hidden"
|
||||||
:name="`values[${field.uuid}]`"
|
:name="`values[${field.uuid}]`"
|
||||||
>
|
>
|
||||||
|
<input
|
||||||
|
v-if="isTouchAttachment"
|
||||||
|
:value="touchAttachmentUuid"
|
||||||
|
type="hidden"
|
||||||
|
name="touch_attachment_uuid"
|
||||||
|
>
|
||||||
<img
|
<img
|
||||||
v-if="modelValue || computedPreviousValue"
|
v-if="modelValue || computedPreviousValue"
|
||||||
:src="attachmentsIndex[modelValue || computedPreviousValue].url"
|
:src="attachmentsIndex[modelValue || computedPreviousValue].url"
|
||||||
@@ -310,7 +316,7 @@ import FileDropzone from './dropzone'
|
|||||||
import MarkdownContent from './markdown_content'
|
import MarkdownContent from './markdown_content'
|
||||||
import { v4 } from 'uuid'
|
import { v4 } from 'uuid'
|
||||||
|
|
||||||
let isFontLoaded = false
|
let fontLoadPromise = null
|
||||||
|
|
||||||
const scale = 3
|
const scale = 3
|
||||||
|
|
||||||
@@ -334,6 +340,10 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
values: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
requireSigningReason: {
|
requireSigningReason: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -388,6 +398,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: ''
|
default: ''
|
||||||
},
|
},
|
||||||
|
touchAttachmentUuid: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
reason: {
|
reason: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -399,13 +414,14 @@ export default {
|
|||||||
default: ''
|
default: ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['attached', 'update:model-value', 'start', 'minimize', 'update:reason'],
|
emits: ['attached', 'update:model-value', 'start', 'minimize', 'update:reason', 'touch-attachment'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
isSignatureStarted: false,
|
isSignatureStarted: false,
|
||||||
isShowQr: false,
|
isShowQr: false,
|
||||||
isOtherReason: false,
|
isOtherReason: false,
|
||||||
isUsePreviousValue: true,
|
isUsePreviousValue: true,
|
||||||
|
isTouchAttachment: false,
|
||||||
isTextSignature: this.field.preferences?.format === 'typed' || this.field.preferences?.format === 'typed_or_upload',
|
isTextSignature: this.field.preferences?.format === 'typed' || this.field.preferences?.format === 'typed_or_upload',
|
||||||
uploadImageInputKey: Math.random().toString()
|
uploadImageInputKey: Math.random().toString()
|
||||||
}
|
}
|
||||||
@@ -466,6 +482,14 @@ export default {
|
|||||||
if (entry.isIntersecting) {
|
if (entry.isIntersecting) {
|
||||||
this.setCanvasSize()
|
this.setCanvasSize()
|
||||||
|
|
||||||
|
if (this.isTextSignature) {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (this.$refs.textInput) {
|
||||||
|
this.initTypedSignature()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
this.intersectionObserver?.disconnect()
|
this.intersectionObserver?.disconnect()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -488,6 +512,10 @@ export default {
|
|||||||
})
|
})
|
||||||
|
|
||||||
this.resizeObserver.observe(this.$refs.canvas.parentNode)
|
this.resizeObserver.observe(this.$refs.canvas.parentNode)
|
||||||
|
|
||||||
|
if (this.isTextSignature) {
|
||||||
|
this.loadFont()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeUnmount () {
|
beforeUnmount () {
|
||||||
@@ -514,7 +542,7 @@ export default {
|
|||||||
redrawCanvas (oldWidth, oldHeight) {
|
redrawCanvas (oldWidth, oldHeight) {
|
||||||
const canvas = this.$refs.canvas
|
const canvas = this.$refs.canvas
|
||||||
|
|
||||||
if (this.pad && !this.isTextSignature && !this.pad.isEmpty()) {
|
if (this.pad && !this.isTextSignature && !this.pad.isEmpty() && oldWidth > 0 && oldHeight > 0) {
|
||||||
const sx = canvas.width / oldWidth
|
const sx = canvas.width / oldWidth
|
||||||
const sy = canvas.height / oldHeight
|
const sy = canvas.height / oldHeight
|
||||||
|
|
||||||
@@ -525,7 +553,7 @@ export default {
|
|||||||
|
|
||||||
this.pad.fromData(scaledData)
|
this.pad.fromData(scaledData)
|
||||||
} else if (this.isTextSignature && this.$refs.textInput) {
|
} else if (this.isTextSignature && this.$refs.textInput) {
|
||||||
this.updateWrittenSignature({ target: { value: this.$refs.textInput.value } })
|
this.updateWrittenSignature({ target: this.$refs.textInput })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
remove () {
|
remove () {
|
||||||
@@ -535,17 +563,17 @@ export default {
|
|||||||
this.isSignatureStarted = false
|
this.isSignatureStarted = false
|
||||||
},
|
},
|
||||||
loadFont () {
|
loadFont () {
|
||||||
if (!isFontLoaded) {
|
if (!fontLoadPromise) {
|
||||||
const font = new FontFace('Dancing Script', `url(${this.baseUrl}/fonts/DancingScript-Regular.otf) format("opentype")`)
|
const font = new FontFace('Dancing Script', `url(${this.baseUrl}/fonts/DancingScript-Regular.otf) format("opentype")`)
|
||||||
|
|
||||||
font.load().then((loadedFont) => {
|
fontLoadPromise = font.load().then((loadedFont) => {
|
||||||
document.fonts.add(loadedFont)
|
document.fonts.add(loadedFont)
|
||||||
|
|
||||||
isFontLoaded = true
|
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
console.error('Font loading failed:', error)
|
console.error('Font loading failed:', error)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return fontLoadPromise
|
||||||
},
|
},
|
||||||
showQr () {
|
showQr () {
|
||||||
this.isShowQr = true
|
this.isShowQr = true
|
||||||
@@ -645,14 +673,29 @@ export default {
|
|||||||
|
|
||||||
if (this.isTextSignature) {
|
if (this.isTextSignature) {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.$refs.textInput.focus()
|
if (this.$refs.textInput) {
|
||||||
|
if (!this.submitter.name) {
|
||||||
|
this.$refs.textInput.focus()
|
||||||
|
}
|
||||||
|
|
||||||
this.loadFont()
|
this.initTypedSignature()
|
||||||
|
|
||||||
this.$emit('start')
|
this.$emit('start')
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async initTypedSignature () {
|
||||||
|
if (this.submitter.name) {
|
||||||
|
this.$refs.textInput.value = this.submitter.name
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.loadFont()
|
||||||
|
|
||||||
|
if (this.$refs.textInput.value) {
|
||||||
|
this.updateWrittenSignature({ target: this.$refs.textInput })
|
||||||
|
}
|
||||||
|
},
|
||||||
drawImage (event) {
|
drawImage (event) {
|
||||||
this.remove()
|
this.remove()
|
||||||
this.clear()
|
this.clear()
|
||||||
@@ -731,6 +774,13 @@ export default {
|
|||||||
},
|
},
|
||||||
async submit () {
|
async submit () {
|
||||||
if (this.modelValue || this.computedPreviousValue) {
|
if (this.modelValue || this.computedPreviousValue) {
|
||||||
|
if (this.touchAttachmentUuid && this.computedPreviousValue === this.touchAttachmentUuid && !Object.values(this.values).includes(this.touchAttachmentUuid)) {
|
||||||
|
this.isTouchAttachment = true
|
||||||
|
this.$emit('touch-attachment', this.touchAttachmentUuid)
|
||||||
|
} else {
|
||||||
|
this.isTouchAttachment = false
|
||||||
|
}
|
||||||
|
|
||||||
if (this.computedPreviousValue) {
|
if (this.computedPreviousValue) {
|
||||||
this.$emit('update:model-value', this.computedPreviousValue)
|
this.$emit('update:model-value', this.computedPreviousValue)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ export default {
|
|||||||
docId: this.eidEasyData.doc_id,
|
docId: this.eidEasyData.doc_id,
|
||||||
language: this.locale,
|
language: this.locale,
|
||||||
countryCode: this.countryCode,
|
countryCode: this.countryCode,
|
||||||
|
sandbox: ['demo.docuseal.tech'].includes(location.host),
|
||||||
enabledMethods: {
|
enabledMethods: {
|
||||||
signature: this.eidEasyData.available_methods
|
signature: this.eidEasyData.available_methods
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -134,6 +134,7 @@
|
|||||||
@focusout="maybeBlurSettings"
|
@focusout="maybeBlurSettings"
|
||||||
>
|
>
|
||||||
<FieldSettings
|
<FieldSettings
|
||||||
|
v-if="isMobile"
|
||||||
:field="field"
|
:field="field"
|
||||||
:default-field="defaultField"
|
:default-field="defaultField"
|
||||||
:editable="editable"
|
:editable="editable"
|
||||||
@@ -150,6 +151,12 @@
|
|||||||
@save="save"
|
@save="save"
|
||||||
@scroll-to="[selectedAreasRef.value = [$event], $emit('scroll-to', $event)]"
|
@scroll-to="[selectedAreasRef.value = [$event], $emit('scroll-to', $event)]"
|
||||||
/>
|
/>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="whitespace-normal"
|
||||||
|
>
|
||||||
|
The dots menu is retired in favor of the field context menu. Right-click the field to access field settings. Double-click the field to set a default value.
|
||||||
|
</div>
|
||||||
</ul>
|
</ul>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -252,7 +259,8 @@
|
|||||||
</span>
|
</span>
|
||||||
<div
|
<div
|
||||||
v-else-if="field.type === 'cells' && field.default_value"
|
v-else-if="field.type === 'cells' && field.default_value"
|
||||||
class="w-full flex items-center"
|
class="w-full flex"
|
||||||
|
:class="fontClasses"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="(char, index) in field.default_value"
|
v-for="(char, index) in field.default_value"
|
||||||
@@ -334,6 +342,7 @@
|
|||||||
:editable="editable && !defaultField"
|
:editable="editable && !defaultField"
|
||||||
:default-field="defaultField"
|
:default-field="defaultField"
|
||||||
:build-default-name="buildDefaultName"
|
:build-default-name="buildDefaultName"
|
||||||
|
@save="save"
|
||||||
@close="isShowFormulaModal = false"
|
@close="isShowFormulaModal = false"
|
||||||
/>
|
/>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
@@ -346,6 +355,7 @@
|
|||||||
:editable="editable && !defaultField"
|
:editable="editable && !defaultField"
|
||||||
:default-field="defaultField"
|
:default-field="defaultField"
|
||||||
:build-default-name="buildDefaultName"
|
:build-default-name="buildDefaultName"
|
||||||
|
@save="save"
|
||||||
@close="isShowFontModal = false"
|
@close="isShowFontModal = false"
|
||||||
/>
|
/>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
@@ -357,6 +367,7 @@
|
|||||||
:item="field"
|
:item="field"
|
||||||
:build-default-name="buildDefaultName"
|
:build-default-name="buildDefaultName"
|
||||||
:default-field="defaultField"
|
:default-field="defaultField"
|
||||||
|
@save="save"
|
||||||
@close="isShowConditionsModal = false"
|
@close="isShowConditionsModal = false"
|
||||||
/>
|
/>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
@@ -369,6 +380,7 @@
|
|||||||
:editable="editable && !defaultField"
|
:editable="editable && !defaultField"
|
||||||
:default-field="defaultField"
|
:default-field="defaultField"
|
||||||
:build-default-name="buildDefaultName"
|
:build-default-name="buildDefaultName"
|
||||||
|
@save="save"
|
||||||
@close="isShowDescriptionModal = false"
|
@close="isShowDescriptionModal = false"
|
||||||
/>
|
/>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
@@ -401,7 +413,7 @@ export default {
|
|||||||
FieldSubmitter,
|
FieldSubmitter,
|
||||||
IconX
|
IconX
|
||||||
},
|
},
|
||||||
inject: ['template', 'save', 't', 'isInlineSize', 'selectedAreasRef', 'isCmdKeyRef'],
|
inject: ['template', 'save', 't', 'isInlineSize', 'selectedAreasRef', 'isCmdKeyRef', 'getFieldTypeIndex'],
|
||||||
props: {
|
props: {
|
||||||
area: {
|
area: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -467,6 +479,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: null
|
default: null
|
||||||
},
|
},
|
||||||
|
isMobile: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
isSelectMode: {
|
isSelectMode: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -579,7 +596,7 @@ export default {
|
|||||||
return this.$el.getRootNode().querySelector('#docuseal_modal_container')
|
return this.$el.getRootNode().querySelector('#docuseal_modal_container')
|
||||||
},
|
},
|
||||||
defaultName () {
|
defaultName () {
|
||||||
return this.buildDefaultName(this.field, this.template.fields)
|
return this.buildDefaultName(this.field)
|
||||||
},
|
},
|
||||||
fontClasses () {
|
fontClasses () {
|
||||||
if (!this.field.preferences) {
|
if (!this.field.preferences) {
|
||||||
|
|||||||
@@ -374,6 +374,7 @@
|
|||||||
:draw-field-type="drawFieldType"
|
:draw-field-type="drawFieldType"
|
||||||
:draw-custom-field="drawCustomField"
|
:draw-custom-field="drawCustomField"
|
||||||
:editable="editable"
|
:editable="editable"
|
||||||
|
:is-mobile="isMobile"
|
||||||
:base-url="baseUrl"
|
:base-url="baseUrl"
|
||||||
:with-fields-detection="withFieldsDetection"
|
:with-fields-detection="withFieldsDetection"
|
||||||
@draw="[onDraw($event), withSelectedFieldType ? '' : drawFieldType = '', drawCustomField = null, showDrawField = false]"
|
@draw="[onDraw($event), withSelectedFieldType ? '' : drawFieldType = '', drawCustomField = null, showDrawField = false]"
|
||||||
@@ -382,9 +383,9 @@
|
|||||||
@paste-field="pasteField"
|
@paste-field="pasteField"
|
||||||
@copy-field="copyField"
|
@copy-field="copyField"
|
||||||
@add-custom-field="addCustomField"
|
@add-custom-field="addCustomField"
|
||||||
|
@set-draw="[drawField = $event.field, drawOption = $event.option]"
|
||||||
@copy-selected-areas="copySelectedAreas"
|
@copy-selected-areas="copySelectedAreas"
|
||||||
@delete-selected-areas="deleteSelectedAreas"
|
@delete-selected-areas="deleteSelectedAreas"
|
||||||
@align-selected-areas="alignSelectedAreas"
|
|
||||||
@autodetect-fields="detectFieldsForPage"
|
@autodetect-fields="detectFieldsForPage"
|
||||||
/>
|
/>
|
||||||
<DocumentControls
|
<DocumentControls
|
||||||
@@ -645,7 +646,8 @@ export default {
|
|||||||
fieldsDragFieldRef: computed(() => this.fieldsDragFieldRef),
|
fieldsDragFieldRef: computed(() => this.fieldsDragFieldRef),
|
||||||
customDragFieldRef: computed(() => this.customDragFieldRef),
|
customDragFieldRef: computed(() => this.customDragFieldRef),
|
||||||
isSelectModeRef: computed(() => this.isSelectModeRef),
|
isSelectModeRef: computed(() => this.isSelectModeRef),
|
||||||
isCmdKeyRef: computed(() => this.isCmdKeyRef)
|
isCmdKeyRef: computed(() => this.isCmdKeyRef),
|
||||||
|
getFieldTypeIndex: this.getFieldTypeIndex
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
@@ -989,6 +991,18 @@ export default {
|
|||||||
|
|
||||||
return areas
|
return areas
|
||||||
},
|
},
|
||||||
|
fieldTypeIndexMap () {
|
||||||
|
const map = {}
|
||||||
|
const typeCounters = {}
|
||||||
|
|
||||||
|
this.template.fields.forEach((f) => {
|
||||||
|
typeCounters[f.type] ||= 0
|
||||||
|
map[f.uuid] = typeCounters[f.type]
|
||||||
|
typeCounters[f.type]++
|
||||||
|
})
|
||||||
|
|
||||||
|
return map
|
||||||
|
},
|
||||||
isAllRequiredFieldsAdded () {
|
isAllRequiredFieldsAdded () {
|
||||||
return !this.defaultRequiredFields?.some((f) => {
|
return !this.defaultRequiredFields?.some((f) => {
|
||||||
return !this.template.fields?.some((field) => field.name === f.name)
|
return !this.template.fields?.some((field) => field.name === f.name)
|
||||||
@@ -1085,6 +1099,9 @@ export default {
|
|||||||
addCustomField (field) {
|
addCustomField (field) {
|
||||||
return this.$refs.fields.addCustomField(field)
|
return this.$refs.fields.addCustomField(field)
|
||||||
},
|
},
|
||||||
|
getFieldTypeIndex (field) {
|
||||||
|
return this.fieldTypeIndexMap[field.uuid]
|
||||||
|
},
|
||||||
addCustomFieldWithoutDraw () {
|
addCustomFieldWithoutDraw () {
|
||||||
const customField = this.drawCustomField
|
const customField = this.drawCustomField
|
||||||
|
|
||||||
@@ -1160,27 +1177,6 @@ export default {
|
|||||||
|
|
||||||
this.debouncedSave()
|
this.debouncedSave()
|
||||||
},
|
},
|
||||||
alignSelectedAreas (direction) {
|
|
||||||
const areas = this.selectedAreasRef.value
|
|
||||||
|
|
||||||
let targetValue
|
|
||||||
|
|
||||||
if (direction === 'left') {
|
|
||||||
targetValue = Math.min(...areas.map(a => a.x))
|
|
||||||
areas.forEach((area) => { area.x = targetValue })
|
|
||||||
} else if (direction === 'right') {
|
|
||||||
targetValue = Math.max(...areas.map(a => a.x + a.w))
|
|
||||||
areas.forEach((area) => { area.x = targetValue - area.w })
|
|
||||||
} else if (direction === 'top') {
|
|
||||||
targetValue = Math.min(...areas.map(a => a.y))
|
|
||||||
areas.forEach((area) => { area.y = targetValue })
|
|
||||||
} else if (direction === 'bottom') {
|
|
||||||
targetValue = Math.max(...areas.map(a => a.y + a.h))
|
|
||||||
areas.forEach((area) => { area.y = targetValue - area.h })
|
|
||||||
}
|
|
||||||
|
|
||||||
this.save()
|
|
||||||
},
|
|
||||||
download () {
|
download () {
|
||||||
this.isDownloading = true
|
this.isDownloading = true
|
||||||
|
|
||||||
@@ -2065,7 +2061,9 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'checkbox' && !this.drawFieldType && (this.template.fields[this.template.fields.length - 1]?.type === 'checkbox' || area.w)) {
|
if (type === 'checkbox' && !this.drawFieldType && (this.template.fields[this.template.fields.length - 1]?.type === 'checkbox' || area.w)) {
|
||||||
const previousField = [...this.template.fields].reverse().find((f) => f.type === type)
|
const previousField = this.template.fields.findLast
|
||||||
|
? this.template.fields.findLast((f) => f.type === type)
|
||||||
|
: [...this.template.fields].reverse().find((f) => f.type === type)
|
||||||
const previousArea = previousField?.areas?.[previousField.areas.length - 1]
|
const previousArea = previousField?.areas?.[previousField.areas.length - 1]
|
||||||
|
|
||||||
if (previousArea || area.w) {
|
if (previousArea || area.w) {
|
||||||
@@ -2329,7 +2327,9 @@ export default {
|
|||||||
assignDropAreaSize (fieldArea, field, area) {
|
assignDropAreaSize (fieldArea, field, area) {
|
||||||
const fieldType = field.type || 'text'
|
const fieldType = field.type || 'text'
|
||||||
|
|
||||||
const previousField = [...this.template.fields].reverse().find((f) => f.type === fieldType)
|
const previousField = this.template.fields.findLast
|
||||||
|
? this.template.fields.findLast((f) => f.type === fieldType)
|
||||||
|
: [...this.template.fields].reverse().find((f) => f.type === fieldType)
|
||||||
|
|
||||||
let baseArea
|
let baseArea
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl">
|
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl">
|
||||||
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
|
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
|
||||||
<span class="modal-title">
|
<span class="modal-title">
|
||||||
{{ t('condition') }} - {{ (defaultField ? (defaultField.title || item.title || item.name) : item.name) || buildDefaultName(item, template.fields) }}
|
{{ t('condition') }} - {{ (defaultField ? (defaultField.title || item.title || item.name) : item.name) || buildDefaultName(item) }}
|
||||||
</span>
|
</span>
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
@@ -83,7 +83,7 @@
|
|||||||
class="text-base-content"
|
class="text-base-content"
|
||||||
:selected="condition.field_uuid === f.uuid"
|
:selected="condition.field_uuid === f.uuid"
|
||||||
>
|
>
|
||||||
{{ f.name || buildDefaultName(f, template.fields) }}
|
{{ f.name || buildDefaultName(f) }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<select
|
<select
|
||||||
@@ -124,6 +124,16 @@
|
|||||||
{{ option.value || `${t('option')} ${index + 1}` }}
|
{{ option.value || `${t('option')} ${index + 1}` }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
|
<input
|
||||||
|
v-else-if="conditionField(condition)?.type === 'number' && ['equal', 'not_equal', 'greater_than', 'less_than'].includes(condition.action)"
|
||||||
|
v-model="condition.value"
|
||||||
|
type="number"
|
||||||
|
step="any"
|
||||||
|
class="input input-bordered input-sm w-full bg-white h-11 pl-4 text-base font-normal"
|
||||||
|
:class="{ 'text-gray-300': !condition.value }"
|
||||||
|
:placeholder="t('type_value')"
|
||||||
|
required
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<a
|
<a
|
||||||
@@ -154,7 +164,7 @@
|
|||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
name: 'ConditionModal',
|
name: 'ConditionModal',
|
||||||
inject: ['t', 'save', 'template', 'withConditions'],
|
inject: ['t', 'template', 'withConditions'],
|
||||||
props: {
|
props: {
|
||||||
item: {
|
item: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -169,18 +179,13 @@ export default {
|
|||||||
type: Function,
|
type: Function,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
withClickSaveEvent: {
|
|
||||||
type: Boolean,
|
|
||||||
required: false,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
excludeFieldUuids: {
|
excludeFieldUuids: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: false,
|
required: false,
|
||||||
default: () => []
|
default: () => []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['close', 'click-save'],
|
emits: ['close', 'save'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
conditions: this.item.conditions?.[0] ? JSON.parse(JSON.stringify(this.item.conditions)) : [{}]
|
conditions: this.item.conditions?.[0] ? JSON.parse(JSON.stringify(this.item.conditions)) : [{}]
|
||||||
@@ -227,6 +232,8 @@ export default {
|
|||||||
actions.push('equal', 'not_equal')
|
actions.push('equal', 'not_equal')
|
||||||
} else if (['multiple'].includes(field.type)) {
|
} else if (['multiple'].includes(field.type)) {
|
||||||
actions.push('contains', 'does_not_contain')
|
actions.push('contains', 'does_not_contain')
|
||||||
|
} else if (field.type === 'number') {
|
||||||
|
actions.push('not_empty', 'empty', 'equal', 'not_equal', 'greater_than', 'less_than')
|
||||||
} else {
|
} else {
|
||||||
actions.push('not_empty', 'empty')
|
actions.push('not_empty', 'empty')
|
||||||
}
|
}
|
||||||
@@ -244,12 +251,7 @@ export default {
|
|||||||
delete this.item.conditions
|
delete this.item.conditions
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.withClickSaveEvent) {
|
this.$emit('save')
|
||||||
this.$emit('click-save')
|
|
||||||
} else {
|
|
||||||
this.save()
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$emit('close')
|
this.$emit('close')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -179,8 +179,11 @@ export default {
|
|||||||
},
|
},
|
||||||
onBlur (e) {
|
onBlur (e) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.value = this.$refs.contenteditable.innerText.trim() || this.modelValue
|
if (this.$refs.contenteditable) {
|
||||||
this.$emit('update:model-value', this.value)
|
this.value = this.$refs.contenteditable.innerText.trim() || this.modelValue
|
||||||
|
this.$emit('update:model-value', this.value)
|
||||||
|
}
|
||||||
|
|
||||||
this.$emit('blur', e)
|
this.$emit('blur', e)
|
||||||
|
|
||||||
this.isEditable = false
|
this.isEditable = false
|
||||||
|
|||||||
@@ -1,546 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
v-if="!isShowFormulaModal && !isShowFontModal && !isShowConditionsModal && !isShowDescriptionModal"
|
|
||||||
ref="menu"
|
|
||||||
class="fixed z-50 p-1 bg-white shadow-lg rounded-lg border border-base-300 min-w-[170px] cursor-default"
|
|
||||||
:style="menuStyle"
|
|
||||||
@mousedown.stop
|
|
||||||
@pointerdown.stop
|
|
||||||
>
|
|
||||||
<label
|
|
||||||
v-if="showRequired"
|
|
||||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm cursor-pointer"
|
|
||||||
@click.stop
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
:checked="isRequired"
|
|
||||||
type="checkbox"
|
|
||||||
class="toggle toggle-xs"
|
|
||||||
@change="handleToggleRequired($event.target.checked)"
|
|
||||||
@click.stop
|
|
||||||
>
|
|
||||||
<span>{{ t('required') }}</span>
|
|
||||||
</label>
|
|
||||||
<label
|
|
||||||
v-if="showReadOnly"
|
|
||||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm cursor-pointer"
|
|
||||||
@click.stop
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
:checked="isReadOnly"
|
|
||||||
type="checkbox"
|
|
||||||
class="toggle toggle-xs"
|
|
||||||
@change="handleToggleReadOnly($event.target.checked)"
|
|
||||||
@click.stop
|
|
||||||
>
|
|
||||||
<span>{{ t('read_only') }}</span>
|
|
||||||
</label>
|
|
||||||
<hr
|
|
||||||
v-if="(showRequired || showReadOnly) && (showFont || showDescription || showCondition || showFormula)"
|
|
||||||
class="my-1 border-base-300"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
v-if="showFont && !isMultiSelection"
|
|
||||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm"
|
|
||||||
@click.stop="openFontModal"
|
|
||||||
>
|
|
||||||
<IconTypography class="w-4 h-4" />
|
|
||||||
<span>{{ t('font') }}</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-if="showDescription"
|
|
||||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm"
|
|
||||||
@click.stop="openDescriptionModal"
|
|
||||||
>
|
|
||||||
<IconInfoCircle class="w-4 h-4" />
|
|
||||||
<span>{{ t('description') }}</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-if="showCondition && !isMultiSelection"
|
|
||||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm"
|
|
||||||
@click.stop="openConditionModal"
|
|
||||||
>
|
|
||||||
<IconRouteAltLeft class="w-4 h-4" />
|
|
||||||
<span>{{ t('condition') }}</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-if="showFormula"
|
|
||||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm"
|
|
||||||
@click.stop="openFormulaModal"
|
|
||||||
>
|
|
||||||
<IconMathFunction class="w-4 h-4" />
|
|
||||||
<span>{{ t('formula') }}</span>
|
|
||||||
</button>
|
|
||||||
<hr
|
|
||||||
v-if="((showFont && !isMultiSelection) || showDescription || (showCondition && !isMultiSelection) || showFormula) && (showCopy || showDelete || showPaste)"
|
|
||||||
class="my-1 border-base-300"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
v-if="isMultiSelection"
|
|
||||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm"
|
|
||||||
@click.stop="$emit('align', 'left')"
|
|
||||||
>
|
|
||||||
<IconLayoutAlignLeft class="w-4 h-4" />
|
|
||||||
<span>{{ t('align_left') }}</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-if="isMultiSelection"
|
|
||||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm"
|
|
||||||
@click.stop="$emit('align', 'right')"
|
|
||||||
>
|
|
||||||
<IconLayoutAlignRight class="w-4 h-4" />
|
|
||||||
<span>{{ t('align_right') }}</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-if="isMultiSelection"
|
|
||||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm"
|
|
||||||
@click.stop="$emit('align', 'top')"
|
|
||||||
>
|
|
||||||
<IconLayoutAlignTop class="w-4 h-4" />
|
|
||||||
<span>{{ t('align_top') }}</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-if="isMultiSelection"
|
|
||||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm"
|
|
||||||
@click.stop="$emit('align', 'bottom')"
|
|
||||||
>
|
|
||||||
<IconLayoutAlignBottom class="w-4 h-4" />
|
|
||||||
<span>{{ t('align_bottom') }}</span>
|
|
||||||
</button>
|
|
||||||
<hr
|
|
||||||
v-if="isMultiSelection && (showFont || showCondition)"
|
|
||||||
class="my-1 border-base-300"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
v-if="showFont && isMultiSelection"
|
|
||||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm"
|
|
||||||
@click.stop="openFontModal"
|
|
||||||
>
|
|
||||||
<IconTypography class="w-4 h-4" />
|
|
||||||
<span>{{ t('font') }}</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-if="showCondition && isMultiSelection"
|
|
||||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm"
|
|
||||||
@click.stop="openConditionModal"
|
|
||||||
>
|
|
||||||
<IconRouteAltLeft class="w-4 h-4" />
|
|
||||||
<span>{{ t('condition') }}</span>
|
|
||||||
</button>
|
|
||||||
<hr
|
|
||||||
v-if="isMultiSelection"
|
|
||||||
class="my-1 border-base-300"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
v-if="showCopy"
|
|
||||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center justify-between text-sm"
|
|
||||||
@click.stop="$emit('copy')"
|
|
||||||
>
|
|
||||||
<span class="flex items-center space-x-2">
|
|
||||||
<IconCopy class="w-4 h-4" />
|
|
||||||
<span>{{ t('copy') }}</span>
|
|
||||||
</span>
|
|
||||||
<span class="text-xs text-base-content/60 ml-4">{{ isMac ? '⌘C' : 'Ctrl+C' }}</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-if="showDelete"
|
|
||||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center justify-between text-sm text-red-600"
|
|
||||||
@click.stop="$emit('delete')"
|
|
||||||
>
|
|
||||||
<span class="flex items-center space-x-2">
|
|
||||||
<IconTrashX class="w-4 h-4" />
|
|
||||||
<span>{{ t('remove') }}</span>
|
|
||||||
</span>
|
|
||||||
<span class="text-xs text-base-content/60 ml-4">Del</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-if="showPaste"
|
|
||||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center justify-between text-sm"
|
|
||||||
:class="!hasClipboardData ? 'opacity-50 cursor-not-allowed' : 'hover:bg-base-100'"
|
|
||||||
:disabled="!hasClipboardData"
|
|
||||||
@click.stop="!hasClipboardData ? null : $emit('paste')"
|
|
||||||
>
|
|
||||||
<span class="flex items-center space-x-2">
|
|
||||||
<IconClipboard class="w-4 h-4" />
|
|
||||||
<span>{{ t('paste') }}</span>
|
|
||||||
</span>
|
|
||||||
<span class="text-xs text-base-content/60 ml-4">{{ isMac ? '⌘V' : 'Ctrl+V' }}</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-if="showSelectFields"
|
|
||||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center justify-between text-sm"
|
|
||||||
@click.stop="handleToggleSelectMode"
|
|
||||||
>
|
|
||||||
<span class="flex items-center space-x-2">
|
|
||||||
<IconClick
|
|
||||||
v-if="!isSelectModeRef.value"
|
|
||||||
class="w-4 h-4"
|
|
||||||
/>
|
|
||||||
<IconNewSection
|
|
||||||
v-else
|
|
||||||
class="w-4 h-4"
|
|
||||||
/>
|
|
||||||
<span>{{ isSelectModeRef.value ? t('draw_fields') : t('select_fields') }}</span>
|
|
||||||
</span>
|
|
||||||
<span class="text-xs text-base-content/60 ml-4">Tab</span>
|
|
||||||
</button>
|
|
||||||
<hr
|
|
||||||
v-if="showAutodetectFields"
|
|
||||||
class="my-1 border-base-300"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
v-if="showAutodetectFields"
|
|
||||||
class="w-full px-2 py-1 rounded-md hover:bg-base-100 flex items-center space-x-2 text-sm"
|
|
||||||
@click.stop="$emit('autodetect-fields')"
|
|
||||||
>
|
|
||||||
<IconSparkles class="w-4 h-4" />
|
|
||||||
<span>{{ t('autodetect_fields') }}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<Teleport
|
|
||||||
v-if="isShowFormulaModal"
|
|
||||||
:to="modalContainerEl"
|
|
||||||
>
|
|
||||||
<FormulaModal
|
|
||||||
:field="field"
|
|
||||||
:editable="editable"
|
|
||||||
:build-default-name="buildDefaultName"
|
|
||||||
@close="closeModal"
|
|
||||||
/>
|
|
||||||
</Teleport>
|
|
||||||
<Teleport
|
|
||||||
v-if="isShowFontModal"
|
|
||||||
:to="modalContainerEl"
|
|
||||||
>
|
|
||||||
<FontModal
|
|
||||||
:field="multiSelectField || field"
|
|
||||||
:area="contextMenu.area"
|
|
||||||
:editable="editable"
|
|
||||||
:build-default-name="buildDefaultName"
|
|
||||||
:with-click-save-event="isMultiSelection"
|
|
||||||
@click-save="handleSaveMultiSelectFontModal"
|
|
||||||
@close="closeModal"
|
|
||||||
/>
|
|
||||||
</Teleport>
|
|
||||||
<Teleport
|
|
||||||
v-if="isShowConditionsModal"
|
|
||||||
:to="modalContainerEl"
|
|
||||||
>
|
|
||||||
<ConditionsModal
|
|
||||||
:item="multiSelectField || field"
|
|
||||||
:build-default-name="buildDefaultName"
|
|
||||||
:exclude-field-uuids="isMultiSelection ? selectedFields.map(f => f.uuid) : []"
|
|
||||||
:with-click-save-event="isMultiSelection"
|
|
||||||
@click-save="handleSaveMultiSelectConditionsModal"
|
|
||||||
@close="closeModal"
|
|
||||||
/>
|
|
||||||
</Teleport>
|
|
||||||
<Teleport
|
|
||||||
v-if="isShowDescriptionModal"
|
|
||||||
:to="modalContainerEl"
|
|
||||||
>
|
|
||||||
<DescriptionModal
|
|
||||||
:field="field"
|
|
||||||
:editable="editable"
|
|
||||||
:build-default-name="buildDefaultName"
|
|
||||||
@close="closeModal"
|
|
||||||
/>
|
|
||||||
</Teleport>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { IconCopy, IconClipboard, IconTrashX, IconTypography, IconInfoCircle, IconRouteAltLeft, IconMathFunction, IconClick, IconNewSection, IconLayoutAlignLeft, IconLayoutAlignRight, IconLayoutAlignTop, IconLayoutAlignBottom, IconSparkles } from '@tabler/icons-vue'
|
|
||||||
import FormulaModal from './formula_modal'
|
|
||||||
import FontModal from './font_modal'
|
|
||||||
import ConditionsModal from './conditions_modal'
|
|
||||||
import DescriptionModal from './description_modal'
|
|
||||||
import Field from './field'
|
|
||||||
import FieldType from './field_type.vue'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'ContextMenu',
|
|
||||||
components: {
|
|
||||||
IconCopy,
|
|
||||||
IconClipboard,
|
|
||||||
IconTrashX,
|
|
||||||
IconTypography,
|
|
||||||
IconInfoCircle,
|
|
||||||
IconRouteAltLeft,
|
|
||||||
IconMathFunction,
|
|
||||||
IconClick,
|
|
||||||
IconNewSection,
|
|
||||||
IconLayoutAlignLeft,
|
|
||||||
IconLayoutAlignRight,
|
|
||||||
IconLayoutAlignTop,
|
|
||||||
IconLayoutAlignBottom,
|
|
||||||
IconSparkles,
|
|
||||||
FormulaModal,
|
|
||||||
FontModal,
|
|
||||||
ConditionsModal,
|
|
||||||
DescriptionModal
|
|
||||||
},
|
|
||||||
inject: ['t', 'save', 'selectedAreasRef', 'isSelectModeRef'],
|
|
||||||
props: {
|
|
||||||
contextMenu: {
|
|
||||||
type: Object,
|
|
||||||
default: null,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
field: {
|
|
||||||
type: Object,
|
|
||||||
default: null
|
|
||||||
},
|
|
||||||
editable: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true
|
|
||||||
},
|
|
||||||
isMultiSelection: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
selectedAreas: {
|
|
||||||
type: Array,
|
|
||||||
default: () => []
|
|
||||||
},
|
|
||||||
template: {
|
|
||||||
type: Object,
|
|
||||||
default: null
|
|
||||||
},
|
|
||||||
withFieldsDetection: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
emits: ['copy', 'paste', 'delete', 'close', 'align', 'autodetect-fields'],
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
isShowFormulaModal: false,
|
|
||||||
isShowFontModal: false,
|
|
||||||
isShowConditionsModal: false,
|
|
||||||
isShowDescriptionModal: false,
|
|
||||||
multiSelectField: null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
fieldNames: FieldType.computed.fieldNames,
|
|
||||||
fieldLabels: FieldType.computed.fieldLabels,
|
|
||||||
modalContainerEl () {
|
|
||||||
return this.$el.getRootNode().querySelector('#docuseal_modal_container')
|
|
||||||
},
|
|
||||||
selectedFields () {
|
|
||||||
if (!this.isMultiSelection) return []
|
|
||||||
|
|
||||||
return this.selectedAreasRef.value.map((area) => {
|
|
||||||
return this.template.fields.find((f) => f.areas?.includes(area))
|
|
||||||
}).filter(Boolean)
|
|
||||||
},
|
|
||||||
isMac () {
|
|
||||||
return (navigator.userAgentData?.platform || navigator.platform)?.toLowerCase()?.includes('mac')
|
|
||||||
},
|
|
||||||
hasClipboardData () {
|
|
||||||
try {
|
|
||||||
const clipboard = localStorage.getItem('docuseal_clipboard')
|
|
||||||
|
|
||||||
if (clipboard) {
|
|
||||||
const data = JSON.parse(clipboard)
|
|
||||||
|
|
||||||
return Date.now() - data.timestamp < 3600000
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
} catch {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
menuStyle () {
|
|
||||||
return {
|
|
||||||
left: this.contextMenu.x + 'px',
|
|
||||||
top: this.contextMenu.y + 'px'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
showCopy () {
|
|
||||||
return !!this.contextMenu.area || this.isMultiSelection
|
|
||||||
},
|
|
||||||
showPaste () {
|
|
||||||
return !this.contextMenu.area && !this.isMultiSelection
|
|
||||||
},
|
|
||||||
showDelete () {
|
|
||||||
return !!this.contextMenu.area || this.isMultiSelection
|
|
||||||
},
|
|
||||||
showFont () {
|
|
||||||
if (this.isMultiSelection) return true
|
|
||||||
if (!this.field) return false
|
|
||||||
|
|
||||||
return ['text', 'number', 'date', 'select', 'heading'].includes(this.field.type)
|
|
||||||
},
|
|
||||||
showDescription () {
|
|
||||||
if (!this.field) return false
|
|
||||||
|
|
||||||
return !['stamp', 'heading', 'strikethrough'].includes(this.field.type)
|
|
||||||
},
|
|
||||||
showCondition () {
|
|
||||||
if (this.isMultiSelection) return true
|
|
||||||
if (!this.field) return false
|
|
||||||
|
|
||||||
return !['stamp', 'heading'].includes(this.field.type)
|
|
||||||
},
|
|
||||||
showFormula () {
|
|
||||||
if (!this.field) return false
|
|
||||||
|
|
||||||
return this.field.type === 'number'
|
|
||||||
},
|
|
||||||
showRequired () {
|
|
||||||
if (!this.field) return false
|
|
||||||
|
|
||||||
return !['phone', 'stamp', 'verification', 'strikethrough', 'heading'].includes(this.field.type)
|
|
||||||
},
|
|
||||||
showReadOnly () {
|
|
||||||
if (!this.field) return false
|
|
||||||
|
|
||||||
return ['text', 'number'].includes(this.field.type)
|
|
||||||
},
|
|
||||||
isRequired () {
|
|
||||||
return this.field?.required || false
|
|
||||||
},
|
|
||||||
isReadOnly () {
|
|
||||||
return this.field?.readonly || false
|
|
||||||
},
|
|
||||||
showSelectFields () {
|
|
||||||
return !this.contextMenu.area && !this.isMultiSelection
|
|
||||||
},
|
|
||||||
showAutodetectFields () {
|
|
||||||
return this.withFieldsDetection && this.editable && !this.contextMenu.area && !this.isMultiSelection
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted () {
|
|
||||||
document.addEventListener('keydown', this.onKeyDown)
|
|
||||||
document.addEventListener('mousedown', this.handleClickOutside)
|
|
||||||
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.checkMenuPosition()
|
|
||||||
})
|
|
||||||
},
|
|
||||||
beforeUnmount () {
|
|
||||||
document.removeEventListener('keydown', this.onKeyDown)
|
|
||||||
document.removeEventListener('mousedown', this.handleClickOutside)
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
buildDefaultName: Field.methods.buildDefaultName,
|
|
||||||
checkMenuPosition () {
|
|
||||||
if (this.$refs.menu) {
|
|
||||||
const rect = this.$refs.menu.getBoundingClientRect()
|
|
||||||
|
|
||||||
if (rect.bottom > window.innerHeight) {
|
|
||||||
this.contextMenu.y = this.contextMenu.y - rect.height
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rect.right > window.innerWidth) {
|
|
||||||
this.contextMenu.x = this.contextMenu.x - rect.width
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
handleToggleRequired (value) {
|
|
||||||
if (this.field) {
|
|
||||||
this.field.required = value
|
|
||||||
this.save()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
handleToggleReadOnly (value) {
|
|
||||||
if (this.field) {
|
|
||||||
this.field.readonly = value
|
|
||||||
this.save()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onKeyDown (event) {
|
|
||||||
if (event.key === 'Escape') {
|
|
||||||
event.preventDefault()
|
|
||||||
event.stopPropagation()
|
|
||||||
|
|
||||||
this.$emit('close')
|
|
||||||
} else if ((event.ctrlKey || event.metaKey) && event.key === 'v' && this.hasClipboardData) {
|
|
||||||
event.preventDefault()
|
|
||||||
event.stopPropagation()
|
|
||||||
|
|
||||||
this.$emit('paste')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
handleClickOutside (event) {
|
|
||||||
if (this.$refs.menu && !this.$refs.menu.contains(event.target)) {
|
|
||||||
this.$emit('close')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
openFontModal () {
|
|
||||||
if (this.isMultiSelection) {
|
|
||||||
this.multiSelectField = {
|
|
||||||
name: this.t('fields_selected').replace('{count}', this.selectedFields.length),
|
|
||||||
preferences: {}
|
|
||||||
}
|
|
||||||
|
|
||||||
const preferencesStrings = this.selectedFields.map((f) => JSON.stringify(f.preferences || {}))
|
|
||||||
|
|
||||||
if (preferencesStrings.every((s) => s === preferencesStrings[0])) {
|
|
||||||
this.multiSelectField.preferences = JSON.parse(preferencesStrings[0])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.isShowFontModal = true
|
|
||||||
},
|
|
||||||
openDescriptionModal () {
|
|
||||||
this.isShowDescriptionModal = true
|
|
||||||
},
|
|
||||||
openConditionModal () {
|
|
||||||
if (this.isMultiSelection) {
|
|
||||||
this.multiSelectField = {
|
|
||||||
name: this.t('fields_selected').replace('{count}', this.selectedFields.length),
|
|
||||||
conditions: []
|
|
||||||
}
|
|
||||||
|
|
||||||
const conditionStrings = this.selectedFields.map((f) => JSON.stringify(f.conditions || []))
|
|
||||||
|
|
||||||
if (conditionStrings.every((s) => s === conditionStrings[0])) {
|
|
||||||
this.multiSelectField.conditions = JSON.parse(conditionStrings[0])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.isShowConditionsModal = true
|
|
||||||
},
|
|
||||||
openFormulaModal () {
|
|
||||||
this.isShowFormulaModal = true
|
|
||||||
},
|
|
||||||
closeModal () {
|
|
||||||
this.isShowFormulaModal = false
|
|
||||||
this.isShowFontModal = false
|
|
||||||
this.isShowConditionsModal = false
|
|
||||||
this.isShowDescriptionModal = false
|
|
||||||
this.multiSelectField = null
|
|
||||||
|
|
||||||
this.$emit('close')
|
|
||||||
},
|
|
||||||
handleSaveMultiSelectFontModal () {
|
|
||||||
this.selectedFields.forEach((field) => {
|
|
||||||
field.preferences = { ...field.preferences, ...this.multiSelectField.preferences }
|
|
||||||
})
|
|
||||||
|
|
||||||
this.save()
|
|
||||||
|
|
||||||
this.closeModal()
|
|
||||||
},
|
|
||||||
handleSaveMultiSelectConditionsModal () {
|
|
||||||
this.selectedFields.forEach((field) => {
|
|
||||||
field.conditions = JSON.parse(JSON.stringify(this.multiSelectField.conditions))
|
|
||||||
})
|
|
||||||
|
|
||||||
this.save()
|
|
||||||
|
|
||||||
this.closeModal()
|
|
||||||
},
|
|
||||||
handleToggleSelectMode () {
|
|
||||||
this.isSelectModeRef.value = !this.isSelectModeRef.value
|
|
||||||
|
|
||||||
this.$emit('close')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -125,6 +125,7 @@
|
|||||||
:field="field"
|
:field="field"
|
||||||
:default-field="defaultField"
|
:default-field="defaultField"
|
||||||
:build-default-name="buildDefaultName"
|
:build-default-name="buildDefaultName"
|
||||||
|
@save="$emit('save')"
|
||||||
@close="isShowFormulaModal = false"
|
@close="isShowFormulaModal = false"
|
||||||
/>
|
/>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
@@ -136,6 +137,7 @@
|
|||||||
:field="field"
|
:field="field"
|
||||||
:default-field="defaultField"
|
:default-field="defaultField"
|
||||||
:build-default-name="buildDefaultName"
|
:build-default-name="buildDefaultName"
|
||||||
|
@save="$emit('save')"
|
||||||
@close="isShowFontModal = false"
|
@close="isShowFontModal = false"
|
||||||
/>
|
/>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
@@ -147,6 +149,7 @@
|
|||||||
:field="field"
|
:field="field"
|
||||||
:default-field="defaultField"
|
:default-field="defaultField"
|
||||||
:build-default-name="buildDefaultName"
|
:build-default-name="buildDefaultName"
|
||||||
|
@save="$emit('save')"
|
||||||
@close="isShowDescriptionModal = false"
|
@close="isShowDescriptionModal = false"
|
||||||
/>
|
/>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl">
|
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl">
|
||||||
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
|
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
|
||||||
<span class="modal-title">
|
<span class="modal-title">
|
||||||
{{ (defaultField ? (defaultField.title || field.title || field.name) : field.name) || buildDefaultName(field, template.fields) }}
|
{{ (defaultField ? (defaultField.title || field.title || field.name) : field.name) || buildDefaultName(field) }}
|
||||||
</span>
|
</span>
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
@@ -67,7 +67,7 @@
|
|||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
name: 'DescriptionModal',
|
name: 'DescriptionModal',
|
||||||
inject: ['t', 'save', 'template'],
|
inject: ['t', 'template'],
|
||||||
props: {
|
props: {
|
||||||
field: {
|
field: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -88,7 +88,7 @@ export default {
|
|||||||
required: true
|
required: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['close'],
|
emits: ['close', 'save'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
description: this.field.description,
|
description: this.field.description,
|
||||||
@@ -103,7 +103,7 @@ export default {
|
|||||||
this.field.description = this.description
|
this.field.description = this.description
|
||||||
this.field.title = this.title
|
this.field.title = this.title
|
||||||
|
|
||||||
this.save()
|
this.$emit('save')
|
||||||
this.$emit('close')
|
this.$emit('close')
|
||||||
},
|
},
|
||||||
resizeTextarea () {
|
resizeTextarea () {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
:with-signature-id="withSignatureId"
|
:with-signature-id="withSignatureId"
|
||||||
:with-prefillable="withPrefillable"
|
:with-prefillable="withPrefillable"
|
||||||
:is-drag="isDrag"
|
:is-drag="isDrag"
|
||||||
|
:is-mobile="isMobile"
|
||||||
:with-field-placeholder="withFieldPlaceholder"
|
:with-field-placeholder="withFieldPlaceholder"
|
||||||
:default-fields="defaultFields"
|
:default-fields="defaultFields"
|
||||||
:drag-field-placeholder="dragFieldPlaceholder"
|
:drag-field-placeholder="dragFieldPlaceholder"
|
||||||
@@ -30,9 +31,9 @@
|
|||||||
@copy-field="$emit('copy-field', $event)"
|
@copy-field="$emit('copy-field', $event)"
|
||||||
@paste-field="$emit('paste-field', { ...$event, attachment_uuid: document.uuid })"
|
@paste-field="$emit('paste-field', { ...$event, attachment_uuid: document.uuid })"
|
||||||
@add-custom-field="$emit('add-custom-field', $event)"
|
@add-custom-field="$emit('add-custom-field', $event)"
|
||||||
|
@set-draw="$emit('set-draw', $event)"
|
||||||
@copy-selected-areas="$emit('copy-selected-areas')"
|
@copy-selected-areas="$emit('copy-selected-areas')"
|
||||||
@delete-selected-areas="$emit('delete-selected-areas')"
|
@delete-selected-areas="$emit('delete-selected-areas')"
|
||||||
@align-selected-areas="$emit('align-selected-areas', $event)"
|
|
||||||
@autodetect-fields="$emit('autodetect-fields', $event)"
|
@autodetect-fields="$emit('autodetect-fields', $event)"
|
||||||
@scroll-to="scrollToArea"
|
@scroll-to="scrollToArea"
|
||||||
@draw="$emit('draw', { area: {...$event.area, attachment_uuid: document.uuid }, isTooSmall: $event.isTooSmall })"
|
@draw="$emit('draw', { area: {...$event.area, attachment_uuid: document.uuid }, isTooSmall: $event.isTooSmall })"
|
||||||
@@ -98,6 +99,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
|
isMobile: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
allowDraw: {
|
allowDraw: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -138,7 +144,7 @@ export default {
|
|||||||
default: false
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['draw', 'drop-field', 'remove-area', 'paste-field', 'copy-field', 'copy-selected-areas', 'delete-selected-areas', 'align-selected-areas', 'autodetect-fields', 'add-custom-field'],
|
emits: ['draw', 'drop-field', 'remove-area', 'paste-field', 'copy-field', 'copy-selected-areas', 'delete-selected-areas', 'autodetect-fields', 'add-custom-field', 'set-draw'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
pageRefs: []
|
pageRefs: []
|
||||||
@@ -180,7 +186,15 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
scrollToArea (area) {
|
scrollToArea (area) {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.pageRefs[area.page].areaRefs.find((e) => e.area === area).$el.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
const pageRef = this.pageRefs[area.page]
|
||||||
|
|
||||||
|
if (pageRef && pageRef.areaRefs) {
|
||||||
|
const areaRef = pageRef.areaRefs.find((e) => e.area === area)
|
||||||
|
|
||||||
|
if (areaRef && areaRef.$el) {
|
||||||
|
areaRef.$el.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
setPageRefs (el) {
|
setPageRefs (el) {
|
||||||
|
|||||||
@@ -269,6 +269,7 @@
|
|||||||
:editable="editable && !defaultField"
|
:editable="editable && !defaultField"
|
||||||
:default-field="defaultField"
|
:default-field="defaultField"
|
||||||
:build-default-name="buildDefaultName"
|
:build-default-name="buildDefaultName"
|
||||||
|
@save="$emit('save')"
|
||||||
@close="isShowFormulaModal = false"
|
@close="isShowFormulaModal = false"
|
||||||
/>
|
/>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
@@ -281,6 +282,7 @@
|
|||||||
:editable="editable && !defaultField"
|
:editable="editable && !defaultField"
|
||||||
:default-field="defaultField"
|
:default-field="defaultField"
|
||||||
:build-default-name="buildDefaultName"
|
:build-default-name="buildDefaultName"
|
||||||
|
@save="$emit('save')"
|
||||||
@close="isShowFontModal = false"
|
@close="isShowFontModal = false"
|
||||||
/>
|
/>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
@@ -292,6 +294,7 @@
|
|||||||
:item="field"
|
:item="field"
|
||||||
:default-field="defaultField"
|
:default-field="defaultField"
|
||||||
:build-default-name="buildDefaultName"
|
:build-default-name="buildDefaultName"
|
||||||
|
@save="$emit('save')"
|
||||||
@close="isShowConditionsModal = false"
|
@close="isShowConditionsModal = false"
|
||||||
/>
|
/>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
@@ -304,6 +307,7 @@
|
|||||||
:editable="editable && !defaultField"
|
:editable="editable && !defaultField"
|
||||||
:default-field="defaultField"
|
:default-field="defaultField"
|
||||||
:build-default-name="buildDefaultName"
|
:build-default-name="buildDefaultName"
|
||||||
|
@save="$emit('save')"
|
||||||
@close="isShowDescriptionModal = false"
|
@close="isShowDescriptionModal = false"
|
||||||
/>
|
/>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
@@ -340,7 +344,7 @@ export default {
|
|||||||
IconMathFunction,
|
IconMathFunction,
|
||||||
FieldType
|
FieldType
|
||||||
},
|
},
|
||||||
inject: ['template', 'backgroundColor', 'selectedAreasRef', 't', 'locale'],
|
inject: ['template', 'backgroundColor', 'selectedAreasRef', 't', 'locale', 'getFieldTypeIndex'],
|
||||||
props: {
|
props: {
|
||||||
field: {
|
field: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -412,7 +416,7 @@ export default {
|
|||||||
return this.$el.getRootNode().querySelector('#docuseal_modal_container')
|
return this.$el.getRootNode().querySelector('#docuseal_modal_container')
|
||||||
},
|
},
|
||||||
defaultName () {
|
defaultName () {
|
||||||
return this.buildDefaultName(this.field, this.template.fields)
|
return this.buildDefaultName(this.field)
|
||||||
},
|
},
|
||||||
areas () {
|
areas () {
|
||||||
return this.field.areas || []
|
return this.field.areas || []
|
||||||
@@ -432,7 +436,7 @@ export default {
|
|||||||
|
|
||||||
this.$emit('save')
|
this.$emit('save')
|
||||||
},
|
},
|
||||||
buildDefaultName (field, fields) {
|
buildDefaultName (field) {
|
||||||
if (field.type === 'payment' && field.preferences?.price && !field.preferences?.formula) {
|
if (field.type === 'payment' && field.preferences?.price && !field.preferences?.formula) {
|
||||||
const { price, currency } = field.preferences || {}
|
const { price, currency } = field.preferences || {}
|
||||||
|
|
||||||
@@ -443,7 +447,7 @@ export default {
|
|||||||
|
|
||||||
return `${this.fieldNames[field.type]} ${formattedPrice}`
|
return `${this.fieldNames[field.type]} ${formattedPrice}`
|
||||||
} else {
|
} else {
|
||||||
const typeIndex = fields.filter((f) => f.type === field.type).indexOf(field)
|
const typeIndex = this.getFieldTypeIndex(field)
|
||||||
|
|
||||||
if (field.type === 'heading' || field.type === 'strikethrough') {
|
if (field.type === 'heading' || field.type === 'strikethrough') {
|
||||||
return `${this.fieldNames[field.type]} ${typeIndex + 1}`
|
return `${this.fieldNames[field.type]} ${typeIndex + 1}`
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<template>
|
||||||
|
<Teleport :to="modalContainerEl">
|
||||||
|
<div class="modal modal-open items-start !animate-none overflow-y-auto">
|
||||||
|
<div
|
||||||
|
class="absolute top-0 bottom-0 right-0 left-0"
|
||||||
|
@click.prevent="$emit('close')"
|
||||||
|
/>
|
||||||
|
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl">
|
||||||
|
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
|
||||||
|
<span class="modal-title">
|
||||||
|
{{ title }}
|
||||||
|
</span>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="text-xl modal-close-button"
|
||||||
|
@click.prevent="$emit('close')"
|
||||||
|
>×</a>
|
||||||
|
</div>
|
||||||
|
<form @submit.prevent="$emit('save')">
|
||||||
|
<slot />
|
||||||
|
<button
|
||||||
|
class="base-button w-full mt-4 modal-save-button"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
{{ t('save') }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'ContextModal',
|
||||||
|
inject: ['t'],
|
||||||
|
props: {
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
modalContainerEl: {
|
||||||
|
type: Element,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['close', 'save']
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="relative"
|
||||||
|
@mouseenter="handleMouseEnter"
|
||||||
|
@mouseleave="handleMouseLeave"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center justify-between text-sm"
|
||||||
|
@click.stop="isOpen ? close() : open()"
|
||||||
|
>
|
||||||
|
<span class="flex items-center space-x-2">
|
||||||
|
<component
|
||||||
|
:is="icon || 'span'"
|
||||||
|
class="w-4 h-4"
|
||||||
|
/>
|
||||||
|
<span>{{ label }}</span>
|
||||||
|
</span>
|
||||||
|
<IconChevronRight class="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
v-if="isOpen"
|
||||||
|
ref="submenu"
|
||||||
|
class="absolute p-1 z-50 left-full bg-white shadow-lg rounded-lg border border-neutral-200 cursor-default"
|
||||||
|
style="min-width: 170px"
|
||||||
|
:style="submenuStyle"
|
||||||
|
:class="menuClass"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<slot>
|
||||||
|
<button
|
||||||
|
v-for="option in options"
|
||||||
|
:key="option.value"
|
||||||
|
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center justify-between space-x-2 text-sm cursor-pointer"
|
||||||
|
@click="handleSelect(option.value)"
|
||||||
|
>
|
||||||
|
<span class="whitespace-nowrap">{{ option.label }}</span>
|
||||||
|
<IconCheck
|
||||||
|
v-if="modelValue === option.value"
|
||||||
|
class="w-4 h-4"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { IconChevronRight, IconCheck } from '@tabler/icons-vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ContextSubmenu',
|
||||||
|
components: {
|
||||||
|
IconChevronRight,
|
||||||
|
IconCheck
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
icon: {
|
||||||
|
type: [Function],
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
modelValue: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
menuClass: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['select', 'update:modelValue'],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
isOpen: false,
|
||||||
|
topOffset: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
submenuStyle () {
|
||||||
|
return {
|
||||||
|
top: this.topOffset + 'px'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeUnmount () {
|
||||||
|
this.clearTimeout()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleMouseEnter () {
|
||||||
|
clearTimeout(this.closeTimeout)
|
||||||
|
|
||||||
|
this.openTimeout = setTimeout(() => this.open(), 200)
|
||||||
|
},
|
||||||
|
handleMouseLeave () {
|
||||||
|
clearTimeout(this.openTimeout)
|
||||||
|
|
||||||
|
this.closeTimeout = setTimeout(() => this.close(), 200)
|
||||||
|
},
|
||||||
|
open () {
|
||||||
|
this.clearTimeout()
|
||||||
|
|
||||||
|
this.isOpen = true
|
||||||
|
this.topOffset = 0
|
||||||
|
|
||||||
|
this.$nextTick(() => setTimeout(() => this.adjustPosition(), 0))
|
||||||
|
},
|
||||||
|
clearTimeout () {
|
||||||
|
if (this.openTimeout) {
|
||||||
|
clearTimeout(this.openTimeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.closeTimeout) {
|
||||||
|
clearTimeout(this.closeTimeout)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
close () {
|
||||||
|
this.clearTimeout()
|
||||||
|
|
||||||
|
this.isOpen = false
|
||||||
|
},
|
||||||
|
handleSelect (value) {
|
||||||
|
this.$emit('select', value)
|
||||||
|
this.$emit('update:modelValue', value)
|
||||||
|
},
|
||||||
|
adjustPosition () {
|
||||||
|
if (!this.$refs.submenu) return
|
||||||
|
|
||||||
|
const rect = this.$refs.submenu.getBoundingClientRect()
|
||||||
|
const overflow = rect.bottom - window.innerHeight
|
||||||
|
|
||||||
|
if (overflow > 0) {
|
||||||
|
this.topOffset = -overflow - 4
|
||||||
|
} else {
|
||||||
|
this.topOffset = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
@change="[field.preferences ||= {}, field.preferences.method = $event.target.value, $emit('save')]"
|
@change="[field.preferences ||= {}, field.preferences.method = $event.target.value, $emit('save')]"
|
||||||
>
|
>
|
||||||
<option
|
<option
|
||||||
v-for="method in ['QeS', 'AeS']"
|
v-for="method in verificationMethods"
|
||||||
:key="method"
|
:key="method"
|
||||||
:value="method.toLowerCase()"
|
:value="method.toLowerCase()"
|
||||||
:selected="method.toLowerCase() === field.preferences?.method || (method === 'QeS' && !field.preferences?.method)"
|
:selected="method.toLowerCase() === field.preferences?.method || (method === 'QeS' && !field.preferences?.method)"
|
||||||
@@ -26,32 +26,6 @@
|
|||||||
{{ t('method') }}
|
{{ t('method') }}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
v-if="['cells'].includes(field.type)"
|
|
||||||
class="py-1.5 px-1 relative"
|
|
||||||
@click.stop
|
|
||||||
>
|
|
||||||
<select
|
|
||||||
class="select select-bordered select-xs w-full max-w-xs h-7 !outline-0 font-normal bg-transparent"
|
|
||||||
@change="[field.preferences ||= {}, field.preferences.align = $event.target.value, $emit('save')]"
|
|
||||||
>
|
|
||||||
<option
|
|
||||||
v-for="value in ['left', 'right', field.type === 'cells' ? null : 'center'].filter(Boolean)"
|
|
||||||
:key="value"
|
|
||||||
:selected="field.preferences?.align ? value === field.preferences.align : value === 'left'"
|
|
||||||
:value="value"
|
|
||||||
>
|
|
||||||
{{ t(value) }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
<label
|
|
||||||
:style="{ backgroundColor }"
|
|
||||||
class="absolute -top-1 left-2.5 px-1 h-4"
|
|
||||||
style="font-size: 8px"
|
|
||||||
>
|
|
||||||
{{ t('align') }}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
v-if="['select', 'radio'].includes(field.type) && !defaultField"
|
v-if="['select', 'radio'].includes(field.type) && !defaultField"
|
||||||
class="py-1.5 px-1 relative"
|
class="py-1.5 px-1 relative"
|
||||||
@@ -348,7 +322,7 @@
|
|||||||
{{ t('any') }}
|
{{ t('any') }}
|
||||||
</option>
|
</option>
|
||||||
<option
|
<option
|
||||||
v-for="type in ['drawn', 'typed', 'drawn_or_typed', 'drawn_or_upload', 'upload']"
|
v-for="type in signatureFormats"
|
||||||
:key="type"
|
:key="type"
|
||||||
:value="type"
|
:value="type"
|
||||||
:selected="field.preferences?.format === type"
|
:selected="field.preferences?.format === type"
|
||||||
@@ -437,7 +411,7 @@
|
|||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
v-if="['text', 'number'].includes(field.type)"
|
v-if="['text', 'number', 'radio', 'multiple', 'select'].includes(field.type)"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<label class="cursor-pointer py-1.5">
|
<label class="cursor-pointer py-1.5">
|
||||||
@@ -452,7 +426,7 @@
|
|||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
v-if="withPrefillable && ['text', 'number', 'cells', 'date', 'checkbox', 'select', 'radio', 'phone'].includes(field['type'])"
|
v-if="withPrefillable && prefillableFieldTypes.includes(field['type'])"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<label class="cursor-pointer py-1.5">
|
<label class="cursor-pointer py-1.5">
|
||||||
@@ -470,7 +444,7 @@
|
|||||||
v-if="field.type != 'stamp'"
|
v-if="field.type != 'stamp'"
|
||||||
class="pb-0.5 mt-0.5"
|
class="pb-0.5 mt-0.5"
|
||||||
>
|
>
|
||||||
<li v-if="['text', 'number', 'date', 'select', 'heading'].includes(field.type)">
|
<li v-if="['text', 'number', 'date', 'select', 'heading', 'cells'].includes(field.type)">
|
||||||
<label
|
<label
|
||||||
class="label-text cursor-pointer text-center w-full flex items-center"
|
class="label-text cursor-pointer text-center w-full flex items-center"
|
||||||
@click="$emit('click-font')"
|
@click="$emit('click-font')"
|
||||||
@@ -718,7 +692,7 @@ export default {
|
|||||||
},
|
},
|
||||||
lengthValidation () {
|
lengthValidation () {
|
||||||
if (this.field.validation?.pattern && this.selectedValidation !== 'custom') {
|
if (this.field.validation?.pattern && this.selectedValidation !== 'custom') {
|
||||||
return this.field.validation.pattern.match(/^\.{(?<min>\d+),(?<max>\d+)?}$/)?.groups
|
return this.parseLengthPattern(this.field.validation.pattern)
|
||||||
} else {
|
} else {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -735,6 +709,15 @@ export default {
|
|||||||
'^[a-zA-Z]+$': 'letters_only'
|
'^[a-zA-Z]+$': 'letters_only'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
signatureFormats () {
|
||||||
|
return ['drawn', 'typed', 'drawn_or_typed', 'drawn_or_upload', 'upload']
|
||||||
|
},
|
||||||
|
verificationMethods () {
|
||||||
|
return ['QeS', 'AeS']
|
||||||
|
},
|
||||||
|
prefillableFieldTypes () {
|
||||||
|
return ['text', 'number', 'cells', 'date', 'checkbox', 'select', 'radio', 'phone']
|
||||||
|
},
|
||||||
sortedAreas () {
|
sortedAreas () {
|
||||||
return (this.field.areas || []).sort((a, b) => {
|
return (this.field.areas || []).sort((a, b) => {
|
||||||
return this.schemaAttachmentsIndexes[a.attachment_uuid] - this.schemaAttachmentsIndexes[b.attachment_uuid]
|
return this.schemaAttachmentsIndexes[a.attachment_uuid] - this.schemaAttachmentsIndexes[b.attachment_uuid]
|
||||||
@@ -798,6 +781,9 @@ export default {
|
|||||||
return number
|
return number
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
parseLengthPattern (pattern) {
|
||||||
|
return pattern?.match(/^\.{(?<min>\d+),(?<max>\d+)?}$/)?.groups || null
|
||||||
|
},
|
||||||
formatDate (date, format) {
|
formatDate (date, format) {
|
||||||
const monthFormats = {
|
const monthFormats = {
|
||||||
M: 'numeric',
|
M: 'numeric',
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
:key="field.uuid"
|
:key="field.uuid"
|
||||||
:data-uuid="field.uuid"
|
:data-uuid="field.uuid"
|
||||||
:field="field"
|
:field="field"
|
||||||
:type-index="fields.filter((f) => f.type === field.type).indexOf(field)"
|
:type-index="getFieldTypeIndex(field)"
|
||||||
:editable="editable"
|
:editable="editable"
|
||||||
:with-signature-id="withSignatureId"
|
:with-signature-id="withSignatureId"
|
||||||
:with-prefillable="withPrefillable"
|
:with-prefillable="withPrefillable"
|
||||||
@@ -127,7 +127,7 @@
|
|||||||
>{{ t('custom') }}</a>
|
>{{ t('custom') }}</a>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="showCustomTab && editable"
|
v-if="showCustomTab && editable && (customFields.length || newCustomField)"
|
||||||
ref="customFields"
|
ref="customFields"
|
||||||
class="custom-fields"
|
class="custom-fields"
|
||||||
@dragover.prevent="onCustomFieldDragover"
|
@dragover.prevent="onCustomFieldDragover"
|
||||||
@@ -376,7 +376,7 @@ export default {
|
|||||||
IconDrag,
|
IconDrag,
|
||||||
IconLock
|
IconLock
|
||||||
},
|
},
|
||||||
inject: ['save', 'backgroundColor', 'withPhone', 'withVerification', 'withKba', 'withPayment', 't', 'fieldsDragFieldRef', 'customDragFieldRef', 'baseFetch', 'selectedAreasRef'],
|
inject: ['save', 'backgroundColor', 'withPhone', 'withVerification', 'withKba', 'withPayment', 't', 'fieldsDragFieldRef', 'customDragFieldRef', 'baseFetch', 'selectedAreasRef', 'getFieldTypeIndex'],
|
||||||
props: {
|
props: {
|
||||||
fields: {
|
fields: {
|
||||||
type: Array,
|
type: Array,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl">
|
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl">
|
||||||
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
|
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
|
||||||
<span class="modal-title">
|
<span class="modal-title">
|
||||||
{{ t('font') }} - {{ (defaultField ? (defaultField.title || field.title || field.name) : field.name) || buildDefaultName(field, template.fields) }}
|
{{ t('font') }} - {{ (defaultField ? (defaultField.title || field.title || field.name) : field.name) || buildDefaultName(field) }}
|
||||||
</span>
|
</span>
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
@@ -171,7 +171,7 @@
|
|||||||
contenteditable="true"
|
contenteditable="true"
|
||||||
class="outline-none whitespace-nowrap truncate"
|
class="outline-none whitespace-nowrap truncate"
|
||||||
>
|
>
|
||||||
{{ field.default_value || field.name || buildDefaultName(field, template.fields) }}
|
{{ field.default_value || field.name || buildDefaultName(field) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -196,7 +196,7 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
IconChevronDown
|
IconChevronDown
|
||||||
},
|
},
|
||||||
inject: ['t', 'save', 'template'],
|
inject: ['t', 'template'],
|
||||||
props: {
|
props: {
|
||||||
field: {
|
field: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -212,17 +212,12 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
withClickSaveEvent: {
|
|
||||||
type: Boolean,
|
|
||||||
required: false,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
buildDefaultName: {
|
buildDefaultName: {
|
||||||
type: Function,
|
type: Function,
|
||||||
required: true
|
required: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['close', 'click-save'],
|
emits: ['close', 'save'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
preferences: {}
|
preferences: {}
|
||||||
@@ -262,6 +257,7 @@ export default {
|
|||||||
colors () {
|
colors () {
|
||||||
return [
|
return [
|
||||||
{ label: '⬛', value: 'black' },
|
{ label: '⬛', value: 'black' },
|
||||||
|
{ label: '⬜', value: 'white' },
|
||||||
{ label: '🟦', value: 'blue' },
|
{ label: '🟦', value: 'blue' },
|
||||||
{ label: '🟥', value: 'red' }
|
{ label: '🟥', value: 'red' }
|
||||||
]
|
]
|
||||||
@@ -276,6 +272,7 @@ export default {
|
|||||||
'items-center': !this.preferences.valign || this.preferences.valign === 'center',
|
'items-center': !this.preferences.valign || this.preferences.valign === 'center',
|
||||||
'items-start': this.preferences.valign === 'top',
|
'items-start': this.preferences.valign === 'top',
|
||||||
'items-end': this.preferences.valign === 'bottom',
|
'items-end': this.preferences.valign === 'bottom',
|
||||||
|
'bg-black': this.preferences.color === 'white',
|
||||||
'font-bold': ['bold_italic', 'bold'].includes(this.preferences.font_type),
|
'font-bold': ['bold_italic', 'bold'].includes(this.preferences.font_type),
|
||||||
italic: ['bold_italic', 'italic'].includes(this.preferences.font_type)
|
italic: ['bold_italic', 'italic'].includes(this.preferences.font_type)
|
||||||
}
|
}
|
||||||
@@ -327,12 +324,7 @@ export default {
|
|||||||
|
|
||||||
Object.assign(this.field.preferences, this.preferences)
|
Object.assign(this.field.preferences, this.preferences)
|
||||||
|
|
||||||
if (this.withClickSaveEvent) {
|
this.$emit('save')
|
||||||
this.$emit('click-save')
|
|
||||||
} else {
|
|
||||||
this.save()
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$emit('close')
|
this.$emit('close')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl">
|
<div class="modal-box pt-4 pb-6 px-6 mt-20 max-h-none w-full max-w-xl">
|
||||||
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
|
<div class="flex justify-between items-center border-b pb-2 mb-2 font-medium">
|
||||||
<span class="modal-title">
|
<span class="modal-title">
|
||||||
{{ t('formula') }} - {{ (defaultField ? (defaultField.title || field.title || field.name) : field.name) || buildDefaultName(field, template.fields) }}
|
{{ t('formula') }} - {{ (defaultField ? (defaultField.title || field.title || field.name) : field.name) || buildDefaultName(field) }}
|
||||||
</span>
|
</span>
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
v-for="f in fields"
|
v-for="f in fields"
|
||||||
:key="f.uuid"
|
:key="f.uuid"
|
||||||
class="mr-1 flex btn btn-neutral btn-outline border-base-content/20 btn-sm normal-case font-normal bg-white !rounded-xl"
|
class="mr-1 flex btn btn-neutral btn-outline border-base-content/20 btn-sm normal-case font-normal bg-white !rounded-xl"
|
||||||
@click.prevent="insertTextUnderCursor(`{{${f.name || buildDefaultName(f, template.fields)}}}`)"
|
@click.prevent="insertTextUnderCursor(`{{${f.name || buildDefaultName(f)}}}`)"
|
||||||
>
|
>
|
||||||
<IconMathFunction
|
<IconMathFunction
|
||||||
v-if="f.preferences?.formula"
|
v-if="f.preferences?.formula"
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
height="20"
|
height="20"
|
||||||
stroke-width="1.5"
|
stroke-width="1.5"
|
||||||
/>
|
/>
|
||||||
{{ f.name || buildDefaultName(f, template.fields) }}
|
{{ f.name || buildDefaultName(f) }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -131,7 +131,7 @@ export default {
|
|||||||
IconCodePlus,
|
IconCodePlus,
|
||||||
IconMathFunction
|
IconMathFunction
|
||||||
},
|
},
|
||||||
inject: ['t', 'save', 'template', 'withFormula'],
|
inject: ['t', 'template', 'withFormula'],
|
||||||
props: {
|
props: {
|
||||||
field: {
|
field: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -152,7 +152,7 @@ export default {
|
|||||||
required: true
|
required: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['close'],
|
emits: ['close', 'save'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
formula: ''
|
formula: ''
|
||||||
@@ -181,7 +181,7 @@ export default {
|
|||||||
const foundField = this.fields.find((f) => f.uuid === uuid)
|
const foundField = this.fields.find((f) => f.uuid === uuid)
|
||||||
|
|
||||||
if (foundField) {
|
if (foundField) {
|
||||||
return `{{${foundField.name || this.buildDefaultName(foundField, this.template.fields)}}}`
|
return `{{${foundField.name || this.buildDefaultName(foundField)}}}`
|
||||||
} else {
|
} else {
|
||||||
return '{{FIELD NOT FOUND}}'
|
return '{{FIELD NOT FOUND}}'
|
||||||
}
|
}
|
||||||
@@ -190,7 +190,7 @@ export default {
|
|||||||
normalizeFormula (text) {
|
normalizeFormula (text) {
|
||||||
return text.replace(/{{(.*?)}}/g, (match, name) => {
|
return text.replace(/{{(.*?)}}/g, (match, name) => {
|
||||||
const foundField = this.fields.find((f) => {
|
const foundField = this.fields.find((f) => {
|
||||||
return (f.name || this.buildDefaultName(f, this.template.fields)).trim() === name.trim()
|
return (f.name || this.buildDefaultName(f)).trim() === name.trim()
|
||||||
})
|
})
|
||||||
|
|
||||||
if (foundField) {
|
if (foundField) {
|
||||||
@@ -212,11 +212,14 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
this.field.preferences.formula = normalizedFormula
|
this.field.preferences.formula = normalizedFormula
|
||||||
|
|
||||||
if (this.field.type !== 'payment') {
|
if (this.field.type === 'payment') {
|
||||||
|
delete this.field.preferences.price
|
||||||
|
delete this.field.preferences.payment_link_id
|
||||||
|
} else {
|
||||||
this.field.readonly = !!normalizedFormula
|
this.field.readonly = !!normalizedFormula
|
||||||
}
|
}
|
||||||
|
|
||||||
this.save()
|
this.$emit('save')
|
||||||
|
|
||||||
this.$emit('close')
|
this.$emit('close')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
const en = {
|
const en = {
|
||||||
|
fixed: 'Fixed',
|
||||||
default: 'Default',
|
default: 'Default',
|
||||||
save_as_custom_field: 'Save as Custom Field',
|
save_as_custom_field: 'Save as custom field',
|
||||||
kba: 'KBA',
|
kba: 'KBA',
|
||||||
analyzing_: 'Analyzing...',
|
analyzing_: 'Analyzing...',
|
||||||
download: 'Download',
|
download: 'Download',
|
||||||
@@ -31,6 +32,9 @@ const en = {
|
|||||||
field_not_found: 'Field not found',
|
field_not_found: 'Field not found',
|
||||||
clear: 'Clear',
|
clear: 'Clear',
|
||||||
align: 'Align',
|
align: 'Align',
|
||||||
|
resize: 'Resize',
|
||||||
|
width: 'Width',
|
||||||
|
height: 'Height',
|
||||||
add_all_required_fields_to_continue: 'Add all required fields to continue',
|
add_all_required_fields_to_continue: 'Add all required fields to continue',
|
||||||
uploaded_pdf_contains_form_fields_keep_or_remove_them: 'Uploaded PDF contains form fields. Keep or remove them?',
|
uploaded_pdf_contains_form_fields_keep_or_remove_them: 'Uploaded PDF contains form fields. Keep or remove them?',
|
||||||
keep: 'Keep',
|
keep: 'Keep',
|
||||||
@@ -48,6 +52,8 @@ const en = {
|
|||||||
type_value: 'Type value',
|
type_value: 'Type value',
|
||||||
equal: 'Equal',
|
equal: 'Equal',
|
||||||
not_equal: 'Not equal',
|
not_equal: 'Not equal',
|
||||||
|
greater_than: 'Greater than',
|
||||||
|
less_than: 'Less than',
|
||||||
contains: 'Contains',
|
contains: 'Contains',
|
||||||
does_not_contain: 'Does not contain',
|
does_not_contain: 'Does not contain',
|
||||||
not_empty: 'Not empty',
|
not_empty: 'Not empty',
|
||||||
@@ -91,8 +97,9 @@ const en = {
|
|||||||
format: 'Format',
|
format: 'Format',
|
||||||
read_only: 'Read-only',
|
read_only: 'Read-only',
|
||||||
page: 'Page',
|
page: 'Page',
|
||||||
draw_new_area: 'Draw New Area',
|
draw_new_area: 'Draw new area',
|
||||||
copy_to_all_pages: 'Copy to All Pages',
|
copy_to_all_pages: 'Copy to all pages',
|
||||||
|
more: 'More',
|
||||||
add_option: 'Add option',
|
add_option: 'Add option',
|
||||||
option: 'Option',
|
option: 'Option',
|
||||||
options: 'Options',
|
options: 'Options',
|
||||||
@@ -173,6 +180,9 @@ const en = {
|
|||||||
numbers_only: 'Numbers only',
|
numbers_only: 'Numbers only',
|
||||||
letters_only: 'Letters only',
|
letters_only: 'Letters only',
|
||||||
regexp_validation: 'Regexp validation',
|
regexp_validation: 'Regexp validation',
|
||||||
|
custom_validation: 'Custom Validation',
|
||||||
|
length_validation: 'Length Validation',
|
||||||
|
number_range: 'Number Range',
|
||||||
enter_pdf_password: 'Enter PDF password',
|
enter_pdf_password: 'Enter PDF password',
|
||||||
wrong_password: 'Wrong password',
|
wrong_password: 'Wrong password',
|
||||||
currency: 'Currency',
|
currency: 'Currency',
|
||||||
@@ -202,6 +212,7 @@ const en = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const es = {
|
const es = {
|
||||||
|
fixed: 'Fijo',
|
||||||
default: 'Predeterminado',
|
default: 'Predeterminado',
|
||||||
save_as_custom_field: 'Guardar como personalizado',
|
save_as_custom_field: 'Guardar como personalizado',
|
||||||
kba: 'KBA',
|
kba: 'KBA',
|
||||||
@@ -235,6 +246,9 @@ const es = {
|
|||||||
clear: 'Borrar',
|
clear: 'Borrar',
|
||||||
type_value: 'Escriba valor',
|
type_value: 'Escriba valor',
|
||||||
align: 'Alinear',
|
align: 'Alinear',
|
||||||
|
resize: 'Redimensionar',
|
||||||
|
width: 'Ancho',
|
||||||
|
height: 'Alto',
|
||||||
add_all_required_fields_to_continue: 'Agregar todos los campos requeridos para continuar',
|
add_all_required_fields_to_continue: 'Agregar todos los campos requeridos para continuar',
|
||||||
uploaded_pdf_contains_form_fields_keep_or_remove_them: 'El PDF cargado tiene campos. ¿Mantenerlos o eliminarlos?',
|
uploaded_pdf_contains_form_fields_keep_or_remove_them: 'El PDF cargado tiene campos. ¿Mantenerlos o eliminarlos?',
|
||||||
keep: 'Mantener',
|
keep: 'Mantener',
|
||||||
@@ -252,6 +266,8 @@ const es = {
|
|||||||
price: 'Precio',
|
price: 'Precio',
|
||||||
equal: 'Igual',
|
equal: 'Igual',
|
||||||
not_equal: 'No es igual',
|
not_equal: 'No es igual',
|
||||||
|
greater_than: 'Mayor que',
|
||||||
|
less_than: 'Menor que',
|
||||||
contains: 'Contiene',
|
contains: 'Contiene',
|
||||||
does_not_contain: 'No contiene',
|
does_not_contain: 'No contiene',
|
||||||
not_empty: 'No vacío',
|
not_empty: 'No vacío',
|
||||||
@@ -292,6 +308,7 @@ const es = {
|
|||||||
page: 'Página',
|
page: 'Página',
|
||||||
draw_new_area: 'Dibujar nueva área',
|
draw_new_area: 'Dibujar nueva área',
|
||||||
copy_to_all_pages: 'Copiar a todas las páginas',
|
copy_to_all_pages: 'Copiar a todas las páginas',
|
||||||
|
more: 'Más',
|
||||||
add_option: 'Agregar opción',
|
add_option: 'Agregar opción',
|
||||||
option: 'Opción',
|
option: 'Opción',
|
||||||
options: 'Opciones',
|
options: 'Opciones',
|
||||||
@@ -376,6 +393,9 @@ const es = {
|
|||||||
numbers_only: 'Solo números',
|
numbers_only: 'Solo números',
|
||||||
letters_only: 'Solo letras',
|
letters_only: 'Solo letras',
|
||||||
regexp_validation: 'Validación de expresión regular',
|
regexp_validation: 'Validación de expresión regular',
|
||||||
|
custom_validation: 'Validación Personalizada',
|
||||||
|
length_validation: 'Validación de Longitud',
|
||||||
|
number_range: 'Rango de Números',
|
||||||
enter_pdf_password: 'Ingrese la contraseña del PDF',
|
enter_pdf_password: 'Ingrese la contraseña del PDF',
|
||||||
wrong_password: 'Contraseña incorrecta',
|
wrong_password: 'Contraseña incorrecta',
|
||||||
currency: 'Moneda',
|
currency: 'Moneda',
|
||||||
@@ -405,6 +425,7 @@ const es = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const it = {
|
const it = {
|
||||||
|
fixed: 'Fisso',
|
||||||
default: 'Predefinito',
|
default: 'Predefinito',
|
||||||
save_as_custom_field: 'Salva come personalizzato',
|
save_as_custom_field: 'Salva come personalizzato',
|
||||||
kba: 'KBA',
|
kba: 'KBA',
|
||||||
@@ -437,6 +458,9 @@ const it = {
|
|||||||
field_not_found: 'Campo non trovato',
|
field_not_found: 'Campo non trovato',
|
||||||
clear: 'Cancella',
|
clear: 'Cancella',
|
||||||
align: 'Allinea',
|
align: 'Allinea',
|
||||||
|
resize: 'Ridimensiona',
|
||||||
|
width: 'Larghezza',
|
||||||
|
height: 'Altezza',
|
||||||
add_all_required_fields_to_continue: 'Aggiungi tutti i campi obbligatori per continuare',
|
add_all_required_fields_to_continue: 'Aggiungi tutti i campi obbligatori per continuare',
|
||||||
uploaded_pdf_contains_form_fields_keep_or_remove_them: 'Il PDF caricato contiene campi del modulo. Mantenerli o rimuoverli?',
|
uploaded_pdf_contains_form_fields_keep_or_remove_them: 'Il PDF caricato contiene campi del modulo. Mantenerli o rimuoverli?',
|
||||||
keep: 'Mantieni',
|
keep: 'Mantieni',
|
||||||
@@ -454,6 +478,8 @@ const it = {
|
|||||||
type_value: 'Inserisci valore',
|
type_value: 'Inserisci valore',
|
||||||
equal: 'Uguale',
|
equal: 'Uguale',
|
||||||
not_equal: 'Non uguale',
|
not_equal: 'Non uguale',
|
||||||
|
greater_than: 'Maggiore di',
|
||||||
|
less_than: 'Minore di',
|
||||||
contains: 'Contiene',
|
contains: 'Contiene',
|
||||||
does_not_contain: 'Non contiene',
|
does_not_contain: 'Non contiene',
|
||||||
not_empty: 'Non vuoto',
|
not_empty: 'Non vuoto',
|
||||||
@@ -499,6 +525,7 @@ const it = {
|
|||||||
page: 'Pagina',
|
page: 'Pagina',
|
||||||
draw_new_area: 'Disegna nuova area',
|
draw_new_area: 'Disegna nuova area',
|
||||||
copy_to_all_pages: 'Copia in tutte le pagine',
|
copy_to_all_pages: 'Copia in tutte le pagine',
|
||||||
|
more: 'Altro',
|
||||||
add_option: 'Aggiungi opzione',
|
add_option: 'Aggiungi opzione',
|
||||||
option: 'Opzione',
|
option: 'Opzione',
|
||||||
options: 'Opzioni',
|
options: 'Opzioni',
|
||||||
@@ -579,6 +606,9 @@ const it = {
|
|||||||
numbers_only: 'Solo numeri',
|
numbers_only: 'Solo numeri',
|
||||||
letters_only: 'Solo lettere',
|
letters_only: 'Solo lettere',
|
||||||
regexp_validation: 'Validazione regexp',
|
regexp_validation: 'Validazione regexp',
|
||||||
|
custom_validation: 'Validazione Personalizzata',
|
||||||
|
length_validation: 'Validazione Lunghezza',
|
||||||
|
number_range: 'Intervallo Numerico',
|
||||||
enter_pdf_password: 'Inserisci password PDF',
|
enter_pdf_password: 'Inserisci password PDF',
|
||||||
wrong_password: 'Password errata',
|
wrong_password: 'Password errata',
|
||||||
currency: 'Valuta',
|
currency: 'Valuta',
|
||||||
@@ -608,6 +638,7 @@ const it = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pt = {
|
const pt = {
|
||||||
|
fixed: 'Fixo',
|
||||||
default: 'Padrão',
|
default: 'Padrão',
|
||||||
save_as_custom_field: 'Salvar como personalizado',
|
save_as_custom_field: 'Salvar como personalizado',
|
||||||
kba: 'KBA',
|
kba: 'KBA',
|
||||||
@@ -644,6 +675,9 @@ const pt = {
|
|||||||
uploaded_pdf_contains_form_fields_keep_or_remove_them: 'O PDF carregado contém campos. Manter ou removê-los?',
|
uploaded_pdf_contains_form_fields_keep_or_remove_them: 'O PDF carregado contém campos. Manter ou removê-los?',
|
||||||
keep: 'Manter',
|
keep: 'Manter',
|
||||||
align: 'Alinhar',
|
align: 'Alinhar',
|
||||||
|
resize: 'Redimensionar',
|
||||||
|
width: 'Largura',
|
||||||
|
height: 'Altura',
|
||||||
left: 'Esquerda',
|
left: 'Esquerda',
|
||||||
heading: 'Cabeçalho',
|
heading: 'Cabeçalho',
|
||||||
validation: 'Validação',
|
validation: 'Validação',
|
||||||
@@ -658,6 +692,8 @@ const pt = {
|
|||||||
price: 'Preço',
|
price: 'Preço',
|
||||||
equal: 'Igual',
|
equal: 'Igual',
|
||||||
not_equal: 'Não é igual',
|
not_equal: 'Não é igual',
|
||||||
|
greater_than: 'Maior que',
|
||||||
|
less_than: 'Menor que',
|
||||||
contains: 'Contém',
|
contains: 'Contém',
|
||||||
does_not_contain: 'Não contém',
|
does_not_contain: 'Não contém',
|
||||||
not_empty: 'Não vazio',
|
not_empty: 'Não vazio',
|
||||||
@@ -698,6 +734,7 @@ const pt = {
|
|||||||
page: 'Página',
|
page: 'Página',
|
||||||
draw_new_area: 'Desenhar nova área',
|
draw_new_area: 'Desenhar nova área',
|
||||||
copy_to_all_pages: 'Copiar para todas as páginas',
|
copy_to_all_pages: 'Copiar para todas as páginas',
|
||||||
|
more: 'Mais',
|
||||||
add_option: 'Adicionar opção',
|
add_option: 'Adicionar opção',
|
||||||
option: 'Opção',
|
option: 'Opção',
|
||||||
options: 'Opções',
|
options: 'Opções',
|
||||||
@@ -782,6 +819,9 @@ const pt = {
|
|||||||
numbers_only: 'Somente números',
|
numbers_only: 'Somente números',
|
||||||
letters_only: 'Somente letras',
|
letters_only: 'Somente letras',
|
||||||
regexp_validation: 'Validação de expressão regular',
|
regexp_validation: 'Validação de expressão regular',
|
||||||
|
custom_validation: 'Validação Personalizada',
|
||||||
|
length_validation: 'Validação de Comprimento',
|
||||||
|
number_range: 'Intervalo de Números',
|
||||||
enter_pdf_password: 'Digite a senha do PDF',
|
enter_pdf_password: 'Digite a senha do PDF',
|
||||||
wrong_password: 'Senha incorreta',
|
wrong_password: 'Senha incorreta',
|
||||||
currency: 'Moeda',
|
currency: 'Moeda',
|
||||||
@@ -811,6 +851,7 @@ const pt = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fr = {
|
const fr = {
|
||||||
|
fixed: 'Fixe',
|
||||||
default: 'Par défaut',
|
default: 'Par défaut',
|
||||||
save_as_custom_field: 'Enregistrer comme personnalisé',
|
save_as_custom_field: 'Enregistrer comme personnalisé',
|
||||||
kba: 'KBA',
|
kba: 'KBA',
|
||||||
@@ -843,6 +884,9 @@ const fr = {
|
|||||||
field_not_found: 'Champ introuvable',
|
field_not_found: 'Champ introuvable',
|
||||||
clear: 'Effacer',
|
clear: 'Effacer',
|
||||||
align: 'Aligner',
|
align: 'Aligner',
|
||||||
|
resize: 'Redimensionner',
|
||||||
|
width: 'Largeur',
|
||||||
|
height: 'Hauteur',
|
||||||
add_all_required_fields_to_continue: 'Ajoutez tous les champs obligatoires pour continuer',
|
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 ?',
|
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',
|
keep: 'Conserver',
|
||||||
@@ -860,6 +904,8 @@ const fr = {
|
|||||||
type_value: 'Saisir une valeur',
|
type_value: 'Saisir une valeur',
|
||||||
equal: 'Égal',
|
equal: 'Égal',
|
||||||
not_equal: 'Différent',
|
not_equal: 'Différent',
|
||||||
|
greater_than: 'Supérieur à',
|
||||||
|
less_than: 'Inférieur à',
|
||||||
contains: 'Contient',
|
contains: 'Contient',
|
||||||
does_not_contain: 'Ne contient pas',
|
does_not_contain: 'Ne contient pas',
|
||||||
not_empty: 'Non vide',
|
not_empty: 'Non vide',
|
||||||
@@ -905,6 +951,7 @@ const fr = {
|
|||||||
page: 'Page',
|
page: 'Page',
|
||||||
draw_new_area: 'Dessiner une 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',
|
||||||
|
more: 'Plus',
|
||||||
add_option: 'Ajouter une option',
|
add_option: 'Ajouter une option',
|
||||||
option: 'Option',
|
option: 'Option',
|
||||||
options: 'Options',
|
options: 'Options',
|
||||||
@@ -985,6 +1032,9 @@ 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',
|
||||||
|
custom_validation: 'Validation Personnalisée',
|
||||||
|
length_validation: 'Validation de Longueur',
|
||||||
|
number_range: 'Plage de Nombres',
|
||||||
enter_pdf_password: 'Saisir le mot de passe du 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',
|
||||||
@@ -1014,6 +1064,7 @@ const fr = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const de = {
|
const de = {
|
||||||
|
fixed: 'Fest',
|
||||||
default: 'Standard',
|
default: 'Standard',
|
||||||
save_as_custom_field: 'Als benutzerdefiniert speichern',
|
save_as_custom_field: 'Als benutzerdefiniert speichern',
|
||||||
kba: 'KBA',
|
kba: 'KBA',
|
||||||
@@ -1046,6 +1097,9 @@ const de = {
|
|||||||
field_not_found: 'Feld nicht gefunden',
|
field_not_found: 'Feld nicht gefunden',
|
||||||
clear: 'Leeren',
|
clear: 'Leeren',
|
||||||
align: 'Ausrichten',
|
align: 'Ausrichten',
|
||||||
|
resize: 'Größe ändern',
|
||||||
|
width: 'Breite',
|
||||||
|
height: 'Höhe',
|
||||||
add_all_required_fields_to_continue: 'Fügen Sie alle erforderlichen Felder hinzu, um fortzufahren',
|
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?',
|
uploaded_pdf_contains_form_fields_keep_or_remove_them: 'Das hochgeladene PDF enthält Formularfelder. Beibehalten oder entfernen?',
|
||||||
keep: 'Beibehalten',
|
keep: 'Beibehalten',
|
||||||
@@ -1063,6 +1117,8 @@ const de = {
|
|||||||
type_value: 'Wert eingeben',
|
type_value: 'Wert eingeben',
|
||||||
equal: 'Gleich',
|
equal: 'Gleich',
|
||||||
not_equal: 'Ungleich',
|
not_equal: 'Ungleich',
|
||||||
|
greater_than: 'Größer als',
|
||||||
|
less_than: 'Kleiner als',
|
||||||
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',
|
||||||
@@ -1108,6 +1164,7 @@ const de = {
|
|||||||
page: 'Seite',
|
page: 'Seite',
|
||||||
draw_new_area: 'Bereich zeichnen',
|
draw_new_area: 'Bereich zeichnen',
|
||||||
copy_to_all_pages: 'Auf alle Seiten kopieren',
|
copy_to_all_pages: 'Auf alle Seiten kopieren',
|
||||||
|
more: 'Mehr',
|
||||||
add_option: 'Option hinzufügen',
|
add_option: 'Option hinzufügen',
|
||||||
option: 'Option',
|
option: 'Option',
|
||||||
options: 'Optionen',
|
options: 'Optionen',
|
||||||
@@ -1188,6 +1245,9 @@ const de = {
|
|||||||
numbers_only: 'Nur Zahlen',
|
numbers_only: 'Nur Zahlen',
|
||||||
letters_only: 'Nur Buchstaben',
|
letters_only: 'Nur Buchstaben',
|
||||||
regexp_validation: 'RegExp-Validierung',
|
regexp_validation: 'RegExp-Validierung',
|
||||||
|
custom_validation: 'Benutzerdefinierte Validierung',
|
||||||
|
length_validation: 'Längenvalidierung',
|
||||||
|
number_range: 'Zahlenbereich',
|
||||||
enter_pdf_password: 'PDF-Passwort eingeben',
|
enter_pdf_password: 'PDF-Passwort eingeben',
|
||||||
wrong_password: 'Falsches Passwort',
|
wrong_password: 'Falsches Passwort',
|
||||||
currency: 'Währung',
|
currency: 'Währung',
|
||||||
@@ -1217,6 +1277,7 @@ const de = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const nl = {
|
const nl = {
|
||||||
|
fixed: 'Vast',
|
||||||
default: 'Standaard',
|
default: 'Standaard',
|
||||||
save_as_custom_field: 'Opslaan als aangepast',
|
save_as_custom_field: 'Opslaan als aangepast',
|
||||||
kba: 'KBA',
|
kba: 'KBA',
|
||||||
@@ -1249,6 +1310,9 @@ const nl = {
|
|||||||
field_not_found: 'Veld niet gevonden',
|
field_not_found: 'Veld niet gevonden',
|
||||||
clear: 'Wissen',
|
clear: 'Wissen',
|
||||||
align: 'Uitlijnen',
|
align: 'Uitlijnen',
|
||||||
|
resize: 'Formaat wijzigen',
|
||||||
|
width: 'Breedte',
|
||||||
|
height: 'Hoogte',
|
||||||
add_all_required_fields_to_continue: 'Voeg alle vereiste velden toe om door te gaan',
|
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?',
|
uploaded_pdf_contains_form_fields_keep_or_remove_them: 'Geüploade PDF bevat formuliervelden. Behouden of verwijderen?',
|
||||||
keep: 'Behouden',
|
keep: 'Behouden',
|
||||||
@@ -1266,6 +1330,8 @@ const nl = {
|
|||||||
type_value: 'Typ waarde',
|
type_value: 'Typ waarde',
|
||||||
equal: 'Gelijk aan',
|
equal: 'Gelijk aan',
|
||||||
not_equal: 'Niet gelijk aan',
|
not_equal: 'Niet gelijk aan',
|
||||||
|
greater_than: 'Groter dan',
|
||||||
|
less_than: 'Kleiner dan',
|
||||||
contains: 'Bevat',
|
contains: 'Bevat',
|
||||||
does_not_contain: 'Bevat niet',
|
does_not_contain: 'Bevat niet',
|
||||||
not_empty: 'Niet leeg',
|
not_empty: 'Niet leeg',
|
||||||
@@ -1311,6 +1377,7 @@ const nl = {
|
|||||||
page: 'Pagina',
|
page: 'Pagina',
|
||||||
draw_new_area: 'Nieuw gebied tekenen',
|
draw_new_area: 'Nieuw gebied tekenen',
|
||||||
copy_to_all_pages: 'Kopieer naar alle pag.',
|
copy_to_all_pages: 'Kopieer naar alle pag.',
|
||||||
|
more: 'Meer',
|
||||||
add_option: 'Optie toevoegen',
|
add_option: 'Optie toevoegen',
|
||||||
option: 'Optie',
|
option: 'Optie',
|
||||||
options: 'Opties',
|
options: 'Opties',
|
||||||
@@ -1391,6 +1458,9 @@ const nl = {
|
|||||||
numbers_only: 'Alleen cijfers',
|
numbers_only: 'Alleen cijfers',
|
||||||
letters_only: 'Alleen letters',
|
letters_only: 'Alleen letters',
|
||||||
regexp_validation: 'Regex validatie',
|
regexp_validation: 'Regex validatie',
|
||||||
|
custom_validation: 'Aangepaste Validatie',
|
||||||
|
length_validation: 'Lengte Validatie',
|
||||||
|
number_range: 'Getalbereik',
|
||||||
enter_pdf_password: 'Voer PDF-wachtwoord in',
|
enter_pdf_password: 'Voer PDF-wachtwoord in',
|
||||||
wrong_password: 'Onjuist wachtwoord',
|
wrong_password: 'Onjuist wachtwoord',
|
||||||
currency: 'Valuta',
|
currency: 'Valuta',
|
||||||
|
|||||||
@@ -2,87 +2,20 @@
|
|||||||
<svg
|
<svg
|
||||||
height="40"
|
height="40"
|
||||||
width="40"
|
width="40"
|
||||||
version="1.1"
|
style="color: #e0753f"
|
||||||
|
viewBox="0 0 180 180"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
||||||
viewBox="0 0 511.998 511.998"
|
|
||||||
xml:space="preserve"
|
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
style="fill:#AA968C;"
|
fill="currentColor"
|
||||||
d="M503.999,247.999c0,128.13-111.033,240-248,240S8,376.129,8,247.999s111.033-224,248-224
|
d="M 178.224 72.09 c -0.296 -1.463 -0.627 -2.919 -0.996 -4.364 -0.293 -1.151 -0.616 -2.293 -0.956 -3.433 -0.301 -1.008 -0.612 -2.014 -0.95 -3.012 -0.531 -1.578 -1.113 -3.142 -1.735 -4.694 -0.216 -0.54 -0.433 -1.082 -0.661 -1.618 -0.195 -0.462 -0.399 -0.917 -0.601 -1.375 -0.262 -0.591 -0.53 -1.177 -0.804 -1.762 -0.074 -0.159 -0.151 -0.315 -0.226 -0.474 -0.209 -0.441 -0.422 -0.881 -0.638 -1.318 -0.076 -0.154 -0.153 -0.306 -0.229 -0.459 -0.236 -0.471 -0.477 -0.939 -0.721 -1.406 -0.053 -0.101 -0.105 -0.201 -0.158 -0.302 -1.143 -2.16 -2.367 -4.269 -3.68 -6.322 -0.116 -0.181 -0.237 -0.359 -0.355 -0.539 -0.094 -0.144 -0.189 -0.288 -0.284 -0.432 -0.284 -0.431 -0.57 -0.861 -0.862 -1.287 -0.112 -0.164 -0.225 -0.326 -0.338 -0.489 -0.193 -0.279 -0.382 -0.56 -0.579 -0.836 -0.089 -0.125 -0.182 -0.249 -0.273 -0.374 -0.13 -0.182 -0.264 -0.362 -0.395 -0.542 -0.277 -0.38 -0.556 -0.76 -0.838 -1.135 -0.15 -0.199 -0.303 -0.395 -0.454 -0.593 -0.21 -0.274 -0.417 -0.552 -0.63 -0.823 -0.055 -0.069 -0.111 -0.136 -0.166 -0.205 -0.482 -0.61 -0.971 -1.216 -1.47 -1.814 -0.129 -0.155 -0.262 -0.306 -0.392 -0.461 -0.402 -0.476 -0.808 -0.95 -1.22 -1.417 -0.186 -0.212 -0.375 -0.422 -0.563 -0.631 -0.384 -0.428 -0.773 -0.854 -1.167 -1.276 -0.176 -0.189 -0.351 -0.379 -0.529 -0.567 -0.564 -0.595 -1.134 -1.186 -1.716 -1.768 -1.091 -1.091 -2.207 -2.15 -3.346 -3.178 -1.016 -0.919 -2.05 -1.815 -3.103 -2.684 -0.772 -0.636 -1.557 -1.255 -2.348 -1.864 -3.465 -2.67 -7.112 -5.075 -10.927 -7.209 -2.869 -1.604 -5.83 -3.06 -8.883 -4.351 -2.443 -1.033 -4.922 -1.948 -7.428 -2.756 -8.879 -2.863 -18.13 -4.318 -27.605 -4.318 -3.19 0 -6.354 0.169 -9.488 0.496 -4.036 0.421 -8.019 1.114 -11.94 2.073 -1.732 0.423 -3.452 0.892 -5.157 1.42 -2.856 0.883 -5.673 1.912 -8.447 3.085 -2.645 1.118 -5.222 2.357 -7.729 3.711 -2.574 1.39 -5.073 2.901 -7.494 4.533 -1.195 0.805 -2.37 1.64 -3.527 2.503 -1.156 0.864 -2.292 1.756 -3.408 2.676 -0.553 0.456 -1.1 0.919 -1.643 1.389 -1.649 1.427 -3.252 2.92 -4.806 4.473 -2.582 2.582 -4.991 5.299 -7.222 8.138 -0.892 1.135 -1.756 2.292 -2.59 3.467 -0.417 0.588 -0.827 1.18 -1.23 1.778 -0.403 0.597 -0.798 1.199 -1.186 1.806 -0.388 0.607 -0.769 1.218 -1.143 1.835 -2.241 3.697 -4.216 7.562 -5.916 11.582 -1.095 2.589 -2.059 5.217 -2.901 7.877 -0.153 0.482 -0.3 0.965 -0.444 1.449 -0.339 1.14 -0.663 2.282 -0.956 3.433 -0.369 1.446 -0.7 2.901 -0.996 4.364 -1.034 5.121 -1.618 10.343 -1.749 15.637 -0.018 0.757 -0.028 1.514 -0.028 2.274 0 1.123 0.02 2.244 0.062 3.361 0.285 7.82 1.568 15.475 3.825 22.879 0.044 0.147 0.088 0.295 0.133 0.441 0.877 2.823 1.894 5.608 3.054 8.35 0.85 2.009 1.769 3.98 2.755 5.912 0.539 1.057 1.105 2.099 1.685 3.132 4.013 7.142 8.98 13.698 14.846 19.564 7.713 7.713 16.611 13.878 26.477 18.352 0.705 0.32 1.415 0.632 2.131 0.935 2.081 0.88 4.185 1.679 6.313 2.396 9.217 3.106 18.85 4.677 28.719 4.677 8.031 0 15.902 -1.047 23.522 -3.107 0.633 -0.172 1.266 -0.35 1.895 -0.535 0.757 -0.222 1.509 -0.456 2.26 -0.698 0.717 -0.232 1.431 -0.474 2.145 -0.723 1.752 -0.616 3.49 -1.281 5.211 -2.009 0.755 -0.319 1.503 -0.651 2.247 -0.989 1.237 -0.563 2.459 -1.15 3.664 -1.766 0.644 -0.328 1.283 -0.665 1.917 -1.009 1.654 -0.896 3.274 -1.848 4.865 -2.844 5.736 -3.591 11.06 -7.827 15.912 -12.679 0.775 -0.775 1.534 -1.562 2.278 -2.36 5.204 -5.59 9.636 -11.754 13.246 -18.417 0.343 -0.634 0.68 -1.274 1.009 -1.917 0.482 -0.944 0.943 -1.9 1.392 -2.863 0.471 -1.007 0.928 -2.021 1.364 -3.049 1.22 -2.886 2.281 -5.82 3.187 -8.793 0.559 -1.833 1.056 -3.68 1.494 -5.542 0.108 -0.458 0.211 -0.916 0.312 -1.376 0.194 -0.883 0.373 -1.77 0.539 -2.659 1.02 -5.455 1.542 -11.02 1.542 -16.663 0 -6.074 -0.595 -12.058 -1.776 -17.911 z m -161.733 19.614 c -1.118 -56.662 44.604 -74.877 60.998 -67.647 2.187 0.965 4.732 2.431 7.042 2.96 5.295 1.213 13.432 -3.113 13.521 6.273 0.078 8.156 -3.389 13.108 -10.797 16.177 -7.539 3.124 -14.777 9.181 -19.95 15.493 -21.487 26.216 -31.231 68.556 -7.565 94.296 -13.679 -5.545 -42.418 -25.467 -43.248 -67.552 z m 91.109 72.619 c -0.053 0.008 -4.171 0.775 -4.171 0.775 0 0 -15.862 -22.957 -23.509 -21.719 11.291 16.04 12.649 22.625 12.649 22.625 -0.053 0.001 -0.107 0.001 -0.161 0.003 -51.831 2.131 -42.785 -64.026 -28.246 -86.502 -1.555 13.073 8.878 39.992 39.034 44.1 9.495 1.293 32.302 -3.275 41.015 -11.38 0.098 1.825 0.163 3.85 0.159 6.013 -0.046 23.538 -13.47 42.743 -36.77 46.085 z m 30.575 -15.708 c 9.647 -9.263 12.869 -27.779 9.103 -44.137 -4.608 -20.011 -28.861 -32.383 -40.744 -35.564 5.766 -8.089 27.908 -14.274 39.567 5.363 -5.172 -10.519 -13.556 -23.023 -1.732 -33.128 12.411 13.329 19.411 29.94 20.161 48.7 0.75 18.753 -6.64 41.768 -26.355 58.765 z"
|
||||||
S503.999,119.869,503.999,247.999z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
style="fill:#AA968C;"
|
|
||||||
d="M255.999,23.999C119.033,23.999,8,119.868,8,247.999c0,24.631,4.138,48.647,11.74,71.397
|
|
||||||
c25.518,34.29,66.232,56.602,112.26,56.602c53.893,0,100.6-30.495,124-75.13c23.4,44.636,70.107,75.13,124,75.13
|
|
||||||
c46.028,0,86.743-22.312,112.26-56.602c7.602-22.75,11.74-46.767,11.74-71.397C503.999,119.868,392.966,23.999,255.999,23.999z"
|
|
||||||
/>
|
/>
|
||||||
<circle
|
<circle
|
||||||
style="fill:#C8AF9B;"
|
fill="currentColor"
|
||||||
cx="256"
|
cx="71.927"
|
||||||
cy="351.999"
|
cy="32.004"
|
||||||
r="136"
|
r="2.829"
|
||||||
/>
|
/>
|
||||||
<g>
|
|
||||||
<circle
|
|
||||||
style="fill:#464655;"
|
|
||||||
cx="132"
|
|
||||||
cy="203.999"
|
|
||||||
r="28"
|
|
||||||
/>
|
|
||||||
<circle
|
|
||||||
style="fill:#464655;"
|
|
||||||
cx="380"
|
|
||||||
cy="203.999"
|
|
||||||
r="28"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
style="fill:#464655;"
|
|
||||||
d="M269.949,284.516c-7.672,10.741-20.227,10.741-27.899,0l-12.101-16.941
|
|
||||||
c-7.672-10.741-3.15-19.53,10.05-19.53h32c13.2,0,17.723,8.788,10.05,19.53L269.949,284.516z"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
<path
|
|
||||||
style="fill:#AA968C;"
|
|
||||||
d="M 350.964 399.998 C 316.628 399.998 299.021 351.998 255.882 351.998 C 212.742 351.998 195.135 399.998 160.801 399.998 C 145.395 399.998 131.723 394.147 120.621 374.798 C 131.595 439.03 187.893 487.998 255.881 487.998 C 323.868 487.998 380.168 439.03 391.14 374.798 C 380.04 394.148 366.368 399.998 350.964 399.998 Z"
|
|
||||||
/>
|
|
||||||
<g>
|
|
||||||
<path
|
|
||||||
style="fill:#8C7873;"
|
|
||||||
d="M32,423.998c-3.172,0-6.18-1.906-7.43-5.031c-1.641-4.105,0.359-8.758,4.461-10.402l160.008-64
|
|
||||||
c4.062-1.617,8.758,0.352,10.398,4.457s-0.359,8.758-4.461,10.402l-160.008,64C34,423.811,32.992,423.998,32,423.998z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
style="fill:#8C7873;"
|
|
||||||
d="M15.992,375.995c-3.547,0-6.781-2.375-7.727-5.965c-1.125-4.273,1.422-8.648,5.695-9.773l152-40
|
|
||||||
c4.289-1.121,8.648,1.426,9.773,5.703c1.125,4.273-1.422,8.648-5.695,9.773l-152,40C17.351,375.913,16.672,375.995,15.992,375.995z
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
style="fill:#8C7873;"
|
|
||||||
d="M7.992,335.995c-3.812,0-7.187-2.73-7.867-6.609c-0.773-4.352,2.133-8.5,6.484-9.27l136-24
|
|
||||||
c4.328-0.77,8.508,2.141,9.266,6.488c0.773,4.352-2.133,8.5-6.484,9.27l-136,24C8.922,335.956,8.453,335.995,7.992,335.995z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
style="fill:#8C7873;"
|
|
||||||
d="M480,423.998c3.172,0,6.18-1.906,7.43-5.031c1.641-4.105-0.359-8.758-4.461-10.402l-160.008-64
|
|
||||||
c-4.063-1.617-8.758,0.352-10.398,4.457s0.359,8.758,4.461,10.402l160.008,64C478,423.811,479.007,423.998,480,423.998z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
style="fill:#8C7873;"
|
|
||||||
d="M496.007,375.995c3.547,0,6.781-2.375,7.727-5.965c1.125-4.273-1.422-8.648-5.695-9.773l-152-40
|
|
||||||
c-4.289-1.121-8.648,1.426-9.773,5.703c-1.125,4.273,1.422,8.648,5.695,9.773l152,40
|
|
||||||
C494.648,375.913,495.328,375.995,496.007,375.995z"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
style="fill:#8C7873;"
|
|
||||||
d="M504.007,335.995c3.813,0,7.188-2.73,7.867-6.609c0.773-4.352-2.133-8.5-6.484-9.27l-136-24
|
|
||||||
c-4.328-0.77-8.508,2.141-9.266,6.488c-0.773,4.352,2.133,8.5,6.484,9.27l136,24C503.078,335.956,503.546,335.995,504.007,335.995z
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
</svg>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
:is-drag="isDrag"
|
:is-drag="isDrag"
|
||||||
@move="onSelectionBoxMove"
|
@move="onSelectionBoxMove"
|
||||||
@contextmenu="openSelectionContextMenu"
|
@contextmenu="openSelectionContextMenu"
|
||||||
@close-context-menu="closeSelectionContextMenu"
|
@close-context-menu="closeContextMenu"
|
||||||
/>
|
/>
|
||||||
<FieldArea
|
<FieldArea
|
||||||
v-for="(item, i) in areas"
|
v-for="(item, i) in areas"
|
||||||
@@ -48,6 +48,7 @@
|
|||||||
:default-submitters="defaultSubmitters"
|
:default-submitters="defaultSubmitters"
|
||||||
:max-page="totalPages - 1"
|
:max-page="totalPages - 1"
|
||||||
:is-select-mode="isSelectMode"
|
:is-select-mode="isSelectMode"
|
||||||
|
:is-mobile="isMobile"
|
||||||
@start-resize="resizeDirection = $event"
|
@start-resize="resizeDirection = $event"
|
||||||
@stop-resize="resizeDirection = null"
|
@stop-resize="resizeDirection = null"
|
||||||
@remove="$emit('remove-area', item.area)"
|
@remove="$emit('remove-area', item.area)"
|
||||||
@@ -74,29 +75,39 @@
|
|||||||
class="absolute outline-dashed outline-gray-400 pointer-events-none z-20"
|
class="absolute outline-dashed outline-gray-400 pointer-events-none z-20"
|
||||||
:style="selectionRectStyle"
|
:style="selectionRectStyle"
|
||||||
/>
|
/>
|
||||||
<ContextMenu
|
<FieldContextMenu
|
||||||
v-if="contextMenu"
|
v-if="contextMenu && contextMenu.field"
|
||||||
:context-menu="contextMenu"
|
:context-menu="contextMenu"
|
||||||
:field="contextMenu.field"
|
:field="contextMenu.field"
|
||||||
|
:with-signature-id="withSignatureId"
|
||||||
|
:with-prefillable="withPrefillable"
|
||||||
:editable="editable"
|
:editable="editable"
|
||||||
:with-fields-detection="withFieldsDetection"
|
:default-field="defaultFieldsIndex[contextMenu.field.name]"
|
||||||
@copy="handleCopy"
|
@copy="handleCopy"
|
||||||
@delete="handleDelete"
|
@delete="handleDelete"
|
||||||
@paste="handlePaste"
|
|
||||||
@autodetect-fields="handleAutodetectFields"
|
|
||||||
@close="closeContextMenu"
|
@close="closeContextMenu"
|
||||||
|
@set-draw="$emit('set-draw', $event)"
|
||||||
|
@scroll-to="$emit('scroll-to', $event)"
|
||||||
|
@save="save"
|
||||||
|
@add-custom-field="$emit('add-custom-field', $event)"
|
||||||
/>
|
/>
|
||||||
<ContextMenu
|
<SelectionContextMenu
|
||||||
v-if="selectionContextMenu"
|
v-else-if="contextMenu && contextMenu.areas"
|
||||||
:context-menu="selectionContextMenu"
|
:context-menu="contextMenu"
|
||||||
:editable="editable"
|
:editable="editable"
|
||||||
:is-multi-selection="true"
|
|
||||||
:selected-areas="selectedAreasRef.value"
|
|
||||||
:template="template"
|
:template="template"
|
||||||
@copy="handleSelectionCopy"
|
@copy="handleSelectionCopy"
|
||||||
@delete="handleSelectionDelete"
|
@delete="handleSelectionDelete"
|
||||||
@align="handleSelectionAlign"
|
@close="closeContextMenu"
|
||||||
@close="closeSelectionContextMenu"
|
/>
|
||||||
|
<PageContextMenu
|
||||||
|
v-else-if="contextMenu && !contextMenu.field && !contextMenu.areas"
|
||||||
|
:context-menu="contextMenu"
|
||||||
|
:editable="editable"
|
||||||
|
:with-fields-detection="withFieldsDetection"
|
||||||
|
@paste="handlePaste"
|
||||||
|
@autodetect-fields="handleAutodetectFields"
|
||||||
|
@close="closeContextMenu"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -119,17 +130,21 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import FieldArea from './area'
|
import FieldArea from './area'
|
||||||
import ContextMenu from './context_menu'
|
import FieldContextMenu from './field_context_menu'
|
||||||
|
import SelectionContextMenu from './selection_context_menu'
|
||||||
|
import PageContextMenu from './page_context_menu'
|
||||||
import SelectionBox from './selection_box'
|
import SelectionBox from './selection_box'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'TemplatePage',
|
name: 'TemplatePage',
|
||||||
components: {
|
components: {
|
||||||
FieldArea,
|
FieldArea,
|
||||||
ContextMenu,
|
FieldContextMenu,
|
||||||
|
SelectionContextMenu,
|
||||||
|
PageContextMenu,
|
||||||
SelectionBox
|
SelectionBox
|
||||||
},
|
},
|
||||||
inject: ['fieldTypes', 'defaultDrawFieldType', 'fieldsDragFieldRef', 'customDragFieldRef', 'assignDropAreaSize', 'selectedAreasRef', 'template', 'isSelectModeRef'],
|
inject: ['fieldTypes', 'defaultDrawFieldType', 'fieldsDragFieldRef', 'customDragFieldRef', 'assignDropAreaSize', 'selectedAreasRef', 'template', 'isSelectModeRef', 'save'],
|
||||||
props: {
|
props: {
|
||||||
image: {
|
image: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -140,6 +155,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: null
|
default: null
|
||||||
},
|
},
|
||||||
|
isMobile: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
withSignatureId: {
|
withSignatureId: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -228,7 +248,7 @@ export default {
|
|||||||
default: false
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['draw', 'drop-field', 'remove-area', 'copy-field', 'paste-field', 'scroll-to', 'copy-selected-areas', 'delete-selected-areas', 'align-selected-areas', 'autodetect-fields', 'add-custom-field'],
|
emits: ['draw', 'drop-field', 'remove-area', 'copy-field', 'paste-field', 'scroll-to', 'copy-selected-areas', 'delete-selected-areas', 'autodetect-fields', 'add-custom-field', 'set-draw'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
areaRefs: [],
|
areaRefs: [],
|
||||||
@@ -236,13 +256,12 @@ export default {
|
|||||||
resizeDirection: null,
|
resizeDirection: null,
|
||||||
newAreas: [],
|
newAreas: [],
|
||||||
contextMenu: null,
|
contextMenu: null,
|
||||||
selectionRect: null,
|
selectionRect: null
|
||||||
selectionContextMenu: null
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
isSelectMode () {
|
isSelectMode () {
|
||||||
return this.isSelectModeRef.value && !this.drawFieldType && this.editable && !this.drawField
|
return this.isSelectModeRef.value && !this.drawFieldType && this.editable && !this.drawField && !this.drawCustomField
|
||||||
},
|
},
|
||||||
pageSelectedAreas () {
|
pageSelectedAreas () {
|
||||||
if (!this.selectedAreasRef.value) return []
|
if (!this.selectedAreasRef.value) return []
|
||||||
@@ -305,11 +324,6 @@ export default {
|
|||||||
return 'text'
|
return 'text'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
isMobile () {
|
|
||||||
const isMobileSafariIos = 'ontouchstart' in window && navigator.maxTouchPoints > 0 && /AppleWebKit/i.test(navigator.userAgent)
|
|
||||||
|
|
||||||
return isMobileSafariIos || /android|iphone|ipad/i.test(navigator.userAgent)
|
|
||||||
},
|
|
||||||
resizeDirectionClasses () {
|
resizeDirectionClasses () {
|
||||||
return {
|
return {
|
||||||
nwse: 'cursor-nwse-resize',
|
nwse: 'cursor-nwse-resize',
|
||||||
@@ -399,29 +413,32 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
openSelectionContextMenu (event) {
|
openSelectionContextMenu (event) {
|
||||||
|
if (!this.editable) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
|
||||||
const rect = this.$el.getBoundingClientRect()
|
const rect = this.$el.getBoundingClientRect()
|
||||||
|
|
||||||
this.selectionContextMenu = {
|
this.contextMenu = {
|
||||||
x: event.clientX,
|
x: event.clientX,
|
||||||
y: event.clientY,
|
y: event.clientY,
|
||||||
relativeX: (event.clientX - rect.left) / rect.width,
|
relativeX: (event.clientX - rect.left) / rect.width,
|
||||||
relativeY: (event.clientY - rect.top) / rect.height
|
relativeY: (event.clientY - rect.top) / rect.height,
|
||||||
|
areas: this.selectedAreasRef.value
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
closeSelectionContextMenu () {
|
|
||||||
this.selectionContextMenu = null
|
|
||||||
},
|
|
||||||
handleSelectionCopy () {
|
handleSelectionCopy () {
|
||||||
this.$emit('copy-selected-areas')
|
this.$emit('copy-selected-areas')
|
||||||
this.closeSelectionContextMenu()
|
|
||||||
|
this.closeContextMenu()
|
||||||
},
|
},
|
||||||
handleSelectionDelete () {
|
handleSelectionDelete () {
|
||||||
this.$emit('delete-selected-areas')
|
this.$emit('delete-selected-areas')
|
||||||
this.closeSelectionContextMenu()
|
|
||||||
},
|
this.closeContextMenu()
|
||||||
handleSelectionAlign (direction) {
|
|
||||||
this.$emit('align-selected-areas', direction)
|
|
||||||
this.closeSelectionContextMenu()
|
|
||||||
},
|
},
|
||||||
closeContextMenu () {
|
closeContextMenu () {
|
||||||
this.contextMenu = null
|
this.contextMenu = null
|
||||||
|
|||||||
@@ -0,0 +1,159 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
ref="menu"
|
||||||
|
class="fixed z-50 p-1 bg-white shadow-lg rounded-lg border border-neutral-200 cursor-default"
|
||||||
|
style="min-width: 170px"
|
||||||
|
:style="menuStyle"
|
||||||
|
@mousedown.stop
|
||||||
|
@pointerdown.stop
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center justify-between text-sm"
|
||||||
|
:class="!hasClipboardData ? 'opacity-50 cursor-not-allowed' : 'hover:bg-neutral-100'"
|
||||||
|
:disabled="!hasClipboardData"
|
||||||
|
@click.stop="!hasClipboardData ? null : $emit('paste')"
|
||||||
|
>
|
||||||
|
<span class="flex items-center space-x-2">
|
||||||
|
<IconClipboard class="w-4 h-4" />
|
||||||
|
<span>{{ t('paste') }}</span>
|
||||||
|
</span>
|
||||||
|
<span class="text-xs text-base-content/60 ml-4">{{ isMac ? '⌘V' : 'Ctrl+V' }}</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center justify-between text-sm"
|
||||||
|
@click.stop="handleToggleSelectMode"
|
||||||
|
>
|
||||||
|
<span class="flex items-center space-x-2">
|
||||||
|
<IconClick
|
||||||
|
v-if="!isSelectModeRef.value"
|
||||||
|
class="w-4 h-4"
|
||||||
|
/>
|
||||||
|
<IconNewSection
|
||||||
|
v-else
|
||||||
|
class="w-4 h-4"
|
||||||
|
/>
|
||||||
|
<span>{{ isSelectModeRef.value ? t('draw_fields') : t('select_fields') }}</span>
|
||||||
|
</span>
|
||||||
|
<span class="text-xs text-base-content/60 ml-4">Tab</span>
|
||||||
|
</button>
|
||||||
|
<hr
|
||||||
|
v-if="showAutodetectFields"
|
||||||
|
class="my-1 border-neutral-200"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-if="showAutodetectFields"
|
||||||
|
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
|
||||||
|
@click.stop="$emit('autodetect-fields')"
|
||||||
|
>
|
||||||
|
<IconSparkles class="w-4 h-4" />
|
||||||
|
<span>{{ t('autodetect_fields') }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { IconClipboard, IconClick, IconNewSection, IconSparkles } from '@tabler/icons-vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'PageContextMenu',
|
||||||
|
components: {
|
||||||
|
IconClipboard,
|
||||||
|
IconClick,
|
||||||
|
IconNewSection,
|
||||||
|
IconSparkles
|
||||||
|
},
|
||||||
|
inject: ['t', 'isSelectModeRef'],
|
||||||
|
props: {
|
||||||
|
contextMenu: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
editable: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
withFieldsDetection: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['paste', 'close', 'autodetect-fields'],
|
||||||
|
computed: {
|
||||||
|
isMac () {
|
||||||
|
return (navigator.userAgentData?.platform || navigator.platform)?.toLowerCase()?.includes('mac')
|
||||||
|
},
|
||||||
|
hasClipboardData () {
|
||||||
|
try {
|
||||||
|
const clipboard = localStorage.getItem('docuseal_clipboard')
|
||||||
|
|
||||||
|
if (clipboard) {
|
||||||
|
const data = JSON.parse(clipboard)
|
||||||
|
|
||||||
|
return Date.now() - data.timestamp < 3600000
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
menuStyle () {
|
||||||
|
return {
|
||||||
|
left: this.contextMenu.x + 'px',
|
||||||
|
top: this.contextMenu.y + 'px'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
showAutodetectFields () {
|
||||||
|
return this.withFieldsDetection && this.editable
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
document.addEventListener('keydown', this.onKeyDown)
|
||||||
|
document.addEventListener('mousedown', this.handleClickOutside)
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.checkMenuPosition()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
beforeUnmount () {
|
||||||
|
document.removeEventListener('keydown', this.onKeyDown)
|
||||||
|
document.removeEventListener('mousedown', this.handleClickOutside)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
checkMenuPosition () {
|
||||||
|
if (this.$refs.menu) {
|
||||||
|
const rect = this.$refs.menu.getBoundingClientRect()
|
||||||
|
const overflow = rect.bottom - window.innerHeight
|
||||||
|
|
||||||
|
if (overflow > 0) {
|
||||||
|
this.contextMenu.y = this.contextMenu.y - overflow - 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onKeyDown (event) {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
|
||||||
|
this.$emit('close')
|
||||||
|
} else if ((event.ctrlKey || event.metaKey) && event.key === 'v' && this.hasClipboardData) {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
|
||||||
|
this.$emit('paste')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleClickOutside (event) {
|
||||||
|
if (this.$refs.menu && !this.$refs.menu.contains(event.target)) {
|
||||||
|
this.$emit('close')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleToggleSelectMode () {
|
||||||
|
this.isSelectModeRef.value = !this.isSelectModeRef.value
|
||||||
|
|
||||||
|
this.$emit('close')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -119,6 +119,7 @@
|
|||||||
<ConditionsModal
|
<ConditionsModal
|
||||||
:item="item"
|
:item="item"
|
||||||
:build-default-name="buildDefaultName"
|
:build-default-name="buildDefaultName"
|
||||||
|
@save="$emit('change')"
|
||||||
@close="isShowConditionsModal = false"
|
@close="isShowConditionsModal = false"
|
||||||
/>
|
/>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
@@ -145,7 +146,7 @@ export default {
|
|||||||
GoogleDriveDocumentSettings,
|
GoogleDriveDocumentSettings,
|
||||||
IconSortDescending2
|
IconSortDescending2
|
||||||
},
|
},
|
||||||
inject: ['t'],
|
inject: ['t', 'getFieldTypeIndex'],
|
||||||
props: {
|
props: {
|
||||||
item: {
|
item: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
|||||||
@@ -0,0 +1,348 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
v-if="!isShowFontModal && !isShowConditionsModal"
|
||||||
|
ref="menu"
|
||||||
|
class="fixed z-50 p-1 bg-white shadow-lg rounded-lg border border-neutral-200 cursor-default"
|
||||||
|
style="min-width: 170px"
|
||||||
|
:style="menuStyle"
|
||||||
|
@mousedown.stop
|
||||||
|
@pointerdown.stop
|
||||||
|
>
|
||||||
|
<ContextSubmenu
|
||||||
|
:icon="IconLayoutAlignMiddle"
|
||||||
|
:label="t('align')"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
|
||||||
|
@click.stop="alignSelectedAreas('left')"
|
||||||
|
>
|
||||||
|
<IconLayoutAlignLeft class="w-4 h-4" />
|
||||||
|
<span>{{ t('align_left') }}</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
|
||||||
|
@click.stop="alignSelectedAreas('right')"
|
||||||
|
>
|
||||||
|
<IconLayoutAlignRight class="w-4 h-4" />
|
||||||
|
<span>{{ t('align_right') }}</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
|
||||||
|
@click.stop="alignSelectedAreas('top')"
|
||||||
|
>
|
||||||
|
<IconLayoutAlignTop class="w-4 h-4" />
|
||||||
|
<span>{{ t('align_top') }}</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
|
||||||
|
@click.stop="alignSelectedAreas('bottom')"
|
||||||
|
>
|
||||||
|
<IconLayoutAlignBottom class="w-4 h-4" />
|
||||||
|
<span>{{ t('align_bottom') }}</span>
|
||||||
|
</button>
|
||||||
|
</ContextSubmenu>
|
||||||
|
<ContextSubmenu
|
||||||
|
:icon="IconAspectRatio"
|
||||||
|
:label="t('resize')"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
|
||||||
|
@click.stop="resizeSelectedAreas('width')"
|
||||||
|
>
|
||||||
|
<IconArrowsHorizontal class="w-4 h-4" />
|
||||||
|
<span>{{ t('width') }}</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
|
||||||
|
@click.stop="resizeSelectedAreas('height')"
|
||||||
|
>
|
||||||
|
<IconArrowsVertical class="w-4 h-4" />
|
||||||
|
<span>{{ t('height') }}</span>
|
||||||
|
</button>
|
||||||
|
</ContextSubmenu>
|
||||||
|
<hr
|
||||||
|
v-if="showFont || showCondition"
|
||||||
|
class="my-1 border-neutral-200"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-if="showFont"
|
||||||
|
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
|
||||||
|
@click.stop="openFontModal"
|
||||||
|
>
|
||||||
|
<IconTypography class="w-4 h-4" />
|
||||||
|
<span>{{ t('font') }}</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="showCondition"
|
||||||
|
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
|
||||||
|
@click.stop="openConditionModal"
|
||||||
|
>
|
||||||
|
<IconRouteAltLeft class="w-4 h-4" />
|
||||||
|
<span>{{ t('condition') }}</span>
|
||||||
|
</button>
|
||||||
|
<hr class="my-1 border-neutral-200">
|
||||||
|
<button
|
||||||
|
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center justify-between text-sm"
|
||||||
|
@click.stop="$emit('copy')"
|
||||||
|
>
|
||||||
|
<span class="flex items-center space-x-2">
|
||||||
|
<IconCopy class="w-4 h-4" />
|
||||||
|
<span>{{ t('copy') }}</span>
|
||||||
|
</span>
|
||||||
|
<span class="text-xs text-base-content/60 ml-4">{{ isMac ? '⌘C' : 'Ctrl+C' }}</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center justify-between text-sm text-red-600"
|
||||||
|
@click.stop="$emit('delete')"
|
||||||
|
>
|
||||||
|
<span class="flex items-center space-x-2">
|
||||||
|
<IconTrashX class="w-4 h-4" />
|
||||||
|
<span>{{ t('remove') }}</span>
|
||||||
|
</span>
|
||||||
|
<span class="text-xs text-base-content/60 ml-4">Del</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<Teleport
|
||||||
|
v-if="isShowFontModal"
|
||||||
|
:to="modalContainerEl"
|
||||||
|
>
|
||||||
|
<FontModal
|
||||||
|
:field="multiSelectField"
|
||||||
|
:area="contextMenu.area"
|
||||||
|
:editable="editable"
|
||||||
|
:build-default-name="buildDefaultName"
|
||||||
|
@save="handleSaveMultiSelectFontModal"
|
||||||
|
@close="closeModal"
|
||||||
|
/>
|
||||||
|
</Teleport>
|
||||||
|
<Teleport
|
||||||
|
v-if="isShowConditionsModal"
|
||||||
|
:to="modalContainerEl"
|
||||||
|
>
|
||||||
|
<ConditionsModal
|
||||||
|
:item="multiSelectField"
|
||||||
|
:build-default-name="buildDefaultName"
|
||||||
|
:exclude-field-uuids="selectedFields.map(f => f.uuid)"
|
||||||
|
@save="handleSaveMultiSelectConditionsModal"
|
||||||
|
@close="closeModal"
|
||||||
|
/>
|
||||||
|
</Teleport>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { IconCopy, IconTrashX, IconTypography, IconRouteAltLeft, IconLayoutAlignLeft, IconLayoutAlignRight, IconLayoutAlignTop, IconLayoutAlignBottom, IconLayoutAlignMiddle, IconAspectRatio, IconArrowsHorizontal, IconArrowsVertical } from '@tabler/icons-vue'
|
||||||
|
import FontModal from './font_modal'
|
||||||
|
import ConditionsModal from './conditions_modal'
|
||||||
|
import ContextSubmenu from './field_context_submenu'
|
||||||
|
import Field from './field'
|
||||||
|
import FieldType from './field_type'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'SelectionContextMenu',
|
||||||
|
components: {
|
||||||
|
IconCopy,
|
||||||
|
IconTrashX,
|
||||||
|
IconTypography,
|
||||||
|
IconRouteAltLeft,
|
||||||
|
IconLayoutAlignLeft,
|
||||||
|
IconLayoutAlignRight,
|
||||||
|
IconLayoutAlignTop,
|
||||||
|
IconLayoutAlignBottom,
|
||||||
|
FontModal,
|
||||||
|
IconArrowsHorizontal,
|
||||||
|
IconArrowsVertical,
|
||||||
|
ConditionsModal,
|
||||||
|
ContextSubmenu
|
||||||
|
},
|
||||||
|
inject: ['t', 'save', 'selectedAreasRef', 'getFieldTypeIndex'],
|
||||||
|
props: {
|
||||||
|
contextMenu: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
editable: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
template: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
withCondition: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['copy', 'delete', 'close'],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
isShowFontModal: false,
|
||||||
|
isShowConditionsModal: false,
|
||||||
|
multiSelectField: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
modalContainerEl () {
|
||||||
|
return this.$el.getRootNode().querySelector('#docuseal_modal_container')
|
||||||
|
},
|
||||||
|
selectedFields () {
|
||||||
|
return this.selectedAreasRef.value.map((area) => {
|
||||||
|
return this.template.fields.find((f) => f.areas?.includes(area))
|
||||||
|
}).filter(Boolean)
|
||||||
|
},
|
||||||
|
isMac () {
|
||||||
|
return (navigator.userAgentData?.platform || navigator.platform)?.toLowerCase()?.includes('mac')
|
||||||
|
},
|
||||||
|
menuStyle () {
|
||||||
|
return {
|
||||||
|
left: this.contextMenu.x + 'px',
|
||||||
|
top: this.contextMenu.y + 'px'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
showFont () {
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
showCondition () {
|
||||||
|
return this.withCondition
|
||||||
|
},
|
||||||
|
fieldNames: FieldType.computed.fieldNames,
|
||||||
|
fieldLabels: FieldType.computed.fieldLabels
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
document.addEventListener('keydown', this.onKeyDown)
|
||||||
|
document.addEventListener('mousedown', this.handleClickOutside)
|
||||||
|
|
||||||
|
this.$nextTick(() => this.checkMenuPosition())
|
||||||
|
},
|
||||||
|
beforeUnmount () {
|
||||||
|
document.removeEventListener('keydown', this.onKeyDown)
|
||||||
|
document.removeEventListener('mousedown', this.handleClickOutside)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
IconLayoutAlignMiddle,
|
||||||
|
IconAspectRatio,
|
||||||
|
buildDefaultName: Field.methods.buildDefaultName,
|
||||||
|
checkMenuPosition () {
|
||||||
|
if (this.$refs.menu) {
|
||||||
|
const rect = this.$refs.menu.getBoundingClientRect()
|
||||||
|
const overflow = rect.bottom - window.innerHeight
|
||||||
|
|
||||||
|
if (overflow > 0) {
|
||||||
|
this.contextMenu.y = this.contextMenu.y - overflow - 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onKeyDown (event) {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
|
||||||
|
this.$emit('close')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleClickOutside (event) {
|
||||||
|
if (this.$refs.menu && !this.$refs.menu.contains(event.target)) {
|
||||||
|
this.$emit('close')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
openFontModal () {
|
||||||
|
this.multiSelectField = {
|
||||||
|
name: this.t('fields_selected').replace('{count}', this.selectedFields.length),
|
||||||
|
preferences: {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const preferencesStrings = this.selectedFields.map((f) => JSON.stringify(f.preferences || {}))
|
||||||
|
|
||||||
|
if (preferencesStrings.every((s) => s === preferencesStrings[0])) {
|
||||||
|
this.multiSelectField.preferences = JSON.parse(preferencesStrings[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isShowFontModal = true
|
||||||
|
},
|
||||||
|
openConditionModal () {
|
||||||
|
this.multiSelectField = {
|
||||||
|
name: this.t('fields_selected').replace('{count}', this.selectedFields.length),
|
||||||
|
conditions: []
|
||||||
|
}
|
||||||
|
|
||||||
|
const conditionStrings = this.selectedFields.map((f) => JSON.stringify(f.conditions || []))
|
||||||
|
|
||||||
|
if (conditionStrings.every((s) => s === conditionStrings[0])) {
|
||||||
|
this.multiSelectField.conditions = JSON.parse(conditionStrings[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isShowConditionsModal = true
|
||||||
|
},
|
||||||
|
closeModal () {
|
||||||
|
this.isShowFontModal = false
|
||||||
|
this.isShowConditionsModal = false
|
||||||
|
this.multiSelectField = null
|
||||||
|
|
||||||
|
this.$emit('close')
|
||||||
|
},
|
||||||
|
alignSelectedAreas (direction) {
|
||||||
|
const areas = this.selectedAreasRef.value
|
||||||
|
|
||||||
|
let targetValue
|
||||||
|
|
||||||
|
if (direction === 'left') {
|
||||||
|
targetValue = Math.min(...areas.map(a => a.x))
|
||||||
|
areas.forEach((area) => { area.x = targetValue })
|
||||||
|
} else if (direction === 'right') {
|
||||||
|
targetValue = Math.max(...areas.map(a => a.x + a.w))
|
||||||
|
areas.forEach((area) => { area.x = targetValue - area.w })
|
||||||
|
} else if (direction === 'top') {
|
||||||
|
targetValue = Math.min(...areas.map(a => a.y))
|
||||||
|
areas.forEach((area) => { area.y = targetValue })
|
||||||
|
} else if (direction === 'bottom') {
|
||||||
|
targetValue = Math.max(...areas.map(a => a.y + a.h))
|
||||||
|
areas.forEach((area) => { area.y = targetValue - area.h })
|
||||||
|
}
|
||||||
|
|
||||||
|
this.save()
|
||||||
|
|
||||||
|
this.$emit('close')
|
||||||
|
},
|
||||||
|
resizeSelectedAreas (dimension) {
|
||||||
|
const areas = this.selectedAreasRef.value
|
||||||
|
|
||||||
|
const values = areas.map(a => dimension === 'width' ? a.w : a.h).sort((a, b) => a - b)
|
||||||
|
const medianValue = values[Math.floor(values.length / 2)]
|
||||||
|
|
||||||
|
if (dimension === 'width') {
|
||||||
|
areas.forEach((area) => { area.w = medianValue })
|
||||||
|
} else if (dimension === 'height') {
|
||||||
|
areas.forEach((area) => {
|
||||||
|
const diff = medianValue - area.h
|
||||||
|
area.y = area.y - diff
|
||||||
|
area.h = medianValue
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
this.save()
|
||||||
|
|
||||||
|
this.$emit('close')
|
||||||
|
},
|
||||||
|
handleSaveMultiSelectFontModal () {
|
||||||
|
this.selectedFields.forEach((field) => {
|
||||||
|
field.preferences = { ...field.preferences, ...this.multiSelectField.preferences }
|
||||||
|
})
|
||||||
|
|
||||||
|
this.save()
|
||||||
|
|
||||||
|
this.closeModal()
|
||||||
|
},
|
||||||
|
handleSaveMultiSelectConditionsModal () {
|
||||||
|
this.selectedFields.forEach((field) => {
|
||||||
|
field.conditions = JSON.parse(JSON.stringify(this.multiSelectField.conditions))
|
||||||
|
})
|
||||||
|
|
||||||
|
this.save()
|
||||||
|
|
||||||
|
this.closeModal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -290,7 +290,11 @@ export default {
|
|||||||
if (resp.ok) {
|
if (resp.ok) {
|
||||||
resp.json().then((data) => {
|
resp.json().then((data) => {
|
||||||
this.$emit('success', data)
|
this.$emit('success', data)
|
||||||
this.$refs.input.value = ''
|
|
||||||
|
if (this.$refs.input) {
|
||||||
|
this.$refs.input.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
this.isLoading = false
|
this.isLoading = false
|
||||||
})
|
})
|
||||||
} else if (resp.status === 422) {
|
} else if (resp.status === 422) {
|
||||||
|
|||||||
@@ -164,11 +164,13 @@ class ProcessSubmitterCompletionJob
|
|||||||
next_submitter_items =
|
next_submitter_items =
|
||||||
if submission.template_submitters.any? { |s| s['order'] }
|
if submission.template_submitters.any? { |s| s['order'] }
|
||||||
submitter_groups =
|
submitter_groups =
|
||||||
submission.template_submitters.group_by.with_index { |s, index| s['order'] || index }
|
submission.template_submitters
|
||||||
|
.group_by.with_index { |s, index| s['order'] || index }
|
||||||
|
.sort_by(&:first).pluck(1)
|
||||||
|
|
||||||
current_group_index = submitter_groups.find { |_, group| group.any? { |s| s['uuid'] == submitter.uuid } }&.first
|
current_group_index = submitter_groups.index { |group| group.any? { |s| s['uuid'] == submitter.uuid } }
|
||||||
|
|
||||||
if submitter_groups[current_group_index + 1] &&
|
if current_group_index && submitter_groups[current_group_index + 1] &&
|
||||||
submitters_index.values_at(*submitter_groups[current_group_index].pluck('uuid'))
|
submitters_index.values_at(*submitter_groups[current_group_index].pluck('uuid'))
|
||||||
.compact.all?(&:completed_at?)
|
.compact.all?(&:completed_at?)
|
||||||
submitter_groups[current_group_index + 1]
|
submitter_groups[current_group_index + 1]
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ class SendTestWebhookRequestJob
|
|||||||
Addressable::URI.parse(webhook_url.url).normalize
|
Addressable::URI.parse(webhook_url.url).normalize
|
||||||
end
|
end
|
||||||
|
|
||||||
raise HttpsError, 'Only HTTPS is allowed.' if uri.scheme != 'https'
|
raise HttpsError, 'Only HTTPS is allowed.' if uri.scheme != 'https' || [443, nil].exclude?(uri.port)
|
||||||
raise LocalhostError, "Can't send to localhost." if uri.host.in?(SendWebhookRequest::LOCALHOSTS)
|
raise LocalhostError, "Can't send to localhost." if uri.host.in?(SendWebhookRequest::LOCALHOSTS)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<%= ff.label :password, 'Password', class: 'label' %>
|
<%= ff.label :password, 'Password', class: 'label' %>
|
||||||
<%= ff.password_field :password, value: value['password'], class: 'base-input' %>
|
<%= ff.password_field :password, class: 'base-input', required: value['password'].present?, placeholder: value['password'].present? ? '*************' : '' %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid md:grid-cols-2 gap-4">
|
<div class="grid md:grid-cols-2 gap-4">
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<%= ff.label :security_label, 'SMTP Security', class: 'label' %>
|
<%= ff.label :security_label, 'SMTP Security', class: 'label' %>
|
||||||
<div class="flex items-center space-x-6">
|
<div class="flex items-center space-x-6">
|
||||||
<% [%w[Auto none], %w[SSL ssl], %w[TLS tls], %w[Noverify noverify]].each do |(label, val)| %>
|
<% [%w[STARTTLS none], %w[TLS tls], %w[SSL ssl], %w[Noverify noverify]].each do |(label, val)| %>
|
||||||
<%= ff.label :security, value: val, for: "#{val}_radio", class: 'label' do %>
|
<%= ff.label :security, value: val, for: "#{val}_radio", class: 'label' do %>
|
||||||
<%= ff.radio_button :security, val, checked: (value['security'].blank? && val == 'none') || value['security'] == val, id: "#{val}_radio", class: 'base-radio mr-2' %>
|
<%= ff.radio_button :security, val, checked: (value['security'].blank? && val == 'none') || value['security'] == val, id: "#{val}_radio", class: 'base-radio mr-2' %>
|
||||||
<%= label %>
|
<%= label %>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
<% if params[:modal].present? %>
|
<% if params[:modal].present? %>
|
||||||
<% url_params = Rails.application.routes.recognize_path(params[:modal], method: :get) %>
|
<% url_params = Rails.application.routes.recognize_path(params[:modal], method: :get) %>
|
||||||
<% if url_params[:action] == 'new' %>
|
<% if url_params[:action] == 'new' %>
|
||||||
<open-modal src="<%= params[:modal] %>"></open-modal>
|
<open-modal src="<%= url_for(url_params) %>"></open-modal>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<turbo-frame id="modal"></turbo-frame>
|
<turbo-frame id="modal"></turbo-frame>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"id": "/",
|
"id": "/",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "/logo.svg",
|
"src": "/favicon.svg",
|
||||||
"type": "image/svg+xml",
|
"type": "image/svg+xml",
|
||||||
"sizes": "any"
|
"sizes": "any"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,32 +1,4 @@
|
|||||||
<svg height="<%= local_assigns.fetch(:height, '37') %>" width="<%= local_assigns.fetch(:width, '37') %>" class="<%= local_assigns[:class] %>" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 511.998 511.998" xml:space="preserve">
|
<svg class="<%= local_assigns[:class] %>" height="<%= local_assigns.fetch(:height, '37') %>" width="<%= local_assigns.fetch(:width, '37') %>" style="color: #e0753f" viewBox="0 0 180 180" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path style="fill:#AA968C;" d="M503.999,247.999c0,128.13-111.033,240-248,240S8,376.129,8,247.999s111.033-224,248-224
|
<path fill="currentColor" d="M 178.224 72.09 c -0.296 -1.463 -0.627 -2.919 -0.996 -4.364 -0.293 -1.151 -0.616 -2.293 -0.956 -3.433 -0.301 -1.008 -0.612 -2.014 -0.95 -3.012 -0.531 -1.578 -1.113 -3.142 -1.735 -4.694 -0.216 -0.54 -0.433 -1.082 -0.661 -1.618 -0.195 -0.462 -0.399 -0.917 -0.601 -1.375 -0.262 -0.591 -0.53 -1.177 -0.804 -1.762 -0.074 -0.159 -0.151 -0.315 -0.226 -0.474 -0.209 -0.441 -0.422 -0.881 -0.638 -1.318 -0.076 -0.154 -0.153 -0.306 -0.229 -0.459 -0.236 -0.471 -0.477 -0.939 -0.721 -1.406 -0.053 -0.101 -0.105 -0.201 -0.158 -0.302 -1.143 -2.16 -2.367 -4.269 -3.68 -6.322 -0.116 -0.181 -0.237 -0.359 -0.355 -0.539 -0.094 -0.144 -0.189 -0.288 -0.284 -0.432 -0.284 -0.431 -0.57 -0.861 -0.862 -1.287 -0.112 -0.164 -0.225 -0.326 -0.338 -0.489 -0.193 -0.279 -0.382 -0.56 -0.579 -0.836 -0.089 -0.125 -0.182 -0.249 -0.273 -0.374 -0.13 -0.182 -0.264 -0.362 -0.395 -0.542 -0.277 -0.38 -0.556 -0.76 -0.838 -1.135 -0.15 -0.199 -0.303 -0.395 -0.454 -0.593 -0.21 -0.274 -0.417 -0.552 -0.63 -0.823 -0.055 -0.069 -0.111 -0.136 -0.166 -0.205 -0.482 -0.61 -0.971 -1.216 -1.47 -1.814 -0.129 -0.155 -0.262 -0.306 -0.392 -0.461 -0.402 -0.476 -0.808 -0.95 -1.22 -1.417 -0.186 -0.212 -0.375 -0.422 -0.563 -0.631 -0.384 -0.428 -0.773 -0.854 -1.167 -1.276 -0.176 -0.189 -0.351 -0.379 -0.529 -0.567 -0.564 -0.595 -1.134 -1.186 -1.716 -1.768 -1.091 -1.091 -2.207 -2.15 -3.346 -3.178 -1.016 -0.919 -2.05 -1.815 -3.103 -2.684 -0.772 -0.636 -1.557 -1.255 -2.348 -1.864 -3.465 -2.67 -7.112 -5.075 -10.927 -7.209 -2.869 -1.604 -5.83 -3.06 -8.883 -4.351 -2.443 -1.033 -4.922 -1.948 -7.428 -2.756 -8.879 -2.863 -18.13 -4.318 -27.605 -4.318 -3.19 0 -6.354 0.169 -9.488 0.496 -4.036 0.421 -8.019 1.114 -11.94 2.073 -1.732 0.423 -3.452 0.892 -5.157 1.42 -2.856 0.883 -5.673 1.912 -8.447 3.085 -2.645 1.118 -5.222 2.357 -7.729 3.711 -2.574 1.39 -5.073 2.901 -7.494 4.533 -1.195 0.805 -2.37 1.64 -3.527 2.503 -1.156 0.864 -2.292 1.756 -3.408 2.676 -0.553 0.456 -1.1 0.919 -1.643 1.389 -1.649 1.427 -3.252 2.92 -4.806 4.473 -2.582 2.582 -4.991 5.299 -7.222 8.138 -0.892 1.135 -1.756 2.292 -2.59 3.467 -0.417 0.588 -0.827 1.18 -1.23 1.778 -0.403 0.597 -0.798 1.199 -1.186 1.806 -0.388 0.607 -0.769 1.218 -1.143 1.835 -2.241 3.697 -4.216 7.562 -5.916 11.582 -1.095 2.589 -2.059 5.217 -2.901 7.877 -0.153 0.482 -0.3 0.965 -0.444 1.449 -0.339 1.14 -0.663 2.282 -0.956 3.433 -0.369 1.446 -0.7 2.901 -0.996 4.364 -1.034 5.121 -1.618 10.343 -1.749 15.637 -0.018 0.757 -0.028 1.514 -0.028 2.274 0 1.123 0.02 2.244 0.062 3.361 0.285 7.82 1.568 15.475 3.825 22.879 0.044 0.147 0.088 0.295 0.133 0.441 0.877 2.823 1.894 5.608 3.054 8.35 0.85 2.009 1.769 3.98 2.755 5.912 0.539 1.057 1.105 2.099 1.685 3.132 4.013 7.142 8.98 13.698 14.846 19.564 7.713 7.713 16.611 13.878 26.477 18.352 0.705 0.32 1.415 0.632 2.131 0.935 2.081 0.88 4.185 1.679 6.313 2.396 9.217 3.106 18.85 4.677 28.719 4.677 8.031 0 15.902 -1.047 23.522 -3.107 0.633 -0.172 1.266 -0.35 1.895 -0.535 0.757 -0.222 1.509 -0.456 2.26 -0.698 0.717 -0.232 1.431 -0.474 2.145 -0.723 1.752 -0.616 3.49 -1.281 5.211 -2.009 0.755 -0.319 1.503 -0.651 2.247 -0.989 1.237 -0.563 2.459 -1.15 3.664 -1.766 0.644 -0.328 1.283 -0.665 1.917 -1.009 1.654 -0.896 3.274 -1.848 4.865 -2.844 5.736 -3.591 11.06 -7.827 15.912 -12.679 0.775 -0.775 1.534 -1.562 2.278 -2.36 5.204 -5.59 9.636 -11.754 13.246 -18.417 0.343 -0.634 0.68 -1.274 1.009 -1.917 0.482 -0.944 0.943 -1.9 1.392 -2.863 0.471 -1.007 0.928 -2.021 1.364 -3.049 1.22 -2.886 2.281 -5.82 3.187 -8.793 0.559 -1.833 1.056 -3.68 1.494 -5.542 0.108 -0.458 0.211 -0.916 0.312 -1.376 0.194 -0.883 0.373 -1.77 0.539 -2.659 1.02 -5.455 1.542 -11.02 1.542 -16.663 0 -6.074 -0.595 -12.058 -1.776 -17.911 z m -161.733 19.614 c -1.118 -56.662 44.604 -74.877 60.998 -67.647 2.187 0.965 4.732 2.431 7.042 2.96 5.295 1.213 13.432 -3.113 13.521 6.273 0.078 8.156 -3.389 13.108 -10.797 16.177 -7.539 3.124 -14.777 9.181 -19.95 15.493 -21.487 26.216 -31.231 68.556 -7.565 94.296 -13.679 -5.545 -42.418 -25.467 -43.248 -67.552 z m 91.109 72.619 c -0.053 0.008 -4.171 0.775 -4.171 0.775 0 0 -15.862 -22.957 -23.509 -21.719 11.291 16.04 12.649 22.625 12.649 22.625 -0.053 0.001 -0.107 0.001 -0.161 0.003 -51.831 2.131 -42.785 -64.026 -28.246 -86.502 -1.555 13.073 8.878 39.992 39.034 44.1 9.495 1.293 32.302 -3.275 41.015 -11.38 0.098 1.825 0.163 3.85 0.159 6.013 -0.046 23.538 -13.47 42.743 -36.77 46.085 z m 30.575 -15.708 c 9.647 -9.263 12.869 -27.779 9.103 -44.137 -4.608 -20.011 -28.861 -32.383 -40.744 -35.564 5.766 -8.089 27.908 -14.274 39.567 5.363 -5.172 -10.519 -13.556 -23.023 -1.732 -33.128 12.411 13.329 19.411 29.94 20.161 48.7 0.75 18.753 -6.64 41.768 -26.355 58.765 z" />
|
||||||
S503.999,119.869,503.999,247.999z" />
|
<circle fill="currentColor" cx="71.927" cy="32.004" r="2.829" />
|
||||||
<path style="fill:#AA968C;" d="M255.999,23.999C119.033,23.999,8,119.868,8,247.999c0,24.631,4.138,48.647,11.74,71.397
|
|
||||||
c25.518,34.29,66.232,56.602,112.26,56.602c53.893,0,100.6-30.495,124-75.13c23.4,44.636,70.107,75.13,124,75.13
|
|
||||||
c46.028,0,86.743-22.312,112.26-56.602c7.602-22.75,11.74-46.767,11.74-71.397C503.999,119.868,392.966,23.999,255.999,23.999z" />
|
|
||||||
<circle style="fill:#C8AF9B;" cx="256" cy="351.999" r="136" />
|
|
||||||
<g>
|
|
||||||
<circle style="fill:#464655;" cx="132" cy="203.999" r="28" />
|
|
||||||
<circle style="fill:#464655;" cx="380" cy="203.999" r="28" />
|
|
||||||
<path style="fill:#464655;" d="M269.949,284.516c-7.672,10.741-20.227,10.741-27.899,0l-12.101-16.941
|
|
||||||
c-7.672-10.741-3.15-19.53,10.05-19.53h32c13.2,0,17.723,8.788,10.05,19.53L269.949,284.516z" />
|
|
||||||
</g>
|
|
||||||
<path style="fill:#AA968C;" d="M 350.964 399.998 C 316.628 399.998 299.021 351.998 255.882 351.998 C 212.742 351.998 195.135 399.998 160.801 399.998 C 145.395 399.998 131.723 394.147 120.621 374.798 C 131.595 439.03 187.893 487.998 255.881 487.998 C 323.868 487.998 380.168 439.03 391.14 374.798 C 380.04 394.148 366.368 399.998 350.964 399.998 Z" />
|
|
||||||
<g>
|
|
||||||
<path style="fill:#8C7873;" d="M32,423.998c-3.172,0-6.18-1.906-7.43-5.031c-1.641-4.105,0.359-8.758,4.461-10.402l160.008-64
|
|
||||||
c4.062-1.617,8.758,0.352,10.398,4.457s-0.359,8.758-4.461,10.402l-160.008,64C34,423.811,32.992,423.998,32,423.998z" />
|
|
||||||
<path style="fill:#8C7873;" d="M15.992,375.995c-3.547,0-6.781-2.375-7.727-5.965c-1.125-4.273,1.422-8.648,5.695-9.773l152-40
|
|
||||||
c4.289-1.121,8.648,1.426,9.773,5.703c1.125,4.273-1.422,8.648-5.695,9.773l-152,40C17.351,375.913,16.672,375.995,15.992,375.995z
|
|
||||||
" />
|
|
||||||
<path style="fill:#8C7873;" d="M7.992,335.995c-3.812,0-7.187-2.73-7.867-6.609c-0.773-4.352,2.133-8.5,6.484-9.27l136-24
|
|
||||||
c4.328-0.77,8.508,2.141,9.266,6.488c0.773,4.352-2.133,8.5-6.484,9.27l-136,24C8.922,335.956,8.453,335.995,7.992,335.995z" />
|
|
||||||
<path style="fill:#8C7873;" d="M480,423.998c3.172,0,6.18-1.906,7.43-5.031c1.641-4.105-0.359-8.758-4.461-10.402l-160.008-64
|
|
||||||
c-4.063-1.617-8.758,0.352-10.398,4.457s0.359,8.758,4.461,10.402l160.008,64C478,423.811,479.007,423.998,480,423.998z" />
|
|
||||||
<path style="fill:#8C7873;" d="M496.007,375.995c3.547,0,6.781-2.375,7.727-5.965c1.125-4.273-1.422-8.648-5.695-9.773l-152-40
|
|
||||||
c-4.289-1.121-8.648,1.426-9.773,5.703c-1.125,4.273,1.422,8.648,5.695,9.773l152,40
|
|
||||||
C494.648,375.913,495.328,375.995,496.007,375.995z" />
|
|
||||||
<path style="fill:#8C7873;" d="M504.007,335.995c3.813,0,7.188-2.73,7.867-6.609c0.773-4.352-2.133-8.5-6.484-9.27l-136-24
|
|
||||||
c-4.328-0.77-8.508,2.141-9.266,6.488c-0.773,4.352,2.133,8.5,6.484,9.27l136,24C503.078,335.956,503.546,335.995,504.007,335.995z
|
|
||||||
" />
|
|
||||||
</g>
|
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 4.8 KiB |
@@ -27,4 +27,5 @@
|
|||||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
||||||
<link rel="icon" type="image/png" sizes="96x96" href="/favicon-96x96.png">
|
<link rel="icon" type="image/png" sizes="96x96" href="/favicon-96x96.png">
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
||||||
<meta name="theme-color" content="#faf7f5">
|
<meta name="theme-color" content="#faf7f5">
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
<% if params[:q].present? %>
|
<% if params[:q].present? %>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<a href="<%= url_for(params.to_unsafe_h.except(:q)) %>" title="<%= t('clear') %>" class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-auto text-neutral text-2xl font-extralight">
|
<a href="<%= url_for(params: request.query_parameters.except('q')) %>" title="<%= t('clear') %>" class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-auto text-neutral text-2xl font-extralight">
|
||||||
×
|
×
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<%= fff.label :secret_access_key, class: 'label' %>
|
<%= fff.label :secret_access_key, class: 'label' %>
|
||||||
<%= fff.password_field :secret_access_key, value: configs['secret_access_key'], required: true, class: 'base-input' %>
|
<%= fff.password_field :secret_access_key, required: true, class: 'base-input', placeholder: configs['secret_access_key'].present? ? '*************' : '' %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid md:grid-cols-2 gap-4">
|
<div class="grid md:grid-cols-2 gap-4">
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<%= fff.label :storage_access_key, 'Storage Access Key', class: 'label' %>
|
<%= fff.label :storage_access_key, 'Storage Access Key', class: 'label' %>
|
||||||
<%= fff.password_field :storage_access_key, value: configs['storage_access_key'], required: true, class: 'base-input' %>
|
<%= fff.password_field :storage_access_key, required: true, class: 'base-input', placeholder: configs['storage_access_key'].present? ? '*************' : '' %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<%= fff.label :credentials, 'Credentials (JSON key content)', class: 'label' %>
|
<%= fff.label :credentials, 'Credentials (JSON key content)', class: 'label' %>
|
||||||
<%= fff.text_area :credentials, value: configs['credentials'], required: true, class: 'base-textarea w-full font-mono', rows: 4 %>
|
<%= fff.text_area :credentials, required: true, class: 'base-textarea w-full font-mono', rows: 4, placeholder: configs['credentials'].present? ? "{\n**REDACTED**\n}" : '' %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
@@ -60,7 +60,7 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
<% elsif field['type'] == 'cells' && area['cell_w'].to_f > 0.0 %>
|
<% elsif field['type'] == 'cells' && area['cell_w'].to_f > 0.0 %>
|
||||||
<% cell_width = area['cell_w'] / area['w'] * 100 %>
|
<% cell_width = area['cell_w'] / area['w'] * 100 %>
|
||||||
<div class="w-full flex items-center <%= 'justify-end' if align == 'right' %>">
|
<div class="w-full flex <%= valign == 'top' ? 'items-start' : (valign == 'bottom' ? 'items-end' : 'items-center') %> <%= 'justify-end' if align == 'right' %>">
|
||||||
<% (0..(area['w'] / area['cell_w']).ceil).each do |index| %>
|
<% (0..(area['w'] / area['cell_w']).ceil).each do |index| %>
|
||||||
<% if value[index] %>
|
<% if value[index] %>
|
||||||
<div class="text-center flex-none" style="width: <%= cell_width %>%;"><%= value[index] %></div>
|
<div class="text-center flex-none" style="width: <%= cell_width %>%;"><%= value[index] %></div>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<% require_phone_2fa = @template.preferences['require_phone_2fa'] == true %>
|
<% require_phone_2fa = @template.preferences['require_phone_2fa'] == true %>
|
||||||
<% require_email_2fa = @template.preferences['require_email_2fa'] == true %>
|
<% require_email_2fa = @template.preferences['require_email_2fa'] == true %>
|
||||||
<% prefillable_fields = @template.fields.select { |f| f['prefillable'] } %>
|
<% prefillable_fields = @template.fields.select { |f| f['prefillable'] } %>
|
||||||
|
<% default_tab = cookies.permanent[:add_recipients_tab].presence || 'email' %>
|
||||||
<% recipient_form_fields = Accounts.load_recipient_form_fields(current_account) if prefillable_fields.blank? %>
|
<% recipient_form_fields = Accounts.load_recipient_form_fields(current_account) if prefillable_fields.blank? %>
|
||||||
<% only_detailed = require_phone_2fa || require_email_2fa || prefillable_fields.present? || recipient_form_fields.present? %>
|
<% only_detailed = require_phone_2fa || require_email_2fa || prefillable_fields.present? || recipient_form_fields.present? %>
|
||||||
<%= render 'shared/turbo_modal_large', title: params[:selfsign] ? t('add_recipients') : t('add_new_recipients') do %>
|
<%= render 'shared/turbo_modal_large', title: params[:selfsign] ? t('add_recipients') : t('add_new_recipients') do %>
|
||||||
@@ -8,25 +9,25 @@
|
|||||||
<toggle-visible data-element-ids="<%= options.map(&:last).to_json %>" class="relative text-center px-2 mt-4 block">
|
<toggle-visible data-element-ids="<%= options.map(&:last).to_json %>" class="relative text-center px-2 mt-4 block">
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<% options.each_with_index do |(label, value), index| %>
|
<% options.each_with_index do |(label, value), index| %>
|
||||||
<div>
|
<%= content_tag(value == 'list' ? 'span' : 'toggle-cookies', data: { value:, key: 'add_recipients_tab' }) do %>
|
||||||
<%= radio_button_tag 'option', value, value == (only_detailed ? 'detailed' : 'email'), class: 'peer hidden', data: { action: 'change:toggle-visible#trigger' } %>
|
<%= radio_button_tag 'option', value, value == (only_detailed ? 'detailed' : default_tab), class: 'peer hidden', data: { action: 'change:toggle-visible#trigger' } %>
|
||||||
<label for="option_<%= value %>" class="block bg-base-200 md:min-w-[112px] text-sm font-semibold whitespace-nowrap py-1.5 px-4 peer-checked:bg-base-300 <%= 'hidden sm:inline-block' if value == 'list' %> <%= 'rounded-l-3xl' if index.zero? %> <%= 'rounded-r-3xl sm:rounded-r-none' if value == 'detailed' %> <%= 'rounded-r-3xl' if index == options.size - 1 %>">
|
<label for="option_<%= value %>" class="block bg-base-200 md:min-w-[112px] text-sm font-semibold whitespace-nowrap py-1.5 px-4 peer-checked:bg-base-300 <%= 'hidden sm:inline-block' if value == 'list' %> <%= 'rounded-l-3xl' if index.zero? %> <%= 'rounded-r-3xl sm:rounded-r-none' if value == 'detailed' %> <%= 'rounded-r-3xl' if index == options.size - 1 %>">
|
||||||
<%= label %>
|
<%= label %>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</toggle-visible>
|
</toggle-visible>
|
||||||
<div class="px-5 mb-5 mt-4">
|
<div class="px-5 mb-5 mt-4">
|
||||||
<% unless only_detailed %>
|
<% unless only_detailed %>
|
||||||
<div id="email">
|
<div id="email" class="<%= 'hidden' if default_tab != 'email' %>">
|
||||||
<%= render 'email_form', template: @template %>
|
<%= render 'email_form', template: @template %>
|
||||||
</div>
|
</div>
|
||||||
<div id="phone" class="hidden">
|
<div id="phone" class="<%= 'hidden' if default_tab != 'phone' %>">
|
||||||
<%= render 'phone_form', template: @template %>
|
<%= render 'phone_form', template: @template %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<div id="detailed" class="<%= 'hidden' unless only_detailed %>">
|
<div id="detailed" class="<%= 'hidden' if !only_detailed && default_tab != 'detailed' %>">
|
||||||
<%= render 'detailed_form', template: @template, require_phone_2fa:, require_email_2fa:, prefillable_fields:, recipient_form_fields: %>
|
<%= render 'detailed_form', template: @template, require_phone_2fa:, require_email_2fa:, prefillable_fields:, recipient_form_fields: %>
|
||||||
</div>
|
</div>
|
||||||
<div id="list" class="hidden">
|
<div id="list" class="hidden">
|
||||||
|
|||||||
@@ -68,7 +68,7 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% elsif @submission.submitters.to_a.size == 1 && !@submission.expired? && !@submission.submitters.to_a.first.declined_at? && !@submission.archived_at? %>
|
<% elsif @submission.submitters.to_a.size == 1 && !@submission.expired? && !@submission.submitters.to_a.first.declined_at? && !@submission.archived_at? && !@submission.template&.archived_at? %>
|
||||||
<%= render 'shared/clipboard_copy', text: submit_form_url(slug: @submission.submitters.to_a.first.slug, host: form_link_host), class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: t('copy_share_link'), copied_title: t('copied_to_clipboard') %>
|
<%= render 'shared/clipboard_copy', text: submit_form_url(slug: @submission.submitters.to_a.first.slug, host: form_link_host), class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: t('copy_share_link'), copied_title: t('copied_to_clipboard') %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
@@ -159,7 +159,7 @@
|
|||||||
<%= (@submission.template_submitters || @submission.template.submitters).find { |e| e['uuid'] == submitter&.uuid }&.dig('name') || "#{(index + 1).ordinalize} Submitter" %>
|
<%= (@submission.template_submitters || @submission.template.submitters).find { |e| e['uuid'] == submitter&.uuid }&.dig('name') || "#{(index + 1).ordinalize} Submitter" %>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<% if signed_in? && can?(:update, @submission) && submitter && !submitter.completed_at? && !submitter.declined_at? && !@submission.archived_at? && !@submission.expired? && !submitter.start_form_submission_events.any? %>
|
<% if signed_in? && can?(:update, @submission) && submitter && !submitter.completed_at? && !submitter.declined_at? && !@submission.archived_at? && !@submission.template&.archived_at? && !@submission.expired? && !submitter.start_form_submission_events.any? %>
|
||||||
<span class="tooltip tooltip-left" data-tip="<%= t('edit') %>">
|
<span class="tooltip tooltip-left" data-tip="<%= t('edit') %>">
|
||||||
<%= link_to edit_submitter_path(submitter), class: 'shrink-0 inline md:hidden md:group-hover:inline', data: { turbo_frame: 'modal' } do %>
|
<%= link_to edit_submitter_path(submitter), class: 'shrink-0 inline md:hidden md:group-hover:inline', data: { turbo_frame: 'modal' } do %>
|
||||||
<%= svg_icon('pencil', class: 'w-5 h-5') %>
|
<%= svg_icon('pencil', class: 'w-5 h-5') %>
|
||||||
@@ -225,15 +225,15 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if signed_in? && submitter && submitter.email && !submitter.completed_at && !@submission.archived_at? && can?(:update, @submission) && Accounts.can_send_emails?(current_account) && !@submission.expired? && !submitter.declined_at? %>
|
<% if signed_in? && submitter && submitter.email && !submitter.completed_at && !@submission.archived_at? && !@submission.template&.archived_at? && can?(:update, @submission) && Accounts.can_send_emails?(current_account) && !@submission.expired? && !submitter.declined_at? %>
|
||||||
<div class="mt-2 mb-1">
|
<div class="mt-2 mb-1">
|
||||||
<%= button_to button_title(title: submitter.sent_at? ? t('re_send_email') : t('send_email'), disabled_with: t('sending')), submitter_send_email_index_path(submitter_slug: submitter.slug), class: 'btn btn-sm btn-primary w-full' %>
|
<%= button_to button_title(title: submitter.sent_at? ? t('re_send_email') : t('send_email'), disabled_with: t('sending')), submitter_send_email_index_path(submitter_slug: submitter.slug), class: 'btn btn-sm btn-primary w-full' %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if signed_in? && submitter && submitter.phone && !submitter.completed_at && !@submission.archived_at? && can?(:update, @submission) && !@submission.expired? && !submitter.declined_at? %>
|
<% if signed_in? && submitter && submitter.phone && !submitter.completed_at && !@submission.archived_at? && !@submission.template&.archived_at? && can?(:update, @submission) && !@submission.expired? && !submitter.declined_at? %>
|
||||||
<%= render 'submissions/send_sms_button', submitter: %>
|
<%= render 'submissions/send_sms_button', submitter: %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if signed_in? && submitter && !submitter.completed_at? && !@submission.archived_at? && can?(:create, @submission) && !@submission.expired? && !submitter.declined_at? %>
|
<% if signed_in? && submitter && !submitter.completed_at? && !@submission.archived_at? && !@submission.template&.archived_at? && can?(:create, @submission) && !@submission.expired? && !submitter.declined_at? %>
|
||||||
<div class="mt-2 mb-1">
|
<div class="mt-2 mb-1">
|
||||||
<a class="btn btn-sm btn-primary w-full" target="_blank" href="<%= submit_form_path(slug: submitter.slug) %>">
|
<a class="btn btn-sm btn-primary w-full" target="_blank" href="<%= submit_form_path(slug: submitter.slug) %>">
|
||||||
<%= t('sign_in_person') %>
|
<%= t('sign_in_person') %>
|
||||||
|
|||||||
@@ -35,19 +35,19 @@
|
|||||||
<% if is_show_tabs %>
|
<% if is_show_tabs %>
|
||||||
<div class="flex items-center flex-col md:flex-row md:flex-wrap gap-2 mb-4">
|
<div class="flex items-center flex-col md:flex-row md:flex-wrap gap-2 mb-4">
|
||||||
<div class="flex items-center md:items-end flex-col md:flex-row gap-2 w-full md:w-fit">
|
<div class="flex items-center md:items-end flex-col md:flex-row gap-2 w-full md:w-fit">
|
||||||
<a href="<%= url_for(params.to_unsafe_h.except(:status)) %>" class="<%= params[:status].blank? ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-600">
|
<a href="<%= url_for(params: request.query_parameters.except('status')) %>" class="<%= params[:status].blank? ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-600">
|
||||||
<div class="flex items-center space-x-1">
|
<div class="flex items-center space-x-1">
|
||||||
<%= svg_icon('list', class: 'w-5 h-5') %>
|
<%= svg_icon('list', class: 'w-5 h-5') %>
|
||||||
<span class="font-normal"><%= t('all') %></span>
|
<span class="font-normal"><%= t('all') %></span>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<a href="<%= url_for(params.to_unsafe_h.merge(status: :pending)) %>" class="<%= params[:status] == 'pending' ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-600">
|
<a href="<%= url_for(params: request.query_parameters.merge('status' => 'pending')) %>" class="<%= params[:status] == 'pending' ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-600">
|
||||||
<div class="flex items-center space-x-1">
|
<div class="flex items-center space-x-1">
|
||||||
<%= svg_icon('clock', class: 'w-5 h-5') %>
|
<%= svg_icon('clock', class: 'w-5 h-5') %>
|
||||||
<span class="font-normal"><%= t('pending') %></span>
|
<span class="font-normal"><%= t('pending') %></span>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<a href="<%= url_for(params.to_unsafe_h.merge(status: :completed)) %>" class="<%= params[:status] == 'completed' ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-600">
|
<a href="<%= url_for(params: request.query_parameters.merge('status' => 'completed')) %>" class="<%= params[:status] == 'completed' ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-600">
|
||||||
<div class="flex items-center space-x-1">
|
<div class="flex items-center space-x-1">
|
||||||
<%= svg_icon('circle_check', class: 'w-5 h-5') %>
|
<%= svg_icon('circle_check', class: 'w-5 h-5') %>
|
||||||
<span class="font-normal"><%= t('completed') %></span>
|
<span class="font-normal"><%= t('completed') %></span>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<%= svg_icon(icon, class: 'w-5 h-5 shrink-0') %>
|
<%= svg_icon(icon, class: 'w-5 h-5 shrink-0') %>
|
||||||
<span class="font-normal truncate"><%= t(params[:status]) %></span>
|
<span class="font-normal truncate"><%= t(params[:status]) %></span>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= link_to url_for(params.to_unsafe_h.except(:status)), class: 'rounded-lg ml-1 hover:bg-base-content hover:text-white' do %>
|
<%= link_to url_for(params: request.query_parameters.except('status')), class: 'rounded-lg ml-1 hover:bg-base-content hover:text-white' do %>
|
||||||
<%= svg_icon('x', class: 'w-5 h-5') %>
|
<%= svg_icon('x', class: 'w-5 h-5') %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
<%= svg_icon('folder', class: 'w-5 h-5 shrink-0') %>
|
<%= svg_icon('folder', class: 'w-5 h-5 shrink-0') %>
|
||||||
<span class="font-normal truncate"><%= params[:folder] %></span>
|
<span class="font-normal truncate"><%= params[:folder] %></span>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= link_to url_for(params.to_unsafe_h.except(:folder)), class: 'rounded-lg ml-1 hover:bg-base-content hover:text-white' do %>
|
<%= link_to url_for(params: request.query_parameters.except('folder')), class: 'rounded-lg ml-1 hover:bg-base-content hover:text-white' do %>
|
||||||
<%= svg_icon('x', class: 'w-5 h-5') %>
|
<%= svg_icon('x', class: 'w-5 h-5') %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
<%= svg_icon('user', class: 'w-5 h-5 shrink-0') %>
|
<%= svg_icon('user', class: 'w-5 h-5 shrink-0') %>
|
||||||
<span class="font-normal truncate"><%= current_account.users.accessible_by(current_ability).where(account: current_account).find_by(email: params[:author])&.full_name || 'NA' %></span>
|
<span class="font-normal truncate"><%= current_account.users.accessible_by(current_ability).where(account: current_account).find_by(email: params[:author])&.full_name || 'NA' %></span>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= link_to url_for(params.to_unsafe_h.except(:author)), class: 'rounded-lg ml-1 hover:bg-base-content hover:text-white' do %>
|
<%= link_to url_for(params: request.query_parameters.except('author')), class: 'rounded-lg ml-1 hover:bg-base-content hover:text-white' do %>
|
||||||
<%= svg_icon('x', class: 'w-5 h-5') %>
|
<%= svg_icon('x', class: 'w-5 h-5') %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
</span>
|
</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= link_to url_for(params.to_unsafe_h.except(:completed_at_from, :completed_at_to)), class: 'rounded-lg ml-1 hover:bg-base-content hover:text-white' do %>
|
<%= link_to url_for(params: request.query_parameters.except('completed_at_from', 'completed_at_to')), class: 'rounded-lg ml-1 hover:bg-base-content hover:text-white' do %>
|
||||||
<%= svg_icon('x', class: 'w-5 h-5') %>
|
<%= svg_icon('x', class: 'w-5 h-5') %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
@@ -65,7 +65,7 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
</span>
|
</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= link_to url_for(params.to_unsafe_h.except(:created_at_to, :created_at_from)), class: 'rounded-lg ml-1 hover:bg-base-content hover:text-white' do %>
|
<%= link_to url_for(params: request.query_parameters.except('created_at_to', 'created_at_from')), class: 'rounded-lg ml-1 hover:bg-base-content hover:text-white' do %>
|
||||||
<%= svg_icon('x', class: 'w-5 h-5') %>
|
<%= svg_icon('x', class: 'w-5 h-5') %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<% if params[:with_remove] %>
|
<% if params[:with_remove] %>
|
||||||
<div class="text-center w-full mt-4">
|
<div class="text-center w-full mt-4">
|
||||||
<%= link_to t('remove_filter'), "#{params[:path]}?#{params.to_unsafe_h.slice(:q).merge(local_assigns[:default_params]).to_query}", class: 'link', data: { turbo_frame: :_top } %>
|
<%= link_to t('remove_filter'), "#{params[:path]}?#{request.query_parameters.slice('q').merge(local_assigns[:default_params]).to_query}", class: 'link', data: { turbo_frame: :_top } %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
<% if is_show_tabs %>
|
<% if is_show_tabs %>
|
||||||
<div class="flex items-center flex-col md:flex-row md:flex-wrap gap-2 mb-4">
|
<div class="flex items-center flex-col md:flex-row md:flex-wrap gap-2 mb-4">
|
||||||
<div class="flex items-center md:items-end flex-col md:flex-row gap-2 w-full md:w-fit">
|
<div class="flex items-center md:items-end flex-col md:flex-row gap-2 w-full md:w-fit">
|
||||||
<a href="<%= url_for(params.to_unsafe_h.except(:status)) %>" class="<%= params[:status].blank? ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-700">
|
<a href="<%= url_for(params: request.query_parameters.except('status')) %>" class="<%= params[:status].blank? ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-700">
|
||||||
<div class="flex items-center space-x-1">
|
<div class="flex items-center space-x-1">
|
||||||
<%= svg_icon('list', class: 'w-5 h-5') %>
|
<%= svg_icon('list', class: 'w-5 h-5') %>
|
||||||
<span class="font-normal"><%= t('all') %></span>
|
<span class="font-normal"><%= t('all') %></span>
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</a>
|
</a>
|
||||||
<a href="<%= url_for(params.to_unsafe_h.merge(status: :pending)) %>" class="<%= params[:status] == 'pending' ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-700">
|
<a href="<%= url_for(params: request.query_parameters.merge('status' => 'pending')) %>" class="<%= params[:status] == 'pending' ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-700">
|
||||||
<div class="flex items-center space-x-1">
|
<div class="flex items-center space-x-1">
|
||||||
<%= svg_icon('clock', class: 'w-5 h-5') %>
|
<%= svg_icon('clock', class: 'w-5 h-5') %>
|
||||||
<span class="font-normal"><%= t('pending') %></span>
|
<span class="font-normal"><%= t('pending') %></span>
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</a>
|
</a>
|
||||||
<a href="<%= url_for(params.to_unsafe_h.merge(status: :completed)) %>" class="<%= params[:status] == 'completed' ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-700">
|
<a href="<%= url_for(params: request.query_parameters.merge('status' => 'completed')) %>" class="<%= params[:status] == 'completed' ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-700">
|
||||||
<div class="flex items-center space-x-1">
|
<div class="flex items-center space-x-1">
|
||||||
<%= svg_icon('circle_check', class: 'w-5 h-5') %>
|
<%= svg_icon('circle_check', class: 'w-5 h-5') %>
|
||||||
<span class="font-normal"><%= t('completed') %></span>
|
<span class="font-normal"><%= t('completed') %></span>
|
||||||
|
|||||||
@@ -85,9 +85,9 @@
|
|||||||
<div class="mt-6">
|
<div class="mt-6">
|
||||||
<h2 id="log" class="text-3xl font-bold"><%= t('events_log') %></h2>
|
<h2 id="log" class="text-3xl font-bold"><%= t('events_log') %></h2>
|
||||||
<div class="tabs border-b mt-4">
|
<div class="tabs border-b mt-4">
|
||||||
<%= link_to t('all'), url_for(params.to_unsafe_h.except(:status)), style: 'margin-bottom: -1px', class: "tab h-10 text-base #{params[:status].blank? ? 'tab-active tab-bordered' : 'pb-[3px]'}" %>
|
<%= link_to t('all'), url_for(params: request.query_parameters.except('status')), style: 'margin-bottom: -1px', class: "tab h-10 text-base #{params[:status].blank? ? 'tab-active tab-bordered' : 'pb-[3px]'}" %>
|
||||||
<%= link_to t('succeeded'), url_for(params.to_unsafe_h.merge(status: 'success')), style: 'margin-bottom: -1px', class: "tab h-10 text-base #{params[:status] == 'success' ? 'tab-active tab-bordered' : 'pb-[3px]'}" %>
|
<%= link_to t('succeeded'), url_for(params: request.query_parameters.merge('status' => 'success')), style: 'margin-bottom: -1px', class: "tab h-10 text-base #{params[:status] == 'success' ? 'tab-active tab-bordered' : 'pb-[3px]'}" %>
|
||||||
<%= link_to t('failed'), url_for(params.to_unsafe_h.merge(status: 'error')), style: 'margin-bottom: -1px', class: "tab h-10 text-base #{params[:status] == 'error' ? 'tab-active tab-bordered' : 'pb-[3px]'}" %>
|
<%= link_to t('failed'), url_for(params: request.query_parameters.merge('status' => 'error')), style: 'margin-bottom: -1px', class: "tab h-10 text-base #{params[:status] == 'error' ? 'tab-active tab-bordered' : 'pb-[3px]'}" %>
|
||||||
</div>
|
</div>
|
||||||
<% if @webhook_events.present? %>
|
<% if @webhook_events.present? %>
|
||||||
<div class="divide-y divide-base-300 rounded-lg">
|
<div class="divide-y divide-base-300 rounded-lg">
|
||||||
|
|||||||
@@ -30,9 +30,8 @@ production:
|
|||||||
<% elsif ENV['DATABASE_URL'].match?(/\Apostgres/) %>
|
<% elsif ENV['DATABASE_URL'].match?(/\Apostgres/) %>
|
||||||
<<: *default
|
<<: *default
|
||||||
url: <%= ENV['DATABASE_URL'] %>
|
url: <%= ENV['DATABASE_URL'] %>
|
||||||
<% elsif ENV['DATABASE_URL'].match?(/\Amysql/) %>
|
<% elsif ENV['DATABASE_URL'].match?(/\Amysql|\Atrilogy/) %>
|
||||||
adapter: mysql2
|
|
||||||
encoding: utf8mb4
|
encoding: utf8mb4
|
||||||
pool: <%= ENV.fetch('RAILS_MAX_THREADS', 15).to_i + ENV.fetch('SIDEKIQ_THREADS', 5).to_i %>
|
pool: <%= ENV.fetch('RAILS_MAX_THREADS', 15).to_i + ENV.fetch('SIDEKIQ_THREADS', 5).to_i %>
|
||||||
url: <%= ENV['DATABASE_URL'] %>
|
url: <%= ENV['DATABASE_URL'].sub(/\Amysql2?/, 'trilogy') %>
|
||||||
<% end %>
|
<% end %>
|
||||||
@@ -85,6 +85,7 @@ Rails.application.configure do
|
|||||||
domain: ENV.fetch('SMTP_DOMAIN', nil),
|
domain: ENV.fetch('SMTP_DOMAIN', nil),
|
||||||
user_name: ENV.fetch('SMTP_USERNAME', nil),
|
user_name: ENV.fetch('SMTP_USERNAME', nil),
|
||||||
password: ENV.fetch('SMTP_PASSWORD', nil),
|
password: ENV.fetch('SMTP_PASSWORD', nil),
|
||||||
|
openssl_verify_mode: ENV['SMTP_SSL_VERIFY'] == 'false' ? OpenSSL::SSL::VERIFY_NONE : OpenSSL::SSL::VERIFY_PEER,
|
||||||
authentication: ENV.fetch('SMTP_PASSWORD', nil).present? ? ENV.fetch('SMTP_AUTHENTICATION', 'plain') : nil,
|
authentication: ENV.fetch('SMTP_PASSWORD', nil).present? ? ENV.fetch('SMTP_AUTHENTICATION', 'plain') : nil,
|
||||||
enable_starttls: ENV['SMTP_ENABLE_STARTTLS'] != 'false'
|
enable_starttls: ENV['SMTP_ENABLE_STARTTLS'] != 'false'
|
||||||
}.compact
|
}.compact
|
||||||
|
|||||||
@@ -64,6 +64,16 @@ module HexaPDF
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# fix NoMethodError: undefined method `stream' for an instance of Symbol
|
||||||
|
class TextField
|
||||||
|
def field_value
|
||||||
|
return unless value[:V]
|
||||||
|
return self[:V].to_s if self[:V].is_a?(Symbol)
|
||||||
|
|
||||||
|
self[:V].is_a?(String) ? self[:V] : self[:V].stream
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
class AppearanceGenerator
|
class AppearanceGenerator
|
||||||
def create_push_button_appearances
|
def create_push_button_appearances
|
||||||
nil
|
nil
|
||||||
|
|||||||
@@ -336,7 +336,7 @@ en: &en
|
|||||||
two_factor_authentication: Two-Factor Authentication
|
two_factor_authentication: Two-Factor Authentication
|
||||||
2fa_is_not_configured: 2FA is not configured
|
2fa_is_not_configured: 2FA is not configured
|
||||||
set_up_2fa: Set up 2FA
|
set_up_2fa: Set up 2FA
|
||||||
contact_information_has_been_update: Contact information has been update.
|
contact_information_has_been_update: Contact information has been updated.
|
||||||
password_has_been_changed: Password has been changed.
|
password_has_been_changed: Password has been changed.
|
||||||
email_integration: Email Integration
|
email_integration: Email Integration
|
||||||
send_via_connected_email: Send via connected email
|
send_via_connected_email: Send via connected email
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ Rails.application.routes.draw do
|
|||||||
resources :account_custom_fields, only: %i[create]
|
resources :account_custom_fields, only: %i[create]
|
||||||
resources :user_configs, only: %i[create]
|
resources :user_configs, only: %i[create]
|
||||||
resources :encrypted_user_configs, only: %i[destroy]
|
resources :encrypted_user_configs, only: %i[destroy]
|
||||||
resources :timestamp_server, only: %i[create]
|
resources :timestamp_server, only: %i[create] unless Docuseal.multitenant?
|
||||||
resources :dashboard, only: %i[index]
|
resources :dashboard, only: %i[index]
|
||||||
resources :setup, only: %i[index create]
|
resources :setup, only: %i[index create]
|
||||||
resource :newsletter, only: %i[show update]
|
resource :newsletter, only: %i[show update]
|
||||||
|
|||||||
@@ -47,6 +47,9 @@ module ActionMailerConfigsInterceptor
|
|||||||
|
|
||||||
is_tls = value['security'] == 'tls' || (value['security'].blank? && value['port'].to_s == '465')
|
is_tls = value['security'] == 'tls' || (value['security'].blank? && value['port'].to_s == '465')
|
||||||
is_ssl = value['security'] == 'ssl'
|
is_ssl = value['security'] == 'ssl'
|
||||||
|
is_noverify = value['security'] == 'noverify'
|
||||||
|
|
||||||
|
enable_starttls = is_noverify ? :enable_starttls_auto : :enable_starttls
|
||||||
|
|
||||||
{
|
{
|
||||||
user_name: value['username'],
|
user_name: value['username'],
|
||||||
@@ -54,9 +57,9 @@ module ActionMailerConfigsInterceptor
|
|||||||
address: value['host'],
|
address: value['host'],
|
||||||
port: value['port'],
|
port: value['port'],
|
||||||
domain: value['domain'],
|
domain: value['domain'],
|
||||||
openssl_verify_mode: value['security'] == 'noverify' ? OpenSSL::SSL::VERIFY_NONE : nil,
|
openssl_verify_mode: is_noverify ? OpenSSL::SSL::VERIFY_NONE : nil,
|
||||||
authentication: value['password'].present? ? value.fetch('authentication', 'plain') : nil,
|
authentication: value['password'].present? ? value.fetch('authentication', 'plain') : nil,
|
||||||
enable_starttls: !is_tls && !is_ssl,
|
enable_starttls => !is_tls && !is_ssl,
|
||||||
open_timeout: OPEN_TIMEOUT,
|
open_timeout: OPEN_TIMEOUT,
|
||||||
read_timeout: READ_TIMEOUT,
|
read_timeout: READ_TIMEOUT,
|
||||||
ssl: is_ssl,
|
ssl: is_ssl,
|
||||||
|
|||||||
@@ -35,16 +35,16 @@ module DownloadUtils
|
|||||||
|
|
||||||
module_function
|
module_function
|
||||||
|
|
||||||
def call(url)
|
def call(url, validate: Docuseal.multitenant?)
|
||||||
uri = begin
|
uri = begin
|
||||||
URI(url)
|
URI(url)
|
||||||
rescue URI::Error
|
rescue URI::Error
|
||||||
Addressable::URI.parse(url).normalize
|
Addressable::URI.parse(url).normalize
|
||||||
end
|
end
|
||||||
|
|
||||||
validate_uri!(uri) if Docuseal.multitenant?
|
validate_uri!(uri) if validate
|
||||||
|
|
||||||
resp = conn.get(uri)
|
resp = conn(validate:).get(uri)
|
||||||
|
|
||||||
raise UnableToDownload, "Error loading: #{uri}" if resp.status >= 400
|
raise UnableToDownload, "Error loading: #{uri}" if resp.status >= 400
|
||||||
|
|
||||||
@@ -52,14 +52,15 @@ module DownloadUtils
|
|||||||
end
|
end
|
||||||
|
|
||||||
def validate_uri!(uri)
|
def validate_uri!(uri)
|
||||||
raise UnableToDownload, "Error loading: #{uri}. Only HTTPS is allowed." if uri.scheme != 'https'
|
raise UnableToDownload, "Error loading: #{uri}. Only HTTPS is allowed." if uri.scheme != 'https' ||
|
||||||
|
[443, nil].exclude?(uri.port)
|
||||||
raise UnableToDownload, "Error loading: #{uri}. Can't download from localhost." if uri.host.in?(LOCALHOSTS)
|
raise UnableToDownload, "Error loading: #{uri}. Can't download from localhost." if uri.host.in?(LOCALHOSTS)
|
||||||
end
|
end
|
||||||
|
|
||||||
def conn
|
def conn(validate: Docuseal.multitenant?)
|
||||||
Faraday.new do |faraday|
|
Faraday.new do |faraday|
|
||||||
faraday.response :follow_redirects, callback: lambda { |_, new_env|
|
faraday.response :follow_redirects, callback: lambda { |_, new_env|
|
||||||
validate_uri!(new_env[:url]) if Docuseal.multitenant?
|
validate_uri!(new_env[:url]) if validate
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -6,9 +6,16 @@ class NormalizeClientIpMiddleware
|
|||||||
end
|
end
|
||||||
|
|
||||||
def call(env)
|
def call(env)
|
||||||
if env['HTTP_CLIENT_IP'].present? && env['HTTP_X_CLIENT_IP'].present? &&
|
if env['HTTP_CLIENT_IP'].present?
|
||||||
env['HTTP_CLIENT_IP'].starts_with?("#{env['HTTP_X_CLIENT_IP']}:")
|
if env['HTTP_X_CLIENT_IP'].present? &&
|
||||||
env['HTTP_CLIENT_IP'] = env['HTTP_X_CLIENT_IP']
|
env['HTTP_CLIENT_IP'].starts_with?("#{env['HTTP_X_CLIENT_IP']}:")
|
||||||
|
env['HTTP_CLIENT_IP'] = env['HTTP_X_CLIENT_IP']
|
||||||
|
end
|
||||||
|
|
||||||
|
if env['HTTP_X_FORWARDED_FOR'].present? &&
|
||||||
|
env['HTTP_X_FORWARDED_FOR'].sub(/:\d+\z/, '') == env['HTTP_CLIENT_IP']
|
||||||
|
env['HTTP_X_FORWARDED_FOR'] = env['HTTP_CLIENT_IP']
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@app.call(env)
|
@app.call(env)
|
||||||
|
|||||||
@@ -20,10 +20,6 @@ module PdfIcons
|
|||||||
StringIO.new(logo_data)
|
StringIO.new(logo_data)
|
||||||
end
|
end
|
||||||
|
|
||||||
def logo_new_io
|
|
||||||
StringIO.new(logo_new_data)
|
|
||||||
end
|
|
||||||
|
|
||||||
def stamp_logo_io
|
def stamp_logo_io
|
||||||
StringIO.new(stamp_logo_data)
|
StringIO.new(stamp_logo_data)
|
||||||
end
|
end
|
||||||
@@ -40,10 +36,6 @@ module PdfIcons
|
|||||||
@logo_data ||= PATH.join('logo.png').read
|
@logo_data ||= PATH.join('logo.png').read
|
||||||
end
|
end
|
||||||
|
|
||||||
def logo_new_data
|
|
||||||
@logo_new_data ||= PATH.join('logo_new.png').read
|
|
||||||
end
|
|
||||||
|
|
||||||
def stamp_logo_data
|
def stamp_logo_data
|
||||||
@stamp_logo_data ||= PATH.join('stamp-logo.png').read
|
@stamp_logo_data ||= PATH.join('stamp-logo.png').read
|
||||||
end
|
end
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 14 KiB |
@@ -22,7 +22,7 @@ module SendWebhookRequest
|
|||||||
end
|
end
|
||||||
|
|
||||||
if Docuseal.multitenant?
|
if Docuseal.multitenant?
|
||||||
raise HttpsError, 'Only HTTPS is allowed.' if uri.scheme != 'https' &&
|
raise HttpsError, 'Only HTTPS is allowed.' if (uri.scheme != 'https' || [443, nil].exclude?(uri.port)) &&
|
||||||
!AccountConfig.exists?(key: :allow_http,
|
!AccountConfig.exists?(key: :allow_http,
|
||||||
account_id: webhook_url.account_id)
|
account_id: webhook_url.account_id)
|
||||||
raise LocalhostError, "Can't send to localhost." if uri.host.in?(LOCALHOSTS)
|
raise LocalhostError, "Can't send to localhost." if uri.host.in?(LOCALHOSTS)
|
||||||
@@ -43,7 +43,7 @@ module SendWebhookRequest
|
|||||||
data: data
|
data: data
|
||||||
}.to_json
|
}.to_json
|
||||||
|
|
||||||
req.options.read_timeout = 8
|
req.options.read_timeout = 15
|
||||||
req.options.open_timeout = 8
|
req.options.open_timeout = 8
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ module Submissions
|
|||||||
def plain_search(submissions, keyword, search_values: false, search_template: false)
|
def plain_search(submissions, keyword, search_values: false, search_template: false)
|
||||||
return submissions if keyword.blank?
|
return submissions if keyword.blank?
|
||||||
|
|
||||||
term = "%#{keyword.downcase}%"
|
sanitized = ActiveRecord::Base.sanitize_sql_like(keyword.downcase)
|
||||||
|
term = "%#{sanitized}%"
|
||||||
|
|
||||||
arel_table = Submitter.arel_table
|
arel_table = Submitter.arel_table
|
||||||
|
|
||||||
@@ -31,7 +32,7 @@ module Submissions
|
|||||||
if search_template
|
if search_template
|
||||||
submissions = submissions.left_joins(:template)
|
submissions = submissions.left_joins(:template)
|
||||||
|
|
||||||
arel = arel.or(Template.arel_table[:name].lower.matches("%#{keyword.downcase}%"))
|
arel = arel.or(Template.arel_table[:name].lower.matches("%#{sanitized}%"))
|
||||||
end
|
end
|
||||||
|
|
||||||
submissions.joins(:submitters).where(arel).group(:id)
|
submissions.joins(:submitters).where(arel).group(:id)
|
||||||
|
|||||||
@@ -61,7 +61,8 @@ module Submissions
|
|||||||
|
|
||||||
submission.template_submitters << template_submitter
|
submission.template_submitters << template_submitter
|
||||||
|
|
||||||
is_order_sent = submitters_order == 'random' || (template_submitter['order'] || index).zero?
|
is_order_sent = submitters_order == 'random' ||
|
||||||
|
(template_submitter['order'] || submitter_attrs[:index] || index).zero?
|
||||||
|
|
||||||
build_submitter(submission:, attrs: submitter_attrs,
|
build_submitter(submission:, attrs: submitter_attrs,
|
||||||
uuid:, is_order_sent:, user:, params:,
|
uuid:, is_order_sent:, user:, params:,
|
||||||
@@ -308,7 +309,7 @@ module Submissions
|
|||||||
uuid = attrs[:uuid].presence
|
uuid = attrs[:uuid].presence
|
||||||
uuid ||= submitters.find { |e| e['name'].to_s.casecmp(attrs[:role].to_s).zero? }&.dig('uuid')
|
uuid ||= submitters.find { |e| e['name'].to_s.casecmp(attrs[:role].to_s).zero? }&.dig('uuid')
|
||||||
|
|
||||||
uuid || submitters[index]&.dig('uuid')
|
uuid || submitters[attrs[:index] || index]&.dig('uuid')
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_submitter(submission:, attrs:, uuid:, is_order_sent:, user:, preferences:, params:)
|
def build_submitter(submission:, attrs:, uuid:, is_order_sent:, user:, preferences:, params:)
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ module Submissions
|
|||||||
total_wait_time ||= 0
|
total_wait_time ||= 0
|
||||||
key = ['result_attachments', submitter.id].join(':')
|
key = ['result_attachments', submitter.id].join(':')
|
||||||
|
|
||||||
return submitter.documents.reset if ApplicationRecord.uncached { LockEvent.exists?(key:, event_name: :complete) }
|
return submitter.documents.reload if ApplicationRecord.uncached { LockEvent.exists?(key:, event_name: :complete) }
|
||||||
|
|
||||||
events = ApplicationRecord.uncached { LockEvent.where(key:).order(:id).to_a }
|
events = ApplicationRecord.uncached { LockEvent.where(key:).order(:id).to_a }
|
||||||
|
|
||||||
@@ -32,6 +32,8 @@ module Submissions
|
|||||||
|
|
||||||
LockEvent.create!(key:, event_name: :complete)
|
LockEvent.create!(key:, event_name: :complete)
|
||||||
|
|
||||||
|
submitter.documents.reset
|
||||||
|
|
||||||
documents
|
documents
|
||||||
end
|
end
|
||||||
rescue ActiveRecord::RecordNotUnique
|
rescue ActiveRecord::RecordNotUnique
|
||||||
@@ -60,7 +62,7 @@ module Submissions
|
|||||||
LockEvent.where(key: ['result_attachments', submitter.id].join(':')).order(:id).last
|
LockEvent.where(key: ['result_attachments', submitter.id].join(':')).order(:id).last
|
||||||
end
|
end
|
||||||
|
|
||||||
break submitter.documents.reset if last_event.event_name.in?(%w[complete fail])
|
break submitter.documents.reload if last_event.event_name.in?(%w[complete fail])
|
||||||
|
|
||||||
raise WaitForCompleteTimeout if total_wait_time > CHECK_COMPLETE_TIMEOUT
|
raise WaitForCompleteTimeout if total_wait_time > CHECK_COMPLETE_TIMEOUT
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ module Submissions
|
|||||||
|
|
||||||
pdf.trailer.info[:DocumentID] = document_id
|
pdf.trailer.info[:DocumentID] = document_id
|
||||||
pdf.pages.each do |page|
|
pdf.pages.each do |page|
|
||||||
font_size = (([page.box.width, page.box.height].min / A4_SIZE[0].to_f) * 9).to_i
|
font_size = [(([page.box.width, page.box.height].min / A4_SIZE[0].to_f) * 9).to_i, 4].max
|
||||||
cnv = page.canvas(type: :overlay)
|
cnv = page.canvas(type: :overlay)
|
||||||
|
|
||||||
text =
|
text =
|
||||||
@@ -200,7 +200,9 @@ module Submissions
|
|||||||
|
|
||||||
def fill_submitter_fields(submitter, account, pdfs_index, with_signature_id:, is_flatten:, with_headings: nil,
|
def fill_submitter_fields(submitter, account, pdfs_index, with_signature_id:, is_flatten:, with_headings: nil,
|
||||||
with_submitter_timezone: false, with_signature_id_reason: true, with_file_links: nil)
|
with_submitter_timezone: false, with_signature_id_reason: true, with_file_links: nil)
|
||||||
cell_layouter = HexaPDF::Layout::TextLayouter.new(text_valign: :center, text_align: :center)
|
cell_layouters = Hash.new do |hash, valign|
|
||||||
|
hash[valign] = HexaPDF::Layout::TextLayouter.new(text_valign: valign.to_sym, text_align: :center)
|
||||||
|
end
|
||||||
|
|
||||||
attachments_data_cache = {}
|
attachments_data_cache = {}
|
||||||
|
|
||||||
@@ -550,6 +552,8 @@ module Submissions
|
|||||||
)
|
)
|
||||||
when ->(type) { type == 'cells' && !area['cell_w'].to_f.zero? }
|
when ->(type) { type == 'cells' && !area['cell_w'].to_f.zero? }
|
||||||
cell_width = area['cell_w'] * width
|
cell_width = area['cell_w'] * width
|
||||||
|
cell_valign = field.dig('preferences', 'valign').to_s.presence || 'center'
|
||||||
|
cell_layouter = cell_layouters[cell_valign]
|
||||||
|
|
||||||
if (mask = field.dig('preferences', 'mask').presence)
|
if (mask = field.dig('preferences', 'mask').presence)
|
||||||
value = TextUtils.mask_value(value, mask)
|
value = TextUtils.mask_value(value, mask)
|
||||||
@@ -790,10 +794,10 @@ module Submissions
|
|||||||
def build_pdfs_index(submission, submitter: nil, flatten: true)
|
def build_pdfs_index(submission, submitter: nil, flatten: true)
|
||||||
latest_submitter = find_last_submitter(submission, submitter:)
|
latest_submitter = find_last_submitter(submission, submitter:)
|
||||||
|
|
||||||
Submissions::EnsureResultGenerated.call(latest_submitter) if latest_submitter
|
documents = Submissions::EnsureResultGenerated.call(latest_submitter) if latest_submitter
|
||||||
|
documents ||= submission.schema_documents
|
||||||
|
|
||||||
documents = latest_submitter&.documents&.preload(:blob).to_a.presence
|
ActiveRecord::Associations::Preloader.new(records: documents, associations: [:blob]).call
|
||||||
documents ||= submission.schema_documents.preload(:blob)
|
|
||||||
|
|
||||||
attachment_uuids = Submissions.filtered_conditions_schema(submission).pluck('attachment_uuid')
|
attachment_uuids = Submissions.filtered_conditions_schema(submission).pluck('attachment_uuid')
|
||||||
attachments_index = documents.index_by { |a| a.metadata['original_uuid'] || a.uuid }
|
attachments_index = documents.index_by { |a| a.metadata['original_uuid'] || a.uuid }
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ module Submissions
|
|||||||
|
|
||||||
Array.wrap(submissions_params).each do |submission|
|
Array.wrap(submissions_params).each do |submission|
|
||||||
submission[:submitters].each_with_index do |submitter, index|
|
submission[:submitters].each_with_index do |submitter, index|
|
||||||
_, new_attachments, new_fields = normalize_submitter_params!(submitter, template, index, add_fields:)
|
_, new_attachments, new_fields =
|
||||||
|
normalize_submitter_params!(submitter, template, submitter[:index] || index, add_fields:)
|
||||||
|
|
||||||
attachments.push(*new_attachments)
|
attachments.push(*new_attachments)
|
||||||
fields.push(*new_fields)
|
fields.push(*new_fields)
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ module Submissions
|
|||||||
|
|
||||||
module_function
|
module_function
|
||||||
|
|
||||||
|
# rubocop:disable Metrics
|
||||||
def call(submission, submitters = nil, params = {}, with_events: true, with_documents: true, with_values: true,
|
def call(submission, submitters = nil, params = {}, with_events: true, with_documents: true, with_values: true,
|
||||||
expires_at: Accounts.link_expires_at(Account.new(id: submission.account_id)))
|
expires_at: Accounts.link_expires_at(Account.new(id: submission.account_id)))
|
||||||
submitters ||= submission.submitters.preload(documents_attachments: :blob, attachments_attachments: :blob)
|
submitters ||= submission.submitters.preload(documents_attachments: :blob, attachments_attachments: :blob)
|
||||||
@@ -32,6 +33,10 @@ module Submissions
|
|||||||
json['submission_events'] = Submitters::SerializeForApi.serialize_events(submission.submission_events)
|
json['submission_events'] = Submitters::SerializeForApi.serialize_events(submission.submission_events)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if params[:include].to_s.include?('fields')
|
||||||
|
json['fields'] = submission.template_fields || submission.template&.fields
|
||||||
|
end
|
||||||
|
|
||||||
if submitters.all?(&:completed_at?)
|
if submitters.all?(&:completed_at?)
|
||||||
last_submitter = submitters.max_by(&:completed_at)
|
last_submitter = submitters.max_by(&:completed_at)
|
||||||
|
|
||||||
@@ -57,6 +62,7 @@ module Submissions
|
|||||||
|
|
||||||
json
|
json
|
||||||
end
|
end
|
||||||
|
# rubocop:enable Metrics
|
||||||
|
|
||||||
def build_status(submission, submitters)
|
def build_status(submission, submitters)
|
||||||
if submitters.any?(&:declined_at?)
|
if submitters.any?(&:declined_at?)
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ module Submitters
|
|||||||
UnableToSendCode = Class.new(StandardError)
|
UnableToSendCode = Class.new(StandardError)
|
||||||
InvalidOtp = Class.new(StandardError)
|
InvalidOtp = Class.new(StandardError)
|
||||||
MaliciousFileExtension = Class.new(StandardError)
|
MaliciousFileExtension = Class.new(StandardError)
|
||||||
|
ArgumentError = Class.new(StandardError)
|
||||||
|
|
||||||
DANGEROUS_EXTENSIONS = Set.new(%w[
|
DANGEROUS_EXTENSIONS = Set.new(%w[
|
||||||
exe com bat cmd scr pif vbs vbe js jse wsf wsh msi msp
|
exe com bat cmd scr pif vbs vbe js jse wsf wsh msi msp
|
||||||
@@ -133,7 +134,7 @@ module Submitters
|
|||||||
filename: file.original_filename,
|
filename: file.original_filename,
|
||||||
content_type: file.content_type)
|
content_type: file.content_type)
|
||||||
else
|
else
|
||||||
ActiveStorage::Blob.find_signed(params[:blob_signed_id])
|
raise ArgumentError, 'file param is missing'
|
||||||
end
|
end
|
||||||
|
|
||||||
ActiveStorage::Attachment.create!(
|
ActiveStorage::Attachment.create!(
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Submitters
|
||||||
|
module AuthorizedForForm
|
||||||
|
Unauthorized = Class.new(StandardError)
|
||||||
|
|
||||||
|
module_function
|
||||||
|
|
||||||
|
def call(submitter, current_user, request)
|
||||||
|
pass_email_2fa?(submitter, request) && pass_link_2fa?(submitter, current_user, request)
|
||||||
|
end
|
||||||
|
|
||||||
|
def pass_email_2fa?(submitter, request)
|
||||||
|
return false unless submitter
|
||||||
|
|
||||||
|
return true if submitter.submission.template&.preferences&.dig('require_email_2fa') != true &&
|
||||||
|
submitter.preferences['require_email_2fa'] != true
|
||||||
|
return true if request.cookie_jar.encrypted[:email_2fa_slug] == submitter.slug
|
||||||
|
|
||||||
|
token = request.params[:two_factor_token].presence || request.headers['x-two-factor-token'].presence
|
||||||
|
|
||||||
|
return true if token.present? &&
|
||||||
|
Submitter.signed_id_verifier.verified(token, purpose: :email_two_factor) == submitter.slug
|
||||||
|
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def pass_link_2fa?(submitter, current_user, request)
|
||||||
|
return false unless submitter
|
||||||
|
|
||||||
|
return true if submitter.submission.source != 'link'
|
||||||
|
return true unless submitter.submission.template&.preferences&.dig('shared_link_2fa') == true
|
||||||
|
return true if request.cookie_jar.encrypted[:email_2fa_slug] == submitter.slug
|
||||||
|
return true if submitter.email == current_user&.email && current_user&.account_id == submitter.account_id
|
||||||
|
|
||||||
|
if (token = request.params[:two_factor_token].presence || request.headers['x-two-factor-token'].presence)
|
||||||
|
link_2fa_key = [submitter.email.downcase.squish, submitter.submission.template.slug].join(':')
|
||||||
|
|
||||||
|
return true if Submitter.signed_id_verifier.verified(token, purpose: :email_two_factor) == link_2fa_key
|
||||||
|
end
|
||||||
|
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -236,7 +236,7 @@ module Submitters
|
|||||||
|
|
||||||
return blob if blob
|
return blob if blob
|
||||||
|
|
||||||
data = DownloadUtils.call(url).body
|
data = DownloadUtils.call(url, validate: true).body
|
||||||
|
|
||||||
checksum = Digest::MD5.base64digest(data)
|
checksum = Digest::MD5.base64digest(data)
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,10 @@ module Submitters
|
|||||||
maybe_set_signature_reason!(values, submitter, params)
|
maybe_set_signature_reason!(values, submitter, params)
|
||||||
validate_values!(values, submitter, params, request)
|
validate_values!(values, submitter, params, request)
|
||||||
|
|
||||||
|
if (touch_attachment_uuid = params[:touch_attachment_uuid].presence)
|
||||||
|
ActiveStorage::Attachment.where(uuid: touch_attachment_uuid, record: submitter).touch_all(:created_at)
|
||||||
|
end
|
||||||
|
|
||||||
SubmissionEvents.create_with_tracking_data(submitter, 'complete_form', request) if params[:completed] == 'true'
|
SubmissionEvents.create_with_tracking_data(submitter, 'complete_form', request) if params[:completed] == 'true'
|
||||||
|
|
||||||
submitter.save!
|
submitter.save!
|
||||||
@@ -317,17 +321,33 @@ module Submitters
|
|||||||
end.exclude?(false)
|
end.exclude?(false)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# rubocop:disable Metrics
|
||||||
def check_field_condition(condition, submitter_values, fields_uuid_index)
|
def check_field_condition(condition, submitter_values, fields_uuid_index)
|
||||||
value = submitter_values[condition['field_uuid']]
|
value = submitter_values[condition['field_uuid']]
|
||||||
|
field = fields_uuid_index[condition['field_uuid']]
|
||||||
|
|
||||||
case condition['action']
|
case condition['action']
|
||||||
when 'empty', 'unchecked'
|
when 'empty', 'unchecked'
|
||||||
value.blank?
|
value.blank?
|
||||||
when 'not_empty', 'checked'
|
when 'not_empty', 'checked'
|
||||||
value.present?
|
value.present?
|
||||||
when 'equal', 'contains'
|
when ->(action) { action == 'equal' && field&.dig('type') == 'number' }
|
||||||
field = fields_uuid_index[condition['field_uuid']]
|
return false if value.blank? || condition['value'].blank?
|
||||||
|
|
||||||
|
(value.to_f - condition['value'].to_f).abs < Float::EPSILON
|
||||||
|
when ->(action) { action == 'not_equal' && field&.dig('type') == 'number' }
|
||||||
|
return false if value.blank? || condition['value'].blank?
|
||||||
|
|
||||||
|
(value.to_f - condition['value'].to_f).abs > Float::EPSILON
|
||||||
|
when 'greater_than'
|
||||||
|
return false if field.nil? || value.blank? || condition['value'].blank?
|
||||||
|
|
||||||
|
value.to_f > condition['value'].to_f
|
||||||
|
when 'less_than'
|
||||||
|
return false if field.nil? || value.blank? || condition['value'].blank?
|
||||||
|
|
||||||
|
value.to_f < condition['value'].to_f
|
||||||
|
when 'equal', 'contains'
|
||||||
return true unless field
|
return true unless field
|
||||||
|
|
||||||
values = Array.wrap(value)
|
values = Array.wrap(value)
|
||||||
@@ -340,8 +360,6 @@ module Submitters
|
|||||||
|
|
||||||
values.include?(option['value'].presence || "#{I18n.t('option')} #{field['options'].index(option) + 1}")
|
values.include?(option['value'].presence || "#{I18n.t('option')} #{field['options'].index(option) + 1}")
|
||||||
when 'not_equal', 'does_not_contain'
|
when 'not_equal', 'does_not_contain'
|
||||||
field = fields_uuid_index[condition['field_uuid']]
|
|
||||||
|
|
||||||
return true unless field
|
return true unless field
|
||||||
return false unless field['options']
|
return false unless field['options']
|
||||||
|
|
||||||
@@ -356,6 +374,7 @@ module Submitters
|
|||||||
true
|
true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
# rubocop:enable Metrics
|
||||||
|
|
||||||
def replace_default_variables(value, attrs, submission, with_time: false)
|
def replace_default_variables(value, attrs, submission, with_time: false)
|
||||||
return value if value.in?([true, false]) || value.is_a?(Numeric) || value.is_a?(Array)
|
return value if value.in?([true, false]) || value.is_a?(Numeric) || value.is_a?(Array)
|
||||||
|
|||||||
@@ -20,7 +20,9 @@ module TemplateFolders
|
|||||||
def search(folders, keyword)
|
def search(folders, keyword)
|
||||||
return folders if keyword.blank?
|
return folders if keyword.blank?
|
||||||
|
|
||||||
folders.where(TemplateFolder.arel_table[:name].lower.matches("%#{keyword.downcase}%"))
|
sanitized = ActiveRecord::Base.sanitize_sql_like(keyword.downcase)
|
||||||
|
|
||||||
|
folders.where(TemplateFolder.arel_table[:name].lower.matches("%#{sanitized}%"))
|
||||||
end
|
end
|
||||||
|
|
||||||
def filter_active_folders(template_folders, templates)
|
def filter_active_folders(template_folders, templates)
|
||||||
|
|||||||
@@ -52,7 +52,9 @@ module Templates
|
|||||||
def plain_search(templates, keyword)
|
def plain_search(templates, keyword)
|
||||||
return templates if keyword.blank?
|
return templates if keyword.blank?
|
||||||
|
|
||||||
templates.where(Template.arel_table[:name].lower.matches("%#{keyword.downcase}%"))
|
sanitized = ActiveRecord::Base.sanitize_sql_like(keyword.downcase)
|
||||||
|
|
||||||
|
templates.where(Template.arel_table[:name].lower.matches("%#{sanitized}%"))
|
||||||
end
|
end
|
||||||
|
|
||||||
def fulltext_search(current_user, templates, keyword)
|
def fulltext_search(current_user, templates, keyword)
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 807 B |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,5 @@
|
|||||||
|
<svg height="180" width="180" viewBox="0 0 180 180" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle fill="white" cx="90" cy="90" r="88"/>
|
||||||
|
<path fill="#e0753f" d="M 178.224 72.09 c -0.296 -1.463 -0.627 -2.919 -0.996 -4.364 -0.293 -1.151 -0.616 -2.293 -0.956 -3.433 -0.301 -1.008 -0.612 -2.014 -0.95 -3.012 -0.531 -1.578 -1.113 -3.142 -1.735 -4.694 -0.216 -0.54 -0.433 -1.082 -0.661 -1.618 -0.195 -0.462 -0.399 -0.917 -0.601 -1.375 -0.262 -0.591 -0.53 -1.177 -0.804 -1.762 -0.074 -0.159 -0.151 -0.315 -0.226 -0.474 -0.209 -0.441 -0.422 -0.881 -0.638 -1.318 -0.076 -0.154 -0.153 -0.306 -0.229 -0.459 -0.236 -0.471 -0.477 -0.939 -0.721 -1.406 -0.053 -0.101 -0.105 -0.201 -0.158 -0.302 -1.143 -2.16 -2.367 -4.269 -3.68 -6.322 -0.116 -0.181 -0.237 -0.359 -0.355 -0.539 -0.094 -0.144 -0.189 -0.288 -0.284 -0.432 -0.284 -0.431 -0.57 -0.861 -0.862 -1.287 -0.112 -0.164 -0.225 -0.326 -0.338 -0.489 -0.193 -0.279 -0.382 -0.56 -0.579 -0.836 -0.089 -0.125 -0.182 -0.249 -0.273 -0.374 -0.13 -0.182 -0.264 -0.362 -0.395 -0.542 -0.277 -0.38 -0.556 -0.76 -0.838 -1.135 -0.15 -0.199 -0.303 -0.395 -0.454 -0.593 -0.21 -0.274 -0.417 -0.552 -0.63 -0.823 -0.055 -0.069 -0.111 -0.136 -0.166 -0.205 -0.482 -0.61 -0.971 -1.216 -1.47 -1.814 -0.129 -0.155 -0.262 -0.306 -0.392 -0.461 -0.402 -0.476 -0.808 -0.95 -1.22 -1.417 -0.186 -0.212 -0.375 -0.422 -0.563 -0.631 -0.384 -0.428 -0.773 -0.854 -1.167 -1.276 -0.176 -0.189 -0.351 -0.379 -0.529 -0.567 -0.564 -0.595 -1.134 -1.186 -1.716 -1.768 -1.091 -1.091 -2.207 -2.15 -3.346 -3.178 -1.016 -0.919 -2.05 -1.815 -3.103 -2.684 -0.772 -0.636 -1.557 -1.255 -2.348 -1.864 -3.465 -2.67 -7.112 -5.075 -10.927 -7.209 -2.869 -1.604 -5.83 -3.06 -8.883 -4.351 -2.443 -1.033 -4.922 -1.948 -7.428 -2.756 -8.879 -2.863 -18.13 -4.318 -27.605 -4.318 -3.19 0 -6.354 0.169 -9.488 0.496 -4.036 0.421 -8.019 1.114 -11.94 2.073 -1.732 0.423 -3.452 0.892 -5.157 1.42 -2.856 0.883 -5.673 1.912 -8.447 3.085 -2.645 1.118 -5.222 2.357 -7.729 3.711 -2.574 1.39 -5.073 2.901 -7.494 4.533 -1.195 0.805 -2.37 1.64 -3.527 2.503 -1.156 0.864 -2.292 1.756 -3.408 2.676 -0.553 0.456 -1.1 0.919 -1.643 1.389 -1.649 1.427 -3.252 2.92 -4.806 4.473 -2.582 2.582 -4.991 5.299 -7.222 8.138 -0.892 1.135 -1.756 2.292 -2.59 3.467 -0.417 0.588 -0.827 1.18 -1.23 1.778 -0.403 0.597 -0.798 1.199 -1.186 1.806 -0.388 0.607 -0.769 1.218 -1.143 1.835 -2.241 3.697 -4.216 7.562 -5.916 11.582 -1.095 2.589 -2.059 5.217 -2.901 7.877 -0.153 0.482 -0.3 0.965 -0.444 1.449 -0.339 1.14 -0.663 2.282 -0.956 3.433 -0.369 1.446 -0.7 2.901 -0.996 4.364 -1.034 5.121 -1.618 10.343 -1.749 15.637 -0.018 0.757 -0.028 1.514 -0.028 2.274 0 1.123 0.02 2.244 0.062 3.361 0.285 7.82 1.568 15.475 3.825 22.879 0.044 0.147 0.088 0.295 0.133 0.441 0.877 2.823 1.894 5.608 3.054 8.35 0.85 2.009 1.769 3.98 2.755 5.912 0.539 1.057 1.105 2.099 1.685 3.132 4.013 7.142 8.98 13.698 14.846 19.564 7.713 7.713 16.611 13.878 26.477 18.352 0.705 0.32 1.415 0.632 2.131 0.935 2.081 0.88 4.185 1.679 6.313 2.396 9.217 3.106 18.85 4.677 28.719 4.677 8.031 0 15.902 -1.047 23.522 -3.107 0.633 -0.172 1.266 -0.35 1.895 -0.535 0.757 -0.222 1.509 -0.456 2.26 -0.698 0.717 -0.232 1.431 -0.474 2.145 -0.723 1.752 -0.616 3.49 -1.281 5.211 -2.009 0.755 -0.319 1.503 -0.651 2.247 -0.989 1.237 -0.563 2.459 -1.15 3.664 -1.766 0.644 -0.328 1.283 -0.665 1.917 -1.009 1.654 -0.896 3.274 -1.848 4.865 -2.844 5.736 -3.591 11.06 -7.827 15.912 -12.679 0.775 -0.775 1.534 -1.562 2.278 -2.36 5.204 -5.59 9.636 -11.754 13.246 -18.417 0.343 -0.634 0.68 -1.274 1.009 -1.917 0.482 -0.944 0.943 -1.9 1.392 -2.863 0.471 -1.007 0.928 -2.021 1.364 -3.049 1.22 -2.886 2.281 -5.82 3.187 -8.793 0.559 -1.833 1.056 -3.68 1.494 -5.542 0.108 -0.458 0.211 -0.916 0.312 -1.376 0.194 -0.883 0.373 -1.77 0.539 -2.659 1.02 -5.455 1.542 -11.02 1.542 -16.663 0 -6.074 -0.595 -12.058 -1.776 -17.911 z m -161.733 19.614 c -1.118 -56.662 44.604 -74.877 60.998 -67.647 2.187 0.965 4.732 2.431 7.042 2.96 5.295 1.213 13.432 -3.113 13.521 6.273 0.078 8.156 -3.389 13.108 -10.797 16.177 -7.539 3.124 -14.777 9.181 -19.95 15.493 -21.487 26.216 -31.231 68.556 -7.565 94.296 -13.679 -5.545 -42.418 -25.467 -43.248 -67.552 z m 91.109 72.619 c -0.053 0.008 -4.171 0.775 -4.171 0.775 0 0 -15.862 -22.957 -23.509 -21.719 11.291 16.04 12.649 22.625 12.649 22.625 -0.053 0.001 -0.107 0.001 -0.161 0.003 -51.831 2.131 -42.785 -64.026 -28.246 -86.502 -1.555 13.073 8.878 39.992 39.034 44.1 9.495 1.293 32.302 -3.275 41.015 -11.38 0.098 1.825 0.163 3.85 0.159 6.013 -0.046 23.538 -13.47 42.743 -36.77 46.085 z m 30.575 -15.708 c 9.647 -9.263 12.869 -27.779 9.103 -44.137 -4.608 -20.011 -28.861 -32.383 -40.744 -35.564 5.766 -8.089 27.908 -14.274 39.567 5.363 -5.172 -10.519 -13.556 -23.023 -1.732 -33.128 12.411 13.329 19.411 29.94 20.161 48.7 0.75 18.753 -6.64 41.768 -26.355 58.765 z"/>
|
||||||
|
<circle fill="#e0753f" cx="71.927" cy="32.004" r="2.829"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.7 KiB |