Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d8942803e | ||
|
|
46d9bb2c87 | ||
|
|
34433c0575 | ||
|
|
544306eac4 | ||
|
|
0eeebfd3cf | ||
|
|
050224a10d | ||
|
|
7b6fc7d06f | ||
|
|
7fb148c85c | ||
|
|
dffa59fecd | ||
|
|
24fe2a1192 | ||
|
|
97b8ac4444 | ||
|
|
e34a763dd9 | ||
|
|
298b1d3e1e | ||
|
|
d9978519cc | ||
|
|
b4a68cf79e | ||
|
|
5663e60f92 | ||
|
|
19e8835809 | ||
|
|
330fae927f | ||
|
|
c5a5212947 | ||
|
|
21d81e38bf | ||
|
|
5b1a4ebffd | ||
|
|
5f5dabd0e5 | ||
|
|
ff46993de6 | ||
|
|
7676db2196 | ||
|
|
3ef0d28ffe | ||
|
|
231177f057 | ||
|
|
e5f7739952 | ||
|
|
4a8e2a82e5 | ||
|
|
6efe8027f1 | ||
|
|
96bb4eb6e5 | ||
|
|
38e68814cf | ||
|
|
786c80bee7 | ||
|
|
98dcce299e | ||
|
|
cbff242a5d | ||
|
|
1876b3efba | ||
|
|
51091f05f0 | ||
|
|
d76eee2ae4 | ||
|
|
d7bfa96d57 | ||
|
|
be60608211 | ||
|
|
4e5abb3c09 | ||
|
|
7fee82ded2 | ||
|
|
e1cd62ff40 | ||
|
|
b498291b62 | ||
|
|
def3009846 | ||
|
|
9d43fd9aa5 | ||
|
|
22a21fb037 | ||
|
|
2a9c375273 | ||
|
|
847e70ec1b | ||
|
|
7964d6e0f0 | ||
|
|
d0259791bf | ||
|
|
a9dcaeccbc | ||
|
|
501b91a603 | ||
|
|
2b5f772feb | ||
|
|
7cada0af8b | ||
|
|
67be785e69 | ||
|
|
d1cfc64adc | ||
|
|
4e5157d1ee | ||
|
|
5e27cc36c6 | ||
|
|
6e81a3be3f | ||
|
|
858b2f33fd | ||
|
|
6adcc87b2b | ||
|
|
e1d5a5b6a6 | ||
|
|
f090e79002 | ||
|
|
56b32e35a2 | ||
|
|
b40c27cdbf | ||
|
|
c4505e67f0 | ||
|
|
d0799b8cc1 | ||
|
|
889fcad859 | ||
|
|
c133fef99d | ||
|
|
cfdc978a2a | ||
|
|
bff1651966 | ||
|
|
b97854f2fe | ||
|
|
7a4629bf79 | ||
|
|
2573d8cb19 | ||
|
|
dd559e90f1 | ||
|
|
e5db61e358 | ||
|
|
69376c0faf | ||
|
|
1f48be135c | ||
|
|
1cae12b728 | ||
|
|
86d2cd93f8 | ||
|
|
b0110d9340 | ||
|
|
7e70f4fb14 | ||
|
|
28e3d65e63 | ||
|
|
f1a2b71b59 | ||
|
|
47c7270911 | ||
|
|
4e1fa336a2 | ||
|
|
244bc32136 | ||
|
|
42c30bdfd0 | ||
|
|
0abf48ddba |
No files matched your search
@@ -11,7 +11,7 @@ jobs:
|
|||||||
- name: Install Ruby
|
- name: Install Ruby
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
ruby-version: 3.4.1
|
ruby-version: 3.4.2
|
||||||
- name: Cache gems
|
- name: Cache gems
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
@@ -35,7 +35,7 @@ jobs:
|
|||||||
- name: Install Ruby
|
- name: Install Ruby
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
ruby-version: 3.4.1
|
ruby-version: 3.4.2
|
||||||
- name: Cache gems
|
- name: Cache gems
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
@@ -85,7 +85,7 @@ jobs:
|
|||||||
- name: Install Ruby
|
- name: Install Ruby
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
ruby-version: 3.4.1
|
ruby-version: 3.4.2
|
||||||
- name: Cache gems
|
- name: Cache gems
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
@@ -127,7 +127,7 @@ jobs:
|
|||||||
- name: Install Ruby
|
- name: Install Ruby
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
ruby-version: 3.4.1
|
ruby-version: 3.4.2
|
||||||
- name: Set up Node
|
- name: Set up Node
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM ruby:3.4.1-alpine AS fonts
|
FROM ruby:3.4.2-alpine AS fonts
|
||||||
|
|
||||||
WORKDIR /fonts
|
WORKDIR /fonts
|
||||||
|
|
||||||
@@ -6,7 +6,7 @@ RUN apk --no-cache add fontforge wget && wget https://github.com/satbyy/go-noto-
|
|||||||
|
|
||||||
RUN fontforge -lang=py -c 'font1 = fontforge.open("FreeSans.ttf"); font2 = fontforge.open("NotoSansSymbols2-Regular.ttf"); font1.mergeFonts(font2); font1.generate("FreeSans.ttf")'
|
RUN fontforge -lang=py -c 'font1 = fontforge.open("FreeSans.ttf"); font2 = fontforge.open("NotoSansSymbols2-Regular.ttf"); font1.mergeFonts(font2); font1.generate("FreeSans.ttf")'
|
||||||
|
|
||||||
FROM ruby:3.4.1-alpine AS webpack
|
FROM ruby:3.4.2-alpine AS webpack
|
||||||
|
|
||||||
ENV RAILS_ENV=production
|
ENV RAILS_ENV=production
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
@@ -32,7 +32,7 @@ COPY ./app/views ./app/views
|
|||||||
|
|
||||||
RUN echo "gem 'shakapacker'" > Gemfile && ./bin/shakapacker
|
RUN echo "gem 'shakapacker'" > Gemfile && ./bin/shakapacker
|
||||||
|
|
||||||
FROM ruby:3.4.1-alpine AS app
|
FROM ruby:3.4.2-alpine AS app
|
||||||
|
|
||||||
ENV RAILS_ENV=production
|
ENV RAILS_ENV=production
|
||||||
ENV BUNDLE_WITHOUT="development:test"
|
ENV BUNDLE_WITHOUT="development:test"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
source 'https://rubygems.org'
|
source 'https://rubygems.org'
|
||||||
|
|
||||||
ruby '3.4.1'
|
ruby '3.4.2'
|
||||||
|
|
||||||
gem 'arabic-letter-connector', require: 'arabic-letter-connector/logic'
|
gem 'arabic-letter-connector', require: 'arabic-letter-connector/logic'
|
||||||
gem 'aws-sdk-s3', require: false
|
gem 'aws-sdk-s3', require: false
|
||||||
|
|||||||
@@ -338,18 +338,18 @@ GEM
|
|||||||
net-smtp (0.5.0)
|
net-smtp (0.5.0)
|
||||||
net-protocol
|
net-protocol
|
||||||
nio4r (2.7.4)
|
nio4r (2.7.4)
|
||||||
nokogiri (1.18.5)
|
nokogiri (1.18.8)
|
||||||
mini_portile2 (~> 2.8.2)
|
mini_portile2 (~> 2.8.2)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.5-aarch64-linux-gnu)
|
nokogiri (1.18.8-aarch64-linux-gnu)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.5-aarch64-linux-musl)
|
nokogiri (1.18.8-aarch64-linux-musl)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.5-arm64-darwin)
|
nokogiri (1.18.8-arm64-darwin)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.5-x86_64-linux-gnu)
|
nokogiri (1.18.8-x86_64-linux-gnu)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.18.5-x86_64-linux-musl)
|
nokogiri (1.18.8-x86_64-linux-musl)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
oj (3.16.8)
|
oj (3.16.8)
|
||||||
bigdecimal (>= 3.0)
|
bigdecimal (>= 3.0)
|
||||||
@@ -387,7 +387,7 @@ GEM
|
|||||||
puma (6.5.0)
|
puma (6.5.0)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
racc (1.8.1)
|
racc (1.8.1)
|
||||||
rack (3.1.12)
|
rack (3.1.14)
|
||||||
rack-proxy (0.7.7)
|
rack-proxy (0.7.7)
|
||||||
rack
|
rack
|
||||||
rack-session (2.0.0)
|
rack-session (2.0.0)
|
||||||
@@ -652,7 +652,7 @@ DEPENDENCIES
|
|||||||
webmock
|
webmock
|
||||||
|
|
||||||
RUBY VERSION
|
RUBY VERSION
|
||||||
ruby 3.4.1p0
|
ruby 3.4.2p28
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
2.5.3
|
2.5.3
|
||||||
@@ -42,7 +42,7 @@ DocuSeal is an open source platform that provides secure and efficient digital d
|
|||||||
- PDF signature verification
|
- PDF signature verification
|
||||||
- Users management
|
- Users management
|
||||||
- Mobile-optimized
|
- Mobile-optimized
|
||||||
- 6 UI languages with signing available in 13 languages
|
- 6 UI languages with signing available in 14 languages
|
||||||
- API and Webhooks for integrations
|
- API and Webhooks for integrations
|
||||||
- Easy to deploy in minutes
|
- Easy to deploy in minutes
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ class AccountConfigsController < ApplicationController
|
|||||||
AccountConfig::DOWNLOAD_LINKS_AUTH_KEY,
|
AccountConfig::DOWNLOAD_LINKS_AUTH_KEY,
|
||||||
AccountConfig::FORCE_SSO_AUTH_KEY,
|
AccountConfig::FORCE_SSO_AUTH_KEY,
|
||||||
AccountConfig::FLATTEN_RESULT_PDF_KEY,
|
AccountConfig::FLATTEN_RESULT_PDF_KEY,
|
||||||
|
AccountConfig::ENFORCE_SIGNING_ORDER_KEY,
|
||||||
AccountConfig::WITH_SIGNATURE_ID,
|
AccountConfig::WITH_SIGNATURE_ID,
|
||||||
AccountConfig::COMBINE_PDF_RESULT_KEY,
|
AccountConfig::COMBINE_PDF_RESULT_KEY,
|
||||||
AccountConfig::REQUIRE_SIGNING_REASON_KEY,
|
AccountConfig::REQUIRE_SIGNING_REASON_KEY,
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ class AccountsController < ApplicationController
|
|||||||
def destroy
|
def destroy
|
||||||
authorize!(:manage, current_account)
|
authorize!(:manage, current_account)
|
||||||
|
|
||||||
true_user.update!(locked_at: Time.current)
|
true_user.update!(locked_at: Time.current, email: true_user.email.sub('@', '+removed@'))
|
||||||
|
|
||||||
# rubocop:disable Layout/LineLength
|
# rubocop:disable Layout/LineLength
|
||||||
render turbo_stream: turbo_stream.replace(
|
render turbo_stream: turbo_stream.replace(
|
||||||
|
|||||||
@@ -21,7 +21,11 @@ module Api
|
|||||||
|
|
||||||
blob = ActiveStorage::Blob.find_by!(uuid: blob_uuid)
|
blob = ActiveStorage::Blob.find_by!(uuid: blob_uuid)
|
||||||
|
|
||||||
authorization_check!(blob) if exp.blank?
|
attachment = blob.attachments.take
|
||||||
|
|
||||||
|
@record = attachment.record
|
||||||
|
|
||||||
|
authorization_check!(attachment) if exp.blank?
|
||||||
|
|
||||||
if request.headers['Range'].present?
|
if request.headers['Range'].present?
|
||||||
send_blob_byte_range_data blob, request.headers['Range']
|
send_blob_byte_range_data blob, request.headers['Range']
|
||||||
@@ -37,9 +41,7 @@ module Api
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def authorization_check!(blob)
|
def authorization_check!(attachment)
|
||||||
attachment = blob.attachments.take
|
|
||||||
|
|
||||||
is_authorized = attachment.name.in?(%w[logo preview_images]) ||
|
is_authorized = attachment.name.in?(%w[logo preview_images]) ||
|
||||||
(current_user && attachment.record.account.id == current_user.account_id) ||
|
(current_user && attachment.record.account.id == current_user.account_id) ||
|
||||||
(current_user && !Docuseal.multitenant? && current_user.role == 'superadmin') ||
|
(current_user && !Docuseal.multitenant? && current_user.role == 'superadmin') ||
|
||||||
|
|||||||
@@ -11,13 +11,7 @@ module Api
|
|||||||
|
|
||||||
def index
|
def index
|
||||||
submissions = Submissions.search(@submissions, params[:q])
|
submissions = Submissions.search(@submissions, params[:q])
|
||||||
submissions = submissions.where(template_id: params[:template_id]) if params[:template_id].present?
|
submissions = filter_submissions(submissions, params)
|
||||||
|
|
||||||
if params[:template_folder].present?
|
|
||||||
submissions = submissions.joins(template: :folder).where(folder: { name: params[:template_folder] })
|
|
||||||
end
|
|
||||||
|
|
||||||
submissions = Submissions::Filter.call(submissions, current_user, params)
|
|
||||||
|
|
||||||
submissions = paginate(submissions.preload(:created_by_user, :submitters,
|
submissions = paginate(submissions.preload(:created_by_user, :submitters,
|
||||||
template: :folder,
|
template: :folder,
|
||||||
@@ -111,6 +105,21 @@ module Api
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def filter_submissions(submissions, params)
|
||||||
|
submissions = submissions.where(template_id: params[:template_id]) if params[:template_id].present?
|
||||||
|
submissions = submissions.where(slug: params[:slug]) if params[:slug].present?
|
||||||
|
|
||||||
|
if params[:template_folder].present?
|
||||||
|
submissions = submissions.joins(template: :folder).where(folder: { name: params[:template_folder] })
|
||||||
|
end
|
||||||
|
|
||||||
|
if params.key?(:archived)
|
||||||
|
submissions = params[:archived].in?(['true', true]) ? submissions.archived : submissions.active
|
||||||
|
end
|
||||||
|
|
||||||
|
Submissions::Filter.call(submissions, current_user, params)
|
||||||
|
end
|
||||||
|
|
||||||
def build_create_json(submissions)
|
def build_create_json(submissions)
|
||||||
json = submissions.flat_map do |submission|
|
json = submissions.flat_map do |submission|
|
||||||
submission.submitters.map do |s|
|
submission.submitters.map do |s|
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ module Api
|
|||||||
skip_authorization_check
|
skip_authorization_check
|
||||||
|
|
||||||
def create
|
def create
|
||||||
submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
@submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
||||||
|
|
||||||
if params[:t] == SubmissionEvents.build_tracking_param(submitter, 'click_email')
|
if params[:t] == SubmissionEvents.build_tracking_param(@submitter, 'click_email')
|
||||||
SubmissionEvents.create_with_tracking_data(submitter, 'click_email', request)
|
SubmissionEvents.create_with_tracking_data(@submitter, 'click_email', request)
|
||||||
end
|
end
|
||||||
|
|
||||||
render json: {}
|
render json: {}
|
||||||
|
|||||||
@@ -6,15 +6,15 @@ module Api
|
|||||||
skip_authorization_check
|
skip_authorization_check
|
||||||
|
|
||||||
def create
|
def create
|
||||||
submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
@submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
||||||
|
|
||||||
submitter.opened_at = Time.current
|
@submitter.opened_at = Time.current
|
||||||
submitter.save
|
@submitter.save
|
||||||
|
|
||||||
SubmissionEvents.create_with_tracking_data(submitter, 'view_form', request)
|
SubmissionEvents.create_with_tracking_data(@submitter, 'view_form', request)
|
||||||
|
|
||||||
WebhookUrls.for_account_id(submitter.account_id, 'form.viewed').each do |webhook_url|
|
WebhookUrls.for_account_id(@submitter.account_id, 'form.viewed').each do |webhook_url|
|
||||||
SendFormViewedWebhookRequestJob.perform_async('submitter_id' => submitter.id,
|
SendFormViewedWebhookRequestJob.perform_async('submitter_id' => @submitter.id,
|
||||||
'webhook_url_id' => webhook_url.id)
|
'webhook_url_id' => webhook_url.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -7,15 +7,7 @@ module Api
|
|||||||
def index
|
def index
|
||||||
submitters = Submitters.search(@submitters, params[:q])
|
submitters = Submitters.search(@submitters, params[:q])
|
||||||
|
|
||||||
submitters = submitters.where(external_id: params[:application_key]) if params[:application_key].present?
|
submitters = filter_submitters(submitters, params)
|
||||||
submitters = submitters.where(external_id: params[:external_id]) if params[:external_id].present?
|
|
||||||
submitters = submitters.where(submission_id: params[:submission_id]) if params[:submission_id].present?
|
|
||||||
|
|
||||||
if params[:template_id].present?
|
|
||||||
submitters = submitters.joins(:submission).where(submission: { template_id: params[:template_id] })
|
|
||||||
end
|
|
||||||
|
|
||||||
submitters = maybe_filder_by_completed_at(submitters, params)
|
|
||||||
|
|
||||||
submitters = paginate(
|
submitters = paginate(
|
||||||
submitters.preload(:template, :submission, :submission_events,
|
submitters.preload(:template, :submission, :submission_events,
|
||||||
@@ -163,6 +155,19 @@ module Api
|
|||||||
submitter
|
submitter
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def filter_submitters(submitters, params)
|
||||||
|
submitters = submitters.where(external_id: params[:application_key]) if params[:application_key].present?
|
||||||
|
submitters = submitters.where(external_id: params[:external_id]) if params[:external_id].present?
|
||||||
|
submitters = submitters.where(slug: params[:slug]) if params[:slug].present?
|
||||||
|
submitters = submitters.where(submission_id: params[:submission_id]) if params[:submission_id].present?
|
||||||
|
|
||||||
|
if params[:template_id].present?
|
||||||
|
submitters = submitters.joins(:submission).where(submission: { template_id: params[:template_id] })
|
||||||
|
end
|
||||||
|
|
||||||
|
maybe_filder_by_completed_at(submitters, params)
|
||||||
|
end
|
||||||
|
|
||||||
def assign_external_id(submitter, attrs)
|
def assign_external_id(submitter, attrs)
|
||||||
submitter.external_id = attrs[:application_key] if attrs.key?(:application_key)
|
submitter.external_id = attrs[:application_key] if attrs.key?(:application_key)
|
||||||
submitter.external_id = attrs[:external_id] if attrs.key?(:external_id)
|
submitter.external_id = attrs[:external_id] if attrs.key?(:external_id)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ module Api
|
|||||||
load_and_authorize_resource :template
|
load_and_authorize_resource :template
|
||||||
|
|
||||||
def create
|
def create
|
||||||
authorize!(:manage, @template)
|
authorize!(:create, @template)
|
||||||
|
|
||||||
ActiveRecord::Associations::Preloader.new(
|
ActiveRecord::Associations::Preloader.new(
|
||||||
records: [@template],
|
records: [@template],
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ module Api
|
|||||||
templates = params[:archived].in?(['true', true]) ? templates.archived : templates.active
|
templates = params[:archived].in?(['true', true]) ? templates.archived : templates.active
|
||||||
templates = templates.where(external_id: params[:application_key]) if params[:application_key].present?
|
templates = templates.where(external_id: params[:application_key]) if params[:application_key].present?
|
||||||
templates = templates.where(external_id: params[:external_id]) if params[:external_id].present?
|
templates = templates.where(external_id: params[:external_id]) if params[:external_id].present?
|
||||||
|
templates = templates.where(slug: params[:slug]) if params[:slug].present?
|
||||||
templates = templates.joins(:folder).where(folder: { name: params[:folder] }) if params[:folder].present?
|
templates = templates.joins(:folder).where(folder: { name: params[:folder] }) if params[:folder].present?
|
||||||
|
|
||||||
templates
|
templates
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ class PreviewDocumentPageController < ActionController::API
|
|||||||
|
|
||||||
return head :not_found unless attachment
|
return head :not_found unless attachment
|
||||||
|
|
||||||
|
@template = attachment.record
|
||||||
|
|
||||||
preview_image = attachment.preview_images.joins(:blob)
|
preview_image = attachment.preview_images.joins(:blob)
|
||||||
.find_by(blob: { filename: ["#{params[:id]}.png", "#{params[:id]}.jpg"] })
|
.find_by(blob: { filename: ["#{params[:id]}.png", "#{params[:id]}.jpg"] })
|
||||||
|
|
||||||
|
|||||||
@@ -11,8 +11,11 @@ class StartFormController < ApplicationController
|
|||||||
before_action :load_template
|
before_action :load_template
|
||||||
|
|
||||||
def show
|
def show
|
||||||
|
raise ActionController::RoutingError, I18n.t('not_found') if @template.preferences['require_phone_2fa'] == true
|
||||||
|
|
||||||
@submitter = @template.submissions.new(account_id: @template.account_id)
|
@submitter = @template.submissions.new(account_id: @template.account_id)
|
||||||
.submitters.new(uuid: (filter_undefined_submitters(@template).first ||
|
.submitters.new(account_id: @template.account_id,
|
||||||
|
uuid: (filter_undefined_submitters(@template).first ||
|
||||||
@template.submitters.first)['uuid'])
|
@template.submitters.first)['uuid'])
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -94,6 +97,7 @@ class StartFormController < ApplicationController
|
|||||||
submitter.submission ||= Submission.new(template:,
|
submitter.submission ||= Submission.new(template:,
|
||||||
account_id: template.account_id,
|
account_id: template.account_id,
|
||||||
template_submitters: template.submitters,
|
template_submitters: template.submitters,
|
||||||
|
expire_at: Templates.build_default_expire_at(template),
|
||||||
submitters: [submitter],
|
submitters: [submitter],
|
||||||
source: :link)
|
source: :link)
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,7 @@ class SubmissionsArchivedController < ApplicationController
|
|||||||
@submissions = @submissions.joins(:template)
|
@submissions = @submissions.joins(:template)
|
||||||
@submissions = @submissions.where.not(archived_at: nil)
|
@submissions = @submissions.where.not(archived_at: nil)
|
||||||
.or(@submissions.where.not(templates: { archived_at: nil }))
|
.or(@submissions.where.not(templates: { archived_at: nil }))
|
||||||
.preload(:created_by_user, template: :author)
|
.preload(:template_accesses, :created_by_user, template: :author)
|
||||||
|
|
||||||
@submissions = @submissions.preload(:template_accesses) unless current_user.role.in?(%w[admin superadmin])
|
|
||||||
|
|
||||||
@submissions = Submissions.search(@submissions, params[:q], search_template: true)
|
@submissions = Submissions.search(@submissions, params[:q], search_template: true)
|
||||||
@submissions = Submissions::Filter.call(@submissions, current_user, params)
|
@submissions = Submissions::Filter.call(@submissions, current_user, params)
|
||||||
|
|||||||
@@ -57,6 +57,11 @@ class SubmissionsController < ApplicationController
|
|||||||
Submissions.send_signature_requests(submissions)
|
Submissions.send_signature_requests(submissions)
|
||||||
|
|
||||||
redirect_to template_path(@template), notice: I18n.t('new_recipients_have_been_added')
|
redirect_to template_path(@template), notice: I18n.t('new_recipients_have_been_added')
|
||||||
|
rescue Submissions::CreateFromSubmitters::BaseError => e
|
||||||
|
render turbo_stream: turbo_stream.replace(:submitters_error,
|
||||||
|
partial: 'submissions/error',
|
||||||
|
locals: { error: e.message }),
|
||||||
|
status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
|
|||||||
@@ -8,9 +8,7 @@ class SubmissionsDashboardController < ApplicationController
|
|||||||
|
|
||||||
@submissions = @submissions.where(archived_at: nil)
|
@submissions = @submissions.where(archived_at: nil)
|
||||||
.where(templates: { archived_at: nil })
|
.where(templates: { archived_at: nil })
|
||||||
.preload(:created_by_user, template: :author)
|
.preload(:template_accesses, :created_by_user, template: :author)
|
||||||
|
|
||||||
@submissions = @submissions.preload(:template_accesses) unless current_user.role.in?(%w[admin superadmin])
|
|
||||||
|
|
||||||
@submissions = Submissions.search(@submissions, params[:q], search_template: true)
|
@submissions = Submissions.search(@submissions, params[:q], search_template: true)
|
||||||
@submissions = Submissions::Filter.call(@submissions, current_user, params)
|
@submissions = Submissions::Filter.call(@submissions, current_user, params)
|
||||||
|
|||||||
@@ -8,25 +8,25 @@ class SubmissionsDownloadController < ApplicationController
|
|||||||
FILES_TTL = 5.minutes
|
FILES_TTL = 5.minutes
|
||||||
|
|
||||||
def index
|
def index
|
||||||
submitter = Submitter.find_signed(params[:sig], purpose: :download_completed) if params[:sig].present?
|
@submitter = Submitter.find_signed(params[:sig], purpose: :download_completed) if params[:sig].present?
|
||||||
|
|
||||||
signature_valid =
|
signature_valid =
|
||||||
if submitter&.slug == params[:submitter_slug]
|
if @submitter&.slug == params[:submitter_slug]
|
||||||
true
|
true
|
||||||
else
|
else
|
||||||
submitter = nil
|
@submitter = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
submitter ||= Submitter.find_by!(slug: params[:submitter_slug])
|
@submitter ||= Submitter.find_by!(slug: params[:submitter_slug])
|
||||||
|
|
||||||
Submissions::EnsureResultGenerated.call(submitter)
|
Submissions::EnsureResultGenerated.call(@submitter)
|
||||||
|
|
||||||
last_submitter = submitter.submission.submitters.where.not(completed_at: nil).order(:completed_at).last
|
last_submitter = @submitter.submission.submitters.where.not(completed_at: nil).order(:completed_at).last
|
||||||
|
|
||||||
|
return head :not_found unless last_submitter
|
||||||
|
|
||||||
Submissions::EnsureResultGenerated.call(last_submitter)
|
Submissions::EnsureResultGenerated.call(last_submitter)
|
||||||
|
|
||||||
return head :not_found unless last_submitter.completed_at?
|
|
||||||
|
|
||||||
if last_submitter.completed_at < TTL.ago && !signature_valid && !current_user_submitter?(last_submitter)
|
if last_submitter.completed_at < TTL.ago && !signature_valid && !current_user_submitter?(last_submitter)
|
||||||
Rollbar.info("TTL: #{last_submitter.id}") if defined?(Rollbar)
|
Rollbar.info("TTL: #{last_submitter.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ class SubmissionsDownloadController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
if params[:combined] == 'true'
|
if params[:combined] == 'true'
|
||||||
url = build_combined_url(submitter)
|
url = build_combined_url(@submitter)
|
||||||
|
|
||||||
if url
|
if url
|
||||||
render json: [url]
|
render json: [url]
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ class SubmitFormController < ApplicationController
|
|||||||
skip_before_action :authenticate_user!
|
skip_before_action :authenticate_user!
|
||||||
skip_authorization_check
|
skip_authorization_check
|
||||||
|
|
||||||
|
before_action :load_submitter, only: %i[show update completed]
|
||||||
|
|
||||||
CONFIG_KEYS = [].freeze
|
CONFIG_KEYS = [].freeze
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@submitter = Submitter.find_by!(slug: params[:slug])
|
|
||||||
|
|
||||||
submission = @submitter.submission
|
submission = @submitter.submission
|
||||||
|
|
||||||
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?
|
||||||
@@ -20,7 +20,11 @@ class SubmitFormController < ApplicationController
|
|||||||
@submitter.account.archived_at?
|
@submitter.account.archived_at?
|
||||||
return render :expired if submission.expired?
|
return render :expired if submission.expired?
|
||||||
return render :declined if @submitter.declined_at?
|
return render :declined if @submitter.declined_at?
|
||||||
return render :awaiting if submission.template.preferences['submitters_order'] == 'preserved' &&
|
|
||||||
|
@form_configs = Submitters::FormConfigs.call(@submitter, CONFIG_KEYS)
|
||||||
|
|
||||||
|
return render :awaiting if (@form_configs[:enforce_signing_order] ||
|
||||||
|
submission.template.preferences['submitters_order'] == 'preserved') &&
|
||||||
!Submitters.current_submitter_order?(@submitter)
|
!Submitters.current_submitter_order?(@submitter)
|
||||||
|
|
||||||
Submitters.preload_with_pages(@submitter)
|
Submitters.preload_with_pages(@submitter)
|
||||||
@@ -29,8 +33,6 @@ class SubmitFormController < ApplicationController
|
|||||||
|
|
||||||
@attachments_index = build_attachments_index(submission)
|
@attachments_index = build_attachments_index(submission)
|
||||||
|
|
||||||
@form_configs = Submitters::FormConfigs.call(@submitter, CONFIG_KEYS)
|
|
||||||
|
|
||||||
return unless @form_configs[:prefill_signature]
|
return unless @form_configs[:prefill_signature]
|
||||||
|
|
||||||
if (user_signature = UserConfigs.load_signature(current_user))
|
if (user_signature = UserConfigs.load_signature(current_user))
|
||||||
@@ -48,40 +50,44 @@ class SubmitFormController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
submitter = Submitter.find_by!(slug: params[:slug])
|
if @submitter.completed_at?
|
||||||
|
|
||||||
if submitter.completed_at?
|
|
||||||
return render json: { error: I18n.t('form_has_been_completed_already') }, status: :unprocessable_entity
|
return render json: { error: I18n.t('form_has_been_completed_already') }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
if submitter.template.archived_at? || submitter.submission.archived_at?
|
if @submitter.template.archived_at? || @submitter.submission.archived_at?
|
||||||
return render json: { error: I18n.t('form_has_been_archived') }, status: :unprocessable_entity
|
return render json: { error: I18n.t('form_has_been_archived') }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
if submitter.submission.expired?
|
if @submitter.submission.expired?
|
||||||
return render json: { error: I18n.t('form_has_been_expired') }, status: :unprocessable_entity
|
return render json: { error: I18n.t('form_has_been_expired') }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
if submitter.declined_at?
|
if @submitter.declined_at?
|
||||||
return render json: { error: I18n.t('form_has_been_declined') },
|
return render json: { error: I18n.t('form_has_been_declined') },
|
||||||
status: :unprocessable_entity
|
status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
Submitters::SubmitValues.call(submitter, params, request)
|
Submitters::SubmitValues.call(@submitter, params, request)
|
||||||
|
|
||||||
head :ok
|
head :ok
|
||||||
|
rescue Submitters::SubmitValues::RequiredFieldError => e
|
||||||
|
Rollbar.warning("Required field #{@submitter.id}: #{e.message}") if defined?(Rollbar)
|
||||||
|
|
||||||
|
render json: { field_uuid: e.message }, status: :unprocessable_entity
|
||||||
rescue Submitters::SubmitValues::ValidationError => e
|
rescue Submitters::SubmitValues::ValidationError => e
|
||||||
render json: { error: e.message }, status: :unprocessable_entity
|
render json: { error: e.message }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
def completed
|
def completed; end
|
||||||
@submitter = Submitter.completed.find_by!(slug: params[:submit_form_slug])
|
|
||||||
end
|
|
||||||
|
|
||||||
def success; end
|
def success; end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def load_submitter
|
||||||
|
@submitter = Submitter.find_by!(slug: params[:slug] || params[:submit_form_slug])
|
||||||
|
end
|
||||||
|
|
||||||
def build_attachments_index(submission)
|
def build_attachments_index(submission)
|
||||||
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)
|
||||||
|
|||||||
@@ -7,25 +7,25 @@ class SubmitFormDownloadController < ApplicationController
|
|||||||
FILES_TTL = 5.minutes
|
FILES_TTL = 5.minutes
|
||||||
|
|
||||||
def index
|
def index
|
||||||
submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||||
|
|
||||||
return redirect_to submitter_download_index_path(submitter.slug) if submitter.completed_at?
|
return redirect_to submitter_download_index_path(@submitter.slug) if @submitter.completed_at?
|
||||||
|
|
||||||
return head :unprocessable_entity if submitter.declined_at? ||
|
return head :unprocessable_entity if @submitter.declined_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?
|
||||||
|
|
||||||
last_completed_submitter = submitter.submission.submitters
|
last_completed_submitter = @submitter.submission.submitters
|
||||||
.where.not(id: submitter.id)
|
.where.not(id: @submitter.id)
|
||||||
.where.not(completed_at: nil)
|
.where.not(completed_at: nil)
|
||||||
.max_by(&:completed_at)
|
.max_by(&:completed_at)
|
||||||
|
|
||||||
attachments =
|
attachments =
|
||||||
if last_completed_submitter
|
if last_completed_submitter
|
||||||
Submitters.select_attachments_for_download(last_completed_submitter)
|
Submitters.select_attachments_for_download(last_completed_submitter)
|
||||||
else
|
else
|
||||||
submitter.submission.template.schema_documents.preload(:blob)
|
@submitter.submission.template.schema_documents.preload(:blob)
|
||||||
end
|
end
|
||||||
|
|
||||||
urls = attachments.map do |attachment|
|
urls = attachments.map do |attachment|
|
||||||
|
|||||||
@@ -5,8 +5,9 @@ class TemplateFoldersController < ApplicationController
|
|||||||
|
|
||||||
def show
|
def show
|
||||||
@templates = @template_folder.templates.active.accessible_by(current_ability)
|
@templates = @template_folder.templates.active.accessible_by(current_ability)
|
||||||
.preload(:author, :template_accesses).order(id: :desc)
|
.preload(:author, :template_accesses)
|
||||||
@templates = Templates.search(@templates, params[:q])
|
@templates = Templates.search(@templates, params[:q])
|
||||||
|
@templates = Templates::Order.call(@templates, current_user, cookies.permanent[:dashboard_templates_order])
|
||||||
|
|
||||||
@pagy, @templates = pagy(@templates, limit: 12)
|
@pagy, @templates = pagy(@templates, limit: 12)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class TemplatesCloneAndReplaceController < ApplicationController
|
||||||
|
load_and_authorize_resource :template
|
||||||
|
|
||||||
|
def create
|
||||||
|
return head :unprocessable_entity if params[:files].blank?
|
||||||
|
|
||||||
|
ActiveRecord::Associations::Preloader.new(
|
||||||
|
records: [@template],
|
||||||
|
associations: [schema_documents: :preview_images_attachments]
|
||||||
|
).call
|
||||||
|
|
||||||
|
cloned_template = Templates::Clone.call(@template, author: current_user)
|
||||||
|
cloned_template.name = File.basename(params[:files].first.original_filename, '.*')
|
||||||
|
cloned_template.save!
|
||||||
|
|
||||||
|
documents = Templates::ReplaceAttachments.call(cloned_template, params, extract_fields: true)
|
||||||
|
|
||||||
|
cloned_template.save!
|
||||||
|
|
||||||
|
Templates::CloneAttachments.call(template: cloned_template, original_template: @template,
|
||||||
|
excluded_attachment_uuids: documents.map(&:uuid))
|
||||||
|
|
||||||
|
respond_to do |f|
|
||||||
|
f.html { redirect_to edit_template_path(cloned_template) }
|
||||||
|
f.json { render json: { id: cloned_template.id } }
|
||||||
|
end
|
||||||
|
rescue Templates::CreateAttachments::PdfEncrypted
|
||||||
|
respond_to do |f|
|
||||||
|
f.html { render turbo_stream: turbo_stream.append(params[:form_id], html: helpers.tag.prompt_password) }
|
||||||
|
f.json { render json: { error: 'PDF encrypted', status: 'pdf_encrypted' }, status: :unprocessable_entity }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -21,9 +21,7 @@ class TemplatesController < ApplicationController
|
|||||||
submissions.order(id: :desc)
|
submissions.order(id: :desc)
|
||||||
end
|
end
|
||||||
|
|
||||||
submissions = submissions.preload(:template_accesses) unless current_user.role.in?(%w[admin superadmin])
|
@pagy, @submissions = pagy(submissions.preload(:template_accesses, submitters: :start_form_submission_events))
|
||||||
|
|
||||||
@pagy, @submissions = pagy(submissions.preload(submitters: :start_form_submission_events))
|
|
||||||
rescue ActiveRecord::RecordNotFound
|
rescue ActiveRecord::RecordNotFound
|
||||||
redirect_to root_path
|
redirect_to root_path
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -9,9 +9,11 @@ class TemplatesDashboardController < ApplicationController
|
|||||||
FOLDERS_PER_PAGE = 18
|
FOLDERS_PER_PAGE = 18
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@template_folders = @template_folders.where(id: @templates.active.select(:folder_id)).order(id: :desc)
|
@template_folders = @template_folders.where(id: @templates.active.select(:folder_id))
|
||||||
|
|
||||||
@template_folders = TemplateFolders.search(@template_folders, params[:q])
|
@template_folders = TemplateFolders.search(@template_folders, params[:q])
|
||||||
|
@template_folders = sort_template_folders(@template_folders, current_user,
|
||||||
|
cookies.permanent[:dashboard_templates_order])
|
||||||
|
|
||||||
@pagy, @template_folders = pagy(
|
@pagy, @template_folders = pagy(
|
||||||
@template_folders,
|
@template_folders,
|
||||||
@@ -24,6 +26,7 @@ class TemplatesDashboardController < ApplicationController
|
|||||||
else
|
else
|
||||||
@template_folders = @template_folders.reject { |e| e.name == TemplateFolder::DEFAULT_NAME }
|
@template_folders = @template_folders.reject { |e| e.name == TemplateFolder::DEFAULT_NAME }
|
||||||
@templates = filter_templates(@templates)
|
@templates = filter_templates(@templates)
|
||||||
|
@templates = Templates::Order.call(@templates, current_user, cookies.permanent[:dashboard_templates_order])
|
||||||
|
|
||||||
limit =
|
limit =
|
||||||
if @template_folders.size < 4
|
if @template_folders.size < 4
|
||||||
@@ -39,19 +42,55 @@ class TemplatesDashboardController < ApplicationController
|
|||||||
private
|
private
|
||||||
|
|
||||||
def filter_templates(templates)
|
def filter_templates(templates)
|
||||||
rel = templates.active.preload(:author, :template_accesses).order(id: :desc)
|
rel = templates.active.preload(:author, :template_accesses)
|
||||||
|
|
||||||
if params[:q].blank?
|
if params[:q].blank?
|
||||||
if Docuseal.multitenant? && !current_account.testing?
|
if Docuseal.multitenant? ? current_account.testing? : current_account.linked_account_account
|
||||||
rel = rel.where(folder_id: current_account.default_template_folder.id)
|
shared_account_ids = [current_user.account_id]
|
||||||
else
|
shared_account_ids << TemplateSharing::ALL_ID if !Docuseal.multitenant? && !current_account.testing?
|
||||||
shared_template_ids =
|
|
||||||
TemplateSharing.where(account_id: [current_account.id, TemplateSharing::ALL_ID]).select(:template_id)
|
shared_template_ids = TemplateSharing.where(account_id: shared_account_ids).select(:template_id)
|
||||||
|
|
||||||
rel = rel.where(folder_id: current_account.default_template_folder.id).or(rel.where(id: shared_template_ids))
|
rel = rel.where(folder_id: current_account.default_template_folder.id).or(rel.where(id: shared_template_ids))
|
||||||
|
else
|
||||||
|
rel = rel.where(folder_id: current_account.default_template_folder.id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
Templates.search(rel, params[:q])
|
Templates.search(rel, params[:q])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def sort_template_folders(template_folders, current_user, order)
|
||||||
|
case order
|
||||||
|
when 'used_at'
|
||||||
|
subquery =
|
||||||
|
Template.left_joins(:submissions)
|
||||||
|
.group(:folder_id)
|
||||||
|
.where(account_id: current_user.account_id)
|
||||||
|
.select(
|
||||||
|
:folder_id,
|
||||||
|
Template.arel_table[:updated_at].maximum.as('updated_at_max'),
|
||||||
|
Submission.arel_table[:created_at].maximum.as('submission_created_at_max')
|
||||||
|
)
|
||||||
|
|
||||||
|
template_folders = template_folders.joins(
|
||||||
|
Template.arel_table
|
||||||
|
.join(subquery.arel.as('templates'), Arel::Nodes::OuterJoin)
|
||||||
|
.on(TemplateFolder.arel_table[:id].eq(Template.arel_table[:folder_id]))
|
||||||
|
.join_sources
|
||||||
|
)
|
||||||
|
|
||||||
|
template_folders.order(
|
||||||
|
Arel::Nodes::Case.new
|
||||||
|
.when(Template.arel_table[:submission_created_at_max].gt(Template.arel_table[:updated_at_max]))
|
||||||
|
.then(Template.arel_table[:submission_created_at_max])
|
||||||
|
.else(Template.arel_table[:updated_at_max])
|
||||||
|
.desc
|
||||||
|
)
|
||||||
|
when 'name'
|
||||||
|
template_folders.order(name: :asc)
|
||||||
|
else
|
||||||
|
template_folders.order(id: :desc)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -24,14 +24,22 @@ class TemplatesPreferencesController < ApplicationController
|
|||||||
documents_copy_email_enabled documents_copy_email_attach_audit
|
documents_copy_email_enabled documents_copy_email_attach_audit
|
||||||
documents_copy_email_attach_documents documents_copy_email_reply_to
|
documents_copy_email_attach_documents documents_copy_email_reply_to
|
||||||
completed_notification_email_attach_documents
|
completed_notification_email_attach_documents
|
||||||
completed_redirect_url
|
completed_redirect_url validate_unique_submitters
|
||||||
submitters_order
|
submitters_order require_phone_2fa
|
||||||
|
default_expire_at_duration
|
||||||
|
default_expire_at
|
||||||
completed_notification_email_subject completed_notification_email_body
|
completed_notification_email_subject completed_notification_email_body
|
||||||
completed_notification_email_enabled completed_notification_email_attach_audit] +
|
completed_notification_email_enabled completed_notification_email_attach_audit] +
|
||||||
[completed_message: %i[title body],
|
[completed_message: %i[title body],
|
||||||
submitters: [%i[uuid request_email_subject request_email_body]]]
|
submitters: [%i[uuid request_email_subject request_email_body]]]
|
||||||
).tap do |attrs|
|
).tap do |attrs|
|
||||||
attrs[:preferences].delete(:submitters) if params[:request_email_per_submitter] != '1'
|
attrs[:preferences].delete(:submitters) if params[:request_email_per_submitter] != '1'
|
||||||
|
|
||||||
|
if (default_expire_at = attrs.dig(:preferences, :default_expire_at).presence)
|
||||||
|
attrs[:preferences][:default_expire_at] =
|
||||||
|
(ActiveSupport::TimeZone[current_account.timezone] || Time.zone).parse(default_expire_at).utc
|
||||||
|
end
|
||||||
|
|
||||||
attrs[:preferences] = attrs[:preferences].transform_values do |value|
|
attrs[:preferences] = attrs[:preferences].transform_values do |value|
|
||||||
if %w[true false].include?(value)
|
if %w[true false].include?(value)
|
||||||
value == 'true'
|
value == 'true'
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ class UserConfigsController < ApplicationController
|
|||||||
authorize_resource :user_config
|
authorize_resource :user_config
|
||||||
|
|
||||||
ALLOWED_KEYS = [
|
ALLOWED_KEYS = [
|
||||||
UserConfig::RECEIVE_COMPLETED_EMAIL
|
UserConfig::RECEIVE_COMPLETED_EMAIL,
|
||||||
|
UserConfig::SHOW_APP_TOUR
|
||||||
].freeze
|
].freeze
|
||||||
|
|
||||||
InvalidKey = Class.new(StandardError)
|
InvalidKey = Class.new(StandardError)
|
||||||
@@ -28,6 +29,7 @@ class UserConfigsController < ApplicationController
|
|||||||
def user_config_params
|
def user_config_params
|
||||||
params.required(:user_config).permit(:key, :value, { value: {} }, { value: [] }).tap do |attrs|
|
params.required(:user_config).permit(:key, :value, { value: {} }, { value: [] }).tap do |attrs|
|
||||||
attrs[:value] = attrs[:value] == '1' if attrs[:value].in?(%w[1 0])
|
attrs[:value] = attrs[:value] == '1' if attrs[:value].in?(%w[1 0])
|
||||||
|
attrs[:value] = attrs[:value] == 'true' if attrs[:value].in?(%w[true false])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -32,11 +32,11 @@ import CheckboxGroup from './elements/checkbox_group'
|
|||||||
import MaskedInput from './elements/masked_input'
|
import MaskedInput from './elements/masked_input'
|
||||||
import SetDateButton from './elements/set_date_button'
|
import SetDateButton from './elements/set_date_button'
|
||||||
import IndeterminateCheckbox from './elements/indeterminate_checkbox'
|
import IndeterminateCheckbox from './elements/indeterminate_checkbox'
|
||||||
|
import AppTour from './elements/app_tour'
|
||||||
|
import DashboardDropzone from './elements/dashboard_dropzone'
|
||||||
|
|
||||||
import * as TurboInstantClick from './lib/turbo_instant_click'
|
import * as TurboInstantClick from './lib/turbo_instant_click'
|
||||||
|
|
||||||
import './images/preview.png'
|
|
||||||
|
|
||||||
TurboInstantClick.start()
|
TurboInstantClick.start()
|
||||||
|
|
||||||
document.addEventListener('turbo:before-cache', () => {
|
document.addEventListener('turbo:before-cache', () => {
|
||||||
@@ -101,6 +101,8 @@ safeRegisterElement('checkbox-group', CheckboxGroup)
|
|||||||
safeRegisterElement('masked-input', MaskedInput)
|
safeRegisterElement('masked-input', MaskedInput)
|
||||||
safeRegisterElement('set-date-button', SetDateButton)
|
safeRegisterElement('set-date-button', SetDateButton)
|
||||||
safeRegisterElement('indeterminate-checkbox', IndeterminateCheckbox)
|
safeRegisterElement('indeterminate-checkbox', IndeterminateCheckbox)
|
||||||
|
safeRegisterElement('app-tour', AppTour)
|
||||||
|
safeRegisterElement('dashboard-dropzone', DashboardDropzone)
|
||||||
|
|
||||||
safeRegisterElement('template-builder', class extends HTMLElement {
|
safeRegisterElement('template-builder', class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
@@ -125,8 +127,10 @@ safeRegisterElement('template-builder', class extends HTMLElement {
|
|||||||
withSendButton: this.dataset.withSendButton !== 'false',
|
withSendButton: this.dataset.withSendButton !== 'false',
|
||||||
withSignYourselfButton: this.dataset.withSignYourselfButton !== 'false',
|
withSignYourselfButton: this.dataset.withSignYourselfButton !== 'false',
|
||||||
withConditions: this.dataset.withConditions === 'true',
|
withConditions: this.dataset.withConditions === 'true',
|
||||||
|
withReplaceAndCloneUpload: true,
|
||||||
currencies: (this.dataset.currencies || '').split(',').filter(Boolean),
|
currencies: (this.dataset.currencies || '').split(',').filter(Boolean),
|
||||||
acceptFileTypes: this.dataset.acceptFileTypes
|
acceptFileTypes: this.dataset.acceptFileTypes,
|
||||||
|
showTourStartForm: this.dataset.showTourStartForm === 'true'
|
||||||
})
|
})
|
||||||
|
|
||||||
this.component = this.app.mount(this.appElem)
|
this.component = this.app.mount(this.appElem)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import SignaturePad from 'signature_pad'
|
import SignaturePad from 'signature_pad'
|
||||||
import { cropCanvasAndExportToPNG } from './submission_form/crop_canvas'
|
import { cropCanvasAndExportToPNG } from './submission_form/crop_canvas'
|
||||||
|
import { isValidSignatureCanvas } from './submission_form/validate_signature'
|
||||||
|
|
||||||
window.customElements.define('draw-signature', class extends HTMLElement {
|
window.customElements.define('draw-signature', class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
@@ -43,6 +44,8 @@ window.customElements.define('draw-signature', class extends HTMLElement {
|
|||||||
|
|
||||||
return response
|
return response
|
||||||
})
|
})
|
||||||
|
}).catch(error => {
|
||||||
|
console.log(error)
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
this.submitButton.disabled = false
|
this.submitButton.disabled = false
|
||||||
})
|
})
|
||||||
@@ -65,26 +68,26 @@ window.customElements.define('draw-signature', class extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async submitImage () {
|
async submitImage () {
|
||||||
return new Promise((resolve, reject) => {
|
if (!isValidSignatureCanvas(this.pad.toData())) {
|
||||||
cropCanvasAndExportToPNG(this.canvas, { errorOnTooSmall: true }).then(async (blob) => {
|
alert('Signature is too small or simple. Please redraw.')
|
||||||
const file = new File([blob], 'signature.png', { type: 'image/png' })
|
|
||||||
|
|
||||||
const formData = new FormData()
|
return Promise.reject(new Error('Image too small or simple'))
|
||||||
|
}
|
||||||
|
|
||||||
formData.append('file', file)
|
return cropCanvasAndExportToPNG(this.canvas).then(async (blob) => {
|
||||||
formData.append('submitter_slug', this.dataset.slug)
|
const file = new File([blob], 'signature.png', { type: 'image/png' })
|
||||||
formData.append('name', 'attachments')
|
|
||||||
formData.append('remember_signature', 'true')
|
|
||||||
|
|
||||||
return fetch('/api/attachments', {
|
const formData = new FormData()
|
||||||
method: 'POST',
|
|
||||||
body: formData
|
formData.append('file', file)
|
||||||
}).then((resp) => resp.json()).then((attachment) => {
|
formData.append('submitter_slug', this.dataset.slug)
|
||||||
return resolve(attachment)
|
formData.append('name', 'attachments')
|
||||||
})
|
formData.append('remember_signature', 'true')
|
||||||
}).catch((error) => {
|
|
||||||
return reject(error)
|
return fetch('/api/attachments', {
|
||||||
})
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
}).then(resp => resp.json())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,338 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
async connectedCallback () {
|
||||||
|
this.tourType = this.dataset.type
|
||||||
|
this.nextPagePath = this.dataset.nextPagePath
|
||||||
|
this.I18n = JSON.parse(this.dataset.i18n || '{}')
|
||||||
|
|
||||||
|
if (this.dataset.showTour === 'true') this.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
async start () {
|
||||||
|
if (window.innerWidth < 768) return
|
||||||
|
|
||||||
|
const [{ driver }] = await Promise.all([
|
||||||
|
import('driver.js'),
|
||||||
|
import('driver.js/dist/driver.css')
|
||||||
|
])
|
||||||
|
|
||||||
|
this.driverObj = driver({
|
||||||
|
showProgress: true,
|
||||||
|
nextBtnText: this.I18n.next,
|
||||||
|
prevBtnText: this.I18n.previous,
|
||||||
|
doneBtnText: this.I18n.done,
|
||||||
|
onDestroyStarted: () => {
|
||||||
|
this.disableAppGuide().finally(() => { this.destroy() })
|
||||||
|
},
|
||||||
|
onHighlightStarted: (element) => {
|
||||||
|
if (element) {
|
||||||
|
const clickHandler = () => {
|
||||||
|
this.disableAppGuide().finally(() => { this.destroy() })
|
||||||
|
element.removeEventListener('click', clickHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
element.addEventListener('click', clickHandler)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (this.tourType === 'dashboard') {
|
||||||
|
this.showDashboardTour()
|
||||||
|
} else if (this.tourType === 'builder') {
|
||||||
|
this.showTemplateBuilderTour()
|
||||||
|
} else if (this.tourType === 'account') {
|
||||||
|
this.showAccountTour()
|
||||||
|
} else if (this.tourType === 'template') {
|
||||||
|
this.showTemplateTour()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback () {
|
||||||
|
if (this.driverObj) this.destroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy () {
|
||||||
|
if (this.builderTemplate) this.builderTemplate.fields.shift()
|
||||||
|
if (this.driverObj) this.driverObj.destroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
showTemplateTour () {
|
||||||
|
const steps = [
|
||||||
|
{
|
||||||
|
element: '#share_link_clipboard',
|
||||||
|
popover: {
|
||||||
|
title: this.I18n.copy_and_share_link,
|
||||||
|
description: this.I18n.copy_and_share_link_description,
|
||||||
|
side: 'bottom',
|
||||||
|
align: 'end'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: '#sign_yourself_button',
|
||||||
|
popover: {
|
||||||
|
title: this.I18n.sign_the_document,
|
||||||
|
description: this.I18n.sign_the_document_description,
|
||||||
|
side: 'top',
|
||||||
|
align: 'center'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: '#send_to_recipients_button',
|
||||||
|
popover: {
|
||||||
|
title: this.I18n.send_for_signing,
|
||||||
|
description: this.I18n.add_recipients_description,
|
||||||
|
side: 'top',
|
||||||
|
align: 'center'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: '#add_recipients_button',
|
||||||
|
popover: {
|
||||||
|
title: this.I18n.add_recipients,
|
||||||
|
description: this.I18n.add_recipients_description,
|
||||||
|
side: 'bottom',
|
||||||
|
align: 'end'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: '#account_settings_button',
|
||||||
|
popover: {
|
||||||
|
title: this.I18n.settings,
|
||||||
|
description: this.I18n.settings_template_description,
|
||||||
|
side: 'right',
|
||||||
|
align: 'start',
|
||||||
|
showButtons: this.nextPagePath ? ['next', 'previous', 'close'] : ['previous', 'close'],
|
||||||
|
onNextClick: () => {
|
||||||
|
if (this.nextPagePath) {
|
||||||
|
window.Turbo.visit(this.nextPagePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
].filter((step) => document.querySelector(step.element))
|
||||||
|
|
||||||
|
this.driverObj.setSteps(steps)
|
||||||
|
this.driverObj.drive()
|
||||||
|
}
|
||||||
|
|
||||||
|
showDashboardTour () {
|
||||||
|
this.driverObj.setSteps([
|
||||||
|
{
|
||||||
|
element: '#templates_submissions_toggle',
|
||||||
|
popover: {
|
||||||
|
title: this.I18n.template_and_submissions,
|
||||||
|
description: this.I18n.template_and_submissions_description,
|
||||||
|
side: 'right',
|
||||||
|
align: 'start'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: '#templates_upload_button',
|
||||||
|
popover: {
|
||||||
|
title: this.I18n.upload_a_pdf_file,
|
||||||
|
description: this.I18n.upload_a_pdf_file_description,
|
||||||
|
side: 'left',
|
||||||
|
align: 'start',
|
||||||
|
showButtons: this.nextPagePath ? ['next', 'previous', 'close'] : ['previous', 'close'],
|
||||||
|
onNextClick: () => {
|
||||||
|
if (this.nextPagePath) {
|
||||||
|
window.Turbo.visit(this.nextPagePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onHighlightStarted: () => {}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
this.driverObj.drive()
|
||||||
|
}
|
||||||
|
|
||||||
|
showAccountTour () {
|
||||||
|
this.driverObj.setSteps([
|
||||||
|
{
|
||||||
|
element: '#account_settings_menu',
|
||||||
|
popover: {
|
||||||
|
title: this.I18n.settings,
|
||||||
|
description: this.I18n.settings_account_description,
|
||||||
|
side: 'right',
|
||||||
|
align: 'start'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: '#support_channels',
|
||||||
|
popover: {
|
||||||
|
title: this.I18n.support,
|
||||||
|
description: this.I18n.support_description,
|
||||||
|
side: 'left',
|
||||||
|
align: 'start'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
].filter((step) => document.querySelector(step.element)))
|
||||||
|
|
||||||
|
this.driverObj.drive()
|
||||||
|
}
|
||||||
|
|
||||||
|
showTemplateBuilderTour () {
|
||||||
|
const builderComponent = document.querySelector('template-builder')?.component
|
||||||
|
|
||||||
|
this.builderTemplate = builderComponent?.template
|
||||||
|
|
||||||
|
if (this.builderTemplate) {
|
||||||
|
this.builderTemplate.fields.unshift({
|
||||||
|
uuid: 'b387399b-88dc-4345-9d37-743e97a9b2b3',
|
||||||
|
submitter_uuid: this.builderTemplate.submitters[0].uuid,
|
||||||
|
name: 'First Name',
|
||||||
|
type: 'text'
|
||||||
|
})
|
||||||
|
|
||||||
|
builderComponent.$nextTick(() => {
|
||||||
|
this.driverObj.setSteps([
|
||||||
|
{
|
||||||
|
element: '.roles-dropdown',
|
||||||
|
popover: {
|
||||||
|
title: this.I18n.select_a_signer_party,
|
||||||
|
description: this.I18n.select_a_signer_party_description,
|
||||||
|
side: 'left',
|
||||||
|
align: 'start',
|
||||||
|
onPopoverRender: () => {
|
||||||
|
const rolesDropdown = document.querySelector('.roles-dropdown')
|
||||||
|
|
||||||
|
rolesDropdown.dispatchEvent(new Event('mouseenter', { bubbles: true, cancelable: true }))
|
||||||
|
rolesDropdown.classList.add('dropdown-open')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: '.roles-dropdown .dropdown-content',
|
||||||
|
popover: {
|
||||||
|
title: this.I18n.available_parties,
|
||||||
|
description: this.I18n.available_parties_description,
|
||||||
|
side: 'left',
|
||||||
|
align: 'start',
|
||||||
|
onPopoverRender: () => {
|
||||||
|
document.querySelector('.roles-dropdown .dropdown-content').classList.remove('driver-active-element')
|
||||||
|
},
|
||||||
|
onNextClick: () => {
|
||||||
|
document.querySelector('.roles-dropdown').classList.remove('dropdown-open')
|
||||||
|
this.driverObj.moveNext()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: '#field-types-grid',
|
||||||
|
popover: {
|
||||||
|
title: this.I18n.available_field_types,
|
||||||
|
description: this.I18n.available_field_types_description,
|
||||||
|
side: 'right',
|
||||||
|
align: 'start',
|
||||||
|
onPrevClick: () => {
|
||||||
|
document.querySelector('.roles-dropdown').classList.add('dropdown-open')
|
||||||
|
this.driverObj.movePrevious()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: '#text_type_field_button',
|
||||||
|
popover: {
|
||||||
|
title: this.I18n.text_input_field,
|
||||||
|
description: this.I18n.text_input_field_description,
|
||||||
|
side: 'left',
|
||||||
|
align: 'start'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: '#signature_type_field_button',
|
||||||
|
popover: {
|
||||||
|
title: this.I18n.signature_field,
|
||||||
|
description: this.I18n.signature_field_description,
|
||||||
|
side: 'left',
|
||||||
|
align: 'start'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: '.fields',
|
||||||
|
popover: {
|
||||||
|
title: this.I18n.added_fields,
|
||||||
|
description: this.I18n.added_fields_description,
|
||||||
|
side: 'right',
|
||||||
|
align: 'start'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: '.list-field label:has(svg.tabler-icon-settings)',
|
||||||
|
popover: {
|
||||||
|
title: this.I18n.open_field_settings,
|
||||||
|
description: this.I18n.open_field_settings_description,
|
||||||
|
side: 'bottom',
|
||||||
|
align: 'end',
|
||||||
|
onPopoverRender: () => {
|
||||||
|
const settingsDropdown = document.querySelector('.list-field div:first-child span:has(svg.tabler-icon-settings)')
|
||||||
|
|
||||||
|
document.querySelectorAll('.list-field div:first-child .text-transparent').forEach((e) => e.classList.remove('text-transparent'))
|
||||||
|
settingsDropdown.dispatchEvent(new Event('mouseenter', { bubbles: true, cancelable: true }))
|
||||||
|
settingsDropdown.classList.add('dropdown-open')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: '.list-field div:first-child span:has(svg.tabler-icon-settings) .dropdown-content',
|
||||||
|
popover: {
|
||||||
|
title: this.I18n.field_settings,
|
||||||
|
description: this.I18n.field_settings_description,
|
||||||
|
side: 'left',
|
||||||
|
align: 'start',
|
||||||
|
onPopoverRender: () => {
|
||||||
|
document.querySelector('.list-field div:first-child span:has(svg.tabler-icon-settings) .dropdown-content').classList.remove('driver-active-element')
|
||||||
|
},
|
||||||
|
onNextClick: () => {
|
||||||
|
document.querySelector('.list-field div:first-child span:has(svg.tabler-icon-settings)').classList.remove('dropdown-open')
|
||||||
|
this.driverObj.moveNext()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: '#send_button',
|
||||||
|
popover: {
|
||||||
|
title: this.I18n.send_document,
|
||||||
|
description: this.I18n.send_document_description,
|
||||||
|
side: 'bottom',
|
||||||
|
align: 'end',
|
||||||
|
onPrevClick: () => {
|
||||||
|
document.querySelector('.list-field div:first-child span:has(svg.tabler-icon-settings)').classList.add('dropdown-open')
|
||||||
|
this.driverObj.movePrevious()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: '#sign_yourself_button',
|
||||||
|
popover: {
|
||||||
|
title: this.I18n.sign_yourself,
|
||||||
|
description: this.I18n.sign_yourself_description,
|
||||||
|
side: 'bottom',
|
||||||
|
align: 'end',
|
||||||
|
onNextClick: () => {
|
||||||
|
if (this.nextPagePath) {
|
||||||
|
window.Turbo.visit(this.nextPagePath)
|
||||||
|
} else {
|
||||||
|
this.destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
this.driverObj.drive()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async disableAppGuide () {
|
||||||
|
return fetch('/user_configs', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ key: 'show_app_tour', value: false })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
import { target, targets, targetable } from '@github/catalyst/lib/targetable'
|
||||||
|
|
||||||
|
const loadingIconHtml = `<svg xmlns="http://www.w3.org/2000/svg" class="animate-spin" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<path d="M12 3a9 9 0 1 0 9 9" />
|
||||||
|
</svg>`
|
||||||
|
|
||||||
|
export default targetable(class extends HTMLElement {
|
||||||
|
static [targets.static] = [
|
||||||
|
'hiddenOnDrag',
|
||||||
|
'folderCards',
|
||||||
|
'templateCards'
|
||||||
|
]
|
||||||
|
|
||||||
|
static [target.static] = [
|
||||||
|
'form',
|
||||||
|
'fileDropzone',
|
||||||
|
'fileDropzoneLoading'
|
||||||
|
]
|
||||||
|
|
||||||
|
connectedCallback () {
|
||||||
|
document.addEventListener('drop', this.onWindowDragdrop)
|
||||||
|
document.addEventListener('dragover', this.onWindowDropover)
|
||||||
|
|
||||||
|
window.addEventListener('dragleave', this.onWindowDragleave)
|
||||||
|
|
||||||
|
this.fileDropzone?.addEventListener('drop', this.onDropFile)
|
||||||
|
|
||||||
|
this.folderCards.forEach((el) => el.addEventListener('drop', (e) => this.onDropFolder(e, el)))
|
||||||
|
this.templateCards.forEach((el) => el.addEventListener('drop', this.onDropTemplate))
|
||||||
|
this.templateCards.forEach((el) => el.addEventListener('dragstart', this.onTemplateDragStart))
|
||||||
|
|
||||||
|
return [this.fileDropzone, ...this.folderCards, ...this.templateCards].forEach((el) => {
|
||||||
|
el?.addEventListener('dragover', this.onDragover)
|
||||||
|
el?.addEventListener('dragleave', this.onDragleave)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback () {
|
||||||
|
document.removeEventListener('drop', this.onWindowDragdrop)
|
||||||
|
document.removeEventListener('dragover', this.onWindowDropover)
|
||||||
|
|
||||||
|
window.removeEventListener('dragleave', this.onWindowDragleave)
|
||||||
|
}
|
||||||
|
|
||||||
|
onTemplateDragStart = (e) => {
|
||||||
|
const id = e.target.href.split('/').pop()
|
||||||
|
|
||||||
|
e.dataTransfer.effectAllowed = 'move'
|
||||||
|
|
||||||
|
if (id) {
|
||||||
|
e.dataTransfer.setData('template_id', id)
|
||||||
|
|
||||||
|
const dragPreview = e.target.cloneNode(true)
|
||||||
|
const rect = e.target.getBoundingClientRect()
|
||||||
|
|
||||||
|
const height = e.target.children[0].getBoundingClientRect().height + 50
|
||||||
|
|
||||||
|
dragPreview.children[1].remove()
|
||||||
|
dragPreview.style.width = `${rect.width}px`
|
||||||
|
dragPreview.style.height = `${height}px`
|
||||||
|
dragPreview.style.position = 'absolute'
|
||||||
|
dragPreview.style.top = '-1000px'
|
||||||
|
dragPreview.style.pointerEvents = 'none'
|
||||||
|
dragPreview.style.opacity = '0.9'
|
||||||
|
|
||||||
|
document.body.appendChild(dragPreview)
|
||||||
|
|
||||||
|
e.dataTransfer.setDragImage(dragPreview, rect.width / 2, height / 2)
|
||||||
|
|
||||||
|
setTimeout(() => document.body.removeChild(dragPreview), 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onDropFile = (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
this.fileDropzoneLoading.classList.remove('hidden')
|
||||||
|
this.fileDropzoneLoading.previousElementSibling.classList.add('hidden')
|
||||||
|
this.fileDropzoneLoading.classList.add('opacity-50')
|
||||||
|
|
||||||
|
this.uploadFiles(e.dataTransfer.files, '/templates_upload')
|
||||||
|
}
|
||||||
|
|
||||||
|
onDropFolder = (e, el) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
const templateId = e.dataTransfer.getData('template_id')
|
||||||
|
|
||||||
|
if (e.dataTransfer.files.length || templateId) {
|
||||||
|
const loading = document.createElement('div')
|
||||||
|
const svg = el.querySelector('svg')
|
||||||
|
|
||||||
|
loading.innerHTML = loadingIconHtml
|
||||||
|
loading.children[0].classList.add(...svg.classList)
|
||||||
|
|
||||||
|
el.replaceChild(loading.children[0], svg)
|
||||||
|
el.classList.add('opacity-50')
|
||||||
|
|
||||||
|
if (e.dataTransfer.files.length) {
|
||||||
|
const params = new URLSearchParams({ folder_name: el.innerText }).toString()
|
||||||
|
|
||||||
|
this.uploadFiles(e.dataTransfer.files, `/templates_upload?${params}`)
|
||||||
|
} else {
|
||||||
|
const formData = new FormData()
|
||||||
|
|
||||||
|
formData.append('name', el.innerText)
|
||||||
|
|
||||||
|
fetch(`/templates/${templateId}/folder`, {
|
||||||
|
method: 'PUT',
|
||||||
|
redirect: 'manual',
|
||||||
|
body: formData,
|
||||||
|
headers: {
|
||||||
|
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
|
||||||
|
}
|
||||||
|
}).finally(() => {
|
||||||
|
window.Turbo.cache.clear()
|
||||||
|
window.Turbo.visit(location.href)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onDropTemplate = (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
if (e.dataTransfer.files.length) {
|
||||||
|
const loading = document.createElement('div')
|
||||||
|
loading.classList.add('bottom-5', 'left-0', 'flex', 'justify-center', 'w-full', 'absolute')
|
||||||
|
loading.innerHTML = loadingIconHtml
|
||||||
|
|
||||||
|
e.target.appendChild(loading)
|
||||||
|
e.target.classList.add('opacity-50')
|
||||||
|
|
||||||
|
const id = e.target.href.split('/').pop()
|
||||||
|
|
||||||
|
this.uploadFiles(e.dataTransfer.files, `/templates/${id}/clone_and_replace`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onWindowDragdrop = (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
if (!this.isLoading) this.hideDraghover()
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadFiles (files, url) {
|
||||||
|
this.isLoading = true
|
||||||
|
|
||||||
|
this.form.action = url
|
||||||
|
|
||||||
|
this.form.querySelector('[type="file"]').files = files
|
||||||
|
|
||||||
|
this.form.querySelector('[type="submit"]').click()
|
||||||
|
}
|
||||||
|
|
||||||
|
onWindowDropover = (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
if (e.dataTransfer?.types?.includes('Files')) {
|
||||||
|
this.showDraghover()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onDragover (e) {
|
||||||
|
if (e.dataTransfer?.types?.includes('Files') || this.dataset.targets !== 'dashboard-dropzone.templateCards') {
|
||||||
|
this.style.backgroundColor = '#F7F3F0'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onDragleave () {
|
||||||
|
this.style.backgroundColor = null
|
||||||
|
}
|
||||||
|
|
||||||
|
onWindowDragleave = (e) => {
|
||||||
|
if (e.clientX <= 0 || e.clientY <= 0 || e.clientX >= window.innerWidth || e.clientY >= window.innerHeight) {
|
||||||
|
this.hideDraghover()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showDraghover = () => {
|
||||||
|
if (this.isDrag) return
|
||||||
|
|
||||||
|
this.isDrag = true
|
||||||
|
|
||||||
|
this.fileDropzone?.classList?.remove('hidden')
|
||||||
|
|
||||||
|
this.hiddenOnDrag.forEach((el) => { el.style.display = 'none' })
|
||||||
|
|
||||||
|
return [...this.folderCards, ...this.templateCards].forEach((el) => {
|
||||||
|
el.classList.remove('bg-base-200', 'before:hidden')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
hideDraghover = () => {
|
||||||
|
this.isDrag = false
|
||||||
|
|
||||||
|
this.fileDropzone?.classList?.add('hidden')
|
||||||
|
|
||||||
|
this.hiddenOnDrag.forEach((el) => { el.style.display = null })
|
||||||
|
|
||||||
|
return [...this.folderCards, ...this.templateCards].forEach((el) => {
|
||||||
|
el.classList.add('bg-base-200', 'before:hidden')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
this.form.addEventListener('submit', (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
this.submit()
|
||||||
|
})
|
||||||
|
|
||||||
|
if (this.dataset.onload === 'true') {
|
||||||
|
this.form.querySelector('button').click()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
submit () {
|
||||||
|
fetch(this.form.action, {
|
||||||
|
method: this.form.method,
|
||||||
|
body: new FormData(this.form)
|
||||||
|
}).then(async (resp) => {
|
||||||
|
if (!resp.ok) {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(await resp.text())
|
||||||
|
|
||||||
|
if (data.error) {
|
||||||
|
alert(data.error)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
get form () {
|
||||||
|
return this.querySelector('form')
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -35,7 +35,7 @@ export default actionable(targetable(class extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toggleLoading = (e) => {
|
toggleLoading = (e) => {
|
||||||
if (e && e.target && !e.target.contains(this)) {
|
if (e && e.target && (!e.target.contains(this) || !e.detail?.formSubmission?.formElement?.contains(this))) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
export default class extends HTMLElement {
|
||||||
|
connectedCallback () {
|
||||||
|
this.header = document.querySelector('#signing_form_header')
|
||||||
|
|
||||||
|
window.addEventListener('scroll', this.onScroll)
|
||||||
|
window.addEventListener('resize', this.onResize)
|
||||||
|
|
||||||
|
if (!this.isNarrow() && this.isHeaderNotVisible()) {
|
||||||
|
this.showButtons({ animate: false })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback () {
|
||||||
|
window.removeEventListener('scroll', this.onScroll)
|
||||||
|
window.removeEventListener('resize', this.onResize)
|
||||||
|
}
|
||||||
|
|
||||||
|
onResize = () => {
|
||||||
|
if (this.isNarrow()) {
|
||||||
|
this.hideButtons(true)
|
||||||
|
} else if (this.isHeaderNotVisible()) {
|
||||||
|
this.showButtons()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isNarrow () {
|
||||||
|
return window.innerWidth < 1230
|
||||||
|
}
|
||||||
|
|
||||||
|
onScroll = () => {
|
||||||
|
if (this.isHeaderNotVisible() && !this.isNarrow()) {
|
||||||
|
this.showButtons()
|
||||||
|
} else {
|
||||||
|
this.hideButtons()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isHeaderNotVisible () {
|
||||||
|
const rect = this.header.getBoundingClientRect()
|
||||||
|
return rect.bottom <= 0 || rect.top >= window.innerHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
showButtons ({ animate } = { animate: true }) {
|
||||||
|
if (animate) {
|
||||||
|
this.classList.add('transition-transform', 'duration-300')
|
||||||
|
}
|
||||||
|
|
||||||
|
this.classList.remove('hidden', '-translate-y-10', 'opacity-0')
|
||||||
|
this.classList.add('translate-y-0', 'opacity-100')
|
||||||
|
}
|
||||||
|
|
||||||
|
hideButtons () {
|
||||||
|
this.classList.remove('translate-y-0', 'opacity-100')
|
||||||
|
this.classList.add('-translate-y-10', 'opacity-0')
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this.classList.contains('-translate-y-10')) {
|
||||||
|
this.classList.add('hidden')
|
||||||
|
}
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ export default actionable(class extends HTMLElement {
|
|||||||
const elementIds = JSON.parse(this.dataset.elementIds)
|
const elementIds = JSON.parse(this.dataset.elementIds)
|
||||||
|
|
||||||
elementIds.forEach((elementId) => {
|
elementIds.forEach((elementId) => {
|
||||||
document.getElementById(elementId).classList.toggle('hidden', event.target.value !== elementId)
|
document.getElementById(elementId).classList.toggle('hidden', (event.target.dataset.toggleId || event.target.value) !== elementId)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -3,11 +3,15 @@ import { createApp, reactive } from 'vue'
|
|||||||
import Form from './submission_form/form'
|
import Form from './submission_form/form'
|
||||||
import DownloadButton from './elements/download_button'
|
import DownloadButton from './elements/download_button'
|
||||||
import ToggleSubmit from './elements/toggle_submit'
|
import ToggleSubmit from './elements/toggle_submit'
|
||||||
|
import FetchForm from './elements/fetch_form'
|
||||||
|
import ScrollButtons from './elements/scroll_buttons'
|
||||||
|
|
||||||
const safeRegisterElement = (name, element, options = {}) => !window.customElements.get(name) && window.customElements.define(name, element, options)
|
const safeRegisterElement = (name, element, options = {}) => !window.customElements.get(name) && window.customElements.define(name, element, options)
|
||||||
|
|
||||||
safeRegisterElement('download-button', DownloadButton)
|
safeRegisterElement('download-button', DownloadButton)
|
||||||
safeRegisterElement('toggle-submit', ToggleSubmit)
|
safeRegisterElement('toggle-submit', ToggleSubmit)
|
||||||
|
safeRegisterElement('fetch-form', FetchForm)
|
||||||
|
safeRegisterElement('scroll-buttons', ScrollButtons)
|
||||||
safeRegisterElement('submission-form', class extends HTMLElement {
|
safeRegisterElement('submission-form', class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
this.appElem = document.createElement('div')
|
this.appElem = document.createElement('div')
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 41 KiB |
@@ -183,7 +183,7 @@
|
|||||||
v-else
|
v-else
|
||||||
ref="textContainer"
|
ref="textContainer"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
class="flex items-center px-0.5 w-full"
|
class="flex px-0.5 w-full"
|
||||||
:class="{ ...alignClasses, ...fontClasses }"
|
:class="{ ...alignClasses, ...fontClasses }"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@@ -327,13 +327,16 @@ export default {
|
|||||||
},
|
},
|
||||||
alignClasses () {
|
alignClasses () {
|
||||||
if (!this.field.preferences) {
|
if (!this.field.preferences) {
|
||||||
return {}
|
return { 'items-center': true }
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'text-center': this.field.preferences.align === 'center',
|
'text-center': this.field.preferences.align === 'center',
|
||||||
'text-left': this.field.preferences.align === 'left',
|
'text-left': this.field.preferences.align === 'left',
|
||||||
'text-right': this.field.preferences.align === 'right'
|
'text-right': this.field.preferences.align === 'right',
|
||||||
|
'items-center': !this.field.preferences.valign || this.field.preferences.valign === 'center',
|
||||||
|
'items-start': this.field.preferences.valign === 'top',
|
||||||
|
'items-end': this.field.preferences.valign === 'bottom'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
fontClasses () {
|
fontClasses () {
|
||||||
|
|||||||
@@ -193,9 +193,9 @@ export default {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (window.decline_button) {
|
document.querySelectorAll('#decline_button').forEach((button) => {
|
||||||
window.decline_button.setAttribute('disabled', 'true')
|
button.setAttribute('disabled', 'true')
|
||||||
}
|
})
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
sendCopyToEmail () {
|
sendCopyToEmail () {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
:with-label="!isAnonymousChecboxes && showFieldNames"
|
:with-label="!isAnonymousChecboxes && showFieldNames"
|
||||||
:current-step="currentStepFields"
|
:current-step="currentStepFields"
|
||||||
:scroll-padding="scrollPadding"
|
:scroll-padding="scrollPadding"
|
||||||
@focus-step="[saveStep(), goToStep($event, false, true), currentField.type !== 'checkbox' ? isFormVisible = true : '']"
|
@focus-step="[saveStep(), currentField.type !== 'checkbox' ? isFormVisible = true : '', goToStep($event, false, true)]"
|
||||||
/>
|
/>
|
||||||
<FieldAreas
|
<FieldAreas
|
||||||
:steps="readonlyConditionalFields.map((e) => [e])"
|
:steps="readonlyConditionalFields.map((e) => [e])"
|
||||||
@@ -1404,7 +1404,21 @@ export default {
|
|||||||
if (response.status === 422 || response.status === 500) {
|
if (response.status === 422 || response.status === 500) {
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
|
|
||||||
if (data.error) {
|
if (data.field_uuid) {
|
||||||
|
const field = this.fieldsUuidIndex[data.field_uuid]
|
||||||
|
|
||||||
|
if (field) {
|
||||||
|
const step = this.stepFields.findIndex((fields) => fields.includes(field))
|
||||||
|
|
||||||
|
if (step !== -1) {
|
||||||
|
this.goToStep(step, this.autoscrollFields)
|
||||||
|
|
||||||
|
this.showFillAllRequiredFields = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject(new Error('Required field: ' + data.field_uuid))
|
||||||
|
} else if (data.error) {
|
||||||
const i18nKey = data.error.replace(/\s+/g, '_').toLowerCase()
|
const i18nKey = data.error.replace(/\s+/g, '_').toLowerCase()
|
||||||
|
|
||||||
alert(this.t(i18nKey) !== i18nKey ? this.t(i18nKey) : data.error)
|
alert(this.t(i18nKey) !== i18nKey ? this.t(i18nKey) : data.error)
|
||||||
@@ -1439,11 +1453,7 @@ export default {
|
|||||||
this.isSubmittingComplete = false
|
this.isSubmittingComplete = false
|
||||||
})
|
})
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
if (error?.message === 'Image too small') {
|
console.log(error)
|
||||||
alert(this.t('signature_is_too_small_please_redraw'))
|
|
||||||
} else {
|
|
||||||
console.log(error)
|
|
||||||
}
|
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
this.isSubmitting = false
|
this.isSubmitting = false
|
||||||
this.isSubmittingComplete = false
|
this.isSubmittingComplete = false
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ const en = {
|
|||||||
reupload: 'Reupload',
|
reupload: 'Reupload',
|
||||||
upload: 'Upload',
|
upload: 'Upload',
|
||||||
files: 'Files',
|
files: 'Files',
|
||||||
signature_is_too_small_please_redraw: 'Signature is too small. Please redraw.',
|
signature_is_too_small_or_simple_please_redraw: 'Signature is too small or simple. Please redraw.',
|
||||||
wait_countdown_seconds: 'Wait {countdown} seconds'
|
wait_countdown_seconds: 'Wait {countdown} seconds'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,6 +148,7 @@ const es = {
|
|||||||
sign_now: 'Firmar ahora',
|
sign_now: 'Firmar ahora',
|
||||||
type_here_: 'Escribe aquí...',
|
type_here_: 'Escribe aquí...',
|
||||||
optional: 'opcional',
|
optional: 'opcional',
|
||||||
|
option: 'Opción',
|
||||||
appears_on: 'Aparece en',
|
appears_on: 'Aparece en',
|
||||||
page: 'Página',
|
page: 'Página',
|
||||||
select_your_option: 'Selecciona tu opción',
|
select_your_option: 'Selecciona tu opción',
|
||||||
@@ -191,7 +192,7 @@ const es = {
|
|||||||
reupload: 'Volver a subir',
|
reupload: 'Volver a subir',
|
||||||
upload: 'Subir',
|
upload: 'Subir',
|
||||||
files: 'Archivos',
|
files: 'Archivos',
|
||||||
signature_is_too_small_please_redraw: 'La firma es demasiado pequeña. Por favor, dibújala de nuevo.',
|
signature_is_too_small_or_simple_please_redraw: 'La firma es demasiado pequeña o simple. Por favor, vuelve a dibujarla.',
|
||||||
wait_countdown_seconds: 'Espera {countdown} segundos'
|
wait_countdown_seconds: 'Espera {countdown} segundos'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,6 +246,7 @@ const it = {
|
|||||||
sign_now: 'Firma ora',
|
sign_now: 'Firma ora',
|
||||||
type_here_: 'Digita qui...',
|
type_here_: 'Digita qui...',
|
||||||
optional: 'opzionale',
|
optional: 'opzionale',
|
||||||
|
option: 'Opzione',
|
||||||
appears_on: 'Compare su',
|
appears_on: 'Compare su',
|
||||||
page: 'Pagina',
|
page: 'Pagina',
|
||||||
take_photo: 'Scattare una foto',
|
take_photo: 'Scattare una foto',
|
||||||
@@ -265,7 +267,7 @@ const it = {
|
|||||||
document_has_been_signed: 'Il documento è stato firmato!',
|
document_has_been_signed: 'Il documento è stato firmato!',
|
||||||
documents_have_been_signed: 'I documenti sono stati firmati!',
|
documents_have_been_signed: 'I documenti sono stati firmati!',
|
||||||
create_a_free_account: 'Crea un Account Gratuito',
|
create_a_free_account: 'Crea un Account Gratuito',
|
||||||
powered_by: 'Desarrollado por',
|
powered_by: 'Fornito da',
|
||||||
please_check_the_box_to_continue: 'Si prega di spuntare la casella per continuare.',
|
please_check_the_box_to_continue: 'Si prega di spuntare la casella per continuare.',
|
||||||
open_source_documents_software: 'software di documenti open source',
|
open_source_documents_software: 'software di documenti open source',
|
||||||
verified_phone_number: 'Verifica numero di telefono',
|
verified_phone_number: 'Verifica numero di telefono',
|
||||||
@@ -289,7 +291,7 @@ const it = {
|
|||||||
reupload: 'Ricarica',
|
reupload: 'Ricarica',
|
||||||
upload: 'Carica',
|
upload: 'Carica',
|
||||||
files: 'File',
|
files: 'File',
|
||||||
signature_is_too_small_please_redraw: 'La firma è troppo piccola. Ridisegnala per favore.',
|
signature_is_too_small_or_simple_please_redraw: 'La firma è troppo piccola o semplice. Ridisegnala, per favore.',
|
||||||
wait_countdown_seconds: 'Attendi {countdown} secondi'
|
wait_countdown_seconds: 'Attendi {countdown} secondi'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -343,6 +345,7 @@ const de = {
|
|||||||
sign_now: 'Jetzt unterschreiben',
|
sign_now: 'Jetzt unterschreiben',
|
||||||
type_here_: 'Hier eingeben...',
|
type_here_: 'Hier eingeben...',
|
||||||
optional: 'optional',
|
optional: 'optional',
|
||||||
|
option: 'Option',
|
||||||
appears_on: 'Erscheint auf',
|
appears_on: 'Erscheint auf',
|
||||||
page: 'Seite',
|
page: 'Seite',
|
||||||
take_photo: 'Foto aufnehmen',
|
take_photo: 'Foto aufnehmen',
|
||||||
@@ -387,14 +390,14 @@ const de = {
|
|||||||
reupload: 'Erneut hochladen',
|
reupload: 'Erneut hochladen',
|
||||||
upload: 'Hochladen',
|
upload: 'Hochladen',
|
||||||
files: 'Dateien',
|
files: 'Dateien',
|
||||||
signature_is_too_small_please_redraw: 'Die Unterschrift ist zu klein. Bitte erneut zeichnen.',
|
signature_is_too_small_or_simple_please_redraw: 'Die Unterschrift ist zu klein oder zu einfach. Bitte erneut zeichnen.',
|
||||||
wait_countdown_seconds: 'Warte {countdown} Sekunden'
|
wait_countdown_seconds: 'Warte {countdown} Sekunden'
|
||||||
}
|
}
|
||||||
|
|
||||||
const fr = {
|
const fr = {
|
||||||
complete_all_required_fields_to_proceed_with_identity_verification: "Veuillez remplir tous les champs obligatoires pour continuer la vérification de l'identité.",
|
complete_all_required_fields_to_proceed_with_identity_verification: "Veuillez remplir tous les champs obligatoires pour continuer la vérification de l'identité.",
|
||||||
verif_id: "Vérification de l'ID",
|
verify_id: "Vérification de l'ID",
|
||||||
verif_identite: "Vérification de l'identité",
|
identity_verification: "Vérification de l'identité",
|
||||||
complete: 'Terminer',
|
complete: 'Terminer',
|
||||||
fill_all_required_fields_to_complete: 'Veuillez remplir tous les champs obligatoires pour compléter',
|
fill_all_required_fields_to_complete: 'Veuillez remplir tous les champs obligatoires pour compléter',
|
||||||
sign_and_complete: 'Signer et Terminer',
|
sign_and_complete: 'Signer et Terminer',
|
||||||
@@ -441,6 +444,7 @@ const fr = {
|
|||||||
sign_now: 'Signer maintenant',
|
sign_now: 'Signer maintenant',
|
||||||
type_here_: 'Tapez ici...',
|
type_here_: 'Tapez ici...',
|
||||||
optional: 'facultatif',
|
optional: 'facultatif',
|
||||||
|
option: 'Option',
|
||||||
appears_on: 'Apparaît sur',
|
appears_on: 'Apparaît sur',
|
||||||
page: 'Page',
|
page: 'Page',
|
||||||
take_photo: 'Prendre une photo',
|
take_photo: 'Prendre une photo',
|
||||||
@@ -485,7 +489,7 @@ const fr = {
|
|||||||
reupload: 'Recharger',
|
reupload: 'Recharger',
|
||||||
upload: 'Télécharger',
|
upload: 'Télécharger',
|
||||||
files: 'Fichiers',
|
files: 'Fichiers',
|
||||||
signature_is_too_small_please_redraw: 'La signature est trop petite. Veuillez la redessiner.',
|
signature_is_too_small_or_simple_please_redraw: 'La signature est trop petite ou trop simple. Veuillez la redessiner.',
|
||||||
wait_countdown_seconds: 'Attendez {countdown} secondes'
|
wait_countdown_seconds: 'Attendez {countdown} secondes'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -539,6 +543,7 @@ const pl = {
|
|||||||
sign_now: 'Podpisz teraz',
|
sign_now: 'Podpisz teraz',
|
||||||
type_here_: 'Wpisz tutaj...',
|
type_here_: 'Wpisz tutaj...',
|
||||||
optional: 'opcjonalny',
|
optional: 'opcjonalny',
|
||||||
|
option: 'Opcja',
|
||||||
appears_on: 'Pojawia się na',
|
appears_on: 'Pojawia się na',
|
||||||
page: 'Strona',
|
page: 'Strona',
|
||||||
select_your_option: 'Wybierz swoją opcję',
|
select_your_option: 'Wybierz swoją opcję',
|
||||||
@@ -583,7 +588,8 @@ const pl = {
|
|||||||
reupload: 'Ponowne przesłanie',
|
reupload: 'Ponowne przesłanie',
|
||||||
upload: 'Przesyłanie',
|
upload: 'Przesyłanie',
|
||||||
files: 'Pliki',
|
files: 'Pliki',
|
||||||
signature_is_too_small_please_redraw: 'Podpis jest zbyt mały. Proszę narysować go ponownie.'
|
signature_is_too_small_or_simple_please_redraw: 'Podpis jest zbyt mały lub zbyt prosty. Proszę narysować go ponownie.',
|
||||||
|
wait_countdown_seconds: 'Poczekaj {countdown} sekund'
|
||||||
}
|
}
|
||||||
|
|
||||||
const uk = {
|
const uk = {
|
||||||
@@ -636,6 +642,7 @@ const uk = {
|
|||||||
sign_now: 'Підписати зараз',
|
sign_now: 'Підписати зараз',
|
||||||
type_here_: 'Введіть тут',
|
type_here_: 'Введіть тут',
|
||||||
optional: 'необов’язково',
|
optional: 'необов’язково',
|
||||||
|
option: 'Опція',
|
||||||
appears_on: "З'являється на",
|
appears_on: "З'являється на",
|
||||||
page: 'Сторінка',
|
page: 'Сторінка',
|
||||||
take_photo: 'Зробити фото',
|
take_photo: 'Зробити фото',
|
||||||
@@ -658,7 +665,7 @@ const uk = {
|
|||||||
create_a_free_account: 'Створити безкоштовний обліковий запис',
|
create_a_free_account: 'Створити безкоштовний обліковий запис',
|
||||||
powered_by: 'Працює на базі',
|
powered_by: 'Працює на базі',
|
||||||
please_check_the_box_to_continue: 'Будь ласка, позначте прапорець, щоб продовжити.',
|
please_check_the_box_to_continue: 'Будь ласка, позначте прапорець, щоб продовжити.',
|
||||||
open_source_documents_software: 'відкритий програмний засіб для документів',
|
open_source_documents_software: 'відкрите програмне забезпечення для документів',
|
||||||
verified_phone_number: 'Підтвердіть номер телефону',
|
verified_phone_number: 'Підтвердіть номер телефону',
|
||||||
use_international_format: 'Використовуйте міжнародний формат: +1xxx',
|
use_international_format: 'Використовуйте міжнародний формат: +1xxx',
|
||||||
six_digits_code: '6-значний код',
|
six_digits_code: '6-значний код',
|
||||||
@@ -680,7 +687,7 @@ const uk = {
|
|||||||
reupload: 'Перезавантажити',
|
reupload: 'Перезавантажити',
|
||||||
upload: 'Завантажити',
|
upload: 'Завантажити',
|
||||||
files: 'Файли',
|
files: 'Файли',
|
||||||
signature_is_too_small_please_redraw: 'Підпис занадто малий. Будь ласка, перемалюйте його.',
|
signature_is_too_small_or_simple_please_redraw: 'Підпис занадто маленький або надто простий. Будь ласка, перемалюйте.',
|
||||||
wait_countdown_seconds: 'Зачекайте {countdown} секунд'
|
wait_countdown_seconds: 'Зачекайте {countdown} секунд'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -734,6 +741,7 @@ const cs = {
|
|||||||
sign_now: 'Podepsat nyní',
|
sign_now: 'Podepsat nyní',
|
||||||
type_here_: 'Zadejte zde',
|
type_here_: 'Zadejte zde',
|
||||||
optional: 'volitelné',
|
optional: 'volitelné',
|
||||||
|
option: 'Možnost',
|
||||||
appears_on: 'Zobrazuje se na',
|
appears_on: 'Zobrazuje se na',
|
||||||
page: 'Stránka',
|
page: 'Stránka',
|
||||||
select_your_option: 'Vyberte svou volbu',
|
select_your_option: 'Vyberte svou volbu',
|
||||||
@@ -778,7 +786,7 @@ const cs = {
|
|||||||
reupload: 'Znovu nahrát',
|
reupload: 'Znovu nahrát',
|
||||||
upload: 'Nahrát',
|
upload: 'Nahrát',
|
||||||
files: 'Soubory',
|
files: 'Soubory',
|
||||||
signature_is_too_small_please_redraw: 'Podpis je příliš malý. Prosím, překreslete ho.',
|
signature_is_too_small_or_simple_please_redraw: 'Podpis je příliš malý nebo jednoduchý. Nakreslete jej prosím znovu.',
|
||||||
wait_countdown_seconds: 'Počkejte {countdown} sekund'
|
wait_countdown_seconds: 'Počkejte {countdown} sekund'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -832,6 +840,7 @@ const pt = {
|
|||||||
sign_now: 'Assinar agora',
|
sign_now: 'Assinar agora',
|
||||||
type_here_: 'Digite aqui',
|
type_here_: 'Digite aqui',
|
||||||
optional: 'opcional',
|
optional: 'opcional',
|
||||||
|
option: 'Opção',
|
||||||
appears_on: 'Aparece em',
|
appears_on: 'Aparece em',
|
||||||
page: 'Página',
|
page: 'Página',
|
||||||
take_photo: 'Tirar foto',
|
take_photo: 'Tirar foto',
|
||||||
@@ -876,7 +885,7 @@ const pt = {
|
|||||||
reupload: 'Reenviar',
|
reupload: 'Reenviar',
|
||||||
upload: 'Carregar',
|
upload: 'Carregar',
|
||||||
files: 'Arquivos',
|
files: 'Arquivos',
|
||||||
signature_is_too_small_please_redraw: 'A assinatura é muito pequena. Por favor, redesenhe-a.',
|
signature_is_too_small_or_simple_please_redraw: 'A assinatura é muito pequena ou simples. Por favor, redesenhe.',
|
||||||
wait_countdown_seconds: 'Aguarde {countdown} segundos'
|
wait_countdown_seconds: 'Aguarde {countdown} segundos'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -975,7 +984,7 @@ const he = {
|
|||||||
reupload: 'העלה שוב',
|
reupload: 'העלה שוב',
|
||||||
upload: 'העלאה',
|
upload: 'העלאה',
|
||||||
files: 'קבצים',
|
files: 'קבצים',
|
||||||
signature_is_too_small_please_redraw: 'החתימה קטנה מדי. אנא צייר מחדש.',
|
signature_is_too_small_or_simple_please_redraw: 'החתימה קטנה או פשוטה מדי. אנא חתום מחדש.',
|
||||||
wait_countdown_seconds: 'המתן {countdown} שניות'
|
wait_countdown_seconds: 'המתן {countdown} שניות'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1074,7 +1083,7 @@ const nl = {
|
|||||||
reupload: 'Opnieuw uploaden',
|
reupload: 'Opnieuw uploaden',
|
||||||
upload: 'Uploaden',
|
upload: 'Uploaden',
|
||||||
files: 'Bestanden',
|
files: 'Bestanden',
|
||||||
signature_is_too_small_please_redraw: 'De handtekening is te klein. Teken deze opnieuw, alstublieft.',
|
signature_is_too_small_or_simple_please_redraw: 'De handtekening is te klein of te eenvoudig. Teken opnieuw.',
|
||||||
wait_countdown_seconds: 'Wacht {countdown} seconden'
|
wait_countdown_seconds: 'Wacht {countdown} seconden'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1097,6 +1106,7 @@ const ar = {
|
|||||||
select_a_reason: 'اختر سببًا',
|
select_a_reason: 'اختر سببًا',
|
||||||
value_is_invalid: 'القيمة غير صالحة',
|
value_is_invalid: 'القيمة غير صالحة',
|
||||||
verification_code_is_invalid: 'رمز التحقق غير صالح',
|
verification_code_is_invalid: 'رمز التحقق غير صالح',
|
||||||
|
already_paid: 'تم الدفع بالفعل',
|
||||||
drawn_signature_on_a_touchscreen_device: 'توقيع مرسوم على جهاز بشاشة تعمل باللمس',
|
drawn_signature_on_a_touchscreen_device: 'توقيع مرسوم على جهاز بشاشة تعمل باللمس',
|
||||||
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'امسح رمز الاستجابة السريعة باستخدام تطبيق الكاميرا لفتح النموذج على الهاتف المحمول ورسم توقيعك',
|
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'امسح رمز الاستجابة السريعة باستخدام تطبيق الكاميرا لفتح النموذج على الهاتف المحمول ورسم توقيعك',
|
||||||
by_clicking_you_agree_to_the: 'بالنقر فوق "{button}"، أنت توافق على',
|
by_clicking_you_agree_to_the: 'بالنقر فوق "{button}"، أنت توافق على',
|
||||||
@@ -1172,7 +1182,7 @@ const ar = {
|
|||||||
reupload: 'إعادة التحميل',
|
reupload: 'إعادة التحميل',
|
||||||
upload: 'تحميل',
|
upload: 'تحميل',
|
||||||
files: 'الملفات',
|
files: 'الملفات',
|
||||||
signature_is_too_small_please_redraw: 'التوقيع صغير جدًا. يرجى إعادة الرسم.',
|
signature_is_too_small_or_simple_please_redraw: 'التوقيع صغير جدًا أو بسيط جدًا. يرجى إعادة رسمه.',
|
||||||
wait_countdown_seconds: 'انتظر {countdown} ثانية'
|
wait_countdown_seconds: 'انتظر {countdown} ثانية'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1198,6 +1208,8 @@ const ko = {
|
|||||||
by_clicking_you_agree_to_the: '"{button}"를 클릭함으로써, 다음에 동의하게 됩니다',
|
by_clicking_you_agree_to_the: '"{button}"를 클릭함으로써, 다음에 동의하게 됩니다',
|
||||||
electronic_signature_disclosure: '전자 서명 공개',
|
electronic_signature_disclosure: '전자 서명 공개',
|
||||||
esignature_disclosure: '전자 서명 공개',
|
esignature_disclosure: '전자 서명 공개',
|
||||||
|
value_is_invalid: '값이 올바르지 않습니다',
|
||||||
|
verification_code_is_invalid: '인증 코드가 올바르지 않습니다',
|
||||||
already_paid: '이미 지불됨',
|
already_paid: '이미 지불됨',
|
||||||
text: '텍스트',
|
text: '텍스트',
|
||||||
signature: '서명',
|
signature: '서명',
|
||||||
@@ -1269,10 +1281,109 @@ const ko = {
|
|||||||
reupload: '다시 업로드',
|
reupload: '다시 업로드',
|
||||||
upload: '업로드',
|
upload: '업로드',
|
||||||
files: '파일',
|
files: '파일',
|
||||||
signature_is_too_small_please_redraw: '서명이 너무 작습니다. 다시 그려주세요.',
|
signature_is_too_small_or_simple_please_redraw: '서명이 너무 작거나 단순합니다. 다시 그려주세요.',
|
||||||
wait_countdown_seconds: '{countdown}초 기다리세요'
|
wait_countdown_seconds: '{countdown}초 기다리세요'
|
||||||
}
|
}
|
||||||
|
|
||||||
const i18n = { en, es, it, de, fr, pl, uk, cs, pt, he, nl, ar, ko }
|
const ja = {
|
||||||
|
complete_all_required_fields_to_proceed_with_identity_verification: '本人確認を進めるには、すべての必須項目を入力してください。',
|
||||||
|
verify_id: '本人確認',
|
||||||
|
identity_verification: '本人確認',
|
||||||
|
complete: '完了',
|
||||||
|
fill_all_required_fields_to_complete: '完了するにはすべての必須項目を入力してください',
|
||||||
|
sign_and_complete: '署名して完了',
|
||||||
|
text: 'テキスト',
|
||||||
|
by_clicking_you_agree_to_the: '"{button}" をクリックすることで、次に同意したことになります:',
|
||||||
|
electronic_signature_disclosure: '電子署名に関する開示',
|
||||||
|
esignature_disclosure: '電子署名開示',
|
||||||
|
signature: '署名',
|
||||||
|
initials: 'イニシャル',
|
||||||
|
drawn_signature_on_a_touchscreen_device: 'タッチスクリーンデバイスで描かれた署名',
|
||||||
|
approved: '承認済み',
|
||||||
|
reviewed: '確認済み',
|
||||||
|
other: 'その他',
|
||||||
|
authored_by_me: '自分が作成',
|
||||||
|
invite: '招待',
|
||||||
|
email: 'メール',
|
||||||
|
approved_by: '承認者',
|
||||||
|
reviewed_by: '確認者',
|
||||||
|
authored_by: '作成者',
|
||||||
|
select_a_reason: '理由を選択',
|
||||||
|
scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature: 'QRコードをカメラアプリで読み取り、モバイルでフォームを開いて署名を描いてください',
|
||||||
|
date: '日付',
|
||||||
|
number: '数値',
|
||||||
|
value_is_invalid: '無効な値です',
|
||||||
|
verification_code_is_invalid: '認証コードが無効です',
|
||||||
|
already_paid: 'すでに支払い済み',
|
||||||
|
image: '画像',
|
||||||
|
pay: '支払う',
|
||||||
|
take_photo: '写真を撮る',
|
||||||
|
number_phone_is_invalid: '{number} の電話番号は無効です',
|
||||||
|
file: 'ファイル',
|
||||||
|
digitally_signed_by: '電子署名者:',
|
||||||
|
reason: '理由',
|
||||||
|
select: '選択',
|
||||||
|
checkbox: 'チェックボックス',
|
||||||
|
multiple: '複数選択',
|
||||||
|
radio: 'ラジオボタン',
|
||||||
|
cells: 'セル',
|
||||||
|
stamp: 'スタンプ',
|
||||||
|
minimize: '最小化',
|
||||||
|
payment: '支払い',
|
||||||
|
phone: '電話',
|
||||||
|
start_now: '今すぐ開始',
|
||||||
|
continue: '続行',
|
||||||
|
sign_now: '今すぐ署名',
|
||||||
|
type_here_: 'ここに入力...',
|
||||||
|
optional: '任意',
|
||||||
|
option: 'オプション',
|
||||||
|
appears_on: '表示ページ',
|
||||||
|
page: 'ページ',
|
||||||
|
select_your_option: 'オプションを選択',
|
||||||
|
complete_hightlighted_checkboxes_and_click: 'ハイライトされたチェックボックスを完了し、クリックしてください',
|
||||||
|
submit: '送信',
|
||||||
|
next: '次へ',
|
||||||
|
click_to_upload: 'クリックしてアップロード',
|
||||||
|
or_drag_and_drop_files: 'またはファイルをドラッグ&ドロップ',
|
||||||
|
send_copy_via_email: 'メールでコピーを送信',
|
||||||
|
download: 'ダウンロード',
|
||||||
|
clear: 'クリア',
|
||||||
|
redraw: '描き直す',
|
||||||
|
draw_initials: 'イニシャルを描く',
|
||||||
|
type_signature_here: 'ここに署名を入力',
|
||||||
|
type_initial_here: 'ここにイニシャルを入力',
|
||||||
|
form_has_been_completed: 'フォームが完了しました!',
|
||||||
|
document_has_been_signed: 'ドキュメントが署名されました!',
|
||||||
|
documents_have_been_signed: 'ドキュメントがすべて署名されました!',
|
||||||
|
create_a_free_account: '無料アカウントを作成',
|
||||||
|
powered_by: '提供元:',
|
||||||
|
please_check_the_box_to_continue: '続行するにはボックスにチェックを入れてください。',
|
||||||
|
open_source_documents_software: 'オープンソースのドキュメントソフトウェア',
|
||||||
|
verified_phone_number: '電話番号を認証',
|
||||||
|
use_international_format: '国際形式を使用してください:+1xxx',
|
||||||
|
six_digits_code: '6桁のコード',
|
||||||
|
change_phone_number: '電話番号を変更',
|
||||||
|
sending: '送信中...',
|
||||||
|
resend_code: 'コードを再送信',
|
||||||
|
verification_code_has_been_resent: '認証コードがSMSで再送信されました',
|
||||||
|
please_fill_all_required_fields: 'すべての必須項目を入力してください',
|
||||||
|
set_today: '今日の日付を設定',
|
||||||
|
toggle_multiline_text: '複数行テキスト切り替え',
|
||||||
|
draw_signature: '署名を描く',
|
||||||
|
type_initial: 'イニシャルを入力',
|
||||||
|
draw: '描く',
|
||||||
|
type: '入力',
|
||||||
|
type_text: 'テキストを入力',
|
||||||
|
email_has_been_sent: 'メールが送信されました',
|
||||||
|
processing: '処理中',
|
||||||
|
pay_with_strip: 'Stripeで支払う',
|
||||||
|
reupload: '再アップロード',
|
||||||
|
upload: 'アップロード',
|
||||||
|
files: 'ファイル',
|
||||||
|
signature_is_too_small_or_simple_please_redraw: '署名が小さすぎるか単純すぎます。もう一度描いてください。',
|
||||||
|
wait_countdown_seconds: '{countdown} 秒お待ちください'
|
||||||
|
}
|
||||||
|
|
||||||
|
const i18n = { en, es, it, de, fr, pl, uk, cs, pt, he, nl, ar, ko, ja }
|
||||||
|
|
||||||
export default i18n
|
export default i18n
|
||||||
@@ -118,11 +118,17 @@
|
|||||||
:src="attachmentsIndex[modelValue || computedPreviousValue].url"
|
:src="attachmentsIndex[modelValue || computedPreviousValue].url"
|
||||||
class="mx-auto bg-white border border-base-300 rounded max-h-44"
|
class="mx-auto bg-white border border-base-300 rounded max-h-44"
|
||||||
>
|
>
|
||||||
<canvas
|
<div class="relative">
|
||||||
v-show="!modelValue && !computedPreviousValue"
|
<div
|
||||||
ref="canvas"
|
v-if="!isDrawInitials"
|
||||||
class="bg-white border border-base-300 rounded-2xl w-full draw-canvas"
|
class="absolute top-0 right-0 left-0 bottom-0"
|
||||||
/>
|
/>
|
||||||
|
<canvas
|
||||||
|
v-show="!modelValue && !computedPreviousValue"
|
||||||
|
ref="canvas"
|
||||||
|
class="bg-white border border-base-300 rounded-2xl w-full draw-canvas"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<input
|
<input
|
||||||
v-if="!isDrawInitials && !modelValue && !computedPreviousValue"
|
v-if="!isDrawInitials && !modelValue && !computedPreviousValue"
|
||||||
id="initials_text_input"
|
id="initials_text_input"
|
||||||
@@ -333,8 +339,8 @@ export default {
|
|||||||
return Promise.resolve({})
|
return Promise.resolve({})
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve, reject) => {
|
||||||
cropCanvasAndExportToPNG(this.$refs.canvas).then(async (blob) => {
|
cropCanvasAndExportToPNG(this.$refs.canvas, { errorOnTooSmall: true }).then(async (blob) => {
|
||||||
const file = new File([blob], 'initials.png', { type: 'image/png' })
|
const file = new File([blob], 'initials.png', { type: 'image/png' })
|
||||||
|
|
||||||
if (this.dryRun) {
|
if (this.dryRun) {
|
||||||
@@ -367,6 +373,14 @@ export default {
|
|||||||
return resolve(attachment)
|
return resolve(attachment)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}).catch((error) => {
|
||||||
|
if (this.field.required === true) {
|
||||||
|
alert(this.t('signature_is_too_small_or_simple_please_redraw'))
|
||||||
|
|
||||||
|
return reject(error)
|
||||||
|
} else {
|
||||||
|
return resolve({})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -292,6 +292,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { IconReload, IconCamera, IconSignature, IconTextSize, IconArrowsDiagonalMinimize2, IconQrcode, IconX } from '@tabler/icons-vue'
|
import { IconReload, IconCamera, IconSignature, IconTextSize, IconArrowsDiagonalMinimize2, IconQrcode, IconX } from '@tabler/icons-vue'
|
||||||
import { cropCanvasAndExportToPNG } from './crop_canvas'
|
import { cropCanvasAndExportToPNG } from './crop_canvas'
|
||||||
|
import { isValidSignatureCanvas } from './validate_signature'
|
||||||
import SignaturePad from 'signature_pad'
|
import SignaturePad from 'signature_pad'
|
||||||
import AppearsOn from './appears_on'
|
import AppearsOn from './appears_on'
|
||||||
import FileDropzone from './dropzone'
|
import FileDropzone from './dropzone'
|
||||||
@@ -598,6 +599,7 @@ export default {
|
|||||||
},
|
},
|
||||||
drawImage (event) {
|
drawImage (event) {
|
||||||
this.remove()
|
this.remove()
|
||||||
|
this.clear()
|
||||||
this.isSignatureStarted = true
|
this.isSignatureStarted = true
|
||||||
|
|
||||||
this.drawOnCanvas(event.target.files[0], this.$refs.canvas)
|
this.drawOnCanvas(event.target.files[0], this.$refs.canvas)
|
||||||
@@ -680,6 +682,16 @@ export default {
|
|||||||
return Promise.resolve({})
|
return Promise.resolve({})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.isSignatureStarted && this.pad.toData().length > 0 && !isValidSignatureCanvas(this.pad.toData())) {
|
||||||
|
if (this.field.required === true || this.pad.toData().length > 0) {
|
||||||
|
alert(this.t('signature_is_too_small_or_simple_please_redraw'))
|
||||||
|
|
||||||
|
return Promise.reject(new Error('Image too small or simple'))
|
||||||
|
} else {
|
||||||
|
Promise.resolve({})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
cropCanvasAndExportToPNG(this.$refs.canvas, { errorOnTooSmall: true }).then(async (blob) => {
|
cropCanvasAndExportToPNG(this.$refs.canvas, { errorOnTooSmall: true }).then(async (blob) => {
|
||||||
const file = new File([blob], 'signature.png', { type: 'image/png' })
|
const file = new File([blob], 'signature.png', { type: 'image/png' })
|
||||||
@@ -718,10 +730,12 @@ export default {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
if (error.message === 'Image too small' && this.field.required === false) {
|
if (this.field.required === true) {
|
||||||
return resolve({})
|
alert(this.t('signature_is_too_small_or_simple_please_redraw'))
|
||||||
} else {
|
|
||||||
return reject(error)
|
return reject(error)
|
||||||
|
} else {
|
||||||
|
return resolve({})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
function isValidSignatureCanvas (data) {
|
||||||
|
if (data.length === 0) return false
|
||||||
|
|
||||||
|
const strokes = data.filter(stroke => Array.isArray(stroke.points) && stroke.points.length > 2)
|
||||||
|
|
||||||
|
if (strokes.length === 0) return false
|
||||||
|
|
||||||
|
let skippedStraightLine = 0
|
||||||
|
|
||||||
|
const validStrokes = strokes.filter(stroke => {
|
||||||
|
const points = stroke.points
|
||||||
|
const first = points[0]
|
||||||
|
const last = points[points.length - 1]
|
||||||
|
const A = last.y - first.y
|
||||||
|
const B = first.x - last.x
|
||||||
|
const C = last.x * first.y - first.x * last.y
|
||||||
|
const lineLength = Math.sqrt(A * A + B * B)
|
||||||
|
|
||||||
|
const totalDeviation = points.reduce((sum, p) => {
|
||||||
|
const distanceToLine = Math.abs(A * p.x + B * p.y + C) / lineLength
|
||||||
|
return sum + distanceToLine
|
||||||
|
}, 0)
|
||||||
|
|
||||||
|
const avgDeviation = totalDeviation / points.length
|
||||||
|
|
||||||
|
if (avgDeviation < 3 && skippedStraightLine < 2) {
|
||||||
|
skippedStraightLine++
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
return validStrokes.length > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
export { isValidSignatureCanvas }
|
||||||
@@ -160,15 +160,15 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
ref="touchValueTarget"
|
ref="touchValueTarget"
|
||||||
class="flex items-center h-full w-full field-area"
|
class="flex h-full w-full field-area"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
:class="[isValueInput ? 'bg-opacity-50' : 'bg-opacity-80', field.type === 'heading' ? 'bg-gray-50' : bgColors[submitterIndex % bgColors.length], isDefaultValuePresent || isValueInput || (withFieldPlaceholder && field.areas) ? fontClasses : 'justify-center']"
|
:class="[isValueInput ? 'bg-opacity-50' : 'bg-opacity-80', field.type === 'heading' ? 'bg-gray-50' : bgColors[submitterIndex % bgColors.length], isDefaultValuePresent || isValueInput || (withFieldPlaceholder && field.areas) ? fontClasses : 'justify-center items-center']"
|
||||||
@click="focusValueInput"
|
@click="focusValueInput"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
v-if="field"
|
v-if="field"
|
||||||
class="flex justify-center items-center space-x-1"
|
class="flex justify-center items-center space-x-1"
|
||||||
:class="{ 'w-full': ['cells', 'checkbox'].includes(field.type), 'h-full': !isValueInput }"
|
:class="{ 'w-full': ['cells', 'checkbox'].includes(field.type), 'h-full': !isValueInput && !isDefaultValuePresent }"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="isDefaultValuePresent || isValueInput || (withFieldPlaceholder && field.areas && field.type !== 'checkbox')"
|
v-if="isDefaultValuePresent || isValueInput || (withFieldPlaceholder && field.areas && field.type !== 'checkbox')"
|
||||||
@@ -403,10 +403,13 @@ export default {
|
|||||||
},
|
},
|
||||||
fontClasses () {
|
fontClasses () {
|
||||||
if (!this.field.preferences) {
|
if (!this.field.preferences) {
|
||||||
return {}
|
return { 'items-center': true }
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
'items-center': !this.field.preferences.valign || this.field.preferences.valign === 'center',
|
||||||
|
'items-start': this.field.preferences.valign === 'top',
|
||||||
|
'items-end': this.field.preferences.valign === 'bottom',
|
||||||
'justify-center': this.field.preferences.align === 'center',
|
'justify-center': this.field.preferences.align === 'center',
|
||||||
'justify-start': this.field.preferences.align === 'left',
|
'justify-start': this.field.preferences.align === 'left',
|
||||||
'justify-end': this.field.preferences.align === 'right',
|
'justify-end': this.field.preferences.align === 'right',
|
||||||
|
|||||||
@@ -1,9 +1,31 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
|
ref="dragContainer"
|
||||||
style="max-width: 1600px"
|
style="max-width: 1600px"
|
||||||
class="mx-auto pl-3 h-full"
|
class="mx-auto pl-3 h-full"
|
||||||
:class="isMobile ? 'pl-4' : 'md:pl-4'"
|
:class="isMobile ? 'pl-4' : 'md:pl-4'"
|
||||||
|
@dragover="onDragover"
|
||||||
|
@drop="isDragFile = false"
|
||||||
>
|
>
|
||||||
|
<HoverDropzone
|
||||||
|
v-if="sortedDocuments.length && withUploadButton && editable"
|
||||||
|
:is-dragging="isDragFile"
|
||||||
|
:template-id="template.id"
|
||||||
|
:accept-file-types="acceptFileTypes"
|
||||||
|
:with-replace-and-clone="withReplaceAndCloneUpload"
|
||||||
|
hover-class="bg-base-200/50"
|
||||||
|
@add="[updateFromUpload($event), isDragFile = false]"
|
||||||
|
@replace="[onDocumentsReplace($event), isDragFile = false]"
|
||||||
|
@replace-and-clone="onDocumentsReplaceAndTemplateClone($event)"
|
||||||
|
@error="[onUploadFailed($event), isDragFile = false]"
|
||||||
|
/>
|
||||||
|
<DragPlaceholder
|
||||||
|
ref="dragPlaceholder"
|
||||||
|
:field="fieldsDragFieldRef.value || toRaw(dragField)"
|
||||||
|
:is-field="template.fields.includes(fieldsDragFieldRef.value)"
|
||||||
|
:is-default="defaultFields.includes(toRaw(dragField))"
|
||||||
|
:is-required="defaultRequiredFields.includes(toRaw(dragField))"
|
||||||
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="pendingFieldAttachmentUuids.length && editable"
|
v-if="pendingFieldAttachmentUuids.length && editable"
|
||||||
class="top-1.5 sticky h-0 z-20 max-w-2xl mx-auto"
|
class="top-1.5 sticky h-0 z-20 max-w-2xl mx-auto"
|
||||||
@@ -60,6 +82,7 @@
|
|||||||
<template v-else>
|
<template v-else>
|
||||||
<a
|
<a
|
||||||
v-if="withSignYourselfButton"
|
v-if="withSignYourselfButton"
|
||||||
|
id="sign_yourself_button"
|
||||||
:href="template.submitters.length > 1 ? `/templates/${template.id}/submissions/new?selfsign=true` : `/d/${template.slug}`"
|
:href="template.submitters.length > 1 ? `/templates/${template.id}/submissions/new?selfsign=true` : `/d/${template.slug}`"
|
||||||
class="btn btn-primary btn-ghost text-base hidden md:flex"
|
class="btn btn-primary btn-ghost text-base hidden md:flex"
|
||||||
:target="template.submitters.length > 1 ? '' : '_blank'"
|
:target="template.submitters.length > 1 ? '' : '_blank'"
|
||||||
@@ -76,6 +99,7 @@
|
|||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
v-if="withSendButton"
|
v-if="withSendButton"
|
||||||
|
id="send_button"
|
||||||
:href="`/templates/${template.id}/submissions/new?with_link=true`"
|
:href="`/templates/${template.id}/submissions/new?with_link=true`"
|
||||||
data-turbo-frame="modal"
|
data-turbo-frame="modal"
|
||||||
class="white-button md:!px-6"
|
class="white-button md:!px-6"
|
||||||
@@ -91,6 +115,7 @@
|
|||||||
</a>
|
</a>
|
||||||
<span
|
<span
|
||||||
v-if="editable"
|
v-if="editable"
|
||||||
|
id="save_button_container"
|
||||||
class="flex"
|
class="flex"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
@@ -269,6 +294,7 @@
|
|||||||
:allow-draw="!onlyDefinedFields || drawField"
|
:allow-draw="!onlyDefinedFields || drawField"
|
||||||
:data-document-uuid="document.uuid"
|
:data-document-uuid="document.uuid"
|
||||||
:default-submitters="defaultSubmitters"
|
:default-submitters="defaultSubmitters"
|
||||||
|
:drag-field-placeholder="fieldsDragFieldRef.value || dragField"
|
||||||
:with-field-placeholder="withFieldPlaceholder"
|
:with-field-placeholder="withFieldPlaceholder"
|
||||||
:draw-field="drawField"
|
:draw-field="drawField"
|
||||||
:draw-field-type="drawFieldType"
|
:draw-field-type="drawFieldType"
|
||||||
@@ -374,12 +400,14 @@
|
|||||||
:with-sticky-submitters="withStickySubmitters"
|
:with-sticky-submitters="withStickySubmitters"
|
||||||
:only-defined-fields="onlyDefinedFields"
|
:only-defined-fields="onlyDefinedFields"
|
||||||
:editable="editable"
|
:editable="editable"
|
||||||
|
:show-tour-start-form="showTourStartForm"
|
||||||
@add-field="addField"
|
@add-field="addField"
|
||||||
@set-draw="[drawField = $event.field, drawOption = $event.option]"
|
@set-draw="[drawField = $event.field, drawOption = $event.option]"
|
||||||
@set-draw-type="[drawFieldType = $event, showDrawField = true]"
|
@set-draw-type="[drawFieldType = $event, showDrawField = true]"
|
||||||
@set-drag="dragField = $event"
|
@set-drag="dragField = $event"
|
||||||
|
@set-drag-placeholder="$refs.dragPlaceholder.dragPlaceholder = $event"
|
||||||
@change-submitter="selectedSubmitter = $event"
|
@change-submitter="selectedSubmitter = $event"
|
||||||
@drag-end="dragField = null"
|
@drag-end="[dragField = null, $refs.dragPlaceholder.dragPlaceholder = null]"
|
||||||
@scroll-to-area="scrollToArea"
|
@scroll-to-area="scrollToArea"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -418,6 +446,8 @@
|
|||||||
<script>
|
<script>
|
||||||
import Upload from './upload'
|
import Upload from './upload'
|
||||||
import Dropzone from './dropzone'
|
import Dropzone from './dropzone'
|
||||||
|
import HoverDropzone from './hover_dropzone'
|
||||||
|
import DragPlaceholder from './drag_placeholder'
|
||||||
import Fields from './fields'
|
import Fields from './fields'
|
||||||
import MobileDrawField from './mobile_draw_field'
|
import MobileDrawField from './mobile_draw_field'
|
||||||
import Document from './document'
|
import Document from './document'
|
||||||
@@ -429,13 +459,14 @@ import MobileFields from './mobile_fields'
|
|||||||
import FieldSubmitter from './field_submitter'
|
import FieldSubmitter from './field_submitter'
|
||||||
import { IconPlus, IconUsersPlus, IconDeviceFloppy, IconChevronDown, IconEye, IconWritingSign, IconInnerShadowTop, IconInfoCircle, IconAdjustments } from '@tabler/icons-vue'
|
import { IconPlus, IconUsersPlus, IconDeviceFloppy, IconChevronDown, IconEye, IconWritingSign, IconInnerShadowTop, IconInfoCircle, IconAdjustments } from '@tabler/icons-vue'
|
||||||
import { v4 } from 'uuid'
|
import { v4 } from 'uuid'
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed, toRaw } from 'vue'
|
||||||
import * as i18n from './i18n'
|
import * as i18n from './i18n'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'TemplateBuilder',
|
name: 'TemplateBuilder',
|
||||||
components: {
|
components: {
|
||||||
Upload,
|
Upload,
|
||||||
|
DragPlaceholder,
|
||||||
Document,
|
Document,
|
||||||
Fields,
|
Fields,
|
||||||
IconInfoCircle,
|
IconInfoCircle,
|
||||||
@@ -445,6 +476,7 @@ export default {
|
|||||||
MobileFields,
|
MobileFields,
|
||||||
Logo,
|
Logo,
|
||||||
Dropzone,
|
Dropzone,
|
||||||
|
HoverDropzone,
|
||||||
DocumentPreview,
|
DocumentPreview,
|
||||||
DocumentControls,
|
DocumentControls,
|
||||||
IconInnerShadowTop,
|
IconInnerShadowTop,
|
||||||
@@ -460,6 +492,7 @@ export default {
|
|||||||
template: this.template,
|
template: this.template,
|
||||||
save: this.save,
|
save: this.save,
|
||||||
t: this.t,
|
t: this.t,
|
||||||
|
assignDropAreaSize: this.assignDropAreaSize,
|
||||||
currencies: this.currencies,
|
currencies: this.currencies,
|
||||||
locale: this.locale,
|
locale: this.locale,
|
||||||
baseFetch: this.baseFetch,
|
baseFetch: this.baseFetch,
|
||||||
@@ -647,6 +680,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
|
withReplaceAndCloneUpload: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
withPhone: {
|
withPhone: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -686,6 +724,11 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: false,
|
required: false,
|
||||||
default: () => ({ headers: {} })
|
default: () => ({ headers: {} })
|
||||||
|
},
|
||||||
|
showTourStartForm: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
@@ -701,7 +744,8 @@ export default {
|
|||||||
copiedArea: null,
|
copiedArea: null,
|
||||||
drawFieldType: null,
|
drawFieldType: null,
|
||||||
drawOption: null,
|
drawOption: null,
|
||||||
dragField: null
|
dragField: null,
|
||||||
|
isDragFile: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -813,6 +857,7 @@ export default {
|
|||||||
window.addEventListener('keydown', this.onKeyDown)
|
window.addEventListener('keydown', this.onKeyDown)
|
||||||
|
|
||||||
window.addEventListener('resize', this.onWindowResize)
|
window.addEventListener('resize', this.onWindowResize)
|
||||||
|
window.addEventListener('dragleave', this.onWindowDragLeave)
|
||||||
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
if (document.location.search?.includes('stripe_connect_success')) {
|
if (document.location.search?.includes('stripe_connect_success')) {
|
||||||
@@ -831,11 +876,30 @@ export default {
|
|||||||
window.removeEventListener('keydown', this.onKeyDown)
|
window.removeEventListener('keydown', this.onKeyDown)
|
||||||
|
|
||||||
window.removeEventListener('resize', this.onWindowResize)
|
window.removeEventListener('resize', this.onWindowResize)
|
||||||
|
window.removeEventListener('dragleave', this.onWindowDragLeave)
|
||||||
},
|
},
|
||||||
beforeUpdate () {
|
beforeUpdate () {
|
||||||
this.documentRefs = []
|
this.documentRefs = []
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
toRaw,
|
||||||
|
onDragover (e) {
|
||||||
|
if (this.$refs.dragPlaceholder?.dragPlaceholder) {
|
||||||
|
this.$refs.dragPlaceholder.isMask = e.target.id === 'mask'
|
||||||
|
|
||||||
|
const ref = this.$refs.dragPlaceholder.dragPlaceholder
|
||||||
|
|
||||||
|
ref.x = e.clientX - ref.offsetX
|
||||||
|
ref.y = e.clientY - ref.offsetY
|
||||||
|
} else if (e.dataTransfer?.types?.includes('Files')) {
|
||||||
|
this.isDragFile = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onWindowDragLeave (event) {
|
||||||
|
if (event.clientX <= 0 || event.clientY <= 0 || event.clientX >= window.innerWidth || event.clientY >= window.innerHeight) {
|
||||||
|
this.isDragFile = false
|
||||||
|
}
|
||||||
|
},
|
||||||
reorderFields (item) {
|
reorderFields (item) {
|
||||||
const itemFields = []
|
const itemFields = []
|
||||||
const fields = []
|
const fields = []
|
||||||
@@ -1319,6 +1383,10 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onDropfield (area) {
|
onDropfield (area) {
|
||||||
|
if (this.$refs.dragPlaceholder) {
|
||||||
|
this.$refs.dragPlaceholder.dragPlaceholder = null
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.editable) {
|
if (!this.editable) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1331,6 +1399,10 @@ export default {
|
|||||||
...this.dragField
|
...this.dragField
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!field.type) {
|
||||||
|
field.type = 'text'
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.fieldsDragFieldRef.value) {
|
if (!this.fieldsDragFieldRef.value) {
|
||||||
if (['select', 'multiple', 'radio'].includes(field.type)) {
|
if (['select', 'multiple', 'radio'].includes(field.type)) {
|
||||||
if (this.dragField?.options?.length) {
|
if (this.dragField?.options?.length) {
|
||||||
@@ -1357,70 +1429,18 @@ export default {
|
|||||||
attachment_uuid: area.attachment_uuid
|
attachment_uuid: area.attachment_uuid
|
||||||
}
|
}
|
||||||
|
|
||||||
const previousField = [...this.template.fields].reverse().find((f) => f.type === field.type)
|
this.assignDropAreaSize(fieldArea, field, area)
|
||||||
|
|
||||||
let baseArea
|
|
||||||
|
|
||||||
if (this.selectedField?.type === field.type) {
|
|
||||||
baseArea = this.selectedAreaRef.value
|
|
||||||
} else if (previousField?.areas?.length) {
|
|
||||||
baseArea = previousField.areas[previousField.areas.length - 1]
|
|
||||||
} else {
|
|
||||||
if (['checkbox'].includes(field.type)) {
|
|
||||||
baseArea = {
|
|
||||||
w: area.maskW / 30 / area.maskW,
|
|
||||||
h: area.maskW / 30 / area.maskW * (area.maskW / area.maskH)
|
|
||||||
}
|
|
||||||
} else if (field.type === 'image') {
|
|
||||||
baseArea = {
|
|
||||||
w: area.maskW / 5 / area.maskW,
|
|
||||||
h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH)
|
|
||||||
}
|
|
||||||
} else if (field.type === 'signature' || field.type === 'stamp' || field.type === 'verification') {
|
|
||||||
baseArea = {
|
|
||||||
w: area.maskW / 5 / area.maskW,
|
|
||||||
h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH) / 2
|
|
||||||
}
|
|
||||||
} else if (field.type === 'initials') {
|
|
||||||
baseArea = {
|
|
||||||
w: area.maskW / 10 / area.maskW,
|
|
||||||
h: area.maskW / 35 / area.maskW
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
baseArea = {
|
|
||||||
w: area.maskW / 5 / area.maskW,
|
|
||||||
h: area.maskW / 35 / area.maskW
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fieldArea.w = baseArea.w
|
|
||||||
fieldArea.h = baseArea.h
|
|
||||||
fieldArea.y = fieldArea.y - baseArea.h / 2
|
|
||||||
|
|
||||||
if (field.type === 'cells') {
|
|
||||||
fieldArea.cell_w = baseArea.cell_w || (baseArea.w / 5)
|
|
||||||
}
|
|
||||||
|
|
||||||
field.areas ||= []
|
|
||||||
|
|
||||||
const lastArea = field.areas[field.areas.length - 1]
|
|
||||||
|
|
||||||
if (lastArea) {
|
|
||||||
fieldArea.w = lastArea.w
|
|
||||||
fieldArea.h = lastArea.h
|
|
||||||
}
|
|
||||||
|
|
||||||
if (field.width) {
|
if (field.width) {
|
||||||
fieldArea.w = field.width / area.maskW
|
|
||||||
delete field.width
|
delete field.width
|
||||||
}
|
}
|
||||||
|
|
||||||
if (field.height) {
|
if (field.height) {
|
||||||
fieldArea.h = field.height / area.maskH
|
|
||||||
delete field.height
|
delete field.height
|
||||||
}
|
}
|
||||||
|
|
||||||
|
field.areas ||= []
|
||||||
|
|
||||||
field.areas.push(fieldArea)
|
field.areas.push(fieldArea)
|
||||||
|
|
||||||
this.selectedAreaRef.value = fieldArea
|
this.selectedAreaRef.value = fieldArea
|
||||||
@@ -1444,6 +1464,72 @@ export default {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
assignDropAreaSize (fieldArea, field, area) {
|
||||||
|
const fieldType = field.type || 'text'
|
||||||
|
|
||||||
|
const previousField = [...this.template.fields].reverse().find((f) => f.type === fieldType)
|
||||||
|
|
||||||
|
let baseArea
|
||||||
|
|
||||||
|
if (this.selectedField?.type === fieldType) {
|
||||||
|
baseArea = this.selectedAreaRef.value
|
||||||
|
} else if (previousField?.areas?.length) {
|
||||||
|
baseArea = previousField.areas[previousField.areas.length - 1]
|
||||||
|
} else {
|
||||||
|
if (['checkbox'].includes(fieldType)) {
|
||||||
|
baseArea = {
|
||||||
|
w: area.maskW / 30 / area.maskW,
|
||||||
|
h: area.maskW / 30 / area.maskW * (area.maskW / area.maskH)
|
||||||
|
}
|
||||||
|
} else if (fieldType === 'image') {
|
||||||
|
baseArea = {
|
||||||
|
w: area.maskW / 5 / area.maskW,
|
||||||
|
h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH)
|
||||||
|
}
|
||||||
|
} else if (fieldType === 'signature' || fieldType === 'stamp' || fieldType === 'verification') {
|
||||||
|
baseArea = {
|
||||||
|
w: area.maskW / 5 / area.maskW,
|
||||||
|
h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH) / 2
|
||||||
|
}
|
||||||
|
} else if (fieldType === 'initials') {
|
||||||
|
baseArea = {
|
||||||
|
w: area.maskW / 10 / area.maskW,
|
||||||
|
h: area.maskW / 35 / area.maskW
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
baseArea = {
|
||||||
|
w: area.maskW / 5 / area.maskW,
|
||||||
|
h: area.maskW / 35 / area.maskW
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldArea.w = baseArea.w
|
||||||
|
fieldArea.h = baseArea.h
|
||||||
|
|
||||||
|
if (fieldType === 'cells') {
|
||||||
|
fieldArea.cell_w = baseArea.cell_w || (baseArea.w / 5)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.areas?.length) {
|
||||||
|
const lastArea = field.areas[field.areas.length - 1]
|
||||||
|
|
||||||
|
if (lastArea) {
|
||||||
|
fieldArea.w = lastArea.w
|
||||||
|
fieldArea.h = lastArea.h
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.width) {
|
||||||
|
fieldArea.w = field.width / area.maskW
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.height) {
|
||||||
|
fieldArea.h = field.height / area.maskH
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldArea.y = fieldArea.y - fieldArea.h / 2
|
||||||
|
},
|
||||||
addBlankPage () {
|
addBlankPage () {
|
||||||
this.isLoadingBlankPage = true
|
this.isLoadingBlankPage = true
|
||||||
|
|
||||||
@@ -1473,6 +1559,9 @@ export default {
|
|||||||
})
|
})
|
||||||
}, 'image/png')
|
}, 'image/png')
|
||||||
},
|
},
|
||||||
|
onUploadFailed (error) {
|
||||||
|
if (error) alert(error)
|
||||||
|
},
|
||||||
updateFromUpload (data) {
|
updateFromUpload (data) {
|
||||||
this.template.schema.push(...data.schema)
|
this.template.schema.push(...data.schema)
|
||||||
this.template.documents.push(...data.documents)
|
this.template.documents.push(...data.documents)
|
||||||
@@ -1593,6 +1682,29 @@ export default {
|
|||||||
|
|
||||||
this.save()
|
this.save()
|
||||||
},
|
},
|
||||||
|
onDocumentsReplace (data) {
|
||||||
|
data.schema.forEach((schemaItem, index) => {
|
||||||
|
const existingSchemaItem = this.template.schema[index]
|
||||||
|
|
||||||
|
if (this.template.schema[index]) {
|
||||||
|
this.onDocumentReplace({
|
||||||
|
replaceSchemaItem: existingSchemaItem,
|
||||||
|
schema: [schemaItem],
|
||||||
|
documents: [data.documents.find((doc) => doc.uuid === schemaItem.attachment_uuid)]
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.updateFromUpload({
|
||||||
|
schema: [schemaItem],
|
||||||
|
documents: [data.documents.find((doc) => doc.uuid === schemaItem.attachment_uuid)],
|
||||||
|
fields: data.fields,
|
||||||
|
submitters: data.submitters
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onDocumentsReplaceAndTemplateClone (template) {
|
||||||
|
window.Turbo.visit(`/templates/${template.id}/edit`)
|
||||||
|
},
|
||||||
moveDocument (item, direction) {
|
moveDocument (item, direction) {
|
||||||
const currentIndex = this.template.schema.indexOf(item)
|
const currentIndex = this.template.schema.indexOf(item)
|
||||||
|
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ export default {
|
|||||||
fields () {
|
fields () {
|
||||||
if (this.item.submitter_uuid) {
|
if (this.item.submitter_uuid) {
|
||||||
return this.template.fields.reduce((acc, f) => {
|
return this.template.fields.reduce((acc, f) => {
|
||||||
if (f !== this.item) {
|
if (f !== this.item && (!f.conditions?.length || !f.conditions.find((c) => c.field_uuid === this.item.uuid))) {
|
||||||
acc.push(f)
|
acc.push(f)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
:is-drag="isDrag"
|
:is-drag="isDrag"
|
||||||
:with-field-placeholder="withFieldPlaceholder"
|
:with-field-placeholder="withFieldPlaceholder"
|
||||||
:default-fields="defaultFields"
|
:default-fields="defaultFields"
|
||||||
|
:drag-field-placeholder="dragFieldPlaceholder"
|
||||||
:default-submitters="defaultSubmitters"
|
:default-submitters="defaultSubmitters"
|
||||||
:draw-field="drawField"
|
:draw-field="drawField"
|
||||||
:draw-field-type="drawFieldType"
|
:draw-field-type="drawFieldType"
|
||||||
@@ -39,6 +40,11 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
dragFieldPlaceholder: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
inputMode: {
|
inputMode: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
<template>
|
||||||
|
<Field
|
||||||
|
v-if="dragPlaceholder && isField && !isMask && field"
|
||||||
|
ref="dragPlaceholder"
|
||||||
|
:style="dragPlaceholderStyle"
|
||||||
|
:field="field"
|
||||||
|
:with-options="false"
|
||||||
|
class="fixed z-20 pointer-events-none"
|
||||||
|
:editable="false"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-else-if="dragPlaceholder && (isDefault || isRequired) && !isMask && field"
|
||||||
|
ref="dragPlaceholder"
|
||||||
|
:style="[dragPlaceholderStyle, { backgroundColor: backgroundColor }]"
|
||||||
|
class="fixed z-20 border border-base-300 rounded group default-field fields-list-item pointer-events-none"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between relative cursor-grab">
|
||||||
|
<div class="flex items-center p-1 space-x-1">
|
||||||
|
<IconDrag />
|
||||||
|
<component
|
||||||
|
:is="fieldIcons[field.type || 'text']"
|
||||||
|
:stroke-width="1.6"
|
||||||
|
:width="20"
|
||||||
|
/>
|
||||||
|
<span class="block pl-0.5">
|
||||||
|
{{ field.title || field.name }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
v-if="isRequired"
|
||||||
|
:data-tip="t('required')"
|
||||||
|
class="text-red-400 text-3xl pr-1.5 tooltip tooltip-left h-8"
|
||||||
|
>
|
||||||
|
*
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
v-else-if="dragPlaceholder && !isMask && field"
|
||||||
|
ref="dragPlaceholder"
|
||||||
|
class="fixed field-type-button z-20 flex items-center justify-center border border-dashed w-full rounded border-base-content/20 opacity-90 pointer-events-none"
|
||||||
|
:style="[dragPlaceholderStyle, { backgroundColor }]"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex items-console cursor-grab h-full absolute left-0 bg-base-200/50"
|
||||||
|
>
|
||||||
|
<IconDrag class="my-auto" />
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center flex-col px-2 py-2">
|
||||||
|
<component :is="fieldIcons[field.type || 'text']" />
|
||||||
|
<span class="text-xs mt-1">
|
||||||
|
{{ fieldNames[field.type || 'text'] }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Field from './field'
|
||||||
|
import IconDrag from './icon_drag'
|
||||||
|
import FieldType from './field_type'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'DragPlaceholder',
|
||||||
|
components: {
|
||||||
|
Field,
|
||||||
|
IconDrag
|
||||||
|
},
|
||||||
|
inject: ['t', 'backgroundColor'],
|
||||||
|
props: {
|
||||||
|
field: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
isDefault: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
isRequired: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
isField: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
isMask: false,
|
||||||
|
dragPlaceholder: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
dragPlaceholderStyle () {
|
||||||
|
if (this.dragPlaceholder) {
|
||||||
|
return {
|
||||||
|
left: this.dragPlaceholder.x + 'px',
|
||||||
|
top: this.dragPlaceholder.y + 'px',
|
||||||
|
width: this.dragPlaceholder.w + 'px',
|
||||||
|
height: this.dragPlaceholder.h + 'px'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fieldNames: FieldType.computed.fieldNames,
|
||||||
|
fieldIcons: FieldType.computed.fieldIcons
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -2,34 +2,42 @@
|
|||||||
<div
|
<div
|
||||||
class="flex h-60 w-full"
|
class="flex h-60 w-full"
|
||||||
@dragover.prevent
|
@dragover.prevent
|
||||||
|
@dragenter="isDragEntering = true"
|
||||||
|
@dragleave="isDragEntering = false"
|
||||||
@drop.prevent="onDropFiles"
|
@drop.prevent="onDropFiles"
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
id="document_dropzone"
|
id="document_dropzone"
|
||||||
class="w-full relative hover:bg-base-200/30 rounded-md border border-2 border-base-content/10 border-dashed"
|
class="w-full relative rounded-md border-2 border-base-content/10 border-dashed"
|
||||||
:for="inputId"
|
:for="inputId"
|
||||||
:class="{ 'opacity-50': isLoading || isProcessing }"
|
:class="[{ 'opacity-50': isLoading, 'hover:bg-base-200/30': !hoverClass }, isDragEntering && hoverClass ? hoverClass : '']"
|
||||||
>
|
>
|
||||||
<div class="absolute top-0 right-0 left-0 bottom-0 flex items-center justify-center">
|
<div class="absolute top-0 right-0 left-0 bottom-0 flex items-center justify-center pointer-events-none">
|
||||||
<div class="flex flex-col items-center">
|
<div class="flex flex-col items-center">
|
||||||
<IconInnerShadowTop
|
<IconInnerShadowTop
|
||||||
v-if="isLoading || isProcessing"
|
v-if="isLoading"
|
||||||
class="animate-spin"
|
class="animate-spin"
|
||||||
:width="40"
|
:width="40"
|
||||||
:height="40"
|
:height="40"
|
||||||
/>
|
/>
|
||||||
<IconCloudUpload
|
<component
|
||||||
|
:is="icon"
|
||||||
v-else
|
v-else
|
||||||
|
class="stroke-[1.5px]"
|
||||||
:width="40"
|
:width="40"
|
||||||
:height="40"
|
:height="40"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="message"
|
v-if="message"
|
||||||
class="font-medium text-lg mb-1"
|
class="text-lg mb-1"
|
||||||
|
:class="{ 'mt-1': !withDescription, 'font-medium': withDescription }"
|
||||||
>
|
>
|
||||||
{{ message }}
|
{{ message }}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm">
|
<div
|
||||||
|
v-if="withDescription"
|
||||||
|
class="text-sm"
|
||||||
|
>
|
||||||
<span class="font-medium">{{ t('click_to_upload') }}</span> {{ t('or_drag_and_drop_files') }}
|
<span class="font-medium">{{ t('click_to_upload') }}</span> {{ t('or_drag_and_drop_files') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -54,13 +62,16 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Upload from './upload'
|
import Upload from './upload'
|
||||||
import { IconCloudUpload, IconInnerShadowTop } from '@tabler/icons-vue'
|
import { IconCloudUpload, IconFilePlus, IconFileSymlink, IconFiles, IconInnerShadowTop } from '@tabler/icons-vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FileDropzone',
|
name: 'FileDropzone',
|
||||||
components: {
|
components: {
|
||||||
|
IconFilePlus,
|
||||||
IconCloudUpload,
|
IconCloudUpload,
|
||||||
IconInnerShadowTop
|
IconInnerShadowTop,
|
||||||
|
IconFileSymlink,
|
||||||
|
IconFiles
|
||||||
},
|
},
|
||||||
inject: ['baseFetch', 't'],
|
inject: ['baseFetch', 't'],
|
||||||
props: {
|
props: {
|
||||||
@@ -68,35 +79,70 @@ export default {
|
|||||||
type: [Number, String],
|
type: [Number, String],
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
icon: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: 'IconCloudUpload'
|
||||||
|
},
|
||||||
|
hoverClass: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
cloneTemplateOnUpload: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
withDescription: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
acceptFileTypes: {
|
acceptFileTypes: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
default: 'image/*, application/pdf'
|
default: 'image/*, application/pdf'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['success'],
|
emits: ['success', 'error', 'loading'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
isProcessing: false
|
isDragEntering: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
inputId () {
|
inputId () {
|
||||||
return 'el' + Math.random().toString(32).split('.')[1]
|
return 'el' + Math.random().toString(32).split('.')[1]
|
||||||
},
|
},
|
||||||
|
uploadUrl () {
|
||||||
|
if (this.cloneTemplateOnUpload) {
|
||||||
|
return `/templates/${this.templateId}/clone_and_replace`
|
||||||
|
} else {
|
||||||
|
return `/templates/${this.templateId}/documents`
|
||||||
|
}
|
||||||
|
},
|
||||||
message () {
|
message () {
|
||||||
if (this.isLoading) {
|
if (this.isLoading) {
|
||||||
return this.t('uploading')
|
return this.t('uploading')
|
||||||
} else if (this.isProcessing) {
|
|
||||||
return this.t('processing_')
|
|
||||||
} else if (this.acceptFileTypes === 'image/*, application/pdf') {
|
} else if (this.acceptFileTypes === 'image/*, application/pdf') {
|
||||||
return this.t('add_pdf_documents_or_images')
|
return this.title || this.t('add_pdf_documents_or_images')
|
||||||
} else {
|
} else {
|
||||||
return this.t('add_documents_or_images')
|
return this.title || this.t('add_documents_or_images')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
isLoading (value) {
|
||||||
|
this.$emit('loading', value)
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
upload: Upload.methods.upload,
|
upload: Upload.methods.upload,
|
||||||
onDropFiles (e) {
|
onDropFiles (e) {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
class="list-field group mb-2"
|
class="list-field group mb-2"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="border border-base-300 rounded rounded-tr-none relative group fields-list-item"
|
class="border border-base-300 rounded relative group fields-list-item"
|
||||||
:style="{ backgroundColor: backgroundColor }"
|
:style="{ backgroundColor: backgroundColor }"
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-between relative group/contenteditable-container">
|
<div class="flex items-center justify-between relative group/contenteditable-container">
|
||||||
@@ -149,7 +149,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="field.options"
|
v-if="field.options && withOptions"
|
||||||
ref="options"
|
ref="options"
|
||||||
class="border-t border-base-300 mx-2 pt-2 space-y-1.5"
|
class="border-t border-base-300 mx-2 pt-2 space-y-1.5"
|
||||||
draggable="true"
|
draggable="true"
|
||||||
@@ -302,6 +302,11 @@ export default {
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
withOptions: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
defaultField: {
|
defaultField: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: false,
|
required: false,
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
v-for="(icon, type) in fieldIconsSorted"
|
v-for="(icon, type) in fieldIconsSorted"
|
||||||
:key="type"
|
:key="type"
|
||||||
>
|
>
|
||||||
<li v-if="(fieldTypes.length === 0 || fieldTypes.includes(type)) && (withPhone || type != 'phone') && (withPayment || type != 'payment') && (withVerification || type != 'verification')">
|
<li v-if="fieldTypes.includes(type) || ((withPhone || type != 'phone') && (withPayment || type != 'payment') && (withVerification || type != 'verification'))">
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
class="text-sm py-1 px-2"
|
class="text-sm py-1 px-2"
|
||||||
|
|||||||
@@ -25,11 +25,11 @@
|
|||||||
: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="fields.filter((f) => f.type === field.type).indexOf(field)"
|
||||||
:editable="editable && (!fieldsDragFieldRef.value || fieldsDragFieldRef.value !== field)"
|
:editable="editable"
|
||||||
:default-field="defaultFieldsIndex[field.name]"
|
:default-field="defaultFieldsIndex[field.name]"
|
||||||
:draggable="editable"
|
:draggable="editable"
|
||||||
@dragstart="fieldsDragFieldRef.value = field"
|
@dragstart="[fieldsDragFieldRef.value = field, removeDragOverlay($event), setDragPlaceholder($event)]"
|
||||||
@dragend="fieldsDragFieldRef.value = null"
|
@dragend="[fieldsDragFieldRef.value = null, $emit('set-drag-placeholder', null)]"
|
||||||
@remove="removeField"
|
@remove="removeField"
|
||||||
@scroll-to="$emit('scroll-to-area', $event)"
|
@scroll-to="$emit('scroll-to-area', $event)"
|
||||||
@set-draw="$emit('set-draw', $event)"
|
@set-draw="$emit('set-draw', $event)"
|
||||||
@@ -74,8 +74,8 @@
|
|||||||
<div
|
<div
|
||||||
:style="{ backgroundColor }"
|
:style="{ backgroundColor }"
|
||||||
draggable="true"
|
draggable="true"
|
||||||
class="border border-base-300 rounded rounded-tr-none relative group mb-2 default-field fields-list-item"
|
class="border border-base-300 rounded relative group mb-2 default-field fields-list-item"
|
||||||
@dragstart="onDragstart({ type: 'text', ...field })"
|
@dragstart="onDragstart($event, field)"
|
||||||
@dragend="$emit('drag-end')"
|
@dragend="$emit('drag-end')"
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-between relative cursor-grab">
|
<div class="flex items-center justify-between relative cursor-grab">
|
||||||
@@ -104,6 +104,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="editable && !onlyDefinedFields"
|
v-if="editable && !onlyDefinedFields"
|
||||||
|
id="field-types-grid"
|
||||||
class="grid grid-cols-3 gap-1 pb-2 fields-grid"
|
class="grid grid-cols-3 gap-1 pb-2 fields-grid"
|
||||||
>
|
>
|
||||||
<template
|
<template
|
||||||
@@ -111,12 +112,13 @@
|
|||||||
:key="type"
|
:key="type"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
v-if="(fieldTypes.length === 0 || fieldTypes.includes(type)) && (withPhone || type != 'phone') && (withPayment || type != 'payment') && (withVerification || type != 'verification')"
|
v-if="fieldTypes.includes(type) || ((withPhone || type != 'phone') && (withPayment || type != 'payment') && (withVerification || type != 'verification'))"
|
||||||
|
:id="`${type}_type_field_button`"
|
||||||
draggable="true"
|
draggable="true"
|
||||||
class="field-type-button group flex items-center justify-center border border-dashed w-full rounded relative fields-grid-item"
|
class="field-type-button group flex items-center justify-center border border-dashed w-full rounded relative fields-grid-item"
|
||||||
:style="{ backgroundColor }"
|
:style="{ backgroundColor }"
|
||||||
:class="drawFieldType === type ? 'border-base-content/40' : 'border-base-300 hover:border-base-content/20'"
|
:class="drawFieldType === type ? 'border-base-content/40' : 'border-base-300 hover:border-base-content/20'"
|
||||||
@dragstart="onDragstart({ type: type })"
|
@dragstart="onDragstart($event, { type: type })"
|
||||||
@dragend="$emit('drag-end')"
|
@dragend="$emit('drag-end')"
|
||||||
@click="['file', 'payment', 'verification'].includes(type) ? $emit('add-field', type) : $emit('set-draw-type', type)"
|
@click="['file', 'payment', 'verification'].includes(type) ? $emit('add-field', type) : $emit('set-draw-type', type)"
|
||||||
>
|
>
|
||||||
@@ -166,7 +168,7 @@
|
|||||||
:data-tip="t('obtain_qualified_electronic_signature_with_the_trusted_provider_click_to_learn_more')"
|
:data-tip="t('obtain_qualified_electronic_signature_with_the_trusted_provider_click_to_learn_more')"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
href="https://www.docuseal.com/contact"
|
href="https://www.docuseal.com/qualified-electronic-signature"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="opacity-50 flex items-center justify-center border border-dashed border-base-300 w-full rounded relative fields-grid-item"
|
class="opacity-50 flex items-center justify-center border border-dashed border-base-300 w-full rounded relative fields-grid-item"
|
||||||
:style="{ backgroundColor }"
|
:style="{ backgroundColor }"
|
||||||
@@ -189,7 +191,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="fields.length < 4 && editable && withHelp"
|
v-if="fields.length < 4 && editable && withHelp && !showTourStartForm"
|
||||||
class="text-xs p-2 border border-base-200 rounded"
|
class="text-xs p-2 border border-base-200 rounded"
|
||||||
>
|
>
|
||||||
<ul class="list-disc list-outside ml-3">
|
<ul class="list-disc list-outside ml-3">
|
||||||
@@ -204,6 +206,23 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-show="fields.length < 4 && editable && withHelp && showTourStartForm"
|
||||||
|
class="rounded py-2 px-4 w-full border border-dashed border-base-300"
|
||||||
|
>
|
||||||
|
<div class="text-center text-sm">
|
||||||
|
{{ t('start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document') }}
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<label
|
||||||
|
for="start_tour_button"
|
||||||
|
class="btn btn-sm btn-warning w-40 mt-2"
|
||||||
|
@click="startTour"
|
||||||
|
>
|
||||||
|
{{ t('start_tour') }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -290,9 +309,14 @@ export default {
|
|||||||
selectedSubmitter: {
|
selectedSubmitter: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
|
},
|
||||||
|
showTourStartForm: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['add-field', 'set-draw', 'set-draw-type', 'set-drag', 'drag-end', 'scroll-to-area', 'change-submitter'],
|
emits: ['add-field', 'set-draw', 'set-draw-type', 'set-drag', 'drag-end', 'scroll-to-area', 'change-submitter', 'set-drag-placeholder'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
defaultFieldsSearch: ''
|
defaultFieldsSearch: ''
|
||||||
@@ -343,22 +367,56 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onDragstart (field) {
|
onDragstart (event, field) {
|
||||||
|
this.removeDragOverlay(event)
|
||||||
|
|
||||||
|
this.setDragPlaceholder(event)
|
||||||
|
|
||||||
this.$emit('set-drag', field)
|
this.$emit('set-drag', field)
|
||||||
},
|
},
|
||||||
|
setDragPlaceholder (event) {
|
||||||
|
this.$emit('set-drag-placeholder', {
|
||||||
|
offsetX: event.offsetX,
|
||||||
|
offsetY: event.offsetY,
|
||||||
|
x: event.clientX - event.offsetX,
|
||||||
|
y: event.clientY - event.offsetY,
|
||||||
|
w: event.currentTarget.clientWidth + 2,
|
||||||
|
h: event.currentTarget.clientHeight + 2
|
||||||
|
})
|
||||||
|
},
|
||||||
|
removeDragOverlay (event) {
|
||||||
|
const root = this.$el.getRootNode()
|
||||||
|
const hiddenEl = document.createElement('div')
|
||||||
|
|
||||||
|
hiddenEl.style.width = '1px'
|
||||||
|
hiddenEl.style.height = '1px'
|
||||||
|
hiddenEl.style.opacity = '0'
|
||||||
|
hiddenEl.style.position = 'fixed'
|
||||||
|
|
||||||
|
root.querySelector('#docuseal_modal_container').appendChild(hiddenEl)
|
||||||
|
|
||||||
|
event.dataTransfer.setDragImage(hiddenEl, 0, 0)
|
||||||
|
|
||||||
|
setTimeout(() => { hiddenEl.remove() }, 1000)
|
||||||
|
},
|
||||||
|
startTour () {
|
||||||
|
document.querySelector('app-tour').start()
|
||||||
|
},
|
||||||
onFieldDragover (e) {
|
onFieldDragover (e) {
|
||||||
const targetField = e.target.closest('[data-uuid]')
|
if (this.fieldsDragFieldRef.value) {
|
||||||
const dragField = this.$refs.fields.querySelector(`[data-uuid="${this.fieldsDragFieldRef.value.uuid}"]`)
|
const targetField = e.target.closest('[data-uuid]')
|
||||||
|
const dragField = this.$refs.fields.querySelector(`[data-uuid="${this.fieldsDragFieldRef.value.uuid}"]`)
|
||||||
|
|
||||||
if (dragField && targetField && targetField !== dragField) {
|
if (dragField && targetField && targetField !== dragField) {
|
||||||
const fields = Array.from(this.$refs.fields.children)
|
const fields = Array.from(this.$refs.fields.children)
|
||||||
const currentIndex = fields.indexOf(dragField)
|
const currentIndex = fields.indexOf(dragField)
|
||||||
const targetIndex = fields.indexOf(targetField)
|
const targetIndex = fields.indexOf(targetField)
|
||||||
|
|
||||||
if (currentIndex < targetIndex) {
|
if (currentIndex < targetIndex) {
|
||||||
targetField.after(dragField)
|
targetField.after(dragField)
|
||||||
} else {
|
} else {
|
||||||
targetField.before(dragField)
|
targetField.before(dragField)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -57,7 +57,7 @@
|
|||||||
<span class="relative">
|
<span class="relative">
|
||||||
<select
|
<select
|
||||||
class="select input-bordered bg-white select-sm text-center pl-2"
|
class="select input-bordered bg-white select-sm text-center pl-2"
|
||||||
style="font-size: 16px; width: 86px; text-align-last: center;"
|
style="font-size: 16px; line-height: 12px; width: 86px; text-align-last: center;"
|
||||||
@change="$event.target.value ? preferences.font_size = parseInt($event.target.value) : delete preferences.font_size"
|
@change="$event.target.value ? preferences.font_size = parseInt($event.target.value) : delete preferences.font_size"
|
||||||
>
|
>
|
||||||
<option
|
<option
|
||||||
@@ -77,12 +77,12 @@
|
|||||||
</select>
|
</select>
|
||||||
<span
|
<span
|
||||||
class="border-l pl-1.5 absolute bg-white bottom-0 pointer-events-none text-sm h-5"
|
class="border-l pl-1.5 absolute bg-white bottom-0 pointer-events-none text-sm h-5"
|
||||||
style="right: 13px; top: 7px"
|
style="right: 13px; top: 6px"
|
||||||
>
|
>
|
||||||
pt
|
pt
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span class="flex">
|
||||||
<div
|
<div
|
||||||
class="join"
|
class="join"
|
||||||
style="height: 32px"
|
style="height: 32px"
|
||||||
@@ -98,7 +98,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span class="flex">
|
||||||
<div
|
<div
|
||||||
class="join"
|
class="join"
|
||||||
style="height: 32px"
|
style="height: 32px"
|
||||||
@@ -114,6 +114,32 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
|
<span class="flex">
|
||||||
|
<div class="dropdown modal-field-font-dropdown">
|
||||||
|
<label
|
||||||
|
tabindex="0"
|
||||||
|
class="cursor-pointer flex bg-white border input-bordered rounded-md h-8 items-center justify-center px-1"
|
||||||
|
style="-webkit-appearance: none; -moz-appearance: none;"
|
||||||
|
>
|
||||||
|
<component :is="valigns.find((v) => v.value === (preferences.valign || 'center'))?.icon" />
|
||||||
|
</label>
|
||||||
|
<div
|
||||||
|
tabindex="0"
|
||||||
|
class="dropdown-content p-0 mt-1 block z-10 menu shadow bg-white border border-base-300 rounded-md"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="(valign, index) in valigns"
|
||||||
|
:key="index"
|
||||||
|
:value="valign.value"
|
||||||
|
:class="{ 'bg-base-300': preferences.valign == valign.value }"
|
||||||
|
class="hover:bg-base-300 px-2 py-1.5 cursor-pointer"
|
||||||
|
@click="[valign.value ? preferences.valign = valign.value : delete preferences.valign, closeDropdown()]"
|
||||||
|
>
|
||||||
|
<component :is="valign.icon" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
<span>
|
<span>
|
||||||
<select
|
<select
|
||||||
class="input input-bordered bg-white input-sm text-lg rounded-md"
|
class="input input-bordered bg-white input-sm text-lg rounded-md"
|
||||||
@@ -134,7 +160,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<div
|
<div
|
||||||
class="flex items-center border border-base-content/20 rounded-xl bg-white px-4 h-16 modal-field-font-preview"
|
class="flex border border-base-content/20 rounded-xl bg-white px-4 h-16 modal-field-font-preview"
|
||||||
:style="{
|
:style="{
|
||||||
color: preferences.color || 'black',
|
color: preferences.color || 'black',
|
||||||
fontSize: (preferences.font_size || 12) + 'pt',
|
fontSize: (preferences.font_size || 12) + 'pt',
|
||||||
@@ -143,7 +169,7 @@
|
|||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
contenteditable="true"
|
contenteditable="true"
|
||||||
class="outline-none"
|
class="outline-none whitespace-nowrap truncate"
|
||||||
>
|
>
|
||||||
{{ field.default_value || field.name || buildDefaultName(field, template.fields) }}
|
{{ field.default_value || field.name || buildDefaultName(field, template.fields) }}
|
||||||
</span>
|
</span>
|
||||||
@@ -163,7 +189,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { IconChevronDown, IconBold, IconItalic, IconAlignLeft, IconAlignRight, IconAlignCenter } from '@tabler/icons-vue'
|
import { IconChevronDown, IconBold, IconItalic, IconAlignLeft, IconAlignRight, IconAlignCenter, IconAlignBoxCenterTop, IconAlignBoxCenterBottom, IconAlignBoxCenterMiddle } from '@tabler/icons-vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FontModal',
|
name: 'FontModal',
|
||||||
@@ -213,6 +239,13 @@ export default {
|
|||||||
{ icon: IconAlignRight, value: 'right' }
|
{ icon: IconAlignRight, value: 'right' }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
valigns () {
|
||||||
|
return [
|
||||||
|
{ icon: IconAlignBoxCenterTop, value: 'top' },
|
||||||
|
{ icon: IconAlignBoxCenterMiddle, value: 'center' },
|
||||||
|
{ icon: IconAlignBoxCenterBottom, value: 'bottom' }
|
||||||
|
]
|
||||||
|
},
|
||||||
sizes () {
|
sizes () {
|
||||||
return [...Array(23).keys()].map(i => i + 6)
|
return [...Array(23).keys()].map(i => i + 6)
|
||||||
},
|
},
|
||||||
@@ -230,12 +263,15 @@ export default {
|
|||||||
'justify-center': this.preferences.align === 'center',
|
'justify-center': this.preferences.align === 'center',
|
||||||
'justify-start': this.preferences.align === 'left',
|
'justify-start': this.preferences.align === 'left',
|
||||||
'justify-end': this.preferences.align === 'right',
|
'justify-end': this.preferences.align === 'right',
|
||||||
|
'items-center': !this.preferences.valign || this.preferences.valign === 'center',
|
||||||
|
'items-start': this.preferences.valign === 'top',
|
||||||
|
'items-end': this.preferences.valign === 'bottom',
|
||||||
'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)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
keys () {
|
keys () {
|
||||||
return ['font_type', 'font_size', 'color', 'align', 'font']
|
return ['font_type', 'font_size', 'color', 'align', 'valign', 'font']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created () {
|
created () {
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ export default {
|
|||||||
computed: {
|
computed: {
|
||||||
fields () {
|
fields () {
|
||||||
return this.template.fields.reduce((acc, f) => {
|
return this.template.fields.reduce((acc, f) => {
|
||||||
if (f !== this.field && f.submitter_uuid === this.field.submitter_uuid && ['number'].includes(f.type) && !f.preferences?.formula) {
|
if (f !== this.field && ['number'].includes(f.type) && (!f.preferences?.formula || f.submitter_uuid !== this.field.submitter_uuid)) {
|
||||||
acc.push(f)
|
acc.push(f)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="isDragging || isLoading"
|
||||||
|
class="modal modal-open"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col gap-2 p-4 items-center bg-base-100 h-full max-h-[85vh] max-w-6xl rounded-2xl w-full">
|
||||||
|
<Dropzone
|
||||||
|
class="flex-1 h-full"
|
||||||
|
hover-class="bg-base-200/50"
|
||||||
|
icon="IconFilePlus"
|
||||||
|
:template-id="templateId"
|
||||||
|
:accept-file-types="acceptFileTypes"
|
||||||
|
:with-description="false"
|
||||||
|
:title="t('upload_a_new_document')"
|
||||||
|
type="add_files"
|
||||||
|
@loading="isLoading = $event"
|
||||||
|
@success="$emit('add', $event)"
|
||||||
|
@error="$emit('error', $event)"
|
||||||
|
/>
|
||||||
|
<div class="flex-1 flex gap-2 w-full">
|
||||||
|
<Dropzone
|
||||||
|
class="flex-1 h-full"
|
||||||
|
hover-class="bg-base-200/50"
|
||||||
|
icon="IconFileSymlink"
|
||||||
|
:template-id="templateId"
|
||||||
|
:accept-file-types="acceptFileTypes"
|
||||||
|
:with-description="false"
|
||||||
|
:title="t('replace_existing_document')"
|
||||||
|
@loading="isLoading = $event"
|
||||||
|
@success="$emit('replace', $event)"
|
||||||
|
@error="$emit('error', $event)"
|
||||||
|
/>
|
||||||
|
<Dropzone
|
||||||
|
v-if="withReplaceAndClone"
|
||||||
|
class="flex-1 h-full"
|
||||||
|
hover-class="bg-base-200/50"
|
||||||
|
icon="IconFiles"
|
||||||
|
:template-id="templateId"
|
||||||
|
:accept-file-types="acceptFileTypes"
|
||||||
|
:with-description="false"
|
||||||
|
:clone-template-on-upload="true"
|
||||||
|
:title="t('clone_and_replace_documents')"
|
||||||
|
@loading="isLoading = $event"
|
||||||
|
@success="$emit('replace-and-clone', $event)"
|
||||||
|
@error="$emit('error', $event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Dropzone from './dropzone'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'HoverDropzone',
|
||||||
|
components: {
|
||||||
|
Dropzone
|
||||||
|
},
|
||||||
|
inject: ['t'],
|
||||||
|
props: {
|
||||||
|
isDragging: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
templateId: {
|
||||||
|
type: [Number, String],
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
withReplaceAndClone: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
acceptFileTypes: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: 'image/*, application/pdf'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['add', 'replace', 'replace-and-clone', 'error'],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
isLoading: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -63,6 +63,9 @@ const en = {
|
|||||||
processing_: 'Processing...',
|
processing_: 'Processing...',
|
||||||
add_pdf_documents_or_images: 'Add PDF documents or images',
|
add_pdf_documents_or_images: 'Add PDF documents or images',
|
||||||
add_documents_or_images: 'Add documents or images',
|
add_documents_or_images: 'Add documents or images',
|
||||||
|
upload_a_new_document: 'Upload a new document',
|
||||||
|
replace_existing_document: 'Replace existing document',
|
||||||
|
clone_and_replace_documents: 'Clone and replace documents',
|
||||||
required: 'Required',
|
required: 'Required',
|
||||||
default_value: 'Default value',
|
default_value: 'Default value',
|
||||||
format: 'Format',
|
format: 'Format',
|
||||||
@@ -158,7 +161,9 @@ const en = {
|
|||||||
some_fields_are_missing_in_the_formula: 'Some fields are missing in the formula.',
|
some_fields_are_missing_in_the_formula: 'Some fields are missing in the formula.',
|
||||||
learn_more: 'Learn more',
|
learn_more: 'Learn more',
|
||||||
and: 'and',
|
and: 'and',
|
||||||
or: 'or'
|
or: 'or',
|
||||||
|
start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: 'Start a quick tour to learn how to create an send your first document',
|
||||||
|
start_tour: 'Start Tour'
|
||||||
}
|
}
|
||||||
|
|
||||||
const es = {
|
const es = {
|
||||||
@@ -223,6 +228,9 @@ const es = {
|
|||||||
processing_: 'Procesando...',
|
processing_: 'Procesando...',
|
||||||
add_pdf_documents_or_images: 'Agregar documentos PDF o imágenes',
|
add_pdf_documents_or_images: 'Agregar documentos PDF o imágenes',
|
||||||
add_documents_or_images: 'Agregar documentos o imágenes',
|
add_documents_or_images: 'Agregar documentos o imágenes',
|
||||||
|
upload_a_new_document: 'Subir un nuevo documento',
|
||||||
|
replace_existing_document: 'Reemplazar documento existente',
|
||||||
|
clone_and_replace_documents: 'Clonar y reemplazar documentos',
|
||||||
required: 'Requerido',
|
required: 'Requerido',
|
||||||
default_value: 'Valor predeterminado',
|
default_value: 'Valor predeterminado',
|
||||||
format: 'Formato',
|
format: 'Formato',
|
||||||
@@ -321,7 +329,9 @@ const es = {
|
|||||||
some_fields_are_missing_in_the_formula: 'Faltan algunos campos en la fórmula.',
|
some_fields_are_missing_in_the_formula: 'Faltan algunos campos en la fórmula.',
|
||||||
learn_more: 'Aprende más',
|
learn_more: 'Aprende más',
|
||||||
and: 'y',
|
and: 'y',
|
||||||
or: 'o'
|
or: 'o',
|
||||||
|
start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: 'Inicia una guía rápida para aprender a crear y enviar tu primer documento.',
|
||||||
|
start_tour: 'Iniciar guía'
|
||||||
}
|
}
|
||||||
|
|
||||||
const it = {
|
const it = {
|
||||||
@@ -389,6 +399,9 @@ const it = {
|
|||||||
processing_: 'Elaborazione...',
|
processing_: 'Elaborazione...',
|
||||||
add_pdf_documents_or_images: 'Aggiungi documenti PDF o immagini',
|
add_pdf_documents_or_images: 'Aggiungi documenti PDF o immagini',
|
||||||
add_documents_or_images: 'Aggiungi documenti o immagini',
|
add_documents_or_images: 'Aggiungi documenti o immagini',
|
||||||
|
upload_a_new_document: 'Carica un nuovo documento',
|
||||||
|
replace_existing_document: 'Sostituisci documento esistente',
|
||||||
|
clone_and_replace_documents: 'Clona e sostituisci documenti',
|
||||||
required: 'Obbligatorio',
|
required: 'Obbligatorio',
|
||||||
default_value: 'Valore predefinito',
|
default_value: 'Valore predefinito',
|
||||||
format: 'Formato',
|
format: 'Formato',
|
||||||
@@ -484,7 +497,9 @@ const it = {
|
|||||||
some_fields_are_missing_in_the_formula: 'Alcuni campi mancano nella formula.',
|
some_fields_are_missing_in_the_formula: 'Alcuni campi mancano nella formula.',
|
||||||
learn_more: 'Scopri di più',
|
learn_more: 'Scopri di più',
|
||||||
and: 'e',
|
and: 'e',
|
||||||
or: 'o'
|
or: 'o',
|
||||||
|
start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: 'Inizia un tour rapido per imparare a creare e inviare il tuo primo documento.',
|
||||||
|
start_tour: 'Inizia il tour'
|
||||||
}
|
}
|
||||||
|
|
||||||
const pt = {
|
const pt = {
|
||||||
@@ -549,6 +564,9 @@ const pt = {
|
|||||||
processing_: 'Processando...',
|
processing_: 'Processando...',
|
||||||
add_pdf_documents_or_images: 'Adicionar documentos PDF ou imagens',
|
add_pdf_documents_or_images: 'Adicionar documentos PDF ou imagens',
|
||||||
add_documents_or_images: 'Adicionar documentos ou imagens',
|
add_documents_or_images: 'Adicionar documentos ou imagens',
|
||||||
|
upload_a_new_document: 'Enviar um novo documento',
|
||||||
|
replace_existing_document: 'Substituir documento existente',
|
||||||
|
clone_and_replace_documents: 'Clonar e substituir documentos',
|
||||||
required: 'Obrigatório',
|
required: 'Obrigatório',
|
||||||
default_value: 'Valor padrão',
|
default_value: 'Valor padrão',
|
||||||
format: 'Formato',
|
format: 'Formato',
|
||||||
@@ -647,7 +665,9 @@ const pt = {
|
|||||||
some_fields_are_missing_in_the_formula: 'Faltam alguns campos na fórmula.',
|
some_fields_are_missing_in_the_formula: 'Faltam alguns campos na fórmula.',
|
||||||
learn_more: 'Saiba mais',
|
learn_more: 'Saiba mais',
|
||||||
and: 'e',
|
and: 'e',
|
||||||
or: 'ou'
|
or: 'ou',
|
||||||
|
start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: 'Comece um tour rápido para aprender a criar e enviar seu primeiro documento.',
|
||||||
|
start_tour: 'Iniciar tour'
|
||||||
}
|
}
|
||||||
|
|
||||||
const fr = {
|
const fr = {
|
||||||
@@ -711,6 +731,9 @@ const fr = {
|
|||||||
processing_: 'Traitement en cours...',
|
processing_: 'Traitement en cours...',
|
||||||
add_pdf_documents_or_images: 'Ajoutez des documents PDF ou des images',
|
add_pdf_documents_or_images: 'Ajoutez des documents PDF ou des images',
|
||||||
add_documents_or_images: 'Ajoutez des documents ou des images',
|
add_documents_or_images: 'Ajoutez des documents ou des images',
|
||||||
|
upload_a_new_document: 'Téléverser un nouveau document',
|
||||||
|
replace_existing_document: 'Remplacer le document existant',
|
||||||
|
clone_and_replace_documents: 'Cloner et remplacer les documents',
|
||||||
required: 'Requis',
|
required: 'Requis',
|
||||||
default_value: 'Valeur par défaut',
|
default_value: 'Valeur par défaut',
|
||||||
format: 'Format',
|
format: 'Format',
|
||||||
@@ -810,7 +833,9 @@ const fr = {
|
|||||||
some_fields_are_missing_in_the_formula: 'Certains champs manquent dans la formule.',
|
some_fields_are_missing_in_the_formula: 'Certains champs manquent dans la formule.',
|
||||||
learn_more: 'En savoir plus',
|
learn_more: 'En savoir plus',
|
||||||
and: 'et',
|
and: 'et',
|
||||||
or: 'ou'
|
or: 'ou',
|
||||||
|
start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: 'Lancez une visite rapide pour apprendre à créer et envoyer votre premier document.',
|
||||||
|
start_tour: 'Démarrer'
|
||||||
}
|
}
|
||||||
|
|
||||||
const de = {
|
const de = {
|
||||||
@@ -874,6 +899,9 @@ const de = {
|
|||||||
processing_: 'Verarbeitung...',
|
processing_: 'Verarbeitung...',
|
||||||
add_pdf_documents_or_images: 'PDF-Dokumente oder Bilder hinzufügen',
|
add_pdf_documents_or_images: 'PDF-Dokumente oder Bilder hinzufügen',
|
||||||
add_documents_or_images: 'Dokumente oder Bilder hinzufügen',
|
add_documents_or_images: 'Dokumente oder Bilder hinzufügen',
|
||||||
|
upload_a_new_document: 'Neues Dokument hochladen',
|
||||||
|
replace_existing_document: 'Vorhandenes Dokument ersetzen',
|
||||||
|
clone_and_replace_documents: 'Dokumente klonen und ersetzen',
|
||||||
required: 'Erforderlich',
|
required: 'Erforderlich',
|
||||||
default_value: 'Standardwert',
|
default_value: 'Standardwert',
|
||||||
format: 'Format',
|
format: 'Format',
|
||||||
@@ -973,7 +1001,9 @@ const de = {
|
|||||||
some_fields_are_missing_in_the_formula: 'Einige Felder fehlen in der Formel.',
|
some_fields_are_missing_in_the_formula: 'Einige Felder fehlen in der Formel.',
|
||||||
learn_more: 'Erfahren Sie mehr',
|
learn_more: 'Erfahren Sie mehr',
|
||||||
and: 'und',
|
and: 'und',
|
||||||
or: 'oder'
|
or: 'oder',
|
||||||
|
start_a_quick_tour_to_learn_how_to_create_an_send_your_first_document: 'Starte eine kurze Tour, um zu lernen, wie du dein erstes Dokument erstellst und versendest.',
|
||||||
|
start_tour: 'Starten'
|
||||||
}
|
}
|
||||||
|
|
||||||
export { en, es, it, pt, fr, de }
|
export { en, es, it, pt, fr, de }
|
||||||
@@ -54,7 +54,7 @@
|
|||||||
v-for="(icon, type) in fieldIconsSorted"
|
v-for="(icon, type) in fieldIconsSorted"
|
||||||
:key="type"
|
:key="type"
|
||||||
>
|
>
|
||||||
<li v-if="(fieldTypes.length === 0 || fieldTypes.includes(type)) && (withPhone || type != 'phone') && (withPayment || type != 'payment') && (withVerification || type != 'verification')">
|
<li v-if="fieldTypes.includes(type) || ((withPhone || type != 'phone') && (withPayment || type != 'payment') && (withVerification || type != 'verification'))">
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
class="text-sm py-1 px-2"
|
class="text-sm py-1 px-2"
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
<FieldArea
|
<FieldArea
|
||||||
v-if="newArea"
|
v-if="newArea"
|
||||||
:is-draw="true"
|
:is-draw="true"
|
||||||
:field="{ submitter_uuid: selectedSubmitter.uuid, type: drawField?.type || defaultFieldType }"
|
:field="{ submitter_uuid: selectedSubmitter.uuid, type: drawField?.type || dragFieldPlaceholder?.type || defaultFieldType }"
|
||||||
:area="newArea"
|
:area="newArea"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -49,7 +49,9 @@
|
|||||||
:class="{ 'z-10': !isMobile, 'cursor-grab': isDrag, 'cursor-nwse-resize': drawField, [resizeDirectionClasses[resizeDirection]]: !!resizeDirectionClasses }"
|
:class="{ 'z-10': !isMobile, 'cursor-grab': isDrag, 'cursor-nwse-resize': drawField, [resizeDirectionClasses[resizeDirection]]: !!resizeDirectionClasses }"
|
||||||
@pointermove="onPointermove"
|
@pointermove="onPointermove"
|
||||||
@pointerdown="onStartDraw"
|
@pointerdown="onStartDraw"
|
||||||
@dragover.prevent
|
@dragover.prevent="onDragover"
|
||||||
|
@dragenter="onDragenter"
|
||||||
|
@dragleave="newArea = null"
|
||||||
@drop="onDrop"
|
@drop="onDrop"
|
||||||
@pointerup="onPointerup"
|
@pointerup="onPointerup"
|
||||||
/>
|
/>
|
||||||
@@ -64,12 +66,17 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
FieldArea
|
FieldArea
|
||||||
},
|
},
|
||||||
inject: ['fieldTypes', 'defaultDrawFieldType', 'fieldsDragFieldRef'],
|
inject: ['fieldTypes', 'defaultDrawFieldType', 'fieldsDragFieldRef', 'assignDropAreaSize'],
|
||||||
props: {
|
props: {
|
||||||
image: {
|
image: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
dragFieldPlaceholder: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
areas: {
|
areas: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -192,7 +199,24 @@ export default {
|
|||||||
this.areaRefs.push(el)
|
this.areaRefs.push(el)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onDragenter (e) {
|
||||||
|
this.newArea = {}
|
||||||
|
|
||||||
|
this.assignDropAreaSize(this.newArea, this.dragFieldPlaceholder, {
|
||||||
|
maskW: this.$refs.mask.clientWidth,
|
||||||
|
maskH: this.$refs.mask.clientHeight
|
||||||
|
})
|
||||||
|
|
||||||
|
this.newArea.x = (e.offsetX - 6) / this.$refs.mask.clientWidth
|
||||||
|
this.newArea.y = e.offsetY / this.$refs.mask.clientHeight - this.newArea.h / 2
|
||||||
|
},
|
||||||
|
onDragover (e) {
|
||||||
|
this.newArea.x = (e.offsetX - 6) / this.$refs.mask.clientWidth
|
||||||
|
this.newArea.y = e.offsetY / this.$refs.mask.clientHeight - this.newArea.h / 2
|
||||||
|
},
|
||||||
onDrop (e) {
|
onDrop (e) {
|
||||||
|
this.newArea = null
|
||||||
|
|
||||||
this.$emit('drop-field', {
|
this.$emit('drop-field', {
|
||||||
x: e.offsetX,
|
x: e.offsetX,
|
||||||
y: e.offsetY,
|
y: e.offsetY,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<label
|
<label
|
||||||
:for="inputId"
|
:for="inputId"
|
||||||
class="btn btn-neutral btn-xs text-white transition-none replace-document-button"
|
class="btn btn-neutral btn-xs text-white transition-none replace-document-button"
|
||||||
:class="{ 'opacity-100': isLoading || isProcessing }"
|
:class="{ 'opacity-100': isLoading }"
|
||||||
>
|
>
|
||||||
{{ message }}
|
{{ message }}
|
||||||
<form
|
<form
|
||||||
@@ -41,19 +41,19 @@ export default {
|
|||||||
emits: ['success'],
|
emits: ['success'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
isLoading: false,
|
isLoading: false
|
||||||
isProcessing: false
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
inputId () {
|
inputId () {
|
||||||
return 'el' + Math.random().toString(32).split('.')[1]
|
return 'el' + Math.random().toString(32).split('.')[1]
|
||||||
},
|
},
|
||||||
|
uploadUrl () {
|
||||||
|
return `/templates/${this.templateId}/documents`
|
||||||
|
},
|
||||||
message () {
|
message () {
|
||||||
if (this.isLoading) {
|
if (this.isLoading) {
|
||||||
return this.t('uploading_')
|
return this.t('uploading_')
|
||||||
} else if (this.isProcessing) {
|
|
||||||
return this.t('processing_')
|
|
||||||
} else {
|
} else {
|
||||||
return this.t('replace')
|
return this.t('replace')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,10 @@
|
|||||||
id="add_document_button"
|
id="add_document_button"
|
||||||
:for="inputId"
|
:for="inputId"
|
||||||
class="btn btn-outline w-full add-document-button"
|
class="btn btn-outline w-full add-document-button"
|
||||||
:class="{ 'btn-disabled': isLoading || isProcessing }"
|
:class="{ 'btn-disabled': isLoading }"
|
||||||
>
|
>
|
||||||
<IconInnerShadowTop
|
<IconInnerShadowTop
|
||||||
v-if="isLoading || isProcessing"
|
v-if="isLoading"
|
||||||
width="20"
|
width="20"
|
||||||
class="animate-spin"
|
class="animate-spin"
|
||||||
/>
|
/>
|
||||||
@@ -18,9 +18,6 @@
|
|||||||
<span v-if="isLoading">
|
<span v-if="isLoading">
|
||||||
{{ t('uploading_') }}
|
{{ t('uploading_') }}
|
||||||
</span>
|
</span>
|
||||||
<span v-else-if="isProcessing">
|
|
||||||
{{ t('processing_') }}
|
|
||||||
</span>
|
|
||||||
<span v-else>
|
<span v-else>
|
||||||
{{ t('add_document') }}
|
{{ t('add_document') }}
|
||||||
</span>
|
</span>
|
||||||
@@ -63,30 +60,34 @@ export default {
|
|||||||
default: 'image/*, application/pdf'
|
default: 'image/*, application/pdf'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['success'],
|
emits: ['success', 'error'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
isLoading: false,
|
isLoading: false
|
||||||
isProcessing: false
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
inputId () {
|
inputId () {
|
||||||
return 'el' + Math.random().toString(32).split('.')[1]
|
return 'el' + Math.random().toString(32).split('.')[1]
|
||||||
|
},
|
||||||
|
uploadUrl () {
|
||||||
|
return `/templates/${this.templateId}/documents`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async upload () {
|
async upload () {
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
|
|
||||||
this.baseFetch(`/templates/${this.templateId}/documents`, {
|
this.baseFetch(this.uploadUrl, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
headers: { Accept: 'application/json' },
|
||||||
body: new FormData(this.$refs.form)
|
body: new FormData(this.$refs.form)
|
||||||
}).then((resp) => {
|
}).then((resp) => {
|
||||||
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 = ''
|
this.$refs.input.value = ''
|
||||||
|
this.isLoading = false
|
||||||
})
|
})
|
||||||
} else if (resp.status === 422) {
|
} else if (resp.status === 422) {
|
||||||
resp.json().then((data) => {
|
resp.json().then((data) => {
|
||||||
@@ -95,21 +96,33 @@ export default {
|
|||||||
|
|
||||||
formData.append('password', prompt(this.t('enter_pdf_password')))
|
formData.append('password', prompt(this.t('enter_pdf_password')))
|
||||||
|
|
||||||
this.baseFetch(`/templates/${this.templateId}/documents`, {
|
this.baseFetch(this.uploadUrl, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData
|
body: formData
|
||||||
}).then(async (resp) => {
|
}).then(async (resp) => {
|
||||||
if (resp.ok) {
|
if (resp.ok) {
|
||||||
this.$emit('success', await resp.json())
|
this.$emit('success', await resp.json())
|
||||||
this.$refs.input.value = ''
|
this.$refs.input.value = ''
|
||||||
|
this.isLoading = false
|
||||||
} else {
|
} else {
|
||||||
alert(this.t('wrong_password'))
|
alert(this.t('wrong_password'))
|
||||||
|
|
||||||
|
this.$emit('error', await resp.json().error)
|
||||||
|
this.isLoading = false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
this.$emit('error', data.error)
|
||||||
|
this.isLoading = false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
resp.json().then((data) => {
|
||||||
|
this.$emit('error', data.error)
|
||||||
|
this.isLoading = false
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}).finally(() => {
|
}).catch(() => {
|
||||||
this.isLoading = false
|
this.isLoading = false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,11 +94,27 @@ class ProcessSubmitterCompletionJob
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
to = build_to_addresses(submitter)
|
maybe_enqueue_copy_emails(submitter)
|
||||||
|
end
|
||||||
|
|
||||||
return if to.blank? || submitter.template.preferences['documents_copy_email_enabled'] == false
|
def maybe_enqueue_copy_emails(submitter)
|
||||||
|
return if submitter.template.preferences['documents_copy_email_enabled'] == false
|
||||||
|
|
||||||
SubmitterMailer.documents_copy_email(submitter, to:).deliver_later!
|
configs = AccountConfigs.find_or_initialize_for_key(submitter.account,
|
||||||
|
AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY)
|
||||||
|
|
||||||
|
return if configs.value['enabled'] == false
|
||||||
|
|
||||||
|
to = submitter.submission.submitters.reject { |e| e.preferences['send_email'] == false }
|
||||||
|
.sort_by(&:completed_at).select(&:email?).map(&:friendly_name)
|
||||||
|
|
||||||
|
return if to.blank?
|
||||||
|
|
||||||
|
if configs.value['bcc_recipients'] == true
|
||||||
|
to.each { |to| SubmitterMailer.documents_copy_email(submitter, to:).deliver_later! }
|
||||||
|
else
|
||||||
|
SubmitterMailer.documents_copy_email(submitter, to: to.join(', ')).deliver_later!
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_bcc_addresses(submission)
|
def build_bcc_addresses(submission)
|
||||||
@@ -110,11 +126,6 @@ class ProcessSubmitterCompletionJob
|
|||||||
bcc.to_s.scan(User::EMAIL_REGEXP)
|
bcc.to_s.scan(User::EMAIL_REGEXP)
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_to_addresses(submitter)
|
|
||||||
submitter.submission.submitters.reject { |e| e.preferences['send_email'] == false }
|
|
||||||
.sort_by(&:completed_at).select(&:email?).map(&:friendly_name).join(', ')
|
|
||||||
end
|
|
||||||
|
|
||||||
def enqueue_next_submitter_request_notification(submitter)
|
def enqueue_next_submitter_request_notification(submitter)
|
||||||
next_submitter_item =
|
next_submitter_item =
|
||||||
submitter.submission.template_submitters.find do |e|
|
submitter.submission.template_submitters.find do |e|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ class AccountConfig < ApplicationRecord
|
|||||||
ALLOW_TO_RESUBMIT = 'allow_to_resubmit'
|
ALLOW_TO_RESUBMIT = 'allow_to_resubmit'
|
||||||
ALLOW_TO_DECLINE_KEY = 'allow_to_decline'
|
ALLOW_TO_DECLINE_KEY = 'allow_to_decline'
|
||||||
SUBMITTER_REMINDERS = 'submitter_reminders'
|
SUBMITTER_REMINDERS = 'submitter_reminders'
|
||||||
|
ENFORCE_SIGNING_ORDER_KEY = 'enforce_signing_order'
|
||||||
FORM_COMPLETED_BUTTON_KEY = 'form_completed_button'
|
FORM_COMPLETED_BUTTON_KEY = 'form_completed_button'
|
||||||
FORM_COMPLETED_MESSAGE_KEY = 'form_completed_message'
|
FORM_COMPLETED_MESSAGE_KEY = 'form_completed_message'
|
||||||
FORM_WITH_CONFETTI_KEY = 'form_with_confetti'
|
FORM_WITH_CONFETTI_KEY = 'form_with_confetti'
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ class Submission < ApplicationRecord
|
|||||||
through: :template, source: :documents_attachments
|
through: :template, source: :documents_attachments
|
||||||
|
|
||||||
scope :active, -> { where(archived_at: nil) }
|
scope :active, -> { where(archived_at: nil) }
|
||||||
|
scope :archived, -> { where.not(archived_at: nil) }
|
||||||
scope :pending, -> { joins(:submitters).where(submitters: { completed_at: nil }).group(:id) }
|
scope :pending, -> { joins(:submitters).where(submitters: { completed_at: nil }).group(:id) }
|
||||||
scope :completed, lambda {
|
scope :completed, lambda {
|
||||||
where.not(Submitter.where(Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id])
|
where.not(Submitter.where(Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id])
|
||||||
@@ -89,6 +90,10 @@ class Submission < ApplicationRecord
|
|||||||
expire_at && expire_at <= Time.current
|
expire_at && expire_at <= Time.current
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def fields_uuid_index
|
||||||
|
@fields_uuid_index ||= (template_fields || template.fields).index_by { |f| f['uuid'] }
|
||||||
|
end
|
||||||
|
|
||||||
def audit_trail_url
|
def audit_trail_url
|
||||||
return if audit_trail.blank?
|
return if audit_trail.blank?
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ class SubmissionEvent < ApplicationRecord
|
|||||||
|
|
||||||
enum :event_type, {
|
enum :event_type, {
|
||||||
send_email: 'send_email',
|
send_email: 'send_email',
|
||||||
|
bounce_email: 'bounce_email',
|
||||||
|
complaint_email: 'complaint_email',
|
||||||
send_reminder_email: 'send_reminder_email',
|
send_reminder_email: 'send_reminder_email',
|
||||||
send_sms: 'send_sms',
|
send_sms: 'send_sms',
|
||||||
send_2fa_sms: 'send_2fa_sms',
|
send_2fa_sms: 'send_2fa_sms',
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ class Submitter < ApplicationRecord
|
|||||||
has_many_attached :documents
|
has_many_attached :documents
|
||||||
has_many_attached :attachments
|
has_many_attached :attachments
|
||||||
has_many_attached :preview_documents
|
has_many_attached :preview_documents
|
||||||
has_many :template_accesses, through: :template
|
has_many :template_accesses, through: :submission
|
||||||
has_many :email_events, as: :emailable, dependent: (Docuseal.multitenant? ? nil : :destroy)
|
has_many :email_events, as: :emailable, dependent: (Docuseal.multitenant? ? nil : :destroy)
|
||||||
|
|
||||||
has_many :document_generation_events, dependent: :destroy
|
has_many :document_generation_events, dependent: :destroy
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ class UserConfig < ApplicationRecord
|
|||||||
INITIALS_KEY = 'initials'
|
INITIALS_KEY = 'initials'
|
||||||
RECEIVE_COMPLETED_EMAIL = 'receive_completed_email'
|
RECEIVE_COMPLETED_EMAIL = 'receive_completed_email'
|
||||||
RECEIVE_DECLINED_EMAIL = 'receive_declined_email'
|
RECEIVE_DECLINED_EMAIL = 'receive_declined_email'
|
||||||
|
SHOW_APP_TOUR = 'show_app_tour'
|
||||||
|
|
||||||
belongs_to :user
|
belongs_to :user
|
||||||
|
|
||||||
|
|||||||
@@ -151,6 +151,20 @@
|
|||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
<% if !Docuseal.multitenant? || can?(:manage, :personalization_advanced) %>
|
||||||
|
<% account_config = AccountConfig.find_or_initialize_by(account: current_account, key: AccountConfig::ENFORCE_SIGNING_ORDER_KEY) %>
|
||||||
|
<% if can?(:manage, account_config) %>
|
||||||
|
<%= form_for account_config, url: account_configs_path, method: :post do |f| %>
|
||||||
|
<%= f.hidden_field :key %>
|
||||||
|
<div class="flex items-center justify-between py-2.5">
|
||||||
|
<span>
|
||||||
|
<%= t('always_enforce_signing_order') %>
|
||||||
|
</span>
|
||||||
|
<%= f.check_box :value, class: 'toggle', checked: account_config.value, onchange: 'this.form.requestSubmit()' %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= render 'compliances' %>
|
<%= render 'compliances' %>
|
||||||
@@ -160,9 +174,10 @@
|
|||||||
<h2 class="text-2xl font-bold mb-2">
|
<h2 class="text-2xl font-bold mb-2">
|
||||||
<%= t('danger_zone') %>
|
<%= t('danger_zone') %>
|
||||||
</h2>
|
</h2>
|
||||||
<%= button_to button_title(title: t('delete_my_account')), settings_account_path, class: 'btn btn-outline btn-error block', data: { turbo_confirm: t('schedule_account_for_deletion_') }, method: :delete, id: :account_delete_button %>
|
<%= button_to button_title(title: t('delete_my_account')), settings_account_path, class: 'btn btn-outline btn-error block', data: { turbo_confirm: t('you_are_scheduling_your_account_for_deletion_after_deletion_your_data_will_be_permanently_removed_and_cannot_be_recovered_click_ok_if_you_would_like_to_continue') }, method: :delete, id: :account_delete_button %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-0 md:w-52"></div>
|
<div class="w-0 md:w-52"></div>
|
||||||
</div>
|
</div>
|
||||||
|
<%= render 'shared/app_tour', type: 'account' %>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<form action="<%= root_path %>" method="get" class="bg-base-200 px-1.5 rounded-xl py-1 whitespace-nowrap">
|
<form action="<%= root_path %>" method="get" id="templates_submissions_toggle" class="bg-base-200 px-1.5 rounded-xl py-1 whitespace-nowrap">
|
||||||
<toggle-cookies data-value="templates" data-key="dashboard_view" class="sm:tooltip tooltip-top" data-tip="<%= t('templates') %>">
|
<toggle-cookies data-value="templates" data-key="dashboard_view" class="sm:tooltip tooltip-top" data-tip="<%= t('templates') %>">
|
||||||
<button class="<%= local_assigns[:selected] == 'submissions' ? 'btn !border !rounded-lg btn-square !p-0 !btn-sm !h-8 !w-9' : 'btn btn-neutral !rounded-lg btn-square !p-0 hover:text-neutral-300 !btn-sm !h-8 !w-9 disabled:btn-neutral' %>">
|
<button class="<%= local_assigns[:selected] == 'submissions' ? 'btn !border !rounded-lg btn-square !p-0 !btn-sm !h-8 !w-9' : 'btn btn-neutral !rounded-lg btn-square !p-0 hover:text-neutral-300 !btn-sm !h-8 !w-9 disabled:btn-neutral' %>">
|
||||||
<%= svg_icon('layout_grid', class: 'w-6 h-6 stroke-2') %>
|
<%= svg_icon('layout_grid', class: 'w-6 h-6 stroke-2') %>
|
||||||
|
|||||||
@@ -141,7 +141,7 @@
|
|||||||
<%= t('preferences') %>
|
<%= t('preferences') %>
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<% if can?(:manage, account_config) %>
|
<% if can?(:manage, account_config) && (can?(:manage, :personalization_advanced) || !Docuseal.multitenant?) %>
|
||||||
<%= form_for account_config, url: account_configs_path, method: :post do |f| %>
|
<%= form_for account_config, url: account_configs_path, method: :post do |f| %>
|
||||||
<%= f.hidden_field :key %>
|
<%= f.hidden_field :key %>
|
||||||
<div class="flex items-center justify-between py-2.5">
|
<div class="flex items-center justify-between py-2.5">
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<path d="M3 9l4 -4l4 4m-4 -4v14" />
|
||||||
|
<path d="M21 15l-4 4l-4 -4m4 4v-14" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 362 B |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M20.986 12.502a9 9 0 1 0 -5.973 7.98" /><path d="M12 7v5l3 3" /><path d="M19 16v3" /><path d="M19 22v.01" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 400 B |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M14 3v4a1 1 0 0 0 1 1h4" /><path d="M17 21h-10a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2z" /><path d="M12 11v6" /><path d="M9.5 13.5l2.5 -2.5l2.5 2.5" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 462 B |
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<path d="M15 3v4a1 1 0 0 0 1 1h4" />
|
||||||
|
<path d="M18 17h-7a2 2 0 0 1 -2 -2v-10a2 2 0 0 1 2 -2h4l5 5v7a2 2 0 0 1 -2 2z" />
|
||||||
|
<path d="M16 17v2a2 2 0 0 1 -2 2h-7a2 2 0 0 1 -2 -2v-10a2 2 0 0 1 2 -2h2" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 485 B |
@@ -1,9 +1,3 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M4 8v-2a2 2 0 0 1 2 -2h2" /><path d="M4 16v2a2 2 0 0 0 2 2h2" /><path d="M16 4h2a2 2 0 0 1 2 2v2" /><path d="M16 20h2a2 2 0 0 0 2 -2v-2" /><path d="M8 11m0 1a1 1 0 0 1 1 -1h6a1 1 0 0 1 1 1v3a1 1 0 0 1 -1 1h-6a1 1 0 0 1 -1 -1z" /><path d="M10 11v-2a2 2 0 1 1 4 0v2" />
|
||||||
<path d="M4 8v-2a2 2 0 0 1 2 -2h2"></path>
|
|
||||||
<path d="M4 16v2a2 2 0 0 0 2 2h2"></path>
|
|
||||||
<path d="M16 4h2a2 2 0 0 1 2 2v2"></path>
|
|
||||||
<path d="M16 20h2a2 2 0 0 0 2 -2v-2"></path>
|
|
||||||
<path d="M8 11m0 1a1 1 0 0 1 1 -1h6a1 1 0 0 1 1 1v3a1 1 0 0 1 -1 1h-6a1 1 0 0 1 -1 -1z"></path>
|
|
||||||
<path d="M10 11v-2a2 2 0 1 1 4 0v2"></path>
|
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 619 B After Width: | Height: | Size: 559 B |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" width="24" height="24" stroke-width="2">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M12 19h-7a2 2 0 0 1 -2 -2v-10a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v5.5" /><path d="M19 22v-6" /><path d="M22 19l-3 -3l-3 3" /><path d="M3 7l9 6l9 -6" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 437 B |
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" width="24" height="24" stroke-width="2">
|
||||||
|
<path d="M15 10v-5c0 -1.38 .62 -2 2 -2s2 .62 2 2v5m0 -3h-4"></path>
|
||||||
|
<path d="M19 21h-4l4 -7h-4"></path> <path d="M4 15l3 3l3 -3"></path>
|
||||||
|
<path d="M7 6v12"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 395 B |
@@ -0,0 +1,7 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" width="24" height="24" stroke-width="2">
|
||||||
|
<path d="M4 15l3 3l3 -3"></path>
|
||||||
|
<path d="M7 6v12"></path>
|
||||||
|
<path d="M17 14a2 2 0 0 1 2 2v3a2 2 0 1 1 -4 0v-3a2 2 0 0 1 2 -2z"></path>
|
||||||
|
<path d="M17 5m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0"></path>
|
||||||
|
<path d="M19 5v3a2 2 0 0 1 -2 2h-1.5"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 474 B |
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" width="24" height="24" stroke-width="2">
|
||||||
|
<path d="M10 15l-3 3l-3 -3"></path>
|
||||||
|
<path d="M7 6v12"></path>
|
||||||
|
<path d="M14 18.333c0 .369 .298 .667 .667 .667h2.666a.667 .667 0 0 0 .667 -.667v-2.666a.667 .667 0 0 0 -.667 -.667h-2.666a.667 .667 0 0 0 -.667 .667v2.666z"></path>
|
||||||
|
<path d="M14 10.833c0 .645 .522 1.167 1.167 1.167h4.666c.645 0 1.167 -.522 1.167 -1.167v-4.666c0 -.645 -.522 -1.167 -1.167 -1.167h-4.666c-.645 0 -1.167 .522 -1.167 1.167v4.666z"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 647 B |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M16 18a2 2 0 0 1 2 2a2 2 0 0 1 2 -2a2 2 0 0 1 -2 -2a2 2 0 0 1 -2 2zm0 -12a2 2 0 0 1 2 2a2 2 0 0 1 2 -2a2 2 0 0 1 -2 -2a2 2 0 0 1 -2 2zm-7 12a6 6 0 0 1 6 -6a6 6 0 0 1 -6 -6a6 6 0 0 1 -6 6a6 6 0 0 1 6 6z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 497 B |
@@ -7,19 +7,19 @@
|
|||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<%= f.fields_for :value, record do |ff| %>
|
<%= f.fields_for :value, record do |ff| %>
|
||||||
<%= ff.label :first_duration, t('first_reminder_in'), class: 'label truncate' %>
|
<%= ff.label :first_duration, t('first_reminder_in'), class: 'label truncate' %>
|
||||||
<%= ff.select :first_duration, durations, { include_blank: 'None' }, class: 'base-select' %>
|
<%= ff.select :first_duration, durations, { include_blank: t('none') }, class: 'base-select' %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<%= f.fields_for :value, record do |ff| %>
|
<%= f.fields_for :value, record do |ff| %>
|
||||||
<%= ff.label :second_duration, t('second_reminder_in'), class: 'label truncate' %>
|
<%= ff.label :second_duration, t('second_reminder_in'), class: 'label truncate' %>
|
||||||
<%= ff.select :second_duration, durations, { include_blank: 'None' }, class: 'base-select' %>
|
<%= ff.select :second_duration, durations, { include_blank: t('none') }, class: 'base-select' %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<%= f.fields_for :value, record do |ff| %>
|
<%= f.fields_for :value, record do |ff| %>
|
||||||
<%= ff.label :third_duration, t('third_reminder_in'), class: 'label truncate' %>
|
<%= ff.label :third_duration, t('third_reminder_in'), class: 'label truncate' %>
|
||||||
<%= ff.select :third_duration, durations, { include_blank: 'None' }, class: 'base-select' %>
|
<%= ff.select :third_duration, durations, { include_blank: t('none') }, class: 'base-select' %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<div class="collapse-content">
|
<div class="collapse-content">
|
||||||
<%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %>
|
<%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %>
|
||||||
<%= f.hidden_field :key %>
|
<%= f.hidden_field :key %>
|
||||||
<%= f.fields_for :value, Struct.new(:subject, :body, :reply_to, :attach_audit_log, :attach_documents).new(*f.object.value.values_at('subject', 'body', 'reply_to', 'attach_audit_log', 'attach_documents')) do |ff| %>
|
<%= f.fields_for :value, Struct.new(:subject, :body, :reply_to, :attach_audit_log, :attach_documents, :bcc_recipients, :enabled).new(*f.object.value.values_at('subject', 'body', 'reply_to', 'attach_audit_log', 'attach_documents', 'bcc_recipients', 'enabled')) do |ff| %>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<%= ff.label :subject, t('subject'), class: 'label' %>
|
<%= ff.label :subject, t('subject'), class: 'label' %>
|
||||||
<%= ff.text_field :subject, required: true, class: 'base-input', dir: 'auto' %>
|
<%= ff.text_field :subject, required: true, class: 'base-input', dir: 'auto' %>
|
||||||
@@ -24,23 +24,41 @@
|
|||||||
<%= ff.text_area :body, required: true, class: 'base-input w-full py-2', dir: 'auto' %>
|
<%= ff.text_area :body, required: true, class: 'base-input w-full py-2', dir: 'auto' %>
|
||||||
</autoresize-textarea>
|
</autoresize-textarea>
|
||||||
</div>
|
</div>
|
||||||
<% if can?(:manage, :reply_to) %>
|
<% if can?(:manage, :reply_to) || can?(:manage, :personalization_advanced) %>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<%= ff.label :reply_to, t('reply_to'), class: 'label' %>
|
<%= ff.label :reply_to, t('reply_to'), class: 'label' %>
|
||||||
<%= ff.email_field :reply_to, class: 'base-input', dir: 'auto', placeholder: t(:email) %>
|
<%= ff.email_field :reply_to, class: 'base-input', dir: 'auto', placeholder: t(:email) %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<div class="flex items-center justify-between pt-2.5 mx-1">
|
<div class="space-y-3.5">
|
||||||
<span>
|
<div class="flex items-center justify-between mx-1">
|
||||||
<%= t('attach_documents') %>
|
<span>
|
||||||
</span>
|
<%= t('attach_documents') %>
|
||||||
<%= ff.check_box :attach_documents, { checked: ff.object.attach_documents != false, class: 'toggle' }, 'true', 'false' %>
|
</span>
|
||||||
</div>
|
<%= ff.check_box :attach_documents, { checked: ff.object.attach_documents != false, class: 'toggle' }, 'true', 'false' %>
|
||||||
<div class="flex items-center justify-between pb-2.5 mx-1">
|
</div>
|
||||||
<span>
|
<div class="flex items-center justify-between mx-1">
|
||||||
<%= t('attach_audit_log_pdf') %>
|
<span>
|
||||||
</span>
|
<%= t('attach_audit_log_pdf') %>
|
||||||
<%= ff.check_box :attach_audit_log, { checked: ff.object.attach_audit_log != false, class: 'toggle' }, 'true', 'false' %>
|
</span>
|
||||||
|
<%= ff.check_box :attach_audit_log, { checked: ff.object.attach_audit_log != false, class: 'toggle' }, 'true', 'false' %>
|
||||||
|
</div>
|
||||||
|
<% unless Docuseal.multitenant? %>
|
||||||
|
<div class="flex items-center justify-between mx-1">
|
||||||
|
<span>
|
||||||
|
<%= t('bcc_recipients') %>
|
||||||
|
</span>
|
||||||
|
<%= ff.check_box :bcc_recipients, { checked: ff.object.bcc_recipients == true, class: 'toggle' }, 'true', 'false' %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% if !Docuseal.multitenant? || can?(:manage, :personalization_advanced) %>
|
||||||
|
<div class="flex items-center justify-between mx-1">
|
||||||
|
<span>
|
||||||
|
<%= t('send_emails_automatically_on_completion') %>
|
||||||
|
</span>
|
||||||
|
<%= ff.check_box :enabled, { checked: ff.object.enabled != false, class: 'toggle' }, 'true', 'false' %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<div class="form-control pt-2">
|
<div class="form-control pt-2">
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<%= f.label :last_name, t('last_name'), class: 'label' %>
|
<%= f.label :last_name, t('last_name'), class: 'label' %>
|
||||||
<%= f.text_field :last_name, required: true, class: 'base-input', dir: 'auto' %>
|
<%= f.text_field :last_name, required: false, class: 'base-input', dir: 'auto' %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<% if current_user.created_at > 2.weeks.ago || params[:tour] == 'true' %>
|
||||||
|
<% user_config = current_user.user_configs.find_by(key: UserConfig::SHOW_APP_TOUR) || UserConfig.new(key: UserConfig::SHOW_APP_TOUR, user: current_user) %>
|
||||||
|
<% if user_config.new_record? || user_config.value || params[:tour] == 'true' %>
|
||||||
|
<app-tour data-show-tour="<%= params[:tour] == 'true' || user_config.value %>" data-type="<%= local_assigns[:type] %>" data-next-page-path="<%= local_assigns[:next_page_path] %>" data-i18n="<%= t('app_tour').to_json %>"></app-tour>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
@@ -1 +1 @@
|
|||||||
<% 'stats stat stat-figure stat-title stat-value text-accent w-fit hover:bg-white' %>
|
<% 'stats stat stat-figure stat-title stat-value text-accent w-fit hover:bg-white dropdown-open' %>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<clipboard-copy data-text="<%= text %>">
|
<clipboard-copy data-text="<%= text %>">
|
||||||
<label class="<%= local_assigns[:class] %>">
|
<label id="<%= local_assigns[:id] %>" class="<%= local_assigns[:class] %>">
|
||||||
<input type="radio" class="peer hidden">
|
<input type="radio" class="peer hidden">
|
||||||
<span class="peer-checked:hidden flex items-center space-x-2">
|
<span class="peer-checked:hidden flex items-center space-x-2">
|
||||||
<%= svg_icon(local_assigns[:icon] || 'link', class: local_assigns[:icon_class] || 'w-6 h-6 text-white') %>
|
<%= svg_icon(local_assigns[:icon] || 'link', class: local_assigns[:icon_class] || 'w-6 h-6 text-white') %>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<% uuid = SecureRandom.uuid %>
|
<% uuid = local_assigns[:uuid] || SecureRandom.uuid %>
|
||||||
<label id="<%= local_assigns[:button_id] %>" for="<%= uuid %>" class="<%= local_assigns[:btn_class] %>"><%= local_assigns[:btn_text] %></label>
|
<label id="<%= local_assigns[:button_id] %>" for="<%= uuid %>" class="<%= local_assigns[:btn_class] %>"><%= local_assigns[:btn_text] %></label>
|
||||||
<input type="checkbox" id="<%= uuid %>" class="modal-toggle">
|
<input type="checkbox" id="<%= uuid %>" class="modal-toggle">
|
||||||
<div id="<%= local_assigns[:id] %>" class="modal items-start !animate-none overflow-y-auto">
|
<div id="<%= local_assigns[:id] %>" class="modal items-start !animate-none overflow-y-auto">
|
||||||
|
|||||||
@@ -13,10 +13,10 @@
|
|||||||
<meta property="og:image" content="">
|
<meta property="og:image" content="">
|
||||||
<meta name="twitter:image" content="">
|
<meta name="twitter:image" content="">
|
||||||
<% else %>
|
<% else %>
|
||||||
<meta property="og:image" content="<%= image_pack_url('images/preview.png') %>">
|
<meta property="og:image" content="<%= root_url %>preview.png">
|
||||||
<meta property="og:image:width" content="800">
|
<meta property="og:image:width" content="800">
|
||||||
<meta property="og:image:height" content="800">
|
<meta property="og:image:height" content="800">
|
||||||
<meta name="twitter:image" content="<%= image_pack_url('images/preview.png') %>">
|
<meta name="twitter:image" content="<%= root_url %>preview.png">
|
||||||
<% end %>
|
<% end %>
|
||||||
<meta name="twitter:card" content="summary">
|
<meta name="twitter:card" content="summary">
|
||||||
<meta name="twitter:creator" content="@docusealco">
|
<meta name="twitter:creator" content="@docusealco">
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
<% else %>
|
<% else %>
|
||||||
<div class="flex items-center justify-center space-x-4 mr-1">
|
<div class="flex items-center justify-center space-x-4 mr-1">
|
||||||
<%= render 'shared/navbar_buttons' %>
|
<%= render 'shared/navbar_buttons' %>
|
||||||
<%= link_to t('settings'), settings_profile_index_path, class: 'hidden md:inline-flex font-medium text-lg' %>
|
<%= link_to t('settings'), settings_profile_index_path, class: 'hidden md:inline-flex font-medium text-lg', id: 'account_settings_button' %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<div class="dropdown dropdown-end">
|
<div class="dropdown dropdown-end">
|
||||||
@@ -50,6 +50,14 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
</li>
|
</li>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
<% if Docuseal.multitenant? || current_user.role == 'superadmin' %>
|
||||||
|
<li>
|
||||||
|
<%= link_to Docuseal::CHATGPT_URL, target: 'blank', class: 'flex items-center' do %>
|
||||||
|
<%= svg_icon('sparkles', class: 'w-5 h-5 flex-shrink-0 stroke-2') %>
|
||||||
|
<span class="mr-1 whitespace-nowrap"><%= t('ask_ai') %></span>
|
||||||
|
<% end %>
|
||||||
|
</li>
|
||||||
|
<% end %>
|
||||||
<% if (can?(:manage, EncryptedConfig) && current_user == true_user) || (current_user != true_user && current_account.testing?) %>
|
<% if (can?(:manage, EncryptedConfig) && current_user == true_user) || (current_user != true_user && current_account.testing?) %>
|
||||||
<%= form_for '', url: testing_account_path, method: current_account.testing? ? :delete : :get, html: { class: 'w-full py-1' } do |f| %>
|
<%= form_for '', url: testing_account_path, method: current_account.testing? ? :delete : :get, html: { class: 'w-full py-1' } do |f| %>
|
||||||
<label class="flex items-center pl-6 pr-4 py-2 border-y border-base-300 -ml-2 -mr-2" for="testing_toggle">
|
<label class="flex items-center pl-6 pr-4 py-2 border-y border-base-300 -ml-2 -mr-2" for="testing_toggle">
|
||||||
|
|||||||
@@ -5,20 +5,23 @@
|
|||||||
<%= @pagy.from %>-<%= local_assigns.fetch(:to, @pagy.to) %> of <%= local_assigns.fetch(:count, @pagy.count) %> <%= local_assigns[:items_name] || 'items' %>
|
<%= @pagy.from %>-<%= local_assigns.fetch(:to, @pagy.to) %> of <%= local_assigns.fetch(:count, @pagy.count) %> <%= local_assigns[:items_name] || 'items' %>
|
||||||
<%= local_assigns[:left_additional_html] %>
|
<%= local_assigns[:left_additional_html] %>
|
||||||
</div>
|
</div>
|
||||||
<div class="join">
|
<div class="flex items-center space-x-1.5">
|
||||||
<% if @pagy.prev %>
|
<%= local_assigns[:right_additional_html] %>
|
||||||
<%== link.call(@pagy.prev, '«', classes: 'join-item btn min-h-full h-10') %>
|
<div class="join">
|
||||||
<% else %>
|
<% if @pagy.prev %>
|
||||||
<span class="join-item btn btn-disabled !bg-base-200 min-h-full h-10">«</span>
|
<%== link.call(@pagy.prev, '«', classes: 'join-item btn min-h-full h-10') %>
|
||||||
<% end %>
|
<% else %>
|
||||||
<span class="join-item btn uppercase min-h-full h-10">
|
<span class="join-item btn btn-disabled !bg-base-200 min-h-full h-10">«</span>
|
||||||
<%= t('page_number', number: @pagy.page) %>
|
<% end %>
|
||||||
</span>
|
<span class="join-item btn font-medium uppercase min-h-full h-10">
|
||||||
<% if @pagy.next %>
|
<%= t('page_number', number: @pagy.page) %>
|
||||||
<%== link.call(@pagy.next, '»', classes: 'join-item btn min-h-full h-10') %>
|
</span>
|
||||||
<% else %>
|
<% if @pagy.next %>
|
||||||
<span class="join-item btn btn-disabled !bg-base-200 min-h-full h-10">»</span>
|
<%== link.call(@pagy.next, '»', classes: 'join-item btn min-h-full h-10') %>
|
||||||
<% end %>
|
<% else %>
|
||||||
|
<span class="join-item btn btn-disabled !bg-base-200 min-h-full h-10">»</span>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<div class="block w-full md:w-52 flex-none">
|
<div class="block w-full md:w-52 flex-none">
|
||||||
<menu-active>
|
<menu-active>
|
||||||
<ul class="menu px-0">
|
<ul id="account_settings_menu" class="menu px-0">
|
||||||
<li class="menu-title py-0 !bg-transparent mb-3 -mt-5"><a href="<%= '/' %>" class="!bg-transparent !text-neutral font-medium">← <%= t('back') %></a></li>
|
<li class="menu-title py-0 !bg-transparent mb-3 -mt-5"><a href="<%= '/' %>" class="!bg-transparent !text-neutral font-medium">← <%= t('back') %></a></li>
|
||||||
<li class="menu-title py-0 !bg-transparent">
|
<li class="menu-title py-0 !bg-transparent">
|
||||||
<span class="!bg-transparent"><%= t('settings') %></span>
|
<span class="!bg-transparent"><%= t('settings') %></span>
|
||||||
@@ -105,7 +105,7 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</menu-active>
|
</menu-active>
|
||||||
<% if Docuseal.multitenant? || cannot?(:manage, :tenants) %>
|
<% if Docuseal.multitenant? || cannot?(:manage, :tenants) %>
|
||||||
<div class="mx-4 border-t border-base-300 hidden md:block">
|
<div id="support_channels" class="mx-4 border-t border-base-300 hidden md:block">
|
||||||
<div class="text-sm mt-3">
|
<div class="text-sm mt-3">
|
||||||
<%= t('need_help_ask_a_question_') %>
|
<%= t('need_help_ask_a_question_') %>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<% dashboard_templates_order = cookies.permanent[:dashboard_templates_order] || 'created_at' %>
|
||||||
|
<form action="<%= url_for %>" method="get" class="dropdown dropdown-top hidden md:inline">
|
||||||
|
<label tabindex="0" class="btn btn-sm h-10">
|
||||||
|
<%= svg_icon('arrow_sort', class: 'w-5 h-5') %>
|
||||||
|
</label>
|
||||||
|
<ul tabindex="0" class="dropdown-content z-[10] menu p-2 shadow bg-base-100 rounded-box mb-1 min-w-48">
|
||||||
|
<toggle-cookies data-value="created_at" data-key="dashboard_templates_order">
|
||||||
|
<li>
|
||||||
|
<button class="<%= 'bg-base-200' if dashboard_templates_order == 'created_at' %>">
|
||||||
|
<%= svg_icon('sort_descending_numbers', class: 'w-4 h-4') %>
|
||||||
|
<span class="whitespace-nowrap"><%= t('newest_first') %></span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</toggle-cookies>
|
||||||
|
<% if local_assigns[:with_recently_used] != false %>
|
||||||
|
<toggle-cookies data-value="used_at" data-key="dashboard_templates_order">
|
||||||
|
<li>
|
||||||
|
<button class="<%= 'bg-base-200' if dashboard_templates_order == 'used_at' %>">
|
||||||
|
<%= svg_icon('sort_descending_small_big', class: 'w-4 h-4') %>
|
||||||
|
<span class="whitespace-nowrap"><%= t('recently_used') %></span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</toggle-cookies>
|
||||||
|
<% end %>
|
||||||
|
<toggle-cookies data-value="name" data-key="dashboard_templates_order">
|
||||||
|
<li>
|
||||||
|
<button class="<%= 'bg-base-200' if dashboard_templates_order == 'name' %>">
|
||||||
|
<%= svg_icon('sort_ascending_letters', class: 'w-4 h-4') %>
|
||||||
|
<span class="whitespace-nowrap"><%= t('name_a_z') %></span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</toggle-cookies>
|
||||||
|
</ul>
|
||||||
|
</form>
|
||||||
@@ -38,6 +38,8 @@
|
|||||||
<%= t('submission_event_names.invite_party_by_html', invited_submitter_name: [invited_submitter.name || invited_submitter.email || invited_submitter.phone, name].join(' '), submitter_name:) %>
|
<%= t('submission_event_names.invite_party_by_html', invited_submitter_name: [invited_submitter.name || invited_submitter.email || invited_submitter.phone, name].join(' '), submitter_name:) %>
|
||||||
<% elsif event.event_type.include?('send_') %>
|
<% elsif event.event_type.include?('send_') %>
|
||||||
<%= t("submission_event_names.#{event.event_type}_to_html", submitter_name:) %>
|
<%= t("submission_event_names.#{event.event_type}_to_html", submitter_name:) %>
|
||||||
|
<% elsif event.event_type.start_with?('bounce_') || event.event_type.start_with?('complaint_') %>
|
||||||
|
<%= t("submission_event_names.#{event.event_type}_html", submitter_name:) %>
|
||||||
<% else %>
|
<% else %>
|
||||||
<%= t("submission_event_names.#{event.event_type}_by_html", submitter_name:) %>
|
<%= t("submission_event_names.#{event.event_type}_by_html", submitter_name:) %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
</submitters-autocomplete>
|
</submitters-autocomplete>
|
||||||
<submitters-autocomplete data-field="phone">
|
<submitters-autocomplete data-field="phone">
|
||||||
<linked-input data-target-id="<%= "detailed_phone_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
|
<linked-input data-target-id="<%= "detailed_phone_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
|
||||||
<input type="tel" pattern="^\+[0-9\s\-]+$" oninvalid="this.value ? this.setCustomValidity('<%= t('use_international_format_1xxx_') %>') : ''" oninput="this.setCustomValidity('')" name="submission[1][submitters][][phone]" autocomplete="off" class="base-input !h-10 mt-1.5 w-full" placeholder="<%= "#{t('phone')} (#{t('optional')})" %>" id="detailed_phone_<%= item['uuid'] %>">
|
<%= tag.input type: 'tel', pattern: '^\+[0-9\s\-]+$', oninvalid: "this.value ? this.setCustomValidity('#{t('use_international_format_1xxx_')}') : ''", oninput: "this.setCustomValidity('')", name: 'submission[1][submitters][][phone]', autocomplete: 'off', class: 'base-input !h-10 mt-1.5 w-full', placeholder: local_assigns[:require_phone_2fa] == true ? t(:phone) : "#{t('phone')} (#{t('optional')})", id: "detailed_phone_#{item['uuid']}", required: local_assigns[:require_phone_2fa] == true %>
|
||||||
</linked-input>
|
</linked-input>
|
||||||
</submitters-autocomplete>
|
</submitters-autocomplete>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<div id="submitters_error">
|
||||||
|
<% if local_assigns[:error] %>
|
||||||
|
<div class="text-center text-red-500">
|
||||||
|
<%= local_assigns[:error] %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
<% align = field.dig('preferences', 'align') %>
|
<% align = field.dig('preferences', 'align') %>
|
||||||
|
<% valign = field.dig('preferences', 'valign') %>
|
||||||
<% color = field.dig('preferences', 'color') %>
|
<% color = field.dig('preferences', 'color') %>
|
||||||
<% font = field.dig('preferences', 'font') %>
|
<% font = field.dig('preferences', 'font') %>
|
||||||
<% font_type = field.dig('preferences', 'font_type') %>
|
<% font_type = field.dig('preferences', 'font_type') %>
|
||||||
<field-value dir="auto" class="flex absolute text-[1.6vw] lg:text-base <%= 'font-mono' if font == 'Courier' %> <%= 'font-serif' if font == 'Times' %> <%= 'font-bold' if font_type == 'bold' || font_type == 'bold_italic' %> <%= 'italic' if font_type == 'italic' || font_type == 'bold_italic' %> <%= align == 'right' ? 'justify-end' : (align == 'center' ? 'justify-center' : '') %>" style="<%= "color: #{color}; " if color.present? %>width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%; <%= "font-size: clamp(4pt, 1.6vw, #{field['preferences']['font_size'].to_i * 1.23}pt); line-height: `clamp(6pt, 2.0vw, #{(field['preferences']['font_size'].to_i * 1.23) + 3}pt)`" if field.dig('preferences', 'font_size') %>">
|
<field-value dir="auto" class="flex absolute text-[1.6vw] lg:text-base <%= 'font-mono' if font == 'Courier' %> <%= 'font-serif' if font == 'Times' %> <%= 'font-bold' if font_type == 'bold' || font_type == 'bold_italic' %> <%= 'italic' if font_type == 'italic' || font_type == 'bold_italic' %> <%= align == 'right' ? 'text-right' : (align == 'center' ? 'text-center' : '') %>" style="<%= "color: #{color}; " if color.present? %>width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%; <%= "font-size: clamp(4pt, 1.6vw, #{field['preferences']['font_size'].to_i * 1.23}pt); line-height: `clamp(6pt, 2.0vw, #{(field['preferences']['font_size'].to_i * 1.23) + 3}pt)`" if field.dig('preferences', 'font_size') %>">
|
||||||
<% if field['type'] == 'signature' %>
|
<% if field['type'] == 'signature' %>
|
||||||
<div class="flex flex-col justify-between h-full overflow-hidden">
|
<div class="flex flex-col justify-between h-full overflow-hidden">
|
||||||
<div class="flex-grow flex overflow-hidden" style="min-height: 50%">
|
<div class="flex-grow flex overflow-hidden" style="min-height: 50%">
|
||||||
@@ -61,17 +62,19 @@
|
|||||||
</div>
|
</div>
|
||||||
<% elsif field['type'] == 'date' %>
|
<% elsif field['type'] == 'date' %>
|
||||||
<autosize-field></autosize-field>
|
<autosize-field></autosize-field>
|
||||||
<div class="flex items-center px-0.5">
|
<div class="flex w-full px-0.5 <%= valign == 'top' ? 'items-start' : (valign == 'bottom' ? 'items-end' : 'items-center') %>">
|
||||||
<% value = Time.current.in_time_zone(local_assigns[:timezone]).to_date.to_s if value == '{{date}}' %>
|
<% value = Time.current.in_time_zone(local_assigns[:timezone]).to_date.to_s if value == '{{date}}' %>
|
||||||
<%= TimeUtils.format_date_string(value, field.dig('preferences', 'format'), local_assigns[:locale]) %>
|
<div class="w-full"><%= TimeUtils.format_date_string(value, field.dig('preferences', 'format'), local_assigns[:locale]) %></div>
|
||||||
</div>
|
</div>
|
||||||
<% elsif field['type'] == 'number' %>
|
<% elsif field['type'] == 'number' %>
|
||||||
<autosize-field></autosize-field>
|
<autosize-field></autosize-field>
|
||||||
<div class="flex items-center px-0.5 whitespace-nowrap">
|
<div class="flex w-full px-0.5 whitespace-nowrap <%= valign == 'top' ? 'items-start' : (valign == 'bottom' ? 'items-end' : 'items-center') %>">
|
||||||
<%= NumberUtils.format_number(value, field.dig('preferences', 'format')) %>
|
<div class="w-full"><%= NumberUtils.format_number(value, field.dig('preferences', 'format')) %></div>
|
||||||
</div>
|
</div>
|
||||||
<% else %>
|
<% else %>
|
||||||
<autosize-field></autosize-field>
|
<autosize-field></autosize-field>
|
||||||
<div class="flex items-center px-0.5 whitespace-pre-wrap"><%= Array.wrap(value).join(', ') %></div>
|
<div class="flex w-full px-0.5 whitespace-pre-wrap <%= valign == 'top' ? 'items-start' : (valign == 'bottom' ? 'items-end' : 'items-center') %>">
|
||||||
|
<div class="w-full"><%= Array.wrap(value).join(', ') %></div>
|
||||||
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</field-value>
|
</field-value>
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
|
<% require_phone_2fa = @template.preferences['require_phone_2fa'] == true %>
|
||||||
<%= 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 %>
|
||||||
<% options = [[t('via_email'), 'email'], [t('via_phone'), 'phone'], [t('detailed'), 'detailed'], [t('upload_list'), 'list']].compact %>
|
<% options = [require_phone_2fa ? nil : [t('via_email'), 'email'], require_phone_2fa ? nil : [t('via_phone'), 'phone'], [t('detailed'), 'detailed'], [t('upload_list'), 'list']].compact %>
|
||||||
<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>
|
<div>
|
||||||
<%= radio_button_tag 'option', value, value == 'email', class: 'peer hidden', data: { action: 'change:toggle-visible#trigger' } %>
|
<%= radio_button_tag 'option', value, value == (require_phone_2fa ? 'detailed' : 'email'), 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>
|
||||||
@@ -13,18 +14,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</toggle-visible>
|
</toggle-visible>
|
||||||
<div class="px-5 mb-5 mt-4">
|
<div class="px-5 mb-5 mt-4">
|
||||||
<div id="email">
|
<% unless require_phone_2fa %>
|
||||||
<%= render 'email_form', template: @template %>
|
<div id="email">
|
||||||
</div>
|
<%= render 'email_form', template: @template %>
|
||||||
<div id="phone" class="hidden">
|
</div>
|
||||||
<%= render 'phone_form', template: @template %>
|
<div id="phone" class="hidden">
|
||||||
</div>
|
<%= render 'phone_form', template: @template %>
|
||||||
<div id="detailed" class="hidden">
|
</div>
|
||||||
<%= render 'detailed_form', template: @template %>
|
<% end %>
|
||||||
|
<div id="detailed" class="<%= 'hidden' unless require_phone_2fa %>">
|
||||||
|
<%= render 'detailed_form', template: @template, require_phone_2fa: %>
|
||||||
</div>
|
</div>
|
||||||
<div id="list" class="hidden">
|
<div id="list" class="hidden">
|
||||||
<%= render 'list_form', template: @template %>
|
<%= render 'list_form', template: @template %>
|
||||||
</div>
|
</div>
|
||||||
|
<%= render 'submissions/error' %>
|
||||||
</div>
|
</div>
|
||||||
<%= content_for(:modal_extra) %>
|
<%= content_for(:modal_extra) %>
|
||||||
<% end %>
|
<% end %>
|
||||||
@@ -172,12 +172,26 @@
|
|||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<div class="flex items-center space-x-1 mt-1">
|
<div class="flex items-center space-x-1 mt-1">
|
||||||
<%= svg_icon('writing', class: 'w-5 h-5') %>
|
<% if @submission.expire_at? && submitter && !submitter.completed_at? %>
|
||||||
|
<%= svg_icon('clock_exclamation', class: 'w-5 h-5') %>
|
||||||
|
<% else %>
|
||||||
|
<%= svg_icon('writing', class: 'w-5 h-5') %>
|
||||||
|
<% end %>
|
||||||
<span>
|
<span>
|
||||||
<% if submitter&.declined_at? %>
|
<% if submitter&.declined_at? %>
|
||||||
<%= t('declined_on_time', time: l(submitter.declined_at.in_time_zone(@submission.account.timezone), format: :short, locale: @submission.account.locale)) %>
|
<%= t('declined_on_time', time: l(submitter.declined_at.in_time_zone(@submission.account.timezone), format: :short, locale: @submission.account.locale)) %>
|
||||||
<% elsif submitter %>
|
<% elsif submitter %>
|
||||||
<%= submitter.completed_at? ? l(submitter.completed_at.in_time_zone(@submission.account.timezone), format: :long, locale: @submission.account.locale) : t('not_completed_yet') %>
|
<% if submitter.completed_at? %>
|
||||||
|
<%= l(submitter.completed_at.in_time_zone(@submission.account.timezone), format: :long, locale: @submission.account.locale) %>
|
||||||
|
<% elsif @submission.expire_at? %>
|
||||||
|
<% if @submission.expired? %>
|
||||||
|
<%= t(:expired) %>
|
||||||
|
<% else %>
|
||||||
|
<%= t('expire_on_time', time: l(@submission.expire_at.in_time_zone(@submission.account.timezone), format: :short, locale: @submission.account.locale)) %>
|
||||||
|
<% end %>
|
||||||
|
<% else %>
|
||||||
|
<%= t('not_completed_yet') %>
|
||||||
|
<% end %>
|
||||||
<% else %>
|
<% else %>
|
||||||
<%= t('not_invited_yet') %>
|
<%= t('not_invited_yet') %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -57,6 +57,7 @@
|
|||||||
<div class="flex items-end flex-col md:flex-row gap-2 w-full md:w-fit">
|
<div class="flex items-end flex-col md:flex-row gap-2 w-full md:w-fit">
|
||||||
<%= render 'submissions_filters/applied_filters', filter_params: %>
|
<%= render 'submissions_filters/applied_filters', filter_params: %>
|
||||||
<%= render 'submissions_filters/filter_button', filter_params: %>
|
<%= render 'submissions_filters/filter_button', filter_params: %>
|
||||||
|
<%= render 'submissions_filters/actions', filter_params: %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||