mirror of
https://github.com/docusealco/docuseal
synced 2026-11-04 14:42:32 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ce6c29f4d | ||
|
|
ae50b2e323 | ||
|
|
152cbc11a6 | ||
|
|
3a406de655 | ||
|
|
2e31cda08c | ||
|
|
094b7f47bd | ||
|
|
99ac349ecc | ||
|
|
8830e03cf3 | ||
|
|
706f3d6d65 | ||
|
|
12b49f5fa7 | ||
|
|
85327760b9 | ||
|
|
c94ed9c3d4 | ||
|
|
bc50735d2a | ||
|
|
d05e125fa9 | ||
|
|
48d595ca81 | ||
|
|
a156de5e75 | ||
|
|
a7003bf4d0 | ||
|
|
5fe75c84ff | ||
|
|
4549a04fc5 | ||
|
|
0ea6b912a3 | ||
|
|
9b86fa3d40 | ||
|
|
374f66c59f | ||
|
|
da43d881f1 | ||
|
|
9cbfa3a0ba | ||
|
|
77a925f33b | ||
|
|
3f7d7beedd | ||
|
|
40d91df732 | ||
|
|
c8e783a210 | ||
|
|
61fc24d35d | ||
|
|
73734b0de7 | ||
|
|
09bd831d54 | ||
|
|
2c9f1b999e | ||
|
|
6a3a185310 | ||
|
|
7d381b7aee | ||
|
|
bba5626dea | ||
|
|
bdf5e58929 | ||
|
|
035168230e | ||
|
|
28168f082d | ||
|
|
a29e09ed2f | ||
|
|
e88874e757 | ||
|
|
b6fb7c6977 | ||
|
|
8a60f17d13 | ||
|
|
e1d860a42c | ||
|
|
56da326709 | ||
|
|
672008f8af | ||
|
|
d86b16de6b | ||
|
|
d41710f507 | ||
|
|
2b53166763 | ||
|
|
4e56a16a58 | ||
|
|
ac7cc77017 | ||
|
|
9194879c43 | ||
|
|
1902cdaa55 | ||
|
|
6314987e69 | ||
|
|
65bcbd7737 | ||
|
|
9b25b8538e | ||
|
|
bbf8bb2a94 | ||
|
|
2085227cd9 | ||
|
|
93e0730210 | ||
|
|
25c5c11ab3 | ||
|
|
6ed5b5d35d | ||
|
|
673cc1e0df | ||
|
|
065dcf6f9f | ||
|
|
a66b16ce1c | ||
|
|
e4c0b30284 | ||
|
|
3cb37748e9 | ||
|
|
e54d76f0e2 | ||
|
|
f19e037011 | ||
|
|
ee3f47d918 | ||
|
|
a49a645022 | ||
|
|
cf30441ac6 | ||
|
|
c9bcf866ab | ||
|
|
506609f3cd | ||
|
|
721172c980 | ||
|
|
b183db5b80 | ||
|
|
c7f277f404 | ||
|
|
8ac03a9f8a | ||
|
|
dc1c386d7e | ||
|
|
1eaf4e5bab | ||
|
|
3c0f1c2e73 | ||
|
|
b2790776ef | ||
|
|
2840359396 | ||
|
|
c18ddb99a8 | ||
|
|
432435aa23 | ||
|
|
90c1a6f4ad | ||
|
|
0e8f478c2e | ||
|
|
5f23d5a5ab | ||
|
|
76821a4c2d | ||
|
|
e4a8cca90a | ||
|
|
6024ed4cd1 | ||
|
|
3811f8bbb8 | ||
|
|
306218e5df |
No files matched your search
@@ -30,7 +30,9 @@ jobs:
|
|||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Create .version file
|
- name: Create .version file
|
||||||
run: echo ${{ github.ref_name }} > .version
|
env:
|
||||||
|
REF_NAME: ${{ github.ref_name }}
|
||||||
|
run: echo "$REF_NAME" > .version
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
|
|||||||
+12
-12
@@ -144,12 +144,12 @@ GEM
|
|||||||
cldr-plurals-runtime-rb (1.1.0)
|
cldr-plurals-runtime-rb (1.1.0)
|
||||||
cmdparse (3.0.7)
|
cmdparse (3.0.7)
|
||||||
coderay (1.1.3)
|
coderay (1.1.3)
|
||||||
concurrent-ruby (1.3.6)
|
concurrent-ruby (1.3.7)
|
||||||
connection_pool (3.0.2)
|
connection_pool (3.0.2)
|
||||||
crack (1.0.1)
|
crack (1.0.1)
|
||||||
bigdecimal
|
bigdecimal
|
||||||
rexml
|
rexml
|
||||||
crass (1.0.6)
|
crass (1.0.7)
|
||||||
csv (3.3.5)
|
csv (3.3.5)
|
||||||
csv-safe (3.3.1)
|
csv-safe (3.3.1)
|
||||||
csv (~> 3.0)
|
csv (~> 3.0)
|
||||||
@@ -195,13 +195,13 @@ GEM
|
|||||||
railties (>= 6.1.0)
|
railties (>= 6.1.0)
|
||||||
faker (3.6.1)
|
faker (3.6.1)
|
||||||
i18n (>= 1.8.11, < 2)
|
i18n (>= 1.8.11, < 2)
|
||||||
faraday (2.14.2)
|
faraday (2.14.3)
|
||||||
faraday-net_http (>= 2.0, < 3.5)
|
faraday-net_http (>= 2.0, < 3.5)
|
||||||
json
|
json
|
||||||
logger
|
logger
|
||||||
faraday-follow_redirects (0.5.0)
|
faraday-follow_redirects (0.5.0)
|
||||||
faraday (>= 1, < 3)
|
faraday (>= 1, < 3)
|
||||||
faraday-net_http (3.4.2)
|
faraday-net_http (3.4.4)
|
||||||
net-http (~> 0.5)
|
net-http (~> 0.5)
|
||||||
ferrum (0.17.2)
|
ferrum (0.17.2)
|
||||||
addressable (~> 2.5)
|
addressable (~> 2.5)
|
||||||
@@ -293,7 +293,7 @@ GEM
|
|||||||
activesupport (>= 4)
|
activesupport (>= 4)
|
||||||
railties (>= 4)
|
railties (>= 4)
|
||||||
request_store (~> 1.0)
|
request_store (~> 1.0)
|
||||||
loofah (2.25.1)
|
loofah (2.25.2)
|
||||||
crass (~> 1.0.2)
|
crass (~> 1.0.2)
|
||||||
nokogiri (>= 1.12.0)
|
nokogiri (>= 1.12.0)
|
||||||
mail (2.9.0)
|
mail (2.9.0)
|
||||||
@@ -323,15 +323,15 @@ GEM
|
|||||||
net-smtp (0.5.1)
|
net-smtp (0.5.1)
|
||||||
net-protocol
|
net-protocol
|
||||||
nio4r (2.7.5)
|
nio4r (2.7.5)
|
||||||
nokogiri (1.19.3-aarch64-linux-gnu)
|
nokogiri (1.19.4-aarch64-linux-gnu)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.19.3-aarch64-linux-musl)
|
nokogiri (1.19.4-aarch64-linux-musl)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.19.3-arm64-darwin)
|
nokogiri (1.19.4-arm64-darwin)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.19.3-x86_64-linux-gnu)
|
nokogiri (1.19.4-x86_64-linux-gnu)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.19.3-x86_64-linux-musl)
|
nokogiri (1.19.4-x86_64-linux-musl)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
numo-narray-alt (0.10.3)
|
numo-narray-alt (0.10.3)
|
||||||
onnxruntime (0.10.1-aarch64-linux)
|
onnxruntime (0.10.1-aarch64-linux)
|
||||||
@@ -404,8 +404,8 @@ GEM
|
|||||||
activesupport (>= 5.0.0)
|
activesupport (>= 5.0.0)
|
||||||
minitest
|
minitest
|
||||||
nokogiri (>= 1.6)
|
nokogiri (>= 1.6)
|
||||||
rails-html-sanitizer (1.7.0)
|
rails-html-sanitizer (1.7.1)
|
||||||
loofah (~> 2.25)
|
loofah (~> 2.25, >= 2.25.2)
|
||||||
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
|
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
|
||||||
rails-i18n (8.1.0)
|
rails-i18n (8.1.0)
|
||||||
i18n (>= 0.7, < 2)
|
i18n (>= 0.7, < 2)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ module Api
|
|||||||
(@submission.schema_documents || @submission.template.schema_documents).size > 1
|
(@submission.schema_documents || @submission.template.schema_documents).size > 1
|
||||||
|
|
||||||
documents =
|
documents =
|
||||||
if @submission.submitters.all?(&:completed_at?)
|
if @submission.completed_at?
|
||||||
build_completed_documents(@submission, merge: is_merge)
|
build_completed_documents(@submission, merge: is_merge)
|
||||||
else
|
else
|
||||||
build_preview_documents(@submission, merge: is_merge)
|
build_preview_documents(@submission, merge: is_merge)
|
||||||
@@ -30,7 +30,7 @@ module Api
|
|||||||
private
|
private
|
||||||
|
|
||||||
def build_completed_documents(submission, merge: false)
|
def build_completed_documents(submission, merge: false)
|
||||||
last_submitter = submission.submitters.max_by(&:completed_at)
|
last_submitter = submission.submitters.select(&:completed_at?).max_by(&:completed_at)
|
||||||
|
|
||||||
if merge
|
if merge
|
||||||
if submission.merged_document_attachment.blank?
|
if submission.merged_document_attachment.blank?
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ module Api
|
|||||||
load_and_authorize_resource :submission, parent: false
|
load_and_authorize_resource :submission, parent: false
|
||||||
|
|
||||||
def index
|
def index
|
||||||
submissions = build_completed_query(@submissions)
|
submissions = @submissions.active.where.not(completed_at: nil)
|
||||||
|
|
||||||
params[:after] = Time.zone.at(params[:after].to_i) if params[:after].present?
|
params[:after] = Time.zone.at(params[:after].to_i) if params[:after].present?
|
||||||
params[:before] = Time.zone.at(params[:before].to_i) if params[:before].present?
|
params[:before] = Time.zone.at(params[:before].to_i) if params[:before].present?
|
||||||
@@ -36,20 +36,5 @@ module Api
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def build_completed_query(submissions)
|
|
||||||
submissions = submissions.where(
|
|
||||||
Submitter.where(completed_at: nil).where(
|
|
||||||
Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id])
|
|
||||||
).select(1).arel.exists.not
|
|
||||||
)
|
|
||||||
|
|
||||||
submissions.joins(:submitters)
|
|
||||||
.group(:id)
|
|
||||||
.select(Submission.arel_table[Arel.star],
|
|
||||||
Submitter.arel_table[:completed_at].maximum.as('completed_at'))
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -2,21 +2,39 @@
|
|||||||
|
|
||||||
module Api
|
module Api
|
||||||
class SubmissionsController < ApiBaseController
|
class SubmissionsController < ApiBaseController
|
||||||
|
SUBMISSION_COLUMNS = %i[id name slug source submitters_order expire_at completed_at created_at updated_at
|
||||||
|
archived_at variables template_id template_submitters created_by_user_id].freeze
|
||||||
|
TEMPLATE_COLUMNS = %i[id name external_id created_at updated_at folder_id submitters].freeze
|
||||||
|
|
||||||
load_and_authorize_resource :template, only: :create
|
load_and_authorize_resource :template, only: :create
|
||||||
load_and_authorize_resource :submission, only: %i[show index destroy]
|
load_and_authorize_resource :submission, only: %i[show index update destroy]
|
||||||
|
|
||||||
before_action only: :create do
|
before_action only: :create do
|
||||||
authorize!(:create, Submission)
|
authorize!(:create, Submission)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
before_action :maybe_return_template_error, only: :create
|
||||||
|
|
||||||
def index
|
def index
|
||||||
submissions = Submissions.search(current_user, @submissions, params[:q])
|
submissions = Submissions.search(current_user, @submissions, params[:q])
|
||||||
submissions = filter_submissions(submissions, params)
|
submissions = filter_submissions(submissions, params)
|
||||||
|
|
||||||
submissions = paginate(submissions.preload(:created_by_user, :submitters,
|
with_fields = params[:include].to_s.include?('fields') || params[:include].to_s.include?('combined_document_url')
|
||||||
template: { folder: :parent_folder },
|
|
||||||
combined_document_attachment: :blob,
|
submissions = paginate(
|
||||||
audit_trail_attachment: :blob))
|
submissions.select(with_fields ? nil : SUBMISSION_COLUMNS)
|
||||||
|
.preload(:created_by_user, :submitters, combined_document_attachment: :blob,
|
||||||
|
audit_trail_attachment: :blob)
|
||||||
|
)
|
||||||
|
|
||||||
|
ActiveRecord::Associations::Preloader.new(
|
||||||
|
records: submissions,
|
||||||
|
associations: :template,
|
||||||
|
scope: with_fields ? nil : Template.select(TEMPLATE_COLUMNS)
|
||||||
|
).call
|
||||||
|
|
||||||
|
ActiveRecord::Associations::Preloader.new(records: submissions.filter_map(&:template),
|
||||||
|
associations: { folder: :parent_folder }).call
|
||||||
|
|
||||||
expires_at = Accounts.link_expires_at(current_account)
|
expires_at = Accounts.link_expires_at(current_account)
|
||||||
|
|
||||||
@@ -42,7 +60,7 @@ module Api
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if @submission.audit_trail_attachment.blank? && submitters.all?(&:completed_at?)
|
if @submission.audit_trail_attachment.blank? && @submission.completed_at?
|
||||||
@submission.audit_trail_attachment = Submissions::EnsureAuditGenerated.call(@submission)
|
@submission.audit_trail_attachment = Submissions::EnsureAuditGenerated.call(@submission)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -52,20 +70,6 @@ module Api
|
|||||||
def create
|
def create
|
||||||
Params::SubmissionCreateValidator.call(params)
|
Params::SubmissionCreateValidator.call(params)
|
||||||
|
|
||||||
return render json: { error: 'Template not found' }, status: :unprocessable_content if @template.nil?
|
|
||||||
|
|
||||||
if @template.archived_at?
|
|
||||||
Rollbar.warning("Archived template submission: #{@template.id}") if defined?(Rollbar)
|
|
||||||
|
|
||||||
return render json: { error: 'Template has been archived' }, status: :unprocessable_content
|
|
||||||
end
|
|
||||||
|
|
||||||
if @template.fields.blank?
|
|
||||||
Rollbar.warning("Template does not contain fields: #{@template.id}") if defined?(Rollbar)
|
|
||||||
|
|
||||||
return render json: { error: 'Template does not contain fields' }, status: :unprocessable_content
|
|
||||||
end
|
|
||||||
|
|
||||||
params[:send_email] = true unless params.key?(:send_email)
|
params[:send_email] = true unless params.key?(:send_email)
|
||||||
params[:send_sms] = false unless params.key?(:send_sms)
|
params[:send_sms] = false unless params.key?(:send_sms)
|
||||||
|
|
||||||
@@ -76,10 +80,17 @@ module Api
|
|||||||
Submissions.send_signature_requests(submissions)
|
Submissions.send_signature_requests(submissions)
|
||||||
|
|
||||||
submissions.each do |submission|
|
submissions.each do |submission|
|
||||||
|
if submission.submitters.all? { |s| s.viewer? || s.completed_at? } &&
|
||||||
|
Submissions.maybe_update_completed_at(submission)
|
||||||
|
last_submitter = submission.submitters.reject(&:viewer?).max_by(&:completed_at)
|
||||||
|
end
|
||||||
|
|
||||||
submission.submitters.each do |submitter|
|
submission.submitters.each do |submitter|
|
||||||
next unless submitter.completed_at?
|
next unless submitter.completed_at?
|
||||||
|
|
||||||
ProcessSubmitterCompletionJob.perform_async('submitter_id' => submitter.id, 'send_invitation_email' => false)
|
ProcessSubmitterCompletionJob.perform_async('submitter_id' => submitter.id,
|
||||||
|
'is_last' => submitter == last_submitter,
|
||||||
|
'send_invitation_email' => false)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -93,6 +104,25 @@ module Api
|
|||||||
render json: { error: e.message }, status: :unprocessable_content
|
render json: { error: e.message }, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
@submission = assign_submission_attrs(@submission, submission_params)
|
||||||
|
|
||||||
|
@submission.save!
|
||||||
|
|
||||||
|
if @submission.saved_change_to_archived_at? && @submission.archived_at?
|
||||||
|
WebhookUrls.enqueue_events(@submission, 'submission.archived')
|
||||||
|
end
|
||||||
|
|
||||||
|
if @submission.saved_change_to_expire_at? && @submission.expire_at?
|
||||||
|
ProcessSubmissionExpiredJob.perform_at(@submission.expire_at, 'submission_id' => @submission.id,
|
||||||
|
'expire_at' => @submission.expire_at.to_i)
|
||||||
|
end
|
||||||
|
|
||||||
|
SearchEntries.enqueue_reindex(@submission) if @submission.saved_change_to_name?
|
||||||
|
|
||||||
|
render json: Submissions::SerializeForApi.call(@submission, nil, params, with_events: false)
|
||||||
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
if params[:permanently].in?(['true', true])
|
if params[:permanently].in?(['true', true])
|
||||||
@submission.destroy!
|
@submission.destroy!
|
||||||
@@ -107,6 +137,41 @@ module Api
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def assign_submission_attrs(submission, attrs)
|
||||||
|
archived = attrs.key?(:archived) ? attrs[:archived] : attrs[:archived_at]
|
||||||
|
|
||||||
|
if archived.in?([true, false, 'true', 'false']) && current_ability.can?(:destroy, submission)
|
||||||
|
submission.archived_at = archived.in?(Submitters::TRUE_VALUES) ? Time.current : nil
|
||||||
|
end
|
||||||
|
|
||||||
|
submission.name = attrs[:name] if attrs.key?(:name)
|
||||||
|
submission.expire_at = attrs[:expire_at].presence if attrs.key?(:expire_at)
|
||||||
|
|
||||||
|
submission
|
||||||
|
end
|
||||||
|
|
||||||
|
def submission_params
|
||||||
|
submission_params = params.key?(:submission) ? params.require(:submission) : params
|
||||||
|
|
||||||
|
submission_params.permit(:name, :expire_at, :archived, :archived_at)
|
||||||
|
end
|
||||||
|
|
||||||
|
def maybe_return_template_error
|
||||||
|
return render json: { error: 'Template not found' }, status: :unprocessable_content if @template.nil?
|
||||||
|
|
||||||
|
if @template.archived_at?
|
||||||
|
Rollbar.warning("Archived template submission: #{@template.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
|
return render json: { error: 'Template has been archived' }, status: :unprocessable_content
|
||||||
|
end
|
||||||
|
|
||||||
|
return if @template.fields.present?
|
||||||
|
|
||||||
|
Rollbar.warning("Template does not contain fields: #{@template.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
|
render json: { error: 'Template does not contain fields' }, status: :unprocessable_content
|
||||||
|
end
|
||||||
|
|
||||||
def filter_submissions(submissions, params)
|
def filter_submissions(submissions, params)
|
||||||
submissions = submissions.where(template_id: params[:template_id]) if params[:template_id].present?
|
submissions = submissions.where(template_id: params[:template_id]) if params[:template_id].present?
|
||||||
submissions = submissions.where(slug: params[:slug]) if params[:slug].present?
|
submissions = submissions.where(slug: params[:slug]) if params[:slug].present?
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ module Api
|
|||||||
class SubmittersController < ApiBaseController
|
class SubmittersController < ApiBaseController
|
||||||
load_and_authorize_resource :submitter
|
load_and_authorize_resource :submitter
|
||||||
|
|
||||||
|
before_action :maybe_return_submitter_error, only: :update
|
||||||
|
|
||||||
def index
|
def index
|
||||||
submitters = Submitters.search(current_user, @submitters, params[:q])
|
submitters = Submitters.search(current_user, @submitters, params[:q])
|
||||||
|
|
||||||
@@ -36,14 +38,6 @@ module Api
|
|||||||
|
|
||||||
# rubocop:disable Metrics/MethodLength
|
# rubocop:disable Metrics/MethodLength
|
||||||
def update
|
def update
|
||||||
if @submitter.completed_at?
|
|
||||||
return render json: { error: 'Submitter has already completed the submission.' }, status: :unprocessable_content
|
|
||||||
end
|
|
||||||
|
|
||||||
if @submitter.declined_at?
|
|
||||||
return render json: { error: 'Submitter has already declined the submission.' }, status: :unprocessable_content
|
|
||||||
end
|
|
||||||
|
|
||||||
submission = @submitter.submission
|
submission = @submitter.submission
|
||||||
role = submission.template_submitters.find { |e| e['uuid'] == @submitter.uuid }['name']
|
role = submission.template_submitters.find { |e| e['uuid'] == @submitter.uuid }['name']
|
||||||
|
|
||||||
@@ -73,7 +67,9 @@ module Api
|
|||||||
end
|
end
|
||||||
|
|
||||||
if @submitter.completed_at?
|
if @submitter.completed_at?
|
||||||
ProcessSubmitterCompletionJob.perform_async('submitter_id' => @submitter.id)
|
is_last = Submissions.maybe_update_completed_at(@submitter.submission)
|
||||||
|
|
||||||
|
ProcessSubmitterCompletionJob.perform_async('submitter_id' => @submitter.id, 'is_last' => is_last)
|
||||||
elsif normalized_params[:send_email] || normalized_params[:send_sms]
|
elsif normalized_params[:send_email] || normalized_params[:send_sms]
|
||||||
Submitters.send_signature_requests([@submitter])
|
Submitters.send_signature_requests([@submitter])
|
||||||
end
|
end
|
||||||
@@ -104,7 +100,17 @@ module Api
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def maybe_filder_by_completed_at(submitters, params)
|
def maybe_return_submitter_error
|
||||||
|
if @submitter.completed_at? || @submitter.submission.completed_at?
|
||||||
|
return render json: { error: 'Submitter has already completed the submission.' }, status: :unprocessable_content
|
||||||
|
end
|
||||||
|
|
||||||
|
return unless @submitter.declined_at?
|
||||||
|
|
||||||
|
render json: { error: 'Submitter has already declined the submission.' }, status: :unprocessable_content
|
||||||
|
end
|
||||||
|
|
||||||
|
def maybe_filter_by_completed_at(submitters, params)
|
||||||
if params[:completed_after].present?
|
if params[:completed_after].present?
|
||||||
submitters = submitters.where(completed_at: Time.zone.parse(params[:completed_after])..)
|
submitters = submitters.where(completed_at: Time.zone.parse(params[:completed_after])..)
|
||||||
end
|
end
|
||||||
@@ -177,7 +183,7 @@ module Api
|
|||||||
submitters = submitters.joins(:submission).where(submissions: { template_id: params[:template_id] })
|
submitters = submitters.joins(:submission).where(submissions: { template_id: params[:template_id] })
|
||||||
end
|
end
|
||||||
|
|
||||||
maybe_filder_by_completed_at(submitters, params)
|
maybe_filter_by_completed_at(submitters, params)
|
||||||
end
|
end
|
||||||
|
|
||||||
def assign_external_id(submitter, attrs)
|
def assign_external_id(submitter, attrs)
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ module Api
|
|||||||
load_and_authorize_resource :template
|
load_and_authorize_resource :template
|
||||||
|
|
||||||
def index
|
def index
|
||||||
|
@templates = Templates.shared(current_user) if params[:shared].in?(['true', true])
|
||||||
|
|
||||||
templates = filter_templates(@templates, params)
|
templates = filter_templates(@templates, params)
|
||||||
|
|
||||||
templates = paginate(templates.preload(:author, folder: :parent_folder))
|
templates = paginate(templates.preload(:author, folder: :parent_folder))
|
||||||
@@ -48,16 +50,19 @@ module Api
|
|||||||
|
|
||||||
archived = params.key?(:archived) ? params[:archived] : params.dig(:template, :archived)
|
archived = params.key?(:archived) ? params[:archived] : params.dig(:template, :archived)
|
||||||
|
|
||||||
if archived.in?([true, false])
|
if archived.in?([true, false]) && current_ability.can?(:destroy, @template)
|
||||||
@template.archived_at = archived == true ? Time.current : nil
|
@template.archived_at = archived == true ? Time.current : nil
|
||||||
end
|
end
|
||||||
|
|
||||||
@template.update!(template_params)
|
@template.update!(template_params)
|
||||||
|
|
||||||
SearchEntries.enqueue_reindex(@template)
|
SearchEntries.enqueue_reindex(@template) if @template.saved_change_to_name?
|
||||||
|
|
||||||
WebhookUrls.enqueue_events(@template, 'template.updated')
|
WebhookUrls.enqueue_events(@template, 'template.updated')
|
||||||
WebhookUrls.enqueue_events(@template, 'template.archived') if archived == true
|
|
||||||
|
if @template.saved_change_to_archived_at? && @template.archived_at?
|
||||||
|
WebhookUrls.enqueue_events(@template, 'template.archived')
|
||||||
|
end
|
||||||
|
|
||||||
render json: @template.as_json(only: %i[id updated_at])
|
render json: @template.as_json(only: %i[id updated_at])
|
||||||
end
|
end
|
||||||
@@ -112,7 +117,13 @@ module Api
|
|||||||
end
|
end
|
||||||
|
|
||||||
def filter_templates(templates, params)
|
def filter_templates(templates, params)
|
||||||
templates = Templates.search(current_user, templates, params[:q])
|
templates =
|
||||||
|
if params[:shared].in?(['true', true])
|
||||||
|
Templates.search_shared(current_user, templates, params[:q])
|
||||||
|
else
|
||||||
|
Templates.search(current_user, templates, params[:q])
|
||||||
|
end
|
||||||
|
|
||||||
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?
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
class EmailSmtpSettingsController < ApplicationController
|
class EmailSmtpSettingsController < ApplicationController
|
||||||
before_action :load_encrypted_config
|
before_action :load_encrypted_config
|
||||||
authorize_resource :encrypted_config, only: :index
|
authorize_resource :encrypted_config, only: :index
|
||||||
authorize_resource :encrypted_config, parent: false, only: :create
|
authorize_resource :encrypted_config, parent: false, only: %i[create destroy]
|
||||||
|
|
||||||
def index; end
|
def index; end
|
||||||
|
|
||||||
@@ -23,6 +23,12 @@ class EmailSmtpSettingsController < ApplicationController
|
|||||||
render :index, status: :unprocessable_content
|
render :index, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
@encrypted_config.destroy!
|
||||||
|
|
||||||
|
redirect_to settings_email_index_path, notice: I18n.t('smtp_settings_have_been_reset')
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def load_encrypted_config
|
def load_encrypted_config
|
||||||
|
|||||||
@@ -14,8 +14,7 @@ class EsignSettingsController < ApplicationController
|
|||||||
prepend_before_action :maybe_redirect_com, only: %i[show]
|
prepend_before_action :maybe_redirect_com, only: %i[show]
|
||||||
|
|
||||||
before_action :load_encrypted_config
|
before_action :load_encrypted_config
|
||||||
authorize_resource :encrypted_config, parent: false, only: %i[new create]
|
authorize_resource :encrypted_config, parent: false
|
||||||
authorize_resource :encrypted_config, only: %i[update destroy show]
|
|
||||||
|
|
||||||
def show
|
def show
|
||||||
cert_data = @encrypted_config.value || {}
|
cert_data = @encrypted_config.value || {}
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Mcp
|
||||||
|
class CreateTemplateController < McpBaseController
|
||||||
|
SCHEMA = {
|
||||||
|
name: 'create_template',
|
||||||
|
title: 'Create Template',
|
||||||
|
description: 'Create a document template. Provide a URL to upload a PDF/DOCX file, or provide only a name ' \
|
||||||
|
'to create an empty template and receive an edit URL where the file can be uploaded via the UI.',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Template name (used as the template name and required when url is not provided)'
|
||||||
|
},
|
||||||
|
url: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Optional URL of a PDF or DOCX file to upload. If omitted, an empty template is ' \
|
||||||
|
'created and the returned edit_url can be used to upload a file via the UI.'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: %w[name]
|
||||||
|
},
|
||||||
|
annotations: {
|
||||||
|
readOnlyHint: false,
|
||||||
|
destructiveHint: false,
|
||||||
|
idempotentHint: false,
|
||||||
|
openWorldHint: true
|
||||||
|
}
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
||||||
|
def call
|
||||||
|
account = current_user.account
|
||||||
|
|
||||||
|
@template = Template.new(
|
||||||
|
account:,
|
||||||
|
author: current_user,
|
||||||
|
folder: account.default_template_folder,
|
||||||
|
source: :mcp,
|
||||||
|
name: mcp_params['name'].to_s.presence || 'New Template',
|
||||||
|
fields: [],
|
||||||
|
schema: []
|
||||||
|
)
|
||||||
|
|
||||||
|
authorize!(:create, @template)
|
||||||
|
|
||||||
|
if mcp_params['url'].present?
|
||||||
|
tempfile = Tempfile.new
|
||||||
|
tempfile.binmode
|
||||||
|
tempfile.write(DownloadUtils.call(mcp_params['url'], validate: true).body)
|
||||||
|
tempfile.rewind
|
||||||
|
|
||||||
|
filename = File.basename(URI.decode_www_form_component(mcp_params['url']))
|
||||||
|
|
||||||
|
file = ActionDispatch::Http::UploadedFile.new(
|
||||||
|
tempfile:,
|
||||||
|
filename:,
|
||||||
|
type: Marcel::MimeType.for(tempfile)
|
||||||
|
)
|
||||||
|
|
||||||
|
@template.name = mcp_params['name'].presence || File.basename(filename, '.*')
|
||||||
|
@template.save!
|
||||||
|
|
||||||
|
documents, = Templates::CreateAttachments.call(@template, { files: [file] }, extract_fields: true)
|
||||||
|
schema = documents.map { |doc| { attachment_uuid: doc.uuid, name: doc.filename.base } }
|
||||||
|
|
||||||
|
if @template.fields.blank?
|
||||||
|
@template.fields = Templates::ProcessDocument.normalize_attachment_fields(@template, documents)
|
||||||
|
end
|
||||||
|
|
||||||
|
@template.update!(schema:)
|
||||||
|
else
|
||||||
|
@template.save!
|
||||||
|
end
|
||||||
|
|
||||||
|
WebhookUrls.enqueue_events(@template, 'template.created')
|
||||||
|
|
||||||
|
SearchEntries.enqueue_reindex(@template)
|
||||||
|
|
||||||
|
render_tool_result(
|
||||||
|
id: @template.id,
|
||||||
|
name: @template.name,
|
||||||
|
edit_url: edit_template_url(@template)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Mcp
|
||||||
|
class LoadTemplateController < McpBaseController
|
||||||
|
SCHEMA = {
|
||||||
|
name: 'load_template',
|
||||||
|
title: 'Load Template',
|
||||||
|
description: 'Load a template with its fields. Each field includes name, type, and the signing role name.',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
template_id: {
|
||||||
|
type: 'integer',
|
||||||
|
description: 'Template identifier'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: %w[template_id]
|
||||||
|
},
|
||||||
|
annotations: {
|
||||||
|
readOnlyHint: true,
|
||||||
|
destructiveHint: false,
|
||||||
|
idempotentHint: true,
|
||||||
|
openWorldHint: false
|
||||||
|
}
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
def call
|
||||||
|
@template = Template.accessible_by(current_ability).find(mcp_params['template_id'])
|
||||||
|
|
||||||
|
authorize!(:read, @template)
|
||||||
|
|
||||||
|
submitters_index = @template.submitters.index_by { |s| s['uuid'] }
|
||||||
|
|
||||||
|
roles = @template.submitters.pluck('name')
|
||||||
|
|
||||||
|
fields = @template.fields.filter_map do |field|
|
||||||
|
next if field['name'].blank?
|
||||||
|
|
||||||
|
{
|
||||||
|
name: field['name'],
|
||||||
|
type: field['type'],
|
||||||
|
role: submitters_index[field['submitter_uuid']]&.dig('name')
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
render_tool_result(
|
||||||
|
id: @template.id,
|
||||||
|
name: @template.name,
|
||||||
|
roles: roles,
|
||||||
|
fields: fields
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Mcp
|
||||||
|
class McpBaseController < ActionController::API
|
||||||
|
wrap_parameters false
|
||||||
|
|
||||||
|
before_action :authenticate_user!
|
||||||
|
before_action :verify_mcp_enabled!
|
||||||
|
check_authorization
|
||||||
|
|
||||||
|
before_action do
|
||||||
|
raise CanCan::AccessDenied unless can?(:manage, :mcp)
|
||||||
|
end
|
||||||
|
|
||||||
|
rescue_from CanCan::AccessDenied do
|
||||||
|
render_error(-32_603, 'Forbidden', status: :forbidden)
|
||||||
|
end
|
||||||
|
|
||||||
|
rescue_from ActiveRecord::RecordNotFound do
|
||||||
|
render_tool_error('Not found')
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def default_url_options
|
||||||
|
Docuseal.default_url_options
|
||||||
|
end
|
||||||
|
|
||||||
|
def mcp_body
|
||||||
|
request.request_parameters
|
||||||
|
end
|
||||||
|
|
||||||
|
def mcp_params
|
||||||
|
mcp_body.dig('params', 'arguments') || {}
|
||||||
|
end
|
||||||
|
|
||||||
|
def render_result(result)
|
||||||
|
render json: { jsonrpc: '2.0', id: mcp_body['id'], result: }
|
||||||
|
end
|
||||||
|
|
||||||
|
def render_error(code, message, id: nil, status: :ok)
|
||||||
|
render json: { jsonrpc: '2.0', id:, error: { code:, message: } }, status:
|
||||||
|
end
|
||||||
|
|
||||||
|
def render_tool_result(data)
|
||||||
|
render_result(content: [{ type: 'text', text: data.to_json }])
|
||||||
|
end
|
||||||
|
|
||||||
|
def render_tool_error(message)
|
||||||
|
render_result(content: [{ type: 'text', text: message }], isError: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
def authenticate_user!
|
||||||
|
render json: { error: 'Not authenticated' }, status: :unauthorized unless current_user
|
||||||
|
end
|
||||||
|
|
||||||
|
def verify_mcp_enabled!
|
||||||
|
return if Docuseal.multitenant?
|
||||||
|
|
||||||
|
return if AccountConfig.exists?(account_id: current_user.account_id,
|
||||||
|
key: AccountConfig::ENABLE_MCP_KEY,
|
||||||
|
value: true)
|
||||||
|
|
||||||
|
render json: { error: 'MCP is disabled' }, status: :forbidden
|
||||||
|
end
|
||||||
|
|
||||||
|
def current_user
|
||||||
|
@current_user ||= user_from_api_key
|
||||||
|
end
|
||||||
|
|
||||||
|
def user_from_api_key
|
||||||
|
token = request.headers['Authorization'].to_s[/\ABearer\s+(.+)\z/, 1]
|
||||||
|
|
||||||
|
return if token.blank?
|
||||||
|
|
||||||
|
sha256 = Digest::SHA256.hexdigest(token)
|
||||||
|
|
||||||
|
User.joins(:mcp_tokens).active.find_by(mcp_tokens: { sha256:, archived_at: nil })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Mcp
|
||||||
|
class ProtocolController < McpBaseController
|
||||||
|
skip_authorization_check
|
||||||
|
|
||||||
|
def ok
|
||||||
|
head :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize_request
|
||||||
|
render_result(
|
||||||
|
protocolVersion: '2025-11-25',
|
||||||
|
serverInfo: {
|
||||||
|
name: 'DocuSeal',
|
||||||
|
version: Docuseal.version.to_s
|
||||||
|
},
|
||||||
|
capabilities: {
|
||||||
|
tools: {
|
||||||
|
listChanged: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialized_notification
|
||||||
|
head :accepted
|
||||||
|
end
|
||||||
|
|
||||||
|
def ping
|
||||||
|
render_result({})
|
||||||
|
end
|
||||||
|
|
||||||
|
def tools_list
|
||||||
|
render_result(tools: McpController::TOOLS)
|
||||||
|
end
|
||||||
|
|
||||||
|
def method_not_found
|
||||||
|
render_error(-32_601, "Method not found: #{mcp_body['method']}", id: mcp_body['id'])
|
||||||
|
end
|
||||||
|
|
||||||
|
def tool_not_found
|
||||||
|
render_error(-32_602, "Unknown tool: #{mcp_body.dig('params', 'name')}", id: mcp_body['id'])
|
||||||
|
end
|
||||||
|
|
||||||
|
def parse_error
|
||||||
|
render_error(-32_700, 'Parse error', status: :bad_request)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Mcp
|
||||||
|
class SearchDocumentsController < McpBaseController
|
||||||
|
SCHEMA = {
|
||||||
|
name: 'search_documents',
|
||||||
|
title: 'Search Documents',
|
||||||
|
description: 'Search signed or pending documents by submitter name, email, phone, or template name',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
q: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Search by submitter name, email, phone, or template name'
|
||||||
|
},
|
||||||
|
limit: {
|
||||||
|
type: 'integer',
|
||||||
|
description: 'The number of results to return (default 10)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: %w[q]
|
||||||
|
},
|
||||||
|
annotations: {
|
||||||
|
readOnlyHint: true,
|
||||||
|
destructiveHint: false,
|
||||||
|
idempotentHint: true,
|
||||||
|
openWorldHint: false
|
||||||
|
}
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
def call
|
||||||
|
authorize!(:read, Submission)
|
||||||
|
|
||||||
|
submissions = Submissions.search(current_user, Submission.accessible_by(current_ability).active,
|
||||||
|
mcp_params['q'], search_template: true)
|
||||||
|
|
||||||
|
limit = mcp_params.fetch('limit', 10).to_i
|
||||||
|
limit = 10 if limit <= 0
|
||||||
|
limit = [limit, 100].min
|
||||||
|
submissions = submissions.preload(:submitters, :template)
|
||||||
|
.order(id: :desc)
|
||||||
|
.limit(limit)
|
||||||
|
|
||||||
|
data = submissions.map do |submission|
|
||||||
|
{
|
||||||
|
id: submission.id,
|
||||||
|
template_name: submission.template&.name,
|
||||||
|
status: Submissions::SerializeForApi.build_status(submission, submission.submitters),
|
||||||
|
submitters: submission.submitters.map do |s|
|
||||||
|
{ email: s.email, name: s.name, phone: s.phone, status: s.status }
|
||||||
|
end,
|
||||||
|
documents_url: submission_url(submission.id)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
render_tool_result(data)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Mcp
|
||||||
|
class SearchTemplatesController < McpBaseController
|
||||||
|
SCHEMA = {
|
||||||
|
name: 'search_templates',
|
||||||
|
title: 'Search Templates',
|
||||||
|
description: 'Search document templates by name',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
q: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Search query to filter templates by name'
|
||||||
|
},
|
||||||
|
limit: {
|
||||||
|
type: 'integer',
|
||||||
|
description: 'The number of templates to return (default 10)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: %w[q]
|
||||||
|
},
|
||||||
|
annotations: {
|
||||||
|
readOnlyHint: true,
|
||||||
|
destructiveHint: false,
|
||||||
|
idempotentHint: true,
|
||||||
|
openWorldHint: false
|
||||||
|
}
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
def call
|
||||||
|
authorize!(:read, Template)
|
||||||
|
|
||||||
|
templates = Templates.search(current_user, Template.accessible_by(current_ability).active, mcp_params['q'])
|
||||||
|
|
||||||
|
limit = mcp_params.fetch('limit', 10).to_i
|
||||||
|
limit = 10 if limit <= 0
|
||||||
|
limit = [limit, 100].min
|
||||||
|
templates = templates.order(id: :desc).limit(limit)
|
||||||
|
|
||||||
|
render_tool_result(templates.map { |t| { id: t.id, name: t.name } })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Mcp
|
||||||
|
class SendDocumentsController < McpBaseController
|
||||||
|
SCHEMA = {
|
||||||
|
name: 'send_documents',
|
||||||
|
title: 'Send Documents',
|
||||||
|
description: 'Send a document template for signing to specified submitters',
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
template_id: {
|
||||||
|
type: 'integer',
|
||||||
|
description: 'Template identifier'
|
||||||
|
},
|
||||||
|
submitters: {
|
||||||
|
type: 'array',
|
||||||
|
description: 'The list of submitters (signers)',
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
email: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Submitter email address'
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Submitter name'
|
||||||
|
},
|
||||||
|
phone: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Submitter phone number in E.164 format'
|
||||||
|
},
|
||||||
|
role: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Signing role name from the template'
|
||||||
|
},
|
||||||
|
fields: {
|
||||||
|
type: 'array',
|
||||||
|
description: 'Prefill field values for this submitter (fields become readonly)',
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Field name'
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
description: 'Prefilled value for the field'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: %w[name value]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: %w[template_id submitters]
|
||||||
|
},
|
||||||
|
annotations: {
|
||||||
|
readOnlyHint: false,
|
||||||
|
destructiveHint: true,
|
||||||
|
idempotentHint: false,
|
||||||
|
openWorldHint: true
|
||||||
|
}
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
# rubocop:disable Metrics
|
||||||
|
def call
|
||||||
|
@template = Template.accessible_by(current_ability).find(mcp_params['template_id'])
|
||||||
|
|
||||||
|
authorize!(:read, @template)
|
||||||
|
|
||||||
|
return render_tool_error('Template has been archived') if @template.archived_at?
|
||||||
|
|
||||||
|
authorize!(:create, Submission.new(template: @template, account_id: current_user.account_id))
|
||||||
|
|
||||||
|
return render_tool_error('Template has no fields') if @template.fields.blank?
|
||||||
|
|
||||||
|
submitters = (mcp_params['submitters'] || []).map do |s|
|
||||||
|
attrs = s.slice('email', 'name', 'role', 'phone').compact_blank
|
||||||
|
|
||||||
|
fields = Array.wrap(s['fields']).filter_map do |f|
|
||||||
|
next if f['name'].blank?
|
||||||
|
|
||||||
|
{ 'name' => f['name'], 'default_value' => f['value'], 'readonly' => true }
|
||||||
|
end
|
||||||
|
|
||||||
|
attrs['fields'] = fields if fields.present?
|
||||||
|
|
||||||
|
attrs.with_indifferent_access
|
||||||
|
end
|
||||||
|
|
||||||
|
submissions = Submissions.create_from_submitters(
|
||||||
|
template: @template,
|
||||||
|
user: current_user,
|
||||||
|
source: :mcp,
|
||||||
|
submitters_order: @template.preferences['submitters_order'].presence || 'random',
|
||||||
|
submissions_attrs: { submitters: },
|
||||||
|
params: { 'send_email' => true, 'submitters' => submitters }
|
||||||
|
)
|
||||||
|
|
||||||
|
return render_tool_error('No valid submitters provided') if submissions.blank?
|
||||||
|
|
||||||
|
WebhookUrls.enqueue_events(submissions, 'submission.created')
|
||||||
|
|
||||||
|
Submissions.send_signature_requests(submissions)
|
||||||
|
|
||||||
|
SearchEntries.enqueue_reindex(submissions)
|
||||||
|
|
||||||
|
submission = submissions.first
|
||||||
|
|
||||||
|
render_tool_result(id: submission.id, status: 'pending')
|
||||||
|
rescue Submissions::CreateFromSubmitters::BaseError => e
|
||||||
|
render_tool_error(e.message)
|
||||||
|
end
|
||||||
|
# rubocop:enable Metrics
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,58 +1,44 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class McpController < ActionController::API
|
class McpController < ActionController::Metal
|
||||||
before_action :authenticate_user!
|
TOOL_CONTROLLERS = {
|
||||||
before_action :verify_mcp_enabled!
|
'search_templates' => Mcp::SearchTemplatesController,
|
||||||
|
'load_template' => Mcp::LoadTemplateController,
|
||||||
|
'create_template' => Mcp::CreateTemplateController,
|
||||||
|
'send_documents' => Mcp::SendDocumentsController,
|
||||||
|
'search_documents' => Mcp::SearchDocumentsController
|
||||||
|
}.freeze
|
||||||
|
|
||||||
before_action do
|
TOOLS = TOOL_CONTROLLERS.map { |_, controller| controller::SCHEMA }.freeze
|
||||||
authorize!(:manage, :mcp)
|
|
||||||
end
|
|
||||||
|
|
||||||
def call
|
def call
|
||||||
return head :ok if request.raw_post.blank?
|
return Mcp::ProtocolController.dispatch(:ok, request, response) if request.raw_post.blank?
|
||||||
|
|
||||||
body = JSON.parse(request.raw_post)
|
body = JSON.parse(request.raw_post)
|
||||||
|
body = nil unless body.is_a?(Hash)
|
||||||
|
|
||||||
result = Mcp::HandleRequest.call(body, current_user, current_ability)
|
request.request_parameters = body || {}
|
||||||
|
|
||||||
if result
|
action =
|
||||||
render json: result
|
case body&.dig('method')
|
||||||
else
|
when 'initialize' then :initialize_request
|
||||||
head :accepted
|
when 'notifications/initialized' then :initialized_notification
|
||||||
end
|
when 'ping' then :ping
|
||||||
rescue CanCan::AccessDenied
|
when 'tools/list' then :tools_list
|
||||||
render json: { jsonrpc: '2.0', id: nil, error: { code: -32_603, message: 'Forbidden' } }, status: :forbidden
|
when 'tools/call'
|
||||||
|
tool = TOOL_CONTROLLERS[body.dig('params', 'name')]
|
||||||
|
|
||||||
|
return tool.dispatch(:call, request, response) if tool
|
||||||
|
|
||||||
|
:tool_not_found
|
||||||
|
else
|
||||||
|
:method_not_found
|
||||||
|
end
|
||||||
|
|
||||||
|
Mcp::ProtocolController.dispatch(action, request, response)
|
||||||
rescue JSON::ParserError
|
rescue JSON::ParserError
|
||||||
render json: { jsonrpc: '2.0', id: nil, error: { code: -32_700, message: 'Parse error' } }, status: :bad_request
|
request.request_parameters = {}
|
||||||
end
|
|
||||||
|
|
||||||
private
|
Mcp::ProtocolController.dispatch(:parse_error, request, response)
|
||||||
|
|
||||||
def authenticate_user!
|
|
||||||
render json: { error: 'Not authenticated' }, status: :unauthorized unless current_user
|
|
||||||
end
|
|
||||||
|
|
||||||
def verify_mcp_enabled!
|
|
||||||
return if Docuseal.multitenant?
|
|
||||||
|
|
||||||
return if AccountConfig.exists?(account_id: current_user.account_id,
|
|
||||||
key: AccountConfig::ENABLE_MCP_KEY,
|
|
||||||
value: true)
|
|
||||||
|
|
||||||
render json: { error: 'MCP is disabled' }, status: :forbidden
|
|
||||||
end
|
|
||||||
|
|
||||||
def current_user
|
|
||||||
@current_user ||= user_from_api_key
|
|
||||||
end
|
|
||||||
|
|
||||||
def user_from_api_key
|
|
||||||
token = request.headers['Authorization'].to_s[/\ABearer\s+(.+)\z/, 1]
|
|
||||||
|
|
||||||
return if token.blank?
|
|
||||||
|
|
||||||
sha256 = Digest::SHA256.hexdigest(token)
|
|
||||||
|
|
||||||
User.joins(:mcp_tokens).active.find_by(mcp_tokens: { sha256:, archived_at: nil })
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -16,18 +16,16 @@ class SendSubmissionEmailController < ApplicationController
|
|||||||
@submitter =
|
@submitter =
|
||||||
Submitter.completed.where(submission: template.submissions).find_by(email: params[:email].to_s.downcase)
|
Submitter.completed.where(submission: template.submissions).find_by(email: params[:email].to_s.downcase)
|
||||||
elsif params[:submission_slug]
|
elsif params[:submission_slug]
|
||||||
submission = Submission.find_by(slug: params[:submission_slug])
|
submission = Submission.find_by!(slug: params[:submission_slug])
|
||||||
|
|
||||||
if submission
|
@submitter = submission.submitters.order(:completed_at).find_by(email: params[:email].to_s.downcase)
|
||||||
@submitter = Submitter.completed.find_by(submission: submission, email: params[:email].to_s.downcase)
|
|
||||||
end
|
|
||||||
|
|
||||||
return redirect_to submissions_preview_completed_path(params[:submission_slug], status: :error) unless @submitter
|
return redirect_to submissions_preview_completed_path(params[:submission_slug], status: :error) unless @submitter
|
||||||
else
|
else
|
||||||
@submitter = Submitter.completed.find_by!(slug: params[:submitter_slug])
|
@submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
||||||
end
|
end
|
||||||
|
|
||||||
if @submitter
|
if @submitter && completed_submitter?(@submitter)
|
||||||
RateLimit.call("send-email-#{@submitter.id}", limit: 2, ttl: 5.minutes)
|
RateLimit.call("send-email-#{@submitter.id}", limit: 2, ttl: 5.minutes)
|
||||||
|
|
||||||
SubmitterMailer.documents_copy_email(@submitter, sig: true).deliver_later! if can_send?(@submitter)
|
SubmitterMailer.documents_copy_email(@submitter, sig: true).deliver_later! if can_send?(@submitter)
|
||||||
@@ -41,6 +39,10 @@ class SendSubmissionEmailController < ApplicationController
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def completed_submitter?(submitter)
|
||||||
|
submitter.completed_at? || (submitter.viewer? && submitter.submission.completed_at?)
|
||||||
|
end
|
||||||
|
|
||||||
def can_send?(submitter)
|
def can_send?(submitter)
|
||||||
return false if submitter.account.archived_at?
|
return false if submitter.account.archived_at?
|
||||||
return false if EmailEvent.exists?(tag: :submitter_documents_copy, email: submitter.email, emailable: submitter,
|
return false if EmailEvent.exists?(tag: :submitter_documents_copy, email: submitter.email, emailable: submitter,
|
||||||
|
|||||||
@@ -93,9 +93,12 @@ class StartFormController < ApplicationController
|
|||||||
|
|
||||||
SearchEntries.enqueue_reindex(submitter)
|
SearchEntries.enqueue_reindex(submitter)
|
||||||
|
|
||||||
return unless submitter.submission.expire_at?
|
expire_at = submitter.submission.expire_at
|
||||||
|
|
||||||
ProcessSubmissionExpiredJob.perform_at(submitter.submission.expire_at, 'submission_id' => submitter.submission_id)
|
return unless expire_at
|
||||||
|
|
||||||
|
ProcessSubmissionExpiredJob.perform_at(expire_at, 'submission_id' => submitter.submission_id,
|
||||||
|
'expire_at' => expire_at.to_i)
|
||||||
end
|
end
|
||||||
|
|
||||||
def load_resubmit_submitter
|
def load_resubmit_submitter
|
||||||
@@ -138,8 +141,7 @@ class StartFormController < ApplicationController
|
|||||||
|
|
||||||
submitter ||=
|
submitter ||=
|
||||||
Submitter
|
Submitter
|
||||||
.where(submission: template.submissions.where(expire_at: Time.current..)
|
.where(submission: template.submissions.non_expired.active)
|
||||||
.or(template.submissions.where(expire_at: nil)).where(archived_at: nil))
|
|
||||||
.order(id: :desc)
|
.order(id: :desc)
|
||||||
.where(declined_at: nil)
|
.where(declined_at: nil)
|
||||||
.where(external_id: nil)
|
.where(external_id: nil)
|
||||||
@@ -147,6 +149,8 @@ class StartFormController < ApplicationController
|
|||||||
.then { |rel| params[:resubmit].present? || params[:selfsign].present? ? rel.where(completed_at: nil) : rel }
|
.then { |rel| params[:resubmit].present? || params[:selfsign].present? ? rel.where(completed_at: nil) : rel }
|
||||||
.find_or_initialize_by(find_params)
|
.find_or_initialize_by(find_params)
|
||||||
|
|
||||||
|
submitter = Submitter.new(find_params) if submitter.submission&.completed_at? && submitter.viewer?
|
||||||
|
|
||||||
submitter.name = required_params['name'] if submitter.new_record?
|
submitter.name = required_params['name'] if submitter.new_record?
|
||||||
|
|
||||||
unless @resubmit_submitter
|
unless @resubmit_submitter
|
||||||
|
|||||||
@@ -7,17 +7,27 @@ class SubmissionsArchivedController < ApplicationController
|
|||||||
@submissions = @submissions.left_joins(:template)
|
@submissions = @submissions.left_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(:template_accesses, :created_by_user, template: :author)
|
.preload(:template_accesses, :created_by_user)
|
||||||
|
|
||||||
@submissions = Submissions.search(current_user, @submissions, params[:q], search_template: true)
|
@submissions = Submissions.search(current_user, @submissions, params[:q], search_template: true)
|
||||||
@submissions = Submissions::Filter.call(@submissions, current_user, params)
|
@submissions = Submissions::Filter.call(@submissions, current_user, params)
|
||||||
|
|
||||||
@submissions = if params[:completed_at_from].present? || params[:completed_at_to].present?
|
@submissions =
|
||||||
@submissions.order(Submitter.arel_table[:completed_at].maximum.desc)
|
if params[:status] == 'completed' || params[:completed_at_from].present? || params[:completed_at_to].present?
|
||||||
else
|
@submissions.order(completed_at: :desc)
|
||||||
@submissions.order(id: :desc)
|
else
|
||||||
end
|
@submissions.order(id: :desc)
|
||||||
|
end
|
||||||
|
|
||||||
@pagy, @submissions = pagy_auto(@submissions.preload(submitters: :start_form_submission_events))
|
@pagy, @submissions = pagy_auto(@submissions.select_for_list.preload(submitters: :start_form_submission_events))
|
||||||
|
|
||||||
|
template_scope = @submissions.all?(&:template_submitters) ? Template.select_for_list : nil
|
||||||
|
|
||||||
|
ActiveRecord::Associations::Preloader.new(records: @submissions,
|
||||||
|
associations: :template,
|
||||||
|
scope: template_scope).call
|
||||||
|
|
||||||
|
ActiveRecord::Associations::Preloader.new(records: @submissions.filter_map(&:template),
|
||||||
|
associations: :author).call
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class SubmissionsController < ApplicationController
|
class SubmissionsController < ApplicationController
|
||||||
before_action :load_template, only: %i[new create]
|
load_and_authorize_resource :template, only: %i[new create]
|
||||||
authorize_resource :template, only: %i[new create]
|
|
||||||
|
|
||||||
load_and_authorize_resource :submission, only: %i[show destroy]
|
load_and_authorize_resource :submission, only: %i[show destroy]
|
||||||
|
|
||||||
prepend_before_action :maybe_redirect_com, only: %i[show]
|
prepend_before_action :maybe_redirect_com, only: %i[show]
|
||||||
@@ -23,7 +21,7 @@ class SubmissionsController < ApplicationController
|
|||||||
def show
|
def show
|
||||||
@submission = Submissions.preload_with_pages(@submission)
|
@submission = Submissions.preload_with_pages(@submission)
|
||||||
|
|
||||||
unless @submission.submitters.all?(&:completed_at?)
|
unless @submission.completed_at?
|
||||||
ActiveRecord::Associations::Preloader.new(
|
ActiveRecord::Associations::Preloader.new(
|
||||||
records: [@submission],
|
records: [@submission],
|
||||||
associations: [{ submitters: :start_form_submission_events }]
|
associations: [{ submitters: :start_form_submission_events }]
|
||||||
@@ -89,6 +87,8 @@ class SubmissionsController < ApplicationController
|
|||||||
private
|
private
|
||||||
|
|
||||||
def create_submissions(template, submissions_params, params)
|
def create_submissions(template, submissions_params, params)
|
||||||
|
normalize_message_submitter_uuids!(params)
|
||||||
|
|
||||||
submissions_attrs = submissions_params[:submission].to_h.values
|
submissions_attrs = submissions_params[:submission].to_h.values
|
||||||
|
|
||||||
submissions_attrs, _, new_fields =
|
submissions_attrs, _, new_fields =
|
||||||
@@ -114,7 +114,22 @@ class SubmissionsController < ApplicationController
|
|||||||
params.permit(submission: { submitters: [:uuid, :email, :phone, :name, { values: {} }] })
|
params.permit(submission: { submitters: [:uuid, :email, :phone, :name, { values: {} }] })
|
||||||
end
|
end
|
||||||
|
|
||||||
def load_template
|
def normalize_message_submitter_uuids!(params)
|
||||||
@template = Template.accessible_by(current_ability).find(params[:template_id])
|
return if params[:request_email_per_submitter] == '1'
|
||||||
|
|
||||||
|
uuids = params[:email_message_submitter_uuids]
|
||||||
|
|
||||||
|
return if uuids.blank?
|
||||||
|
return if params[:subject].blank? && params[:body].blank?
|
||||||
|
|
||||||
|
params[:submitter_preferences] =
|
||||||
|
Array.wrap(uuids).index_with { { 'subject' => params[:subject], 'body' => params[:body] } }
|
||||||
|
|
||||||
|
params[:request_email_per_submitter] = '1'
|
||||||
|
|
||||||
|
params.delete(:subject)
|
||||||
|
params.delete(:body)
|
||||||
|
|
||||||
|
params
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -8,17 +8,27 @@ 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(:template_accesses, :created_by_user, template: :author)
|
.preload(:template_accesses, :created_by_user)
|
||||||
|
|
||||||
@submissions = Submissions.search(current_user, @submissions, params[:q], search_template: true)
|
@submissions = Submissions.search(current_user, @submissions, params[:q], search_template: true)
|
||||||
@submissions = Submissions::Filter.call(@submissions, current_user, params)
|
@submissions = Submissions::Filter.call(@submissions, current_user, params)
|
||||||
|
|
||||||
@submissions = if params[:completed_at_from].present? || params[:completed_at_to].present?
|
@submissions =
|
||||||
@submissions.order(Submitter.arel_table[:completed_at].maximum.desc)
|
if params[:status] == 'completed' || params[:completed_at_from].present? || params[:completed_at_to].present?
|
||||||
else
|
@submissions.order(completed_at: :desc)
|
||||||
@submissions.order(id: :desc)
|
else
|
||||||
end
|
@submissions.order(id: :desc)
|
||||||
|
end
|
||||||
|
|
||||||
@pagy, @submissions = pagy_auto(@submissions.preload(submitters: :start_form_submission_events))
|
@pagy, @submissions = pagy_auto(@submissions.select_for_list.preload(submitters: :start_form_submission_events))
|
||||||
|
|
||||||
|
template_scope = @submissions.all?(&:template_submitters) ? Template.select_for_list : nil
|
||||||
|
|
||||||
|
ActiveRecord::Associations::Preloader.new(records: @submissions,
|
||||||
|
associations: :template,
|
||||||
|
scope: template_scope).call
|
||||||
|
|
||||||
|
ActiveRecord::Associations::Preloader.new(records: @submissions.filter_map(&:template),
|
||||||
|
associations: :author).call
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -25,7 +25,7 @@ class SubmissionsPreviewController < ApplicationController
|
|||||||
|
|
||||||
raise ActionController::RoutingError, I18n.t('not_found') if @submission.account.archived_at?
|
raise ActionController::RoutingError, I18n.t('not_found') if @submission.account.archived_at?
|
||||||
|
|
||||||
if !@submission.submitters.all?(&:completed_at?) && !signature_valid &&
|
if !@submission.completed_at? && !signature_valid &&
|
||||||
(!current_user || !current_ability.can?(:read, @submission))
|
(!current_user || !current_ability.can?(:read, @submission))
|
||||||
raise ActionController::RoutingError, I18n.t('not_found')
|
raise ActionController::RoutingError, I18n.t('not_found')
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ class SubmissionsUnarchiveController < ApplicationController
|
|||||||
load_and_authorize_resource :submission
|
load_and_authorize_resource :submission
|
||||||
|
|
||||||
def create
|
def create
|
||||||
authorize!(:update, @submission)
|
authorize!(:destroy, @submission)
|
||||||
|
|
||||||
@submission.update!(archived_at: nil)
|
@submission.update!(archived_at: nil)
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ class SubmitFormCompletedDownloadController < ApplicationController
|
|||||||
|
|
||||||
@submitter ||= Submitter.find_by!(slug: submitter_slug)
|
@submitter ||= Submitter.find_by!(slug: submitter_slug)
|
||||||
|
|
||||||
Submissions::EnsureResultGenerated.call(@submitter)
|
Submissions::EnsureResultGenerated.call(@submitter) if @submitter.completed_at?
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
@@ -28,11 +28,7 @@ class SubmitFormCompletedDownloadController < ApplicationController
|
|||||||
Submissions::EnsureResultGenerated.call(last_submitter)
|
Submissions::EnsureResultGenerated.call(last_submitter)
|
||||||
|
|
||||||
if !signature_valid && !current_user_submitter?(last_submitter)
|
if !signature_valid && !current_user_submitter?(last_submitter)
|
||||||
unless Submitters::AuthorizedForForm.call(@submitter, current_user, request)
|
return head :not_found unless Submitters::AuthorizedForForm.call(@submitter, current_user, request)
|
||||||
Rollbar.info("2FA download error: #{last_submitter.id}") if defined?(Rollbar)
|
|
||||||
|
|
||||||
return head :not_found
|
|
||||||
end
|
|
||||||
|
|
||||||
if last_submitter.completed_at < TTL.ago
|
if last_submitter.completed_at < TTL.ago
|
||||||
Rollbar.info("TTL: #{last_submitter.id}") if defined?(Rollbar)
|
Rollbar.info("TTL: #{last_submitter.id}") if defined?(Rollbar)
|
||||||
|
|||||||
@@ -18,7 +18,10 @@ class SubmitFormController < ApplicationController
|
|||||||
submission = @submitter.submission
|
submission = @submitter.submission
|
||||||
|
|
||||||
return render :email_2fa unless Submitters::AuthorizedForForm.pass_email_2fa?(@submitter, request)
|
return render :email_2fa unless Submitters::AuthorizedForForm.pass_email_2fa?(@submitter, request)
|
||||||
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
|
|
||||||
|
if @submitter.completed_at? || submission.completed_at?
|
||||||
|
return redirect_to submit_form_completed_path(@submitter.slug)
|
||||||
|
end
|
||||||
|
|
||||||
@form_configs = Submitters::FormConfigs.call(@submitter, CONFIG_KEYS)
|
@form_configs = Submitters::FormConfigs.call(@submitter, CONFIG_KEYS)
|
||||||
|
|
||||||
@@ -71,6 +74,12 @@ class SubmitFormController < ApplicationController
|
|||||||
status: :unprocessable_content
|
status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if @submitter.viewer?
|
||||||
|
Rollbar.warning("Submit viewer: #{@submitter.id}") if defined?(Rollbar)
|
||||||
|
|
||||||
|
return render json: { error: I18n.t('form_is_view_only') }, status: :unprocessable_content
|
||||||
|
end
|
||||||
|
|
||||||
Submitters::SubmitValues.call(@submitter, params, request)
|
Submitters::SubmitValues.call(@submitter, params, request)
|
||||||
|
|
||||||
head :ok
|
head :ok
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ class SubmitFormDeclineController < ApplicationController
|
|||||||
@submitter.submission.archived_at? ||
|
@submitter.submission.archived_at? ||
|
||||||
@submitter.submission.expired? ||
|
@submitter.submission.expired? ||
|
||||||
@submitter.submission.template&.archived_at? ||
|
@submitter.submission.template&.archived_at? ||
|
||||||
|
@submitter.viewer? ||
|
||||||
!Submitters::AuthorizedForForm.call(@submitter,
|
!Submitters::AuthorizedForForm.call(@submitter,
|
||||||
current_user,
|
current_user,
|
||||||
request)
|
request)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ class SubmitFormDelegateController < ApplicationController
|
|||||||
@submitter.submission.archived_at? ||
|
@submitter.submission.archived_at? ||
|
||||||
@submitter.submission.expired? ||
|
@submitter.submission.expired? ||
|
||||||
@submitter.submission.template&.archived_at? ||
|
@submitter.submission.template&.archived_at? ||
|
||||||
|
@submitter.viewer? ||
|
||||||
!Submitters::AuthorizedForForm.call(@submitter,
|
!Submitters::AuthorizedForForm.call(@submitter,
|
||||||
current_user,
|
current_user,
|
||||||
request)
|
request)
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ class SubmitFormDownloadController < ApplicationController
|
|||||||
return head :unprocessable_content if @submitter.declined_at? ||
|
return head :unprocessable_content if @submitter.declined_at? ||
|
||||||
@submitter.submission.archived_at? ||
|
@submitter.submission.archived_at? ||
|
||||||
@submitter.submission.expired? ||
|
@submitter.submission.expired? ||
|
||||||
|
@submitter.submission.completed_at? ||
|
||||||
@submitter.submission.template&.archived_at? ||
|
@submitter.submission.template&.archived_at? ||
|
||||||
AccountConfig.exists?(account_id: @submitter.account_id,
|
AccountConfig.exists?(account_id: @submitter.account_id,
|
||||||
key: AccountConfig::ALLOW_TO_PARTIAL_DOWNLOAD_KEY,
|
key: AccountConfig::ALLOW_TO_PARTIAL_DOWNLOAD_KEY,
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ class SubmitFormDrawSignatureController < ApplicationController
|
|||||||
|
|
||||||
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
|
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
|
||||||
|
|
||||||
|
return redirect_to submit_form_path(@submitter.slug) if @submitter.viewer?
|
||||||
|
|
||||||
if @submitter.submission.template&.archived_at? || @submitter.submission.archived_at? ||
|
if @submitter.submission.template&.archived_at? || @submitter.submission.archived_at? ||
|
||||||
!Submitters::AuthorizedForForm.call(@submitter, current_user, request)
|
!Submitters::AuthorizedForForm.call(@submitter, current_user, request)
|
||||||
return redirect_to submit_form_path(@submitter.slug)
|
return redirect_to submit_form_path(@submitter.slug)
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ class SubmitFormInviteController < ApplicationController
|
|||||||
!submitter.submission.archived_at? &&
|
!submitter.submission.archived_at? &&
|
||||||
!submitter.submission.expired? &&
|
!submitter.submission.expired? &&
|
||||||
!submitter.submission.template&.archived_at? &&
|
!submitter.submission.template&.archived_at? &&
|
||||||
|
!submitter.viewer? &&
|
||||||
Submitters::AuthorizedForForm.call(submitter, current_user, request)
|
Submitters::AuthorizedForForm.call(submitter, current_user, request)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -7,13 +7,7 @@ class SubmitFormMetadataController < ApplicationController
|
|||||||
def index
|
def index
|
||||||
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||||
|
|
||||||
return head :not_found if @submitter.declined_at? ||
|
return head :not_found unless authorized_submitter?(@submitter)
|
||||||
@submitter.completed_at? ||
|
|
||||||
@submitter.submission.archived_at? ||
|
|
||||||
@submitter.submission.expired? ||
|
|
||||||
@submitter.submission.template&.archived_at? ||
|
|
||||||
@submitter.account.archived_at? ||
|
|
||||||
!Submitters::AuthorizedForForm.call(@submitter, current_user, request)
|
|
||||||
|
|
||||||
submission = @submitter.submission
|
submission = @submitter.submission
|
||||||
values = submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) }
|
values = submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) }
|
||||||
@@ -34,4 +28,17 @@ class SubmitFormMetadataController < ApplicationController
|
|||||||
|
|
||||||
render json: { text_runs: }
|
render json: { text_runs: }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def authorized_submitter?(submitter)
|
||||||
|
!submitter.declined_at? &&
|
||||||
|
!submitter.completed_at? &&
|
||||||
|
!submitter.submission.archived_at? &&
|
||||||
|
!submitter.submission.completed_at? &&
|
||||||
|
!submitter.submission.expired? &&
|
||||||
|
!submitter.submission.template&.archived_at? &&
|
||||||
|
!submitter.account.archived_at? &&
|
||||||
|
Submitters::AuthorizedForForm.call(submitter, current_user, request)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -50,7 +50,7 @@ class SubmittersController < ApplicationController
|
|||||||
|
|
||||||
def submitter_editable?(submission)
|
def submitter_editable?(submission)
|
||||||
!@submitter.submission_events.exists?(event_type: 'start_form') &&
|
!@submitter.submission_events.exists?(event_type: 'start_form') &&
|
||||||
!@submitter.completed_at? && !@submitter.declined_at? &&
|
!@submitter.completed_at? && !@submitter.declined_at? && !submission.completed_at? &&
|
||||||
!submission.archived_at? && !submission.expired? && !submission.template&.archived_at?
|
!submission.archived_at? && !submission.expired? && !submission.template&.archived_at?
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ class SubmittersController < ApplicationController
|
|||||||
if params[:send_email] == '1' && submitter.email.present?
|
if params[:send_email] == '1' && submitter.email.present?
|
||||||
is_sent_recently = Docuseal.multitenant? &&
|
is_sent_recently = Docuseal.multitenant? &&
|
||||||
EmailEvent.exists?(email: submitter.email,
|
EmailEvent.exists?(email: submitter.email,
|
||||||
tag: 'submitter_invitation',
|
tag: %w[submitter_invitation submitter_view_invitation],
|
||||||
emailable: submitter,
|
emailable: submitter,
|
||||||
event_type: 'send',
|
event_type: 'send',
|
||||||
created_at: 4.hours.ago..Time.current)
|
created_at: 4.hours.ago..Time.current)
|
||||||
|
|||||||
@@ -30,9 +30,11 @@ class TemplateFoldersController < ApplicationController
|
|||||||
(@template_folders.size < 7 ? 9 : 6)
|
(@template_folders.size < 7 ? 9 : 6)
|
||||||
end
|
end
|
||||||
|
|
||||||
@pagy, @templates = pagy_auto(@templates, limit:)
|
@pagy, @templates = pagy_auto(@templates.select_for_list, limit:)
|
||||||
|
|
||||||
load_related_submissions if params[:q].present? && @templates.blank?
|
if params[:q].present? && @templates.blank?
|
||||||
|
@related_submissions_pagy, @related_submissions = load_related_submissions(@template_folder)
|
||||||
|
end
|
||||||
else
|
else
|
||||||
@pagy, @template_folders = pagy(@template_folders, limit: FOLDERS_PER_PAGE)
|
@pagy, @template_folders = pagy(@template_folders, limit: FOLDERS_PER_PAGE)
|
||||||
|
|
||||||
@@ -55,11 +57,10 @@ class TemplateFoldersController < ApplicationController
|
|||||||
|
|
||||||
def selected_order
|
def selected_order
|
||||||
@selected_order ||=
|
@selected_order ||=
|
||||||
if cookies.permanent[:dashboard_templates_order].blank? ||
|
if can?(:manage, :countless)
|
||||||
(cookies.permanent[:dashboard_templates_order] == 'used_at' && can?(:manage, :countless))
|
|
||||||
'created_at'
|
'created_at'
|
||||||
else
|
else
|
||||||
cookies.permanent[:dashboard_templates_order]
|
cookies.permanent[:dashboard_templates_order].presence || 'created_at'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -67,20 +68,20 @@ class TemplateFoldersController < ApplicationController
|
|||||||
params.require(:template_folder).permit(:name)
|
params.require(:template_folder).permit(:name)
|
||||||
end
|
end
|
||||||
|
|
||||||
def load_related_submissions
|
def load_related_submissions(template_folder)
|
||||||
@related_submissions =
|
related_submissions =
|
||||||
Submission.accessible_by(current_ability)
|
Submission.accessible_by(current_ability)
|
||||||
.where(archived_at: nil)
|
.where(archived_at: nil)
|
||||||
.where(template_id: current_account.templates.active
|
.where(template_id: current_account.templates.active
|
||||||
.where(folder: [@template_folder, *@template_folder.subfolders])
|
.where(folder: [template_folder, *template_folder.subfolders])
|
||||||
.select(:id))
|
.select(:id))
|
||||||
.preload(:template_accesses, :created_by_user,
|
.preload(:template_accesses, :created_by_user,
|
||||||
template: :author,
|
template: :author,
|
||||||
submitters: :start_form_submission_events)
|
submitters: :start_form_submission_events)
|
||||||
|
|
||||||
@related_submissions = Submissions.search(current_user, @related_submissions, params[:q])
|
related_submissions = Submissions.search(current_user, related_submissions, params[:q])
|
||||||
.order(id: :desc)
|
.order(id: :desc)
|
||||||
|
|
||||||
@related_submissions_pagy, @related_submissions = pagy_auto(@related_submissions, limit: 5)
|
pagy_auto(related_submissions.select_for_list, limit: 5)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -10,11 +10,17 @@ class TemplatesArchivedController < ApplicationController
|
|||||||
|
|
||||||
@templates = Templates.search(current_user, @templates, params[:q])
|
@templates = Templates.search(current_user, @templates, params[:q])
|
||||||
|
|
||||||
@pagy, @templates = pagy_auto(@templates, limit: 12)
|
@pagy, @templates = pagy_auto(@templates.select_for_list, limit: 12)
|
||||||
|
|
||||||
return unless params[:q].present? && @templates.blank?
|
return unless params[:q].present? && @templates.blank?
|
||||||
|
|
||||||
@related_submissions =
|
@related_submissions_pagy, @related_submissions = load_related_submissions
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def load_related_submissions
|
||||||
|
related_submissions =
|
||||||
Submission.accessible_by(current_ability)
|
Submission.accessible_by(current_ability)
|
||||||
.joins(:template)
|
.joins(:template)
|
||||||
.where.not(templates: { archived_at: nil })
|
.where.not(templates: { archived_at: nil })
|
||||||
@@ -22,9 +28,9 @@ class TemplatesArchivedController < ApplicationController
|
|||||||
template: :author,
|
template: :author,
|
||||||
submitters: :start_form_submission_events)
|
submitters: :start_form_submission_events)
|
||||||
|
|
||||||
@related_submissions = Submissions.search(current_user, @related_submissions, params[:q])
|
related_submissions = Submissions.search(current_user, related_submissions, params[:q])
|
||||||
.order(id: :desc)
|
.order(id: :desc)
|
||||||
|
|
||||||
@related_submissions_pagy, @related_submissions = pagy_auto(@related_submissions, limit: 5)
|
pagy_auto(related_submissions.select_for_list, limit: 5)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -10,12 +10,12 @@ class TemplatesArchivedSubmissionsController < ApplicationController
|
|||||||
@submissions = Submissions::Filter.call(@submissions, current_user, params)
|
@submissions = Submissions::Filter.call(@submissions, current_user, params)
|
||||||
|
|
||||||
@submissions = if params[:completed_at_from].present? || params[:completed_at_to].present?
|
@submissions = if params[:completed_at_from].present? || params[:completed_at_to].present?
|
||||||
@submissions.order(Submitter.arel_table[:completed_at].maximum.desc)
|
@submissions.order(completed_at: :desc)
|
||||||
else
|
else
|
||||||
@submissions.order(id: :desc)
|
@submissions.order(id: :desc)
|
||||||
end
|
end
|
||||||
|
|
||||||
@pagy, @submissions = pagy_auto(@submissions.preload(submitters: :start_form_submission_events))
|
@pagy, @submissions = pagy_auto(@submissions.select_for_list.preload(submitters: :start_form_submission_events))
|
||||||
rescue ActiveRecord::RecordNotFound
|
rescue ActiveRecord::RecordNotFound
|
||||||
redirect_to root_path
|
redirect_to root_path
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -14,12 +14,13 @@ class TemplatesController < ApplicationController
|
|||||||
submissions = Submissions::Filter.filter_by_status(submissions, params)
|
submissions = Submissions::Filter.filter_by_status(submissions, params)
|
||||||
|
|
||||||
submissions = if params[:completed_at_from].present? || params[:completed_at_to].present?
|
submissions = if params[:completed_at_from].present? || params[:completed_at_to].present?
|
||||||
submissions.order(Submitter.arel_table[:completed_at].maximum.desc)
|
submissions.order(completed_at: :desc)
|
||||||
else
|
else
|
||||||
submissions.order(id: :desc)
|
submissions.order(id: :desc)
|
||||||
end
|
end
|
||||||
|
|
||||||
@pagy, @submissions = pagy_auto(submissions.preload(:template_accesses, submitters: :start_form_submission_events))
|
@pagy, @submissions =
|
||||||
|
pagy_auto(submissions.select_for_list.preload(:template_accesses, submitters: :start_form_submission_events))
|
||||||
rescue ActiveRecord::RecordNotFound
|
rescue ActiveRecord::RecordNotFound
|
||||||
redirect_to root_path
|
redirect_to root_path
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -11,86 +11,126 @@ class TemplatesDashboardController < ApplicationController
|
|||||||
helper_method :selected_order
|
helper_method :selected_order
|
||||||
|
|
||||||
def index
|
def index
|
||||||
|
@default_folder = current_account.default_template_folder
|
||||||
|
|
||||||
@template_folders =
|
@template_folders =
|
||||||
TemplateFolders.filter_active_folders(@template_folders.where(parent_folder_id: nil), @templates)
|
TemplateFolders.filter_active_folders(@template_folders.where(parent_folder_id: nil), @templates)
|
||||||
|
|
||||||
|
@template_folders = @template_folders.where.not(id: @default_folder.id) if params[:q].blank?
|
||||||
|
|
||||||
@template_folders = TemplateFolders.search(@template_folders, params[:q])
|
@template_folders = TemplateFolders.search(@template_folders, params[:q])
|
||||||
@template_folders = TemplateFolders.sort(@template_folders, current_user, selected_order)
|
@template_folders = TemplateFolders.sort(@template_folders, current_user, selected_order)
|
||||||
|
|
||||||
@pagy, @template_folders = pagy(
|
@shared_templates = Templates.shared(current_user).active
|
||||||
@template_folders,
|
|
||||||
limit: FOLDERS_PER_PAGE,
|
@pagy, @template_folders, @show_default_folder, @show_shared_folder, @show_shared_inline =
|
||||||
page: @template_folders.count > SHOW_TEMPLATES_FOLDERS_THRESHOLD ? params[:page] : 1
|
load_folders(@template_folders, @templates, @shared_templates)
|
||||||
)
|
|
||||||
|
|
||||||
if @pagy.count > SHOW_TEMPLATES_FOLDERS_THRESHOLD
|
if @pagy.count > SHOW_TEMPLATES_FOLDERS_THRESHOLD
|
||||||
@templates = @templates.none
|
@templates = @templates.none
|
||||||
else
|
else
|
||||||
@template_folders = @template_folders.reject { |e| e.name == TemplateFolder::DEFAULT_NAME }
|
if @show_shared_inline
|
||||||
@templates = filter_templates(@templates).preload(:author, :template_accesses)
|
@templates = @shared_templates.preload(:template_sharings)
|
||||||
@templates = Templates::Order.call(@templates, current_user, selected_order)
|
else
|
||||||
|
@templates = @templates.active
|
||||||
|
@templates = @templates.where(folder_id: @default_folder.id) if params[:q].blank?
|
||||||
|
end
|
||||||
|
|
||||||
limit =
|
@pagy, @templates = load_templates(@templates.select_for_list, @pagy.count,
|
||||||
if @template_folders.size < 4
|
show_shared_inline: @show_shared_inline)
|
||||||
TEMPLATES_PER_PAGE
|
|
||||||
else
|
|
||||||
(@template_folders.size < 7 ? 9 : 6)
|
|
||||||
end
|
|
||||||
|
|
||||||
@pagy, @templates = pagy_auto(@templates, limit:)
|
if params[:q].present? && @templates.blank?
|
||||||
|
@related_submissions_pagy, @related_submissions = load_related_submissions
|
||||||
load_related_submissions if params[:q].present? && @templates.blank?
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def filter_templates(templates)
|
def load_templates(templates, folders_count, show_shared_inline: false)
|
||||||
rel = templates.active
|
templates = templates.preload(:author, :template_accesses)
|
||||||
|
|
||||||
if params[:q].blank?
|
templates =
|
||||||
if Docuseal.multitenant? ? current_account.testing? : current_account.linked_account_account
|
if show_shared_inline
|
||||||
shared_account_ids = [current_user.account_id]
|
Templates.search_shared(current_user, templates, params[:q])
|
||||||
shared_account_ids << TemplateSharing::ALL_ID if !Docuseal.multitenant? && !current_account.testing?
|
|
||||||
|
|
||||||
shared_template_ids = TemplateSharing.where(account_id: shared_account_ids).select(:template_id)
|
|
||||||
|
|
||||||
rel = Template.where(
|
|
||||||
Template.arel_table[:id].in(
|
|
||||||
rel.where(folder_id: current_account.default_template_folder.id).select(:id).arel
|
|
||||||
.union(:all, shared_template_ids.arel)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else
|
else
|
||||||
rel = rel.where(folder_id: current_account.default_template_folder.id)
|
Templates.search(current_user, templates, params[:q])
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
Templates.search(current_user, rel, params[:q])
|
templates = Templates::Order.call(templates, current_user, selected_order)
|
||||||
|
|
||||||
|
limit =
|
||||||
|
if folders_count < 4
|
||||||
|
TEMPLATES_PER_PAGE
|
||||||
|
else
|
||||||
|
(folders_count < 7 ? 9 : 6)
|
||||||
|
end
|
||||||
|
|
||||||
|
pagy_auto(templates, limit:)
|
||||||
|
end
|
||||||
|
|
||||||
|
def load_folders(template_folders, templates, shared_templates)
|
||||||
|
if params[:q].present?
|
||||||
|
pagy(template_folders, limit: FOLDERS_PER_PAGE,
|
||||||
|
page: template_folders.count > SHOW_TEMPLATES_FOLDERS_THRESHOLD ? params[:page] : 1)
|
||||||
|
else
|
||||||
|
load_folders_with_pinned(template_folders, templates, shared_templates)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def load_folders_with_pinned(template_folders, templates, shared_templates)
|
||||||
|
folders_count = template_folders.count
|
||||||
|
|
||||||
|
shared_exists = shared_templates.exists?
|
||||||
|
default_has_templates = templates.active.exists?(folder_id: current_account.default_template_folder.id)
|
||||||
|
|
||||||
|
show_inline_folders =
|
||||||
|
folders_count + (shared_exists && default_has_templates ? 1 : 0) <= SHOW_TEMPLATES_FOLDERS_THRESHOLD
|
||||||
|
|
||||||
|
show_shared_inline = shared_exists && !default_has_templates && show_inline_folders
|
||||||
|
|
||||||
|
show_shared_in_grid = shared_exists && !show_shared_inline
|
||||||
|
show_default_in_grid = !show_inline_folders && default_has_templates
|
||||||
|
|
||||||
|
pinned_count = (show_default_in_grid ? 1 : 0) + (show_shared_in_grid ? 1 : 0)
|
||||||
|
|
||||||
|
pagy = Pagy::Offset.new(count: folders_count + pinned_count,
|
||||||
|
page: show_inline_folders ? 1 : [params[:page].to_s.to_i, 1].max,
|
||||||
|
limit: FOLDERS_PER_PAGE,
|
||||||
|
raise_range_error: true)
|
||||||
|
|
||||||
|
show_default_folder = show_default_in_grid && pagy.page == 1
|
||||||
|
show_shared_folder = show_shared_in_grid && pagy.page == 1
|
||||||
|
|
||||||
|
folder_offset = pagy.page == 1 ? 0 : pagy.offset - pinned_count
|
||||||
|
folder_limit = pagy.page == 1 ? FOLDERS_PER_PAGE - pinned_count : FOLDERS_PER_PAGE
|
||||||
|
|
||||||
|
template_folders = template_folders.offset(folder_offset).limit(folder_limit)
|
||||||
|
|
||||||
|
[pagy, template_folders, show_default_folder, show_shared_folder, show_shared_inline]
|
||||||
end
|
end
|
||||||
|
|
||||||
def selected_order
|
def selected_order
|
||||||
@selected_order ||=
|
@selected_order ||=
|
||||||
if cookies.permanent[:dashboard_templates_order].blank? ||
|
if can?(:manage, :countless)
|
||||||
(cookies.permanent[:dashboard_templates_order] == 'used_at' && can?(:manage, :countless))
|
|
||||||
'created_at'
|
'created_at'
|
||||||
else
|
else
|
||||||
cookies.permanent[:dashboard_templates_order]
|
cookies.permanent[:dashboard_templates_order].presence || 'created_at'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def load_related_submissions
|
def load_related_submissions
|
||||||
@related_submissions = Submission.accessible_by(current_ability)
|
related_submissions = Submission.accessible_by(current_ability)
|
||||||
.left_joins(:template)
|
.left_joins(:template)
|
||||||
.where(archived_at: nil)
|
.where(archived_at: nil)
|
||||||
.where(templates: { archived_at: nil })
|
.where(templates: { archived_at: nil })
|
||||||
.preload(:template_accesses, :created_by_user,
|
.preload(:template_accesses, :created_by_user,
|
||||||
template: :author,
|
template: :author,
|
||||||
submitters: :start_form_submission_events)
|
submitters: :start_form_submission_events)
|
||||||
|
|
||||||
@related_submissions = Submissions.search(current_user, @related_submissions, params[:q])
|
related_submissions = Submissions.search(current_user, related_submissions, params[:q])
|
||||||
.order(id: :desc)
|
.order(id: :desc)
|
||||||
|
|
||||||
@related_submissions_pagy, @related_submissions = pagy_auto(@related_submissions, limit: 5)
|
pagy_auto(related_submissions.select_for_list, limit: 5)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -5,6 +5,8 @@ class TemplatesPreferencesController < ApplicationController
|
|||||||
|
|
||||||
RESETTABLE_PREFERENCE_KEYS = {
|
RESETTABLE_PREFERENCE_KEYS = {
|
||||||
AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY => %w[request_email_subject request_email_body submitters],
|
AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY => %w[request_email_subject request_email_body submitters],
|
||||||
|
AccountConfig::SUBMITTER_VIEW_INVITATION_EMAIL_KEY => %w[invitation_view_email_subject
|
||||||
|
invitation_view_email_body],
|
||||||
AccountConfig::SUBMITTER_INVITATION_REMINDER_EMAIL_KEY => %w[invitation_reminder_email_subject
|
AccountConfig::SUBMITTER_INVITATION_REMINDER_EMAIL_KEY => %w[invitation_reminder_email_subject
|
||||||
invitation_reminder_email_body],
|
invitation_reminder_email_body],
|
||||||
AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY => %w[documents_copy_email_subject documents_copy_email_body],
|
AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY => %w[documents_copy_email_subject documents_copy_email_body],
|
||||||
@@ -48,12 +50,12 @@ class TemplatesPreferencesController < ApplicationController
|
|||||||
def template_params
|
def template_params
|
||||||
params.require(:template).permit(
|
params.require(:template).permit(
|
||||||
preferences: %i[bcc_completed request_email_subject request_email_body
|
preferences: %i[bcc_completed request_email_subject request_email_body
|
||||||
|
invitation_view_email_subject invitation_view_email_body
|
||||||
invitation_reminder_email_subject invitation_reminder_email_body
|
invitation_reminder_email_subject invitation_reminder_email_body
|
||||||
documents_copy_email_subject documents_copy_email_body
|
documents_copy_email_subject documents_copy_email_body
|
||||||
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 validate_unique_submitters
|
||||||
completed_redirect_url validate_unique_submitters
|
|
||||||
require_all_submitters submitters_order require_phone_2fa require_email_2fa
|
require_all_submitters submitters_order require_phone_2fa require_email_2fa
|
||||||
default_expire_at_duration shared_link_2fa default_expire_at request_email_enabled
|
default_expire_at_duration shared_link_2fa default_expire_at request_email_enabled
|
||||||
completed_notification_email_subject completed_notification_email_body
|
completed_notification_email_subject completed_notification_email_body
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ class TemplatesRestoreController < ApplicationController
|
|||||||
load_and_authorize_resource :template
|
load_and_authorize_resource :template
|
||||||
|
|
||||||
def create
|
def create
|
||||||
authorize!(:update, @template)
|
authorize!(:destroy, @template)
|
||||||
|
|
||||||
@template.update!(archived_at: nil)
|
@template.update!(archived_at: nil)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class TemplatesSharedController < ApplicationController
|
||||||
|
def index
|
||||||
|
authorize!(:read, Template)
|
||||||
|
|
||||||
|
@is_archived = params[:archived] == 'true'
|
||||||
|
|
||||||
|
@templates = Templates.shared(current_user)
|
||||||
|
|
||||||
|
@has_archived = !@is_archived && @templates.archived.exists?
|
||||||
|
|
||||||
|
@templates = @is_archived ? @templates.archived : @templates.active
|
||||||
|
|
||||||
|
@templates = @templates.preload(:author, :template_accesses, :template_sharings)
|
||||||
|
.order(id: :desc)
|
||||||
|
|
||||||
|
@templates = Templates.search_shared(current_user, @templates, params[:q])
|
||||||
|
|
||||||
|
@pagy, @templates = pagy_auto(@templates.select_for_list, limit: 12)
|
||||||
|
|
||||||
|
return unless params[:q].present? && @templates.blank?
|
||||||
|
|
||||||
|
@related_submissions_pagy, @related_submissions = load_related_submissions(is_archived: @is_archived)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def load_related_submissions(is_archived:)
|
||||||
|
shared_templates = Templates.shared(current_user)
|
||||||
|
shared_templates = is_archived ? shared_templates.archived : shared_templates.active
|
||||||
|
|
||||||
|
related_submissions =
|
||||||
|
Submission.accessible_by(current_ability)
|
||||||
|
.where(template_id: shared_templates.select(:id))
|
||||||
|
.preload(:template_accesses, :created_by_user,
|
||||||
|
template: :author,
|
||||||
|
submitters: :start_form_submission_events)
|
||||||
|
|
||||||
|
related_submissions = related_submissions.where(archived_at: nil) unless is_archived
|
||||||
|
|
||||||
|
related_submissions = Submissions.search(current_user, related_submissions, params[:q])
|
||||||
|
.order(id: :desc)
|
||||||
|
|
||||||
|
pagy_auto(related_submissions.select_for_list, limit: 5)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -42,6 +42,7 @@ import RequiredCheckboxGroup from './elements/required_checkbox_group'
|
|||||||
import PageContainer from './elements/page_container'
|
import PageContainer from './elements/page_container'
|
||||||
import EmailEditor from './elements/email_editor'
|
import EmailEditor from './elements/email_editor'
|
||||||
import MarkdownEditor from './elements/markdown_editor'
|
import MarkdownEditor from './elements/markdown_editor'
|
||||||
|
import HtmlEditor from './elements/html_editor'
|
||||||
import MountOnClick from './elements/mount_on_click'
|
import MountOnClick from './elements/mount_on_click'
|
||||||
import RemoveOnEvent from './elements/remove_on_event'
|
import RemoveOnEvent from './elements/remove_on_event'
|
||||||
import ScrollTo from './elements/scroll_to'
|
import ScrollTo from './elements/scroll_to'
|
||||||
@@ -135,6 +136,7 @@ safeRegisterElement('required-checkbox-group', RequiredCheckboxGroup)
|
|||||||
safeRegisterElement('page-container', PageContainer)
|
safeRegisterElement('page-container', PageContainer)
|
||||||
safeRegisterElement('email-editor', EmailEditor)
|
safeRegisterElement('email-editor', EmailEditor)
|
||||||
safeRegisterElement('markdown-editor', MarkdownEditor)
|
safeRegisterElement('markdown-editor', MarkdownEditor)
|
||||||
|
safeRegisterElement('html-editor', HtmlEditor)
|
||||||
safeRegisterElement('mount-on-click', MountOnClick)
|
safeRegisterElement('mount-on-click', MountOnClick)
|
||||||
safeRegisterElement('remove-on-event', RemoveOnEvent)
|
safeRegisterElement('remove-on-event', RemoveOnEvent)
|
||||||
safeRegisterElement('scroll-to', ScrollTo)
|
safeRegisterElement('scroll-to', ScrollTo)
|
||||||
@@ -162,6 +164,7 @@ safeRegisterElement('template-builder', class extends HTMLElement {
|
|||||||
this.app = createApp(TemplateBuilder, {
|
this.app = createApp(TemplateBuilder, {
|
||||||
template,
|
template,
|
||||||
customFields: reactive(JSON.parse(this.dataset.customFields || '[]')),
|
customFields: reactive(JSON.parse(this.dataset.customFields || '[]')),
|
||||||
|
dateFormats: JSON.parse(this.dataset.dateFormats || '[]'),
|
||||||
dynamicDocuments: reactive(JSON.parse(this.dataset.dynamicDocuments || '[]')),
|
dynamicDocuments: reactive(JSON.parse(this.dataset.dynamicDocuments || '[]')),
|
||||||
backgroundColor: '#faf7f5',
|
backgroundColor: '#faf7f5',
|
||||||
locale: this.dataset.locale,
|
locale: this.dataset.locale,
|
||||||
|
|||||||
@@ -9,8 +9,9 @@ function loadCodeMirror () {
|
|||||||
import(/* webpackChunkName: "email-editor" */ '@codemirror/commands'),
|
import(/* webpackChunkName: "email-editor" */ '@codemirror/commands'),
|
||||||
import(/* webpackChunkName: "email-editor" */ '@codemirror/language'),
|
import(/* webpackChunkName: "email-editor" */ '@codemirror/language'),
|
||||||
import(/* webpackChunkName: "email-editor" */ '@codemirror/lang-html'),
|
import(/* webpackChunkName: "email-editor" */ '@codemirror/lang-html'),
|
||||||
|
import(/* webpackChunkName: "email-editor" */ '@codemirror/lint'),
|
||||||
import(/* webpackChunkName: "email-editor" */ '@specious/htmlflow')
|
import(/* webpackChunkName: "email-editor" */ '@specious/htmlflow')
|
||||||
]).then(([view, commands, language, html, htmlflow]) => {
|
]).then(([view, commands, language, html, lint, htmlflow]) => {
|
||||||
return {
|
return {
|
||||||
minimalSetup: [
|
minimalSetup: [
|
||||||
commands.history(),
|
commands.history(),
|
||||||
@@ -19,6 +20,8 @@ function loadCodeMirror () {
|
|||||||
],
|
],
|
||||||
EditorView: view.EditorView,
|
EditorView: view.EditorView,
|
||||||
html: html.html,
|
html: html.html,
|
||||||
|
htmlLanguage: html.htmlLanguage,
|
||||||
|
linter: lint.linter,
|
||||||
htmlflow: htmlflow.default || htmlflow
|
htmlflow: htmlflow.default || htmlflow
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -46,6 +49,70 @@ export default targetable(class extends HTMLElement {
|
|||||||
|
|
||||||
this.previewViewTab.addEventListener('click', this.showPreviewView)
|
this.previewViewTab.addEventListener('click', this.showPreviewView)
|
||||||
this.codeViewTab.addEventListener('click', this.showCodeView)
|
this.codeViewTab.addEventListener('click', this.showCodeView)
|
||||||
|
|
||||||
|
this.form = this.closest('form')
|
||||||
|
this.form?.addEventListener('submit', this.validateOnSubmit)
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback () {
|
||||||
|
this.form?.removeEventListener('submit', this.validateOnSubmit)
|
||||||
|
}
|
||||||
|
|
||||||
|
validateOnSubmit = (e) => {
|
||||||
|
if (!this.htmlLanguage) return
|
||||||
|
|
||||||
|
const bodyType = this.form.querySelector('input[name$="[body_type]"]:checked')?.value
|
||||||
|
|
||||||
|
if (bodyType && bodyType !== 'html') return
|
||||||
|
|
||||||
|
const diagnostics = this.buildDiagnostics(this.input.value)
|
||||||
|
|
||||||
|
if (diagnostics.length === 0) return
|
||||||
|
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
this.showCodeView()
|
||||||
|
|
||||||
|
const pos = Math.min(diagnostics[0].from, this.editorView.state.doc.length)
|
||||||
|
|
||||||
|
this.editorView.dispatch({ selection: { anchor: pos }, scrollIntoView: true })
|
||||||
|
this.editorView.focus()
|
||||||
|
|
||||||
|
alert(diagnostics[0].message)
|
||||||
|
}
|
||||||
|
|
||||||
|
buildDiagnostics (value) {
|
||||||
|
const diagnostics = []
|
||||||
|
|
||||||
|
if (!value.trim()) return diagnostics
|
||||||
|
|
||||||
|
if (!/^\s*(<!doctype[^>]*>\s*)?<html/i.test(value)) {
|
||||||
|
diagnostics.push({
|
||||||
|
from: 0,
|
||||||
|
to: Math.min(5, value.length),
|
||||||
|
severity: 'error',
|
||||||
|
message: 'The email template must start with the <html> tag'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const seen = new Set()
|
||||||
|
|
||||||
|
this.htmlLanguage.parser.parse(value).iterate({
|
||||||
|
enter: (node) => {
|
||||||
|
if (!node.type.isError || seen.has(node.from) || seen.size >= 20) return
|
||||||
|
|
||||||
|
seen.add(node.from)
|
||||||
|
|
||||||
|
diagnostics.push({
|
||||||
|
from: node.from,
|
||||||
|
to: Math.min(node.to + 1, value.length),
|
||||||
|
severity: 'error',
|
||||||
|
message: 'The email template contains invalid HTML'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return diagnostics
|
||||||
}
|
}
|
||||||
|
|
||||||
showCodeView = () => {
|
showCodeView = () => {
|
||||||
@@ -76,7 +143,9 @@ export default targetable(class extends HTMLElement {
|
|||||||
this.input = this.querySelector('input[type="hidden"]')
|
this.input = this.querySelector('input[type="hidden"]')
|
||||||
this.input.style.display = 'none'
|
this.input.style.display = 'none'
|
||||||
|
|
||||||
const { EditorView, minimalSetup, html, htmlflow } = await loadCodeMirror()
|
const { EditorView, minimalSetup, html, htmlLanguage, linter, htmlflow } = await loadCodeMirror()
|
||||||
|
|
||||||
|
this.htmlLanguage = htmlLanguage
|
||||||
|
|
||||||
this.editorView = new EditorView({
|
this.editorView = new EditorView({
|
||||||
doc: this.input.value,
|
doc: this.input.value,
|
||||||
@@ -85,8 +154,11 @@ export default targetable(class extends HTMLElement {
|
|||||||
html(),
|
html(),
|
||||||
minimalSetup,
|
minimalSetup,
|
||||||
EditorView.lineWrapping,
|
EditorView.lineWrapping,
|
||||||
|
linter((view) => this.buildDiagnostics(view.state.doc.toString()), { delay: 600 }),
|
||||||
EditorView.updateListener.of(update => {
|
EditorView.updateListener.of(update => {
|
||||||
if (update.docChanged) this.input.value = update.state.doc.toString()
|
if (update.docChanged) {
|
||||||
|
this.input.value = update.state.doc.toString()
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
EditorView.theme({
|
EditorView.theme({
|
||||||
'&': {
|
'&': {
|
||||||
|
|||||||
@@ -0,0 +1,667 @@
|
|||||||
|
import { target, targetable } from '@github/catalyst/lib/targetable'
|
||||||
|
import { actionable } from '@github/catalyst/lib/actionable'
|
||||||
|
import { LinkTooltip } from './markdown_editor'
|
||||||
|
|
||||||
|
async function loadTiptap () {
|
||||||
|
const [core, document, text, hardBreak, gapcursor, dropcursor, extensions, pmState, pmView] = await Promise.all([
|
||||||
|
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/core'),
|
||||||
|
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-document'),
|
||||||
|
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-text'),
|
||||||
|
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-hard-break'),
|
||||||
|
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-gapcursor'),
|
||||||
|
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extension-dropcursor'),
|
||||||
|
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/extensions'),
|
||||||
|
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/pm/state'),
|
||||||
|
import(/* webpackChunkName: "markdown-editor" */ '@tiptap/pm/view')
|
||||||
|
])
|
||||||
|
|
||||||
|
return {
|
||||||
|
Editor: core.Editor,
|
||||||
|
Extension: core.Extension,
|
||||||
|
Node: core.Node,
|
||||||
|
Mark: core.Mark,
|
||||||
|
Document: document.default || document,
|
||||||
|
Text: text.default || text,
|
||||||
|
HardBreak: hardBreak.default || hardBreak,
|
||||||
|
Gapcursor: gapcursor.default || gapcursor,
|
||||||
|
Dropcursor: dropcursor.default || dropcursor,
|
||||||
|
UndoRedo: extensions.UndoRedo,
|
||||||
|
Plugin: pmState.Plugin,
|
||||||
|
Decoration: pmView.Decoration,
|
||||||
|
DecorationSet: pmView.DecorationSet
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const editorStylesheet = new CSSStyleSheet()
|
||||||
|
|
||||||
|
editorStylesheet.replaceSync(`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
max-height: 360px;
|
||||||
|
overflow: auto;
|
||||||
|
border-radius: 0 0 1rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror {
|
||||||
|
word-wrap: break-word;
|
||||||
|
-webkit-font-variant-ligatures: none;
|
||||||
|
font-variant-ligatures: none;
|
||||||
|
font-feature-settings: "liga" 0;
|
||||||
|
outline: none;
|
||||||
|
min-height: 220px;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
img.ProseMirror-separator {
|
||||||
|
display: inline !important;
|
||||||
|
border: none !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
width: 0 !important;
|
||||||
|
height: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror-gapcursor {
|
||||||
|
display: none;
|
||||||
|
pointer-events: none;
|
||||||
|
position: absolute;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror-gapcursor:after {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: -2px;
|
||||||
|
width: 20px;
|
||||||
|
border-top: 1px solid black;
|
||||||
|
animation: ProseMirror-cursor-blink 1.1s steps(2, start) infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ProseMirror-cursor-blink {
|
||||||
|
to {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror-hideselection *::selection {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror-hideselection *::-moz-selection {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror-hideselection * {
|
||||||
|
caret-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ProseMirror-focused .ProseMirror-gapcursor {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.variable-highlight {
|
||||||
|
background-color: #fef3c7;
|
||||||
|
padding: 1px 2px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
|
||||||
|
const DROP_ATTRS = [
|
||||||
|
'srcdoc', 'xlink:href', 'srcset', 'action', 'formaction', 'poster',
|
||||||
|
'background', 'data', 'cite', 'ping', 'longdesc', 'manifest', 'profile'
|
||||||
|
]
|
||||||
|
|
||||||
|
const SAFE_URL_REGEXP = /^(?:https?:\/\/|data:image\/|blob:|mailto:|tel:|\{|#)/i
|
||||||
|
|
||||||
|
function isSafeAttr (name, value) {
|
||||||
|
const lowerName = name.toLowerCase()
|
||||||
|
|
||||||
|
if (lowerName.startsWith('on') || DROP_ATTRS.includes(lowerName)) return false
|
||||||
|
if ((lowerName === 'href' || lowerName === 'src') && !SAFE_URL_REGEXP.test(value.trim())) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectDomAttrs (dom) {
|
||||||
|
const attrs = {}
|
||||||
|
|
||||||
|
for (let i = 0; i < dom.attributes.length; i++) {
|
||||||
|
const { name, value } = dom.attributes[i]
|
||||||
|
|
||||||
|
if (isSafeAttr(name, value)) attrs[name] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
return { htmlAttrs: attrs }
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectSpanDomAttrs (dom) {
|
||||||
|
const result = collectDomAttrs(dom)
|
||||||
|
|
||||||
|
if (result.htmlAttrs.style) {
|
||||||
|
const temp = document.createElement('span')
|
||||||
|
|
||||||
|
temp.style.cssText = result.htmlAttrs.style
|
||||||
|
|
||||||
|
if (['bold', '700'].includes(temp.style.fontWeight)) {
|
||||||
|
temp.style.removeProperty('font-weight')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (temp.style.fontStyle === 'italic') {
|
||||||
|
temp.style.removeProperty('font-style')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (temp.style.textDecoration === 'underline') {
|
||||||
|
temp.style.removeProperty('text-decoration')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (temp.style.cssText) {
|
||||||
|
result.htmlAttrs.style = temp.style.cssText
|
||||||
|
} else {
|
||||||
|
delete result.htmlAttrs.style
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildExtensions ({ Node, Mark, Extension, Plugin, Decoration, DecorationSet }) {
|
||||||
|
const blockNode = (name, tag, content, extra = {}) => Node.create({
|
||||||
|
name,
|
||||||
|
group: 'block',
|
||||||
|
content: content || 'block+',
|
||||||
|
...extra,
|
||||||
|
addAttributes () {
|
||||||
|
return { htmlAttrs: { default: {} } }
|
||||||
|
},
|
||||||
|
parseHTML () {
|
||||||
|
return [{ tag, getAttrs: collectDomAttrs }]
|
||||||
|
},
|
||||||
|
renderHTML ({ node }) {
|
||||||
|
return [tag, node.attrs.htmlAttrs, 0]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const attrsMark = (name, tag) => Mark.create({
|
||||||
|
name,
|
||||||
|
addAttributes () {
|
||||||
|
return { htmlAttrs: { default: {} } }
|
||||||
|
},
|
||||||
|
parseHTML () {
|
||||||
|
return [{ tag, getAttrs: collectDomAttrs }]
|
||||||
|
},
|
||||||
|
renderHTML ({ mark }) {
|
||||||
|
return [tag, mark.attrs.htmlAttrs, 0]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const SpanMark = Mark.create({
|
||||||
|
name: 'span',
|
||||||
|
excludes: '',
|
||||||
|
addAttributes () {
|
||||||
|
return { htmlAttrs: { default: {} } }
|
||||||
|
},
|
||||||
|
parseHTML () {
|
||||||
|
return [{ tag: 'span', getAttrs: collectSpanDomAttrs }]
|
||||||
|
},
|
||||||
|
renderHTML ({ mark }) {
|
||||||
|
return ['span', mark.attrs.htmlAttrs, 0]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const toggleMark = (name, renderTag, parseRules, shortcuts) => Mark.create({
|
||||||
|
name,
|
||||||
|
parseHTML () {
|
||||||
|
return parseRules
|
||||||
|
},
|
||||||
|
renderHTML () {
|
||||||
|
return [renderTag, 0]
|
||||||
|
},
|
||||||
|
addCommands () {
|
||||||
|
const commandName = `toggle${name[0].toUpperCase()}${name.slice(1)}`
|
||||||
|
|
||||||
|
return {
|
||||||
|
[commandName]: () => ({ commands }) => commands.toggleMark(name)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addKeyboardShortcuts () {
|
||||||
|
return {
|
||||||
|
[shortcuts]: () => this.editor.commands.toggleMark(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const Heading = Node.create({
|
||||||
|
name: 'heading',
|
||||||
|
group: 'block',
|
||||||
|
content: 'inline*',
|
||||||
|
addAttributes () {
|
||||||
|
return {
|
||||||
|
htmlAttrs: { default: {} },
|
||||||
|
level: { default: 1 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
parseHTML () {
|
||||||
|
return [1, 2, 3, 4, 5, 6].map((level) => ({
|
||||||
|
tag: `h${level}`,
|
||||||
|
getAttrs: (dom) => ({ ...collectDomAttrs(dom), level })
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
renderHTML ({ node }) {
|
||||||
|
return [`h${node.attrs.level}`, node.attrs.htmlAttrs, 0]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const ImageNode = Node.create({
|
||||||
|
name: 'image',
|
||||||
|
inline: true,
|
||||||
|
group: 'inline',
|
||||||
|
draggable: true,
|
||||||
|
addAttributes () {
|
||||||
|
return { htmlAttrs: { default: {} } }
|
||||||
|
},
|
||||||
|
parseHTML () {
|
||||||
|
return [{ tag: 'img', getAttrs: collectDomAttrs }]
|
||||||
|
},
|
||||||
|
renderHTML ({ node }) {
|
||||||
|
return ['img', node.attrs.htmlAttrs]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const HrNode = Node.create({
|
||||||
|
name: 'horizontalRule',
|
||||||
|
group: 'block',
|
||||||
|
atom: true,
|
||||||
|
addAttributes () {
|
||||||
|
return { htmlAttrs: { default: {} } }
|
||||||
|
},
|
||||||
|
parseHTML () {
|
||||||
|
return [{ tag: 'hr', getAttrs: collectDomAttrs }]
|
||||||
|
},
|
||||||
|
renderHTML ({ node }) {
|
||||||
|
return ['hr', node.attrs.htmlAttrs]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const StyleNode = Node.create({
|
||||||
|
name: 'style',
|
||||||
|
group: 'block',
|
||||||
|
atom: true,
|
||||||
|
selectable: false,
|
||||||
|
addAttributes () {
|
||||||
|
return {
|
||||||
|
htmlAttrs: { default: {} },
|
||||||
|
css: { default: '' }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
parseHTML () {
|
||||||
|
return [{ tag: 'style', getAttrs: (dom) => ({ ...collectDomAttrs(dom), css: dom.textContent }) }]
|
||||||
|
},
|
||||||
|
renderHTML ({ node }) {
|
||||||
|
return ['style', node.attrs.htmlAttrs, node.attrs.css]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const EmptySpanNode = Node.create({
|
||||||
|
name: 'emptySpan',
|
||||||
|
inline: true,
|
||||||
|
group: 'inline',
|
||||||
|
atom: true,
|
||||||
|
addAttributes () {
|
||||||
|
return { htmlAttrs: { default: {} } }
|
||||||
|
},
|
||||||
|
parseHTML () {
|
||||||
|
return [{
|
||||||
|
tag: 'span',
|
||||||
|
priority: 60,
|
||||||
|
getAttrs (dom) {
|
||||||
|
if (dom.childNodes.length === 0 && dom.attributes.length > 0) {
|
||||||
|
return collectDomAttrs(dom)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
renderHTML ({ node }) {
|
||||||
|
return ['span', node.attrs.htmlAttrs]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const LinkMark = Mark.create({
|
||||||
|
name: 'link',
|
||||||
|
inclusive: true,
|
||||||
|
addAttributes () {
|
||||||
|
return { htmlAttrs: { default: {} } }
|
||||||
|
},
|
||||||
|
parseHTML () {
|
||||||
|
return [{ tag: 'a', getAttrs: collectDomAttrs }]
|
||||||
|
},
|
||||||
|
renderHTML ({ mark }) {
|
||||||
|
return ['a', mark.attrs.htmlAttrs, 0]
|
||||||
|
},
|
||||||
|
addCommands () {
|
||||||
|
return {
|
||||||
|
setLink: ({ href }) => ({ editor, commands }) => {
|
||||||
|
const htmlAttrs = { ...(editor.getAttributes('link').htmlAttrs || {}), href }
|
||||||
|
|
||||||
|
return commands.setMark('link', { htmlAttrs })
|
||||||
|
},
|
||||||
|
unsetLink: () => ({ commands }) => commands.unsetMark('link', { extendEmptyMarkRange: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const buildDecorations = (doc) => {
|
||||||
|
const decorations = []
|
||||||
|
const regex = /\{\{?[a-zA-Z0-9_.-]+\}\}?/g
|
||||||
|
|
||||||
|
doc.descendants((node, pos) => {
|
||||||
|
if (!node.isText) return
|
||||||
|
|
||||||
|
let match
|
||||||
|
|
||||||
|
while ((match = regex.exec(node.text)) !== null) {
|
||||||
|
decorations.push(
|
||||||
|
Decoration.inline(pos + match.index, pos + match.index + match[0].length, {
|
||||||
|
class: 'variable-highlight'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return DecorationSet.create(doc, decorations)
|
||||||
|
}
|
||||||
|
|
||||||
|
const VariableHighlight = Extension.create({
|
||||||
|
name: 'variableHighlight',
|
||||||
|
addProseMirrorPlugins () {
|
||||||
|
return [new Plugin({
|
||||||
|
state: {
|
||||||
|
init (_, { doc }) {
|
||||||
|
return buildDecorations(doc)
|
||||||
|
},
|
||||||
|
apply (tr, oldSet) {
|
||||||
|
return tr.docChanged ? buildDecorations(tr.doc) : oldSet
|
||||||
|
}
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
decorations (state) {
|
||||||
|
return this.getState(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return [
|
||||||
|
blockNode('paragraph', 'p', 'inline*'),
|
||||||
|
Heading,
|
||||||
|
blockNode('section', 'section'),
|
||||||
|
blockNode('article', 'article', null, { isolating: true }),
|
||||||
|
blockNode('header', 'header', null, { isolating: true }),
|
||||||
|
blockNode('footer', 'footer', null, { isolating: true }),
|
||||||
|
blockNode('div', 'div'),
|
||||||
|
blockNode('center', 'center'),
|
||||||
|
blockNode('blockquote', 'blockquote'),
|
||||||
|
blockNode('pre', 'pre'),
|
||||||
|
blockNode('orderedList', 'ol', '(listItem | block)+'),
|
||||||
|
blockNode('bulletList', 'ul', '(listItem | block)+'),
|
||||||
|
blockNode('listItem', 'li', 'block+', { group: null }),
|
||||||
|
blockNode('table', 'table', '(colgroup | tableHead | tableBody | tableFoot | tableRow)+'),
|
||||||
|
blockNode('tableHead', 'thead', 'tableRow+', { group: null }),
|
||||||
|
blockNode('tableBody', 'tbody', 'tableRow+', { group: null }),
|
||||||
|
blockNode('tableFoot', 'tfoot', 'tableRow+', { group: null }),
|
||||||
|
blockNode('tableRow', 'tr', '(tableCell | tableHeader)+', { group: null }),
|
||||||
|
blockNode('tableCell', 'td', 'block*', { group: null }),
|
||||||
|
blockNode('tableHeader', 'th', 'block*', { group: null }),
|
||||||
|
blockNode('colgroup', 'colgroup', 'col*', { group: null }),
|
||||||
|
Node.create({
|
||||||
|
name: 'col',
|
||||||
|
atom: true,
|
||||||
|
addAttributes () {
|
||||||
|
return { htmlAttrs: { default: {} } }
|
||||||
|
},
|
||||||
|
parseHTML () {
|
||||||
|
return [{ tag: 'col', getAttrs: collectDomAttrs }]
|
||||||
|
},
|
||||||
|
renderHTML ({ node }) {
|
||||||
|
return ['col', node.attrs.htmlAttrs]
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
ImageNode,
|
||||||
|
HrNode,
|
||||||
|
StyleNode,
|
||||||
|
EmptySpanNode,
|
||||||
|
SpanMark,
|
||||||
|
LinkMark,
|
||||||
|
toggleMark('bold', 'strong', [{ tag: 'strong' }, { tag: 'b' }, { style: 'font-weight=bold' }, { style: 'font-weight=700' }], 'Mod-b'),
|
||||||
|
toggleMark('italic', 'em', [{ tag: 'em' }, { tag: 'i' }, { style: 'font-style=italic' }], 'Mod-i'),
|
||||||
|
toggleMark('underline', 'u', [{ tag: 'u' }, { style: 'text-decoration=underline' }], 'Mod-u'),
|
||||||
|
toggleMark('strike', 's', [{ tag: 's' }, { tag: 'del' }, { tag: 'strike' }, { style: 'text-decoration=line-through' }], 'Mod-Shift-s'),
|
||||||
|
attrsMark('subscript', 'sub'),
|
||||||
|
attrsMark('superscript', 'sup'),
|
||||||
|
VariableHighlight
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default actionable(targetable(class extends HTMLElement {
|
||||||
|
static [target.static] = [
|
||||||
|
'textarea',
|
||||||
|
'editorElement',
|
||||||
|
'boldButton',
|
||||||
|
'italicButton',
|
||||||
|
'underlineButton',
|
||||||
|
'linkButton',
|
||||||
|
'linkTooltipTemplate'
|
||||||
|
]
|
||||||
|
|
||||||
|
async connectedCallback () {
|
||||||
|
if (!this.textarea || !this.editorElement) return
|
||||||
|
|
||||||
|
this.textarea.style.display = 'none'
|
||||||
|
this.adjustShortcutsForPlatform()
|
||||||
|
|
||||||
|
const tiptap = await loadTiptap()
|
||||||
|
|
||||||
|
const { Editor, Extension, Document, Text, HardBreak, UndoRedo, Gapcursor, Dropcursor } = tiptap
|
||||||
|
|
||||||
|
this.emailDocument = new DOMParser().parseFromString(this.textarea.value, 'text/html')
|
||||||
|
|
||||||
|
const shadow = this.editorElement.attachShadow({ mode: 'open' })
|
||||||
|
|
||||||
|
shadow.adoptedStyleSheets = [editorStylesheet]
|
||||||
|
|
||||||
|
this.emailDocument.head.querySelectorAll('style').forEach((style) => {
|
||||||
|
shadow.appendChild(style.cloneNode(true))
|
||||||
|
})
|
||||||
|
|
||||||
|
const container = document.createElement('div')
|
||||||
|
const bodyStyle = this.emailDocument.body.getAttribute('style')
|
||||||
|
|
||||||
|
if (bodyStyle) container.setAttribute('style', bodyStyle)
|
||||||
|
|
||||||
|
shadow.appendChild(container)
|
||||||
|
|
||||||
|
const LinkShortcut = Extension.create({
|
||||||
|
name: 'linkShortcut',
|
||||||
|
addKeyboardShortcuts: () => ({
|
||||||
|
'Mod-k': () => {
|
||||||
|
this.toggleLink()
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
this.editor = new Editor({
|
||||||
|
element: container,
|
||||||
|
extensions: [
|
||||||
|
Document,
|
||||||
|
Text,
|
||||||
|
HardBreak,
|
||||||
|
UndoRedo,
|
||||||
|
Gapcursor,
|
||||||
|
Dropcursor,
|
||||||
|
...buildExtensions(tiptap),
|
||||||
|
LinkShortcut
|
||||||
|
],
|
||||||
|
content: this.emailDocument.body.innerHTML,
|
||||||
|
injectCSS: false,
|
||||||
|
editorProps: {
|
||||||
|
attributes: {
|
||||||
|
dir: 'auto'
|
||||||
|
},
|
||||||
|
handleDOMEvents: {
|
||||||
|
click: (_, event) => {
|
||||||
|
if (event.target.closest('a')) event.preventDefault()
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onUpdate: ({ editor }) => {
|
||||||
|
this.emailDocument.body.innerHTML = editor.getHTML()
|
||||||
|
|
||||||
|
this.textarea.value = this.emailDocument.documentElement.outerHTML
|
||||||
|
this.textarea.dispatchEvent(new Event('input', { bubbles: true }))
|
||||||
|
},
|
||||||
|
onSelectionUpdate: ({ editor }) => {
|
||||||
|
this.updateToolbarState()
|
||||||
|
this.handleLinkTooltip(editor)
|
||||||
|
},
|
||||||
|
onBlur: () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!this.linkTooltip.tooltip.contains(document.activeElement)) {
|
||||||
|
this.linkTooltip.hide()
|
||||||
|
}
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.linkTooltip = new LinkTooltip(this, this.editor, this.linkTooltipTemplate)
|
||||||
|
}
|
||||||
|
|
||||||
|
adjustShortcutsForPlatform () {
|
||||||
|
if ((navigator.userAgentData?.platform)?.toLowerCase()?.includes('mac')) {
|
||||||
|
this.querySelectorAll('.tooltip[data-tip]').forEach(tooltip => {
|
||||||
|
const tip = tooltip.getAttribute('data-tip')
|
||||||
|
|
||||||
|
if (tip && tip.includes('Ctrl')) {
|
||||||
|
tooltip.setAttribute('data-tip', tip.replace(/Ctrl/g, '⌘'))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bold (e) {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
this.editor.chain().focus().toggleBold().run()
|
||||||
|
this.updateToolbarState()
|
||||||
|
}
|
||||||
|
|
||||||
|
italic (e) {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
this.editor.chain().focus().toggleItalic().run()
|
||||||
|
this.updateToolbarState()
|
||||||
|
}
|
||||||
|
|
||||||
|
underline (e) {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
this.editor.chain().focus().toggleUnderline().run()
|
||||||
|
this.updateToolbarState()
|
||||||
|
}
|
||||||
|
|
||||||
|
linkSelection (e) {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
this.toggleLink()
|
||||||
|
this.updateToolbarState()
|
||||||
|
}
|
||||||
|
|
||||||
|
undo (e) {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
this.editor.chain().focus().undo().run()
|
||||||
|
this.updateToolbarState()
|
||||||
|
}
|
||||||
|
|
||||||
|
redo (e) {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
this.editor.chain().focus().redo().run()
|
||||||
|
this.updateToolbarState()
|
||||||
|
}
|
||||||
|
|
||||||
|
updateToolbarState () {
|
||||||
|
this.boldButton.classList.toggle('bg-base-200', this.editor.isActive('bold'))
|
||||||
|
this.italicButton.classList.toggle('bg-base-200', this.editor.isActive('italic'))
|
||||||
|
this.underlineButton.classList.toggle('bg-base-200', this.editor.isActive('underline'))
|
||||||
|
this.linkButton.classList.toggle('bg-base-200', this.editor.isActive('link'))
|
||||||
|
}
|
||||||
|
|
||||||
|
handleLinkTooltip (editor) {
|
||||||
|
const { from } = editor.state.selection
|
||||||
|
const mark = editor.state.doc.resolve(from).marks().find(m => m.type.name === 'link')
|
||||||
|
|
||||||
|
if (!mark) {
|
||||||
|
if (this.linkTooltip.isVisible()) this.linkTooltip.hide()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.linkTooltip.isVisible() && this.linkTooltip.currentMark === mark) return
|
||||||
|
|
||||||
|
let linkStart = from
|
||||||
|
const start = editor.state.doc.resolve(from).start()
|
||||||
|
|
||||||
|
for (let i = from - 1; i >= start; i--) {
|
||||||
|
if (editor.state.doc.resolve(i).marks().some(m => m.eq(mark))) {
|
||||||
|
linkStart = i
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.linkTooltip.hide()
|
||||||
|
this.linkTooltip.show(mark.attrs.htmlAttrs?.href, linkStart > start ? linkStart - 1 : linkStart)
|
||||||
|
this.linkTooltip.currentMark = mark
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleLink () {
|
||||||
|
if (this.editor.isActive('link')) {
|
||||||
|
this.linkTooltip.hide()
|
||||||
|
this.editor.chain().focus().extendMarkRange('link').unsetLink().run()
|
||||||
|
this.updateToolbarState()
|
||||||
|
} else {
|
||||||
|
const { from } = this.editor.state.selection
|
||||||
|
|
||||||
|
this.linkTooltip.hide()
|
||||||
|
this.linkTooltip.show(this.editor.getAttributes('link').htmlAttrs?.href, from, { focus: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
insertVariable (e) {
|
||||||
|
const variable = e.target.closest('[data-variable]')?.dataset.variable
|
||||||
|
|
||||||
|
if (variable) {
|
||||||
|
const { from, to } = this.editor.state.selection
|
||||||
|
|
||||||
|
if (variable.includes('link') && from !== to) {
|
||||||
|
this.editor.chain().focus().setLink({ href: `{${variable}}` }).run()
|
||||||
|
} else {
|
||||||
|
this.editor.chain().focus().insertContent(`{${variable}}`).run()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback () {
|
||||||
|
this.linkTooltip?.hide()
|
||||||
|
|
||||||
|
if (this.editor) {
|
||||||
|
this.editor.destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
@@ -35,7 +35,7 @@ function loadTiptap () {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
class LinkTooltip {
|
export class LinkTooltip {
|
||||||
constructor (container, editor, templateEl) {
|
constructor (container, editor, templateEl) {
|
||||||
this.container = container
|
this.container = container
|
||||||
this.editor = editor
|
this.editor = editor
|
||||||
|
|||||||
@@ -1,19 +1,5 @@
|
|||||||
export default class extends HTMLElement {
|
export default class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
this.input.addEventListener('focus', () => {
|
|
||||||
if (this.title) {
|
|
||||||
this.title.classList.add('hidden', 'md:block')
|
|
||||||
this.input.classList.add('w-60')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
this.input.addEventListener('blur', (e) => {
|
|
||||||
if (this.title && !e.target.value) {
|
|
||||||
this.title.classList.remove('hidden')
|
|
||||||
this.input.classList.remove('w-60')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
this.button.addEventListener('click', (event) => {
|
this.button.addEventListener('click', (event) => {
|
||||||
if (!this.input.value && document.activeElement !== this.input) {
|
if (!this.input.value && document.activeElement !== this.input) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
@@ -21,16 +7,22 @@ export default class extends HTMLElement {
|
|||||||
this.input.focus()
|
this.input.focus()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
document.addEventListener('turbo:before-cache', this.onBeforeCache)
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback () {
|
||||||
|
document.removeEventListener('turbo:before-cache', this.onBeforeCache)
|
||||||
|
}
|
||||||
|
|
||||||
|
onBeforeCache = () => {
|
||||||
|
this.input.value = this.input.getAttribute('value') || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
get input () {
|
get input () {
|
||||||
return this.querySelector('input')
|
return this.querySelector('input')
|
||||||
}
|
}
|
||||||
|
|
||||||
get title () {
|
|
||||||
return document.querySelector(this.dataset.title)
|
|
||||||
}
|
|
||||||
|
|
||||||
get button () {
|
get button () {
|
||||||
return this.querySelector('button')
|
return this.querySelector('button')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -147,7 +147,7 @@
|
|||||||
@submit.prevent="submitStep"
|
@submit.prevent="submitStep"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
v-if="!isFormVisible"
|
v-if="!isFormVisible && currentField"
|
||||||
id="expand_form_button"
|
id="expand_form_button"
|
||||||
class="btn btn-neutral flex text-white absolute bottom-0 w-full mb-3 expand-form-button text-base"
|
class="btn btn-neutral flex text-white absolute bottom-0 w-full mb-3 expand-form-button text-base"
|
||||||
style="width: 96%; margin-left: 2%"
|
style="width: 96%; margin-left: 2%"
|
||||||
@@ -174,6 +174,7 @@
|
|||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<div
|
<div
|
||||||
|
v-if="currentField"
|
||||||
v-show="isFormVisible"
|
v-show="isFormVisible"
|
||||||
id="form_container"
|
id="form_container"
|
||||||
class="shadow-md bg-base-100 absolute bottom-0 w-full border-base-200 border p-4 rounded form-container overflow-hidden"
|
class="shadow-md bg-base-100 absolute bottom-0 w-full border-base-200 border p-4 rounded form-container overflow-hidden"
|
||||||
@@ -289,6 +290,7 @@
|
|||||||
:id="currentField.uuid"
|
:id="currentField.uuid"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
:required="currentField.required"
|
:required="currentField.required"
|
||||||
|
:aria-label="showFieldNames && (currentField.name || currentField.title) ? undefined : (currentField.name || currentField.title || t('select_your_option'))"
|
||||||
:aria-describedby="currentField.description ? currentField.uuid + '-desc' : undefined"
|
:aria-describedby="currentField.description ? currentField.uuid + '-desc' : undefined"
|
||||||
class="select base-input !text-2xl w-full text-center font-normal"
|
class="select base-input !text-2xl w-full text-center font-normal"
|
||||||
:class="{ 'text-gray-300': !values[currentField.uuid] }"
|
:class="{ 'text-gray-300': !values[currentField.uuid] }"
|
||||||
@@ -317,7 +319,7 @@
|
|||||||
<div v-else-if="currentField.type === 'radio'">
|
<div v-else-if="currentField.type === 'radio'">
|
||||||
<label
|
<label
|
||||||
v-if="showFieldNames && (currentField.name || currentField.title)"
|
v-if="showFieldNames && (currentField.name || currentField.title)"
|
||||||
:for="currentField.uuid"
|
:id="currentField.uuid + '-radio-label'"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5 field-name-label"
|
class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5 field-name-label"
|
||||||
:class="{ 'mb-2': !currentField.description }"
|
:class="{ 'mb-2': !currentField.description }"
|
||||||
@@ -356,6 +358,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="space-y-3.5 mx-auto"
|
class="space-y-3.5 mx-auto"
|
||||||
|
role="radiogroup"
|
||||||
|
:aria-labelledby="(currentField.name || currentField.title) ? currentField.uuid + '-radio-label' : null"
|
||||||
:class="{ hidden: !showFieldNames || (currentField.options.every((e) => !e.value) && currentField.options.length > 4) }"
|
:class="{ hidden: !showFieldNames || (currentField.options.every((e) => !e.value) && currentField.options.length > 4) }"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -1169,7 +1173,11 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
isAnonymousChecboxes () {
|
isAnonymousChecboxes () {
|
||||||
return this.currentField.type === 'checkbox' && this.currentStepFields.every((e) => !e.name && !e.required) && this.currentStepFields.length > 4
|
if (this.currentField) {
|
||||||
|
return this.currentField.type === 'checkbox' && this.currentStepFields.every((e) => !e.name && !e.required) && this.currentStepFields.length > 4
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
isButtonDisabled () {
|
isButtonDisabled () {
|
||||||
if (this.recalculateButtonDisabledKey) {
|
if (this.recalculateButtonDisabledKey) {
|
||||||
@@ -1567,6 +1575,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
goToStep (stepIndex, scrollToArea = false, clickUpload = false) {
|
goToStep (stepIndex, scrollToArea = false, clickUpload = false) {
|
||||||
|
this.isInvite = false
|
||||||
this.currentStep = stepIndex
|
this.currentStep = stepIndex
|
||||||
this.showFillAllRequiredFields = false
|
this.showFillAllRequiredFields = false
|
||||||
|
|
||||||
@@ -1591,6 +1600,10 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
saveStep (formData) {
|
saveStep (formData) {
|
||||||
|
if (!formData && !this.$refs.form) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const currentFieldUuids = this.currentStepFields.map((f) => f.uuid)
|
const currentFieldUuids = this.currentStepFields.map((f) => f.uuid)
|
||||||
const currentFieldType = this.currentField.type
|
const currentFieldType = this.currentField.type
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ const en = {
|
|||||||
close: 'Close',
|
close: 'Close',
|
||||||
uploaded_files: 'Uploaded files',
|
uploaded_files: 'Uploaded files',
|
||||||
signature_drawing_area: 'Signature drawing area. Use mouse or touch to draw your signature.',
|
signature_drawing_area: 'Signature drawing area. Use mouse or touch to draw your signature.',
|
||||||
|
initials_drawing_area: 'Initials drawing area. Use mouse or touch to draw your initials.',
|
||||||
kba: 'KBA',
|
kba: 'KBA',
|
||||||
please_upload_an_image_file: 'Please upload an image file',
|
please_upload_an_image_file: 'Please upload an image file',
|
||||||
must_be_characters_length: 'Must be {number} characters long',
|
must_be_characters_length: 'Must be {number} characters long',
|
||||||
@@ -88,6 +89,7 @@ const en = {
|
|||||||
please_check_the_box_to_continue: 'Please check the box to continue.',
|
please_check_the_box_to_continue: 'Please check the box to continue.',
|
||||||
open_source_documents_software: 'open source documents software',
|
open_source_documents_software: 'open source documents software',
|
||||||
verified_phone_number: 'Verify Phone Number',
|
verified_phone_number: 'Verify Phone Number',
|
||||||
|
country_code: 'Country code',
|
||||||
use_international_format: 'Use international format: +1xxx',
|
use_international_format: 'Use international format: +1xxx',
|
||||||
six_digits_code: '6-digit code',
|
six_digits_code: '6-digit code',
|
||||||
change_phone_number: 'Change phone number',
|
change_phone_number: 'Change phone number',
|
||||||
@@ -120,6 +122,7 @@ const es = {
|
|||||||
close: 'Cerrar',
|
close: 'Cerrar',
|
||||||
uploaded_files: 'Archivos subidos',
|
uploaded_files: 'Archivos subidos',
|
||||||
signature_drawing_area: 'Área de dibujo de firma. Use el ratón o toque para dibujar su firma.',
|
signature_drawing_area: 'Área de dibujo de firma. Use el ratón o toque para dibujar su firma.',
|
||||||
|
initials_drawing_area: 'Área de dibujo de iniciales. Use el ratón o toque para dibujar sus iniciales.',
|
||||||
kba: 'KBA',
|
kba: 'KBA',
|
||||||
please_upload_an_image_file: 'Por favor, sube un archivo de imagen',
|
please_upload_an_image_file: 'Por favor, sube un archivo de imagen',
|
||||||
must_be_characters_length: 'Debe tener {number} caracteres de longitud',
|
must_be_characters_length: 'Debe tener {number} caracteres de longitud',
|
||||||
@@ -204,6 +207,7 @@ const es = {
|
|||||||
please_check_the_box_to_continue: 'Por favor marque la casilla para continuar.',
|
please_check_the_box_to_continue: 'Por favor marque la casilla para continuar.',
|
||||||
open_source_documents_software: 'software de documentos de código abierto',
|
open_source_documents_software: 'software de documentos de código abierto',
|
||||||
verified_phone_number: 'Verificar número de teléfono',
|
verified_phone_number: 'Verificar número de teléfono',
|
||||||
|
country_code: 'Código de país',
|
||||||
use_international_format: 'Usar formato internacional: +1xxx',
|
use_international_format: 'Usar formato internacional: +1xxx',
|
||||||
six_digits_code: 'Código de 6 dígitos',
|
six_digits_code: 'Código de 6 dígitos',
|
||||||
change_phone_number: 'Cambiar número de teléfono',
|
change_phone_number: 'Cambiar número de teléfono',
|
||||||
@@ -236,6 +240,7 @@ const it = {
|
|||||||
close: 'Chiudi',
|
close: 'Chiudi',
|
||||||
uploaded_files: 'File caricati',
|
uploaded_files: 'File caricati',
|
||||||
signature_drawing_area: 'Area di disegno della firma. Usa il mouse o il tocco per disegnare la tua firma.',
|
signature_drawing_area: 'Area di disegno della firma. Usa il mouse o il tocco per disegnare la tua firma.',
|
||||||
|
initials_drawing_area: 'Area di disegno delle iniziali. Usa il mouse o il tocco per disegnare le tue iniziali.',
|
||||||
kba: 'KBA',
|
kba: 'KBA',
|
||||||
please_upload_an_image_file: 'Per favore carica un file immagine',
|
please_upload_an_image_file: 'Per favore carica un file immagine',
|
||||||
must_be_characters_length: 'Deve essere lungo {number} caratteri',
|
must_be_characters_length: 'Deve essere lungo {number} caratteri',
|
||||||
@@ -320,6 +325,7 @@ const it = {
|
|||||||
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',
|
||||||
|
country_code: 'Prefisso internazionale',
|
||||||
use_international_format: 'Usa formato internazionale: +1xxx',
|
use_international_format: 'Usa formato internazionale: +1xxx',
|
||||||
six_digits_code: 'Codice a 6 cifre',
|
six_digits_code: 'Codice a 6 cifre',
|
||||||
change_phone_number: 'Cambia numero di telefono',
|
change_phone_number: 'Cambia numero di telefono',
|
||||||
@@ -352,6 +358,7 @@ const de = {
|
|||||||
close: 'Schließen',
|
close: 'Schließen',
|
||||||
uploaded_files: 'Hochgeladene Dateien',
|
uploaded_files: 'Hochgeladene Dateien',
|
||||||
signature_drawing_area: 'Unterschriftszeichenbereich. Verwenden Sie die Maus oder Berührung, um Ihre Unterschrift zu zeichnen.',
|
signature_drawing_area: 'Unterschriftszeichenbereich. Verwenden Sie die Maus oder Berührung, um Ihre Unterschrift zu zeichnen.',
|
||||||
|
initials_drawing_area: 'Zeichenbereich für Initialen. Verwenden Sie die Maus oder Berührung, um Ihre Initialen zu zeichnen.',
|
||||||
kba: 'KBA',
|
kba: 'KBA',
|
||||||
please_upload_an_image_file: 'Bitte laden Sie eine Bilddatei hoch',
|
please_upload_an_image_file: 'Bitte laden Sie eine Bilddatei hoch',
|
||||||
must_be_characters_length: 'Muss {number} Zeichen lang sein',
|
must_be_characters_length: 'Muss {number} Zeichen lang sein',
|
||||||
@@ -436,6 +443,7 @@ const de = {
|
|||||||
please_check_the_box_to_continue: 'Bitte aktivieren Sie das Kontrollkästchen, um fortzufahren.',
|
please_check_the_box_to_continue: 'Bitte aktivieren Sie das Kontrollkästchen, um fortzufahren.',
|
||||||
open_source_documents_software: 'Open-Source-Dokumentensoftware',
|
open_source_documents_software: 'Open-Source-Dokumentensoftware',
|
||||||
verified_phone_number: 'Telefonnummer verifizieren',
|
verified_phone_number: 'Telefonnummer verifizieren',
|
||||||
|
country_code: 'Ländervorwahl',
|
||||||
use_international_format: 'Internationales Format verwenden: +1xxx',
|
use_international_format: 'Internationales Format verwenden: +1xxx',
|
||||||
six_digits_code: '6-stelliger Code',
|
six_digits_code: '6-stelliger Code',
|
||||||
change_phone_number: 'Telefonnummer ändern',
|
change_phone_number: 'Telefonnummer ändern',
|
||||||
@@ -468,6 +476,7 @@ const fr = {
|
|||||||
close: 'Fermer',
|
close: 'Fermer',
|
||||||
uploaded_files: 'Fichiers téléchargés',
|
uploaded_files: 'Fichiers téléchargés',
|
||||||
signature_drawing_area: 'Zone de dessin de signature. Utilisez la souris ou le toucher pour dessiner votre signature.',
|
signature_drawing_area: 'Zone de dessin de signature. Utilisez la souris ou le toucher pour dessiner votre signature.',
|
||||||
|
initials_drawing_area: 'Zone de dessin des initiales. Utilisez la souris ou le toucher pour dessiner vos initiales.',
|
||||||
kba: 'KBA',
|
kba: 'KBA',
|
||||||
please_upload_an_image_file: 'Veuillez téléverser un fichier image',
|
please_upload_an_image_file: 'Veuillez téléverser un fichier image',
|
||||||
must_be_characters_length: 'Doit comporter {number} caractères',
|
must_be_characters_length: 'Doit comporter {number} caractères',
|
||||||
@@ -552,6 +561,7 @@ const fr = {
|
|||||||
please_check_the_box_to_continue: 'Veuillez cocher la case pour continuer.',
|
please_check_the_box_to_continue: 'Veuillez cocher la case pour continuer.',
|
||||||
open_source_documents_software: 'logiciel de documents open source',
|
open_source_documents_software: 'logiciel de documents open source',
|
||||||
verified_phone_number: 'Vérifier le numéro de téléphone',
|
verified_phone_number: 'Vérifier le numéro de téléphone',
|
||||||
|
country_code: 'Indicatif du pays',
|
||||||
use_international_format: 'Utilisez le format international : +1xxx',
|
use_international_format: 'Utilisez le format international : +1xxx',
|
||||||
six_digits_code: 'Code à 6 chiffres',
|
six_digits_code: 'Code à 6 chiffres',
|
||||||
change_phone_number: 'Changer de numéro de téléphone',
|
change_phone_number: 'Changer de numéro de téléphone',
|
||||||
@@ -584,6 +594,7 @@ const pl = {
|
|||||||
close: 'Zamknij',
|
close: 'Zamknij',
|
||||||
uploaded_files: 'Przesłane pliki',
|
uploaded_files: 'Przesłane pliki',
|
||||||
signature_drawing_area: 'Obszar rysowania podpisu. Użyj myszy lub dotyku, aby narysować swój podpis.',
|
signature_drawing_area: 'Obszar rysowania podpisu. Użyj myszy lub dotyku, aby narysować swój podpis.',
|
||||||
|
initials_drawing_area: 'Obszar rysowania inicjałów. Użyj myszy lub dotyku, aby narysować swoje inicjały.',
|
||||||
kba: 'KBA',
|
kba: 'KBA',
|
||||||
please_upload_an_image_file: 'Proszę przesłać plik obrazu',
|
please_upload_an_image_file: 'Proszę przesłać plik obrazu',
|
||||||
must_be_characters_length: 'Musi mieć długość {number} znaków',
|
must_be_characters_length: 'Musi mieć długość {number} znaków',
|
||||||
@@ -668,6 +679,7 @@ const pl = {
|
|||||||
please_check_the_box_to_continue: 'Proszę zaznaczyć pole, aby kontynuować.',
|
please_check_the_box_to_continue: 'Proszę zaznaczyć pole, aby kontynuować.',
|
||||||
open_source_documents_software: 'oprogramowanie do dokumentów open source',
|
open_source_documents_software: 'oprogramowanie do dokumentów open source',
|
||||||
verified_phone_number: 'Zweryfikuj numer telefonu',
|
verified_phone_number: 'Zweryfikuj numer telefonu',
|
||||||
|
country_code: 'Kod kraju',
|
||||||
use_international_format: 'Użyj międzynarodowego formatu: +1xxx',
|
use_international_format: 'Użyj międzynarodowego formatu: +1xxx',
|
||||||
six_digits_code: '6-cyfrowy kod',
|
six_digits_code: '6-cyfrowy kod',
|
||||||
change_phone_number: 'Zmień numer telefonu',
|
change_phone_number: 'Zmień numer telefonu',
|
||||||
@@ -700,6 +712,7 @@ const uk = {
|
|||||||
close: 'Закрити',
|
close: 'Закрити',
|
||||||
uploaded_files: 'Завантажені файли',
|
uploaded_files: 'Завантажені файли',
|
||||||
signature_drawing_area: 'Область малювання підпису. Використовуйте мишу або дотик, щоб намалювати свій підпис.',
|
signature_drawing_area: 'Область малювання підпису. Використовуйте мишу або дотик, щоб намалювати свій підпис.',
|
||||||
|
initials_drawing_area: 'Область малювання ініціалів. Використовуйте мишу або дотик, щоб намалювати свої ініціали.',
|
||||||
kba: 'KBA',
|
kba: 'KBA',
|
||||||
please_upload_an_image_file: 'Будь ласка, завантажте файл зображення',
|
please_upload_an_image_file: 'Будь ласка, завантажте файл зображення',
|
||||||
must_be_characters_length: 'Має містити {number} символів',
|
must_be_characters_length: 'Має містити {number} символів',
|
||||||
@@ -784,6 +797,7 @@ const uk = {
|
|||||||
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: 'Підтвердіть номер телефону',
|
||||||
|
country_code: 'Код країни',
|
||||||
use_international_format: 'Використовуйте міжнародний формат: +1xxx',
|
use_international_format: 'Використовуйте міжнародний формат: +1xxx',
|
||||||
six_digits_code: '6-значний код',
|
six_digits_code: '6-значний код',
|
||||||
change_phone_number: 'Змінити номер телефону',
|
change_phone_number: 'Змінити номер телефону',
|
||||||
@@ -816,6 +830,7 @@ const cs = {
|
|||||||
close: 'Zavřít',
|
close: 'Zavřít',
|
||||||
uploaded_files: 'Nahrané soubory',
|
uploaded_files: 'Nahrané soubory',
|
||||||
signature_drawing_area: 'Oblast pro kreslení podpisu. Použijte myš nebo dotyk k nakreslení podpisu.',
|
signature_drawing_area: 'Oblast pro kreslení podpisu. Použijte myš nebo dotyk k nakreslení podpisu.',
|
||||||
|
initials_drawing_area: 'Oblast pro kreslení iniciál. Použijte myš nebo dotyk k nakreslení iniciál.',
|
||||||
kba: 'KBA',
|
kba: 'KBA',
|
||||||
please_upload_an_image_file: 'Nahrajte prosím obrázkový soubor',
|
please_upload_an_image_file: 'Nahrajte prosím obrázkový soubor',
|
||||||
must_be_characters_length: 'Musí mít délku {number} znaků',
|
must_be_characters_length: 'Musí mít délku {number} znaků',
|
||||||
@@ -900,6 +915,7 @@ const cs = {
|
|||||||
please_check_the_box_to_continue: 'Prosím, zaškrtněte políčko pro pokračování.',
|
please_check_the_box_to_continue: 'Prosím, zaškrtněte políčko pro pokračování.',
|
||||||
open_source_documents_software: 'open source software pro dokumenty',
|
open_source_documents_software: 'open source software pro dokumenty',
|
||||||
verified_phone_number: 'Ověřte telefonní číslo',
|
verified_phone_number: 'Ověřte telefonní číslo',
|
||||||
|
country_code: 'Předvolba země',
|
||||||
use_international_format: 'Použijte mezinárodní formát: +1xxx',
|
use_international_format: 'Použijte mezinárodní formát: +1xxx',
|
||||||
six_digits_code: '6-místný kód',
|
six_digits_code: '6-místný kód',
|
||||||
change_phone_number: 'Změnit telefonní číslo',
|
change_phone_number: 'Změnit telefonní číslo',
|
||||||
@@ -932,6 +948,7 @@ const pt = {
|
|||||||
close: 'Fechar',
|
close: 'Fechar',
|
||||||
uploaded_files: 'Arquivos enviados',
|
uploaded_files: 'Arquivos enviados',
|
||||||
signature_drawing_area: 'Área de desenho da assinatura. Use o mouse ou toque para desenhar sua assinatura.',
|
signature_drawing_area: 'Área de desenho da assinatura. Use o mouse ou toque para desenhar sua assinatura.',
|
||||||
|
initials_drawing_area: 'Área de desenho das iniciais. Use o mouse ou toque para desenhar suas iniciais.',
|
||||||
kba: 'KBA',
|
kba: 'KBA',
|
||||||
please_upload_an_image_file: 'Por favor, envie um arquivo de imagem',
|
please_upload_an_image_file: 'Por favor, envie um arquivo de imagem',
|
||||||
must_be_characters_length: 'Deve ter {number} caracteres',
|
must_be_characters_length: 'Deve ter {number} caracteres',
|
||||||
@@ -1016,6 +1033,7 @@ const pt = {
|
|||||||
please_check_the_box_to_continue: 'Por favor, marque a caixa para continuar.',
|
please_check_the_box_to_continue: 'Por favor, marque a caixa para continuar.',
|
||||||
open_source_documents_software: 'software de documentos de código aberto',
|
open_source_documents_software: 'software de documentos de código aberto',
|
||||||
verified_phone_number: 'Verificar Número de Telefone',
|
verified_phone_number: 'Verificar Número de Telefone',
|
||||||
|
country_code: 'Código do país',
|
||||||
use_international_format: 'Use formato internacional: +1xxx',
|
use_international_format: 'Use formato internacional: +1xxx',
|
||||||
six_digits_code: 'Código de 6 dígitos',
|
six_digits_code: 'Código de 6 dígitos',
|
||||||
change_phone_number: 'Alterar número de telefone',
|
change_phone_number: 'Alterar número de telefone',
|
||||||
@@ -1048,6 +1066,7 @@ const he = {
|
|||||||
close: 'סגור',
|
close: 'סגור',
|
||||||
uploaded_files: 'קבצים שהועלו',
|
uploaded_files: 'קבצים שהועלו',
|
||||||
signature_drawing_area: 'אזור ציור חתימה. השתמש בעכבר או במגע כדי לצייר את החתימה שלך.',
|
signature_drawing_area: 'אזור ציור חתימה. השתמש בעכבר או במגע כדי לצייר את החתימה שלך.',
|
||||||
|
initials_drawing_area: 'אזור ציור ראשי תיבות. השתמש בעכבר או במגע כדי לצייר את ראשי התיבות שלך.',
|
||||||
kba: 'KBA',
|
kba: 'KBA',
|
||||||
please_upload_an_image_file: 'אנא העלה קובץ תמונה',
|
please_upload_an_image_file: 'אנא העלה קובץ תמונה',
|
||||||
must_be_characters_length: 'חייב להיות באורך של {number} תווים',
|
must_be_characters_length: 'חייב להיות באורך של {number} תווים',
|
||||||
@@ -1132,6 +1151,7 @@ const he = {
|
|||||||
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: 'אימות מספר טלפון',
|
||||||
|
country_code: 'קידומת מדינה',
|
||||||
use_international_format: 'השתמש בפורמט בינלאומי: +1xxx',
|
use_international_format: 'השתמש בפורמט בינלאומי: +1xxx',
|
||||||
six_digits_code: 'קוד שש ספרות',
|
six_digits_code: 'קוד שש ספרות',
|
||||||
change_phone_number: 'שינוי מספר טלפון',
|
change_phone_number: 'שינוי מספר טלפון',
|
||||||
@@ -1164,6 +1184,7 @@ const nl = {
|
|||||||
close: 'Sluiten',
|
close: 'Sluiten',
|
||||||
uploaded_files: 'Geüploade bestanden',
|
uploaded_files: 'Geüploade bestanden',
|
||||||
signature_drawing_area: 'Handtekening tekengebied. Gebruik de muis of aanraking om uw handtekening te tekenen.',
|
signature_drawing_area: 'Handtekening tekengebied. Gebruik de muis of aanraking om uw handtekening te tekenen.',
|
||||||
|
initials_drawing_area: 'Tekengebied voor initialen. Gebruik de muis of aanraking om uw initialen te tekenen.',
|
||||||
kba: 'KBA',
|
kba: 'KBA',
|
||||||
please_upload_an_image_file: 'Upload alstublieft een afbeeldingsbestand',
|
please_upload_an_image_file: 'Upload alstublieft een afbeeldingsbestand',
|
||||||
must_be_characters_length: 'Moet {number} tekens lang zijn',
|
must_be_characters_length: 'Moet {number} tekens lang zijn',
|
||||||
@@ -1248,6 +1269,7 @@ const nl = {
|
|||||||
please_check_the_box_to_continue: 'Vink het vakje aan om door te gaan.',
|
please_check_the_box_to_continue: 'Vink het vakje aan om door te gaan.',
|
||||||
open_source_documents_software: 'Open source documenten software',
|
open_source_documents_software: 'Open source documenten software',
|
||||||
verified_phone_number: 'Telefoonnummer verifiëren',
|
verified_phone_number: 'Telefoonnummer verifiëren',
|
||||||
|
country_code: 'Landcode',
|
||||||
use_international_format: 'Gebruik internationaal formaat: +1xxx',
|
use_international_format: 'Gebruik internationaal formaat: +1xxx',
|
||||||
six_digits_code: '6-cijferige code',
|
six_digits_code: '6-cijferige code',
|
||||||
change_phone_number: 'Wijzig telefoonnummer',
|
change_phone_number: 'Wijzig telefoonnummer',
|
||||||
@@ -1280,6 +1302,7 @@ const ar = {
|
|||||||
close: 'إغلاق',
|
close: 'إغلاق',
|
||||||
uploaded_files: 'الملفات المرفوعة',
|
uploaded_files: 'الملفات المرفوعة',
|
||||||
signature_drawing_area: 'منطقة رسم التوقيع. استخدم الماوس أو اللمس لرسم توقيعك.',
|
signature_drawing_area: 'منطقة رسم التوقيع. استخدم الماوس أو اللمس لرسم توقيعك.',
|
||||||
|
initials_drawing_area: 'منطقة رسم الحروف الأولى. استخدم الماوس أو اللمس لرسم الحروف الأولى.',
|
||||||
kba: 'KBA',
|
kba: 'KBA',
|
||||||
please_upload_an_image_file: 'يرجى تحميل ملف صورة',
|
please_upload_an_image_file: 'يرجى تحميل ملف صورة',
|
||||||
must_be_characters_length: 'يجب أن يكون الطول {number} حرفًا',
|
must_be_characters_length: 'يجب أن يكون الطول {number} حرفًا',
|
||||||
@@ -1364,6 +1387,7 @@ const ar = {
|
|||||||
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: 'تحقق من رقم الهاتف',
|
||||||
|
country_code: 'رمز الدولة',
|
||||||
use_international_format: 'استخدم الشكل الدولي: +1xxx',
|
use_international_format: 'استخدم الشكل الدولي: +1xxx',
|
||||||
six_digits_code: 'رمز مكون من 6 أرقام',
|
six_digits_code: 'رمز مكون من 6 أرقام',
|
||||||
change_phone_number: 'تغيير رقم الهاتف',
|
change_phone_number: 'تغيير رقم الهاتف',
|
||||||
@@ -1396,6 +1420,7 @@ const ko = {
|
|||||||
close: '닫기',
|
close: '닫기',
|
||||||
uploaded_files: '업로드된 파일',
|
uploaded_files: '업로드된 파일',
|
||||||
signature_drawing_area: '서명 그리기 영역. 마우스 또는 터치를 사용하여 서명을 그리세요.',
|
signature_drawing_area: '서명 그리기 영역. 마우스 또는 터치를 사용하여 서명을 그리세요.',
|
||||||
|
initials_drawing_area: '이니셜 그리기 영역. 마우스 또는 터치를 사용하여 이니셜을 그리세요.',
|
||||||
kba: 'KBA',
|
kba: 'KBA',
|
||||||
please_upload_an_image_file: '이미지 파일을 업로드해 주세요',
|
please_upload_an_image_file: '이미지 파일을 업로드해 주세요',
|
||||||
must_be_characters_length: '{number}자여야 합니다',
|
must_be_characters_length: '{number}자여야 합니다',
|
||||||
@@ -1480,6 +1505,7 @@ const ko = {
|
|||||||
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: '전화번호 확인됨',
|
||||||
|
country_code: '국가 번호',
|
||||||
use_international_format: '국제 포맷 사용: +1xxx',
|
use_international_format: '국제 포맷 사용: +1xxx',
|
||||||
six_digits_code: '6자리 코드',
|
six_digits_code: '6자리 코드',
|
||||||
change_phone_number: '전화번호 변경',
|
change_phone_number: '전화번호 변경',
|
||||||
@@ -1512,6 +1538,7 @@ const ja = {
|
|||||||
close: '閉じる',
|
close: '閉じる',
|
||||||
uploaded_files: 'アップロードされたファイル',
|
uploaded_files: 'アップロードされたファイル',
|
||||||
signature_drawing_area: '署名描画エリア。マウスまたはタッチを使用して署名を描いてください。',
|
signature_drawing_area: '署名描画エリア。マウスまたはタッチを使用して署名を描いてください。',
|
||||||
|
initials_drawing_area: 'イニシャル描画エリア。マウスまたはタッチを使用してイニシャルを描いてください。',
|
||||||
kba: 'KBA',
|
kba: 'KBA',
|
||||||
please_upload_an_image_file: '画像ファイルをアップロードしてください',
|
please_upload_an_image_file: '画像ファイルをアップロードしてください',
|
||||||
must_be_characters_length: '{number}文字でなければなりません',
|
must_be_characters_length: '{number}文字でなければなりません',
|
||||||
@@ -1596,6 +1623,7 @@ const ja = {
|
|||||||
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: '電話番号を認証',
|
||||||
|
country_code: '国番号',
|
||||||
use_international_format: '国際形式を使用してください:+1xxx',
|
use_international_format: '国際形式を使用してください:+1xxx',
|
||||||
six_digits_code: '6桁のコード',
|
six_digits_code: '6桁のコード',
|
||||||
change_phone_number: '電話番号を変更',
|
change_phone_number: '電話番号を変更',
|
||||||
|
|||||||
@@ -141,6 +141,7 @@
|
|||||||
<img
|
<img
|
||||||
v-if="modelValue || computedPreviousValue"
|
v-if="modelValue || computedPreviousValue"
|
||||||
:src="attachmentsIndex[modelValue || computedPreviousValue].url"
|
:src="attachmentsIndex[modelValue || computedPreviousValue].url"
|
||||||
|
:alt="field.name || t('initials')"
|
||||||
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"
|
||||||
>
|
>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
@@ -156,6 +157,8 @@
|
|||||||
<canvas
|
<canvas
|
||||||
v-show="!modelValue && !computedPreviousValue"
|
v-show="!modelValue && !computedPreviousValue"
|
||||||
ref="canvas"
|
ref="canvas"
|
||||||
|
role="img"
|
||||||
|
:aria-label="t('initials_drawing_area')"
|
||||||
class="bg-white border border-base-300 rounded-2xl w-full draw-canvas"
|
class="bg-white border border-base-300 rounded-2xl w-full draw-canvas"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -165,7 +168,8 @@
|
|||||||
ref="textInput"
|
ref="textInput"
|
||||||
class="base-input !text-2xl w-full mt-6 text-center"
|
class="base-input !text-2xl w-full mt-6 text-center"
|
||||||
:required="field.required && !isInitialsStarted"
|
:required="field.required && !isInitialsStarted"
|
||||||
:placeholder="`${t('type_initial_here')}...`"
|
:aria-label="field.name || t('initials')"
|
||||||
|
:placeholder="`${t('type_initial_here')}${field.required ? '...' : ' (' + t('optional') + ')'}`"
|
||||||
type="text"
|
type="text"
|
||||||
@focus="$emit('focus')"
|
@focus="$emit('focus')"
|
||||||
@input="updateWrittenInitials"
|
@input="updateWrittenInitials"
|
||||||
@@ -289,7 +293,7 @@ export default {
|
|||||||
|
|
||||||
if (!this.isDrawInitials) {
|
if (!this.isDrawInitials) {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
if (this.$refs.textInput) {
|
if (this.$refs.textInput && this.field.required === true) {
|
||||||
this.initTextInitial()
|
this.initTextInitial()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
:placeholder="t('email')"
|
:placeholder="t('email')"
|
||||||
type="email"
|
type="email"
|
||||||
:required="submitters.includes(submitter)"
|
:required="submitters.includes(submitter)"
|
||||||
autofocus="true"
|
:autofocus="index === 0"
|
||||||
name="submission[submitters][][email]"
|
name="submission[submitters][][email]"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<label
|
<label
|
||||||
v-if="showFieldNames && (field.name || field.title)"
|
v-if="showFieldNames && (field.name || field.title)"
|
||||||
:for="field.uuid"
|
:id="field.uuid + '-group-label'"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5 field-name-label"
|
class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5 field-name-label"
|
||||||
:class="{ 'mb-2': !field.description }"
|
:class="{ 'mb-2': !field.description }"
|
||||||
@@ -44,6 +44,8 @@
|
|||||||
<div
|
<div
|
||||||
class="space-y-3.5 mx-auto"
|
class="space-y-3.5 mx-auto"
|
||||||
:class="{ hidden: !showOptions }"
|
:class="{ hidden: !showOptions }"
|
||||||
|
role="group"
|
||||||
|
:aria-labelledby="showFieldNames && (field.name || field.title) ? field.uuid + '-group-label' : undefined"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="(option, index) in field.options"
|
v-for="(option, index) in field.options"
|
||||||
|
|||||||
@@ -81,6 +81,7 @@
|
|||||||
<select
|
<select
|
||||||
id="country_code_select"
|
id="country_code_select"
|
||||||
class="absolute top-0 bottom-0 right-0 left-0 opacity-0 w-full h-full cursor-pointer"
|
class="absolute top-0 bottom-0 right-0 left-0 opacity-0 w-full h-full cursor-pointer"
|
||||||
|
:aria-label="t('country_code')"
|
||||||
:disabled="!!defaultValue"
|
:disabled="!!defaultValue"
|
||||||
@change="onCountrySelect(countries.find((country) => country.flag === $event.target.value))"
|
@change="onCountrySelect(countries.find((country) => country.flag === $event.target.value))"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
id="type_text_button"
|
id="type_text_button"
|
||||||
|
ref="typeTextButton"
|
||||||
type="button"
|
type="button"
|
||||||
:aria-label="t('draw_signature')"
|
:aria-label="t('draw_signature')"
|
||||||
class="btn btn-outline btn-sm font-medium type-text-button"
|
class="btn btn-outline btn-sm font-medium type-text-button"
|
||||||
@@ -49,6 +50,7 @@
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
id="type_text_button"
|
id="type_text_button"
|
||||||
|
ref="typeTextButton"
|
||||||
type="button"
|
type="button"
|
||||||
:aria-label="t('type_text')"
|
:aria-label="t('type_text')"
|
||||||
class="btn btn-outline btn-sm font-medium inline-flex flex-nowrap type-text-button"
|
class="btn btn-outline btn-sm font-medium inline-flex flex-nowrap type-text-button"
|
||||||
@@ -71,7 +73,7 @@
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
:aria-label="t('take_photo')"
|
:aria-label="t('upload')"
|
||||||
class="btn btn-outline btn-sm font-medium inline-flex flex-nowrap upload-image-button"
|
class="btn btn-outline btn-sm font-medium inline-flex flex-nowrap upload-image-button"
|
||||||
@click="$refs.takePhotoInput.click()"
|
@click="$refs.takePhotoInput.click()"
|
||||||
>
|
>
|
||||||
@@ -199,7 +201,7 @@
|
|||||||
<canvas
|
<canvas
|
||||||
v-show="!modelValue && !computedPreviousValue"
|
v-show="!modelValue && !computedPreviousValue"
|
||||||
ref="canvas"
|
ref="canvas"
|
||||||
role="application"
|
role="img"
|
||||||
:aria-label="t('signature_drawing_area')"
|
:aria-label="t('signature_drawing_area')"
|
||||||
style="padding: 1px; 0"
|
style="padding: 1px; 0"
|
||||||
class="bg-white border border-base-300 rounded-2xl w-full draw-canvas"
|
class="bg-white border border-base-300 rounded-2xl w-full draw-canvas"
|
||||||
@@ -228,6 +230,8 @@
|
|||||||
>
|
>
|
||||||
<canvas
|
<canvas
|
||||||
ref="qrCanvas"
|
ref="qrCanvas"
|
||||||
|
role="img"
|
||||||
|
:aria-label="t('scan_the_qr_code_with_the_camera_app_to_open_the_form_on_mobile_and_draw_your_signature')"
|
||||||
class="h-full"
|
class="h-full"
|
||||||
width="132"
|
width="132"
|
||||||
height="132"
|
height="132"
|
||||||
@@ -242,7 +246,8 @@
|
|||||||
ref="textInput"
|
ref="textInput"
|
||||||
class="base-input !text-2xl w-full mt-6"
|
class="base-input !text-2xl w-full mt-6"
|
||||||
:required="field.required && !isSignatureStarted"
|
:required="field.required && !isSignatureStarted"
|
||||||
:placeholder="`${t('type_signature_here')}...`"
|
:aria-label="field.name || t('signature')"
|
||||||
|
:placeholder="`${t('type_signature_here')}${field.required ? '...' : ' (' + t('optional') + ')'}`"
|
||||||
type="text"
|
type="text"
|
||||||
@input="updateWrittenSignature"
|
@input="updateWrittenSignature"
|
||||||
>
|
>
|
||||||
@@ -251,6 +256,7 @@
|
|||||||
class="select base-input !text-2xl w-full mt-6 text-center"
|
class="select base-input !text-2xl w-full mt-6 text-center"
|
||||||
:class="{ 'text-gray-300': !reason }"
|
:class="{ 'text-gray-300': !reason }"
|
||||||
required
|
required
|
||||||
|
:aria-label="t('select_a_reason')"
|
||||||
:name="`values[${field.preferences.reason_field_uuid}]`"
|
:name="`values[${field.preferences.reason_field_uuid}]`"
|
||||||
@change="$event.target.value === 'other' ? [reason = '', isOtherReason = true] : $emit('update:reason', $event.target.value)"
|
@change="$event.target.value === 'other' ? [reason = '', isOtherReason = true] : $emit('update:reason', $event.target.value)"
|
||||||
>
|
>
|
||||||
@@ -296,6 +302,7 @@
|
|||||||
class="base-input !text-2xl w-full mt-6"
|
class="base-input !text-2xl w-full mt-6"
|
||||||
required
|
required
|
||||||
:name="`values[${field.preferences.reason_field_uuid}]`"
|
:name="`values[${field.preferences.reason_field_uuid}]`"
|
||||||
|
:aria-label="t('select_a_reason')"
|
||||||
:placeholder="t('type_here_')"
|
:placeholder="t('type_here_')"
|
||||||
:value="reason"
|
:value="reason"
|
||||||
type="text"
|
type="text"
|
||||||
@@ -528,7 +535,7 @@ export default {
|
|||||||
this.$nextTick(() => this.drawSignatureSrc())
|
this.$nextTick(() => this.drawSignatureSrc())
|
||||||
} else if (this.isTextSignature) {
|
} else if (this.isTextSignature) {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
if (this.$refs.textInput) {
|
if (this.$refs.textInput && this.field.required === true) {
|
||||||
this.initTypedSignature()
|
this.initTypedSignature()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -716,10 +723,12 @@ export default {
|
|||||||
this.clear()
|
this.clear()
|
||||||
this.isTextSignature = !this.isTextSignature
|
this.isTextSignature = !this.isTextSignature
|
||||||
|
|
||||||
if (this.isTextSignature) {
|
this.$nextTick(() => {
|
||||||
this.$nextTick(() => {
|
if (this.isTextSignature) {
|
||||||
if (this.$refs.textInput) {
|
if (this.$refs.textInput) {
|
||||||
if (!this.submitter.name) {
|
if (this.submitter.name) {
|
||||||
|
this.$refs.typeTextButton?.focus()
|
||||||
|
} else {
|
||||||
this.$refs.textInput.focus()
|
this.$refs.textInput.focus()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -727,8 +736,10 @@ export default {
|
|||||||
|
|
||||||
this.$emit('start')
|
this.$emit('start')
|
||||||
}
|
}
|
||||||
})
|
} else {
|
||||||
}
|
this.$refs.typeTextButton?.focus()
|
||||||
|
}
|
||||||
|
})
|
||||||
},
|
},
|
||||||
async initTypedSignature () {
|
async initTypedSignature () {
|
||||||
if (this.signatureText) {
|
if (this.signatureText) {
|
||||||
|
|||||||
@@ -319,7 +319,7 @@ export default {
|
|||||||
default: false
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['start-resize', 'stop-resize', 'start-drag', 'stop-drag', 'remove', 'scroll-to', 'add-custom-field', 'click-title'],
|
emits: ['start-resize', 'stop-resize', 'start-drag', 'stop-drag', 'remove', 'scroll-to', 'add-custom-field', 'click-title', 'multi-select'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
isContenteditable: false,
|
isContenteditable: false,
|
||||||
@@ -726,6 +726,10 @@ export default {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.inputMode && (this.isValueInput || this.isCheckboxInput || this.isSelectInput)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
document.activeElement?.blur()
|
document.activeElement?.blur()
|
||||||
|
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -794,6 +798,8 @@ export default {
|
|||||||
this.selectedAreasRef.value.splice(this.selectedAreasRef.value.indexOf(this.area), 1)
|
this.selectedAreasRef.value.splice(this.selectedAreasRef.value.indexOf(this.area), 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.$emit('multi-select', e)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3206,15 +3206,6 @@ export default {
|
|||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
alert(this.t('please_draw_fields_to_prepare_the_document'))
|
alert(this.t('please_draw_fields_to_prepare_the_document'))
|
||||||
} else {
|
|
||||||
const submitterWithoutFields =
|
|
||||||
this.template.submitters.find((submitter) => !this.template.fields.some((f) => f.submitter_uuid === submitter.uuid))
|
|
||||||
|
|
||||||
if (submitterWithoutFields) {
|
|
||||||
e.preventDefault()
|
|
||||||
|
|
||||||
alert(this.t('please_add_fields_for_the_submitter_name_or_remove_the_submitter_name_if_not_needed').replaceAll('{submitter_name}', submitterWithoutFields.name))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSaveClick () {
|
onSaveClick () {
|
||||||
@@ -3231,32 +3222,25 @@ export default {
|
|||||||
if (!this.template.fields.length) {
|
if (!this.template.fields.length) {
|
||||||
alert(this.t('please_draw_fields_to_prepare_the_document'))
|
alert(this.t('please_draw_fields_to_prepare_the_document'))
|
||||||
} else {
|
} else {
|
||||||
const submitterWithoutFields =
|
this.isSaving = true
|
||||||
this.template.submitters.find((submitter) => !this.template.fields.some((f) => f.submitter_uuid === submitter.uuid))
|
|
||||||
|
|
||||||
if (submitterWithoutFields) {
|
const dynamicDocumentRefs = this.documentRefs.filter((ref) => ref.isDynamic)
|
||||||
alert(this.t('please_add_fields_for_the_submitter_name_or_remove_the_submitter_name_if_not_needed').replaceAll('{submitter_name}', submitterWithoutFields.name))
|
|
||||||
} else {
|
|
||||||
this.isSaving = true
|
|
||||||
|
|
||||||
const dynamicDocumentRefs = this.documentRefs.filter((ref) => ref.isDynamic)
|
dynamicDocumentRefs.map((ref) => ref.update())
|
||||||
|
|
||||||
dynamicDocumentRefs.map((ref) => ref.update())
|
this.rebuildVariablesSchema({ disable: false })
|
||||||
|
|
||||||
this.rebuildVariablesSchema({ disable: false })
|
const dynamicDocumentSaves = dynamicDocumentRefs.map((ref) => ref.saveBody())
|
||||||
|
|
||||||
const dynamicDocumentSaves = dynamicDocumentRefs.map((ref) => ref.saveBody())
|
Promise.all([this.save({ force: true }), ...dynamicDocumentSaves]).then(() => {
|
||||||
|
if (this.withRevisions) {
|
||||||
|
this.captureRevision()
|
||||||
|
}
|
||||||
|
|
||||||
Promise.all([this.save({ force: true }), ...dynamicDocumentSaves]).then(() => {
|
window.Turbo.visit(`/templates/${this.template.id}`)
|
||||||
if (this.withRevisions) {
|
}).finally(() => {
|
||||||
this.captureRevision()
|
this.isSaving = false
|
||||||
}
|
})
|
||||||
|
|
||||||
window.Turbo.visit(`/templates/${this.template.id}`)
|
|
||||||
}).finally(() => {
|
|
||||||
this.isSaving = false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
scrollToArea (area) {
|
scrollToArea (area) {
|
||||||
|
|||||||
@@ -42,6 +42,7 @@
|
|||||||
class="absolute w-5 h-5 -ml-2.5 -mt-2.5 rounded-full bg-white border-2 border-neutral-600 cursor-move shadow"
|
class="absolute w-5 h-5 -ml-2.5 -mt-2.5 rounded-full bg-white border-2 border-neutral-600 cursor-move shadow"
|
||||||
:style="{ left: `${corner.x * 100}%`, top: `${corner.y * 100}%` }"
|
:style="{ left: `${corner.x * 100}%`, top: `${corner.y * 100}%` }"
|
||||||
@mousedown.prevent="onCornerMousedown(cornerIndex)"
|
@mousedown.prevent="onCornerMousedown(cornerIndex)"
|
||||||
|
@touchstart.prevent="onCornerTouchstart(cornerIndex)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -218,6 +219,9 @@ export default {
|
|||||||
beforeUnmount () {
|
beforeUnmount () {
|
||||||
window.removeEventListener('mousemove', this.onMousemove)
|
window.removeEventListener('mousemove', this.onMousemove)
|
||||||
window.removeEventListener('mouseup', this.onMouseup)
|
window.removeEventListener('mouseup', this.onMouseup)
|
||||||
|
window.removeEventListener('touchmove', this.onTouchmove)
|
||||||
|
window.removeEventListener('touchend', this.onTouchend)
|
||||||
|
window.removeEventListener('touchcancel', this.onTouchend)
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
transformPoint (point, rotate, flipH, flipV) {
|
transformPoint (point, rotate, flipH, flipV) {
|
||||||
@@ -276,27 +280,48 @@ export default {
|
|||||||
y: Math.min(Math.max((event.clientY - rect.top) / rect.height, 0), 1)
|
y: Math.min(Math.max((event.clientY - rect.top) / rect.height, 0), 1)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onCornerMousedown (index) {
|
startCornerDrag (index) {
|
||||||
this.draggingIndex = index
|
this.draggingIndex = index
|
||||||
this.cornersTouched = true
|
this.cornersTouched = true
|
||||||
|
},
|
||||||
|
dragCorner (point) {
|
||||||
|
if (this.draggingIndex === null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.corners[this.draggingIndex] = this.inverseTransformPoint(this.pagePoint(point), this.rotate, this.flipH, this.flipV)
|
||||||
|
},
|
||||||
|
onCornerMousedown (index) {
|
||||||
|
this.startCornerDrag(index)
|
||||||
|
|
||||||
window.addEventListener('mousemove', this.onMousemove)
|
window.addEventListener('mousemove', this.onMousemove)
|
||||||
window.addEventListener('mouseup', this.onMouseup, { once: true })
|
window.addEventListener('mouseup', this.onMouseup, { once: true })
|
||||||
},
|
},
|
||||||
onMousemove (event) {
|
onMousemove (event) {
|
||||||
if (this.draggingIndex === null) {
|
this.dragCorner(event)
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const point = this.inverseTransformPoint(this.pagePoint(event), this.rotate, this.flipH, this.flipV)
|
|
||||||
|
|
||||||
this.corners[this.draggingIndex] = point
|
|
||||||
},
|
},
|
||||||
onMouseup () {
|
onMouseup () {
|
||||||
window.removeEventListener('mousemove', this.onMousemove)
|
window.removeEventListener('mousemove', this.onMousemove)
|
||||||
|
|
||||||
this.draggingIndex = null
|
this.draggingIndex = null
|
||||||
},
|
},
|
||||||
|
onCornerTouchstart (index) {
|
||||||
|
this.startCornerDrag(index)
|
||||||
|
|
||||||
|
window.addEventListener('touchmove', this.onTouchmove, { passive: false })
|
||||||
|
window.addEventListener('touchend', this.onTouchend)
|
||||||
|
window.addEventListener('touchcancel', this.onTouchend)
|
||||||
|
},
|
||||||
|
onTouchmove (event) {
|
||||||
|
this.dragCorner(event.touches[0])
|
||||||
|
},
|
||||||
|
onTouchend () {
|
||||||
|
window.removeEventListener('touchmove', this.onTouchmove)
|
||||||
|
window.removeEventListener('touchend', this.onTouchend)
|
||||||
|
window.removeEventListener('touchcancel', this.onTouchend)
|
||||||
|
|
||||||
|
this.draggingIndex = null
|
||||||
|
},
|
||||||
submit (scan) {
|
submit (scan) {
|
||||||
this.isProcessing = scan ? 'scan' : 'crop'
|
this.isProcessing = scan ? 'scan' : 'crop'
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
class="relative mx-auto select-none cursor-crosshair"
|
class="relative mx-auto select-none cursor-crosshair"
|
||||||
:style="pageStyle"
|
:style="pageStyle"
|
||||||
@mousedown.prevent="onMousedown"
|
@mousedown.prevent="onMousedown"
|
||||||
|
@touchstart.prevent="onTouchstart"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
:src="imageUrl"
|
:src="imageUrl"
|
||||||
@@ -251,6 +252,9 @@ export default {
|
|||||||
beforeUnmount () {
|
beforeUnmount () {
|
||||||
window.removeEventListener('mousemove', this.onMousemove)
|
window.removeEventListener('mousemove', this.onMousemove)
|
||||||
window.removeEventListener('mouseup', this.onMouseup)
|
window.removeEventListener('mouseup', this.onMouseup)
|
||||||
|
window.removeEventListener('touchmove', this.onTouchmove)
|
||||||
|
window.removeEventListener('touchend', this.onTouchend)
|
||||||
|
window.removeEventListener('touchcancel', this.onTouchend)
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
inverseRotatePoint (point, rotate) {
|
inverseRotatePoint (point, rotate) {
|
||||||
@@ -357,27 +361,61 @@ export default {
|
|||||||
y: Math.min(Math.max((event.clientY - rect.top) / rect.height, 0), 1)
|
y: Math.min(Math.max((event.clientY - rect.top) / rect.height, 0), 1)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
startMarquee (point) {
|
||||||
|
const start = this.pagePoint(point)
|
||||||
|
|
||||||
|
this.marquee = { x1: start.x, y1: start.y, x2: start.x, y2: start.y }
|
||||||
|
},
|
||||||
|
updateMarquee (point) {
|
||||||
|
if (!this.marquee) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const next = this.pagePoint(point)
|
||||||
|
|
||||||
|
this.marquee.x2 = next.x
|
||||||
|
this.marquee.y2 = next.y
|
||||||
|
},
|
||||||
onMousedown (event) {
|
onMousedown (event) {
|
||||||
if (event.button !== 0 || (!this.imagePage && !this.textNodes)) {
|
if (event.button !== 0 || (!this.imagePage && !this.textNodes)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const point = this.pagePoint(event)
|
this.startMarquee(event)
|
||||||
|
|
||||||
this.marquee = { x1: point.x, y1: point.y, x2: point.x, y2: point.y }
|
|
||||||
|
|
||||||
window.addEventListener('mousemove', this.onMousemove)
|
window.addEventListener('mousemove', this.onMousemove)
|
||||||
window.addEventListener('mouseup', this.onMouseup, { once: true })
|
window.addEventListener('mouseup', this.onMouseup, { once: true })
|
||||||
},
|
},
|
||||||
onMousemove (event) {
|
onMousemove (event) {
|
||||||
const point = this.pagePoint(event)
|
this.updateMarquee(event)
|
||||||
|
|
||||||
this.marquee.x2 = point.x
|
|
||||||
this.marquee.y2 = point.y
|
|
||||||
},
|
},
|
||||||
onMouseup () {
|
onMouseup () {
|
||||||
window.removeEventListener('mousemove', this.onMousemove)
|
window.removeEventListener('mousemove', this.onMousemove)
|
||||||
|
|
||||||
|
this.finishMarquee()
|
||||||
|
},
|
||||||
|
onTouchstart (event) {
|
||||||
|
if (!this.imagePage && !this.textNodes) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.startMarquee(event.touches[0])
|
||||||
|
|
||||||
|
window.addEventListener('touchmove', this.onTouchmove, { passive: false })
|
||||||
|
window.addEventListener('touchend', this.onTouchend)
|
||||||
|
window.addEventListener('touchcancel', this.onTouchend)
|
||||||
|
},
|
||||||
|
onTouchmove (event) {
|
||||||
|
this.updateMarquee(event.touches[0])
|
||||||
|
},
|
||||||
|
onTouchend () {
|
||||||
|
window.removeEventListener('touchmove', this.onTouchmove)
|
||||||
|
window.removeEventListener('touchend', this.onTouchend)
|
||||||
|
window.removeEventListener('touchcancel', this.onTouchend)
|
||||||
|
|
||||||
|
this.finishMarquee()
|
||||||
|
},
|
||||||
|
finishMarquee () {
|
||||||
if (!this.marquee) {
|
if (!this.marquee) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,11 +98,29 @@ dynamic-variable {
|
|||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
}`)
|
}`)
|
||||||
|
|
||||||
|
const DROP_ATTRS = [
|
||||||
|
'srcdoc', 'xlink:href', 'srcset', 'action', 'formaction', 'poster',
|
||||||
|
'background', 'data', 'cite', 'ping', 'longdesc', 'manifest', 'profile'
|
||||||
|
]
|
||||||
|
|
||||||
|
const SAFE_URL_REGEXP = /^(?:https?:\/\/|data:image\/|blob:|mailto:|tel:|\{|#)/i
|
||||||
|
|
||||||
|
function isSafeAttr (name, value) {
|
||||||
|
const lowerName = name.toLowerCase()
|
||||||
|
|
||||||
|
if (lowerName.startsWith('on') || DROP_ATTRS.includes(lowerName)) return false
|
||||||
|
if ((lowerName === 'href' || lowerName === 'src') && !SAFE_URL_REGEXP.test(value.trim())) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
function collectDomAttrs (dom) {
|
function collectDomAttrs (dom) {
|
||||||
const attrs = {}
|
const attrs = {}
|
||||||
|
|
||||||
for (let i = 0; i < dom.attributes.length; i++) {
|
for (let i = 0; i < dom.attributes.length; i++) {
|
||||||
attrs[dom.attributes[i].name] = dom.attributes[i].value
|
const { name, value } = dom.attributes[i]
|
||||||
|
|
||||||
|
if (isSafeAttr(name, value)) attrs[name] = value
|
||||||
}
|
}
|
||||||
|
|
||||||
return { htmlAttrs: attrs }
|
return { htmlAttrs: attrs }
|
||||||
|
|||||||
@@ -646,9 +646,7 @@ export default {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const container = document.createElement('div')
|
const container = new DOMParser().parseFromString(clipboardHtml, 'text/html').body
|
||||||
|
|
||||||
container.innerHTML = clipboardHtml
|
|
||||||
|
|
||||||
const fieldNodes = [...container.querySelectorAll('dynamic-field[data-field][data-area]')]
|
const fieldNodes = [...container.querySelectorAll('dynamic-field[data-field][data-area]')]
|
||||||
|
|
||||||
|
|||||||
@@ -600,7 +600,8 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
showFont () {
|
showFont () {
|
||||||
return ['text', 'number', 'date', 'select', 'heading', 'cells'].includes(this.field.type)
|
return ['text', 'number', 'date', 'select', 'heading', 'cells'].includes(this.field.type) ||
|
||||||
|
(['radio', 'multiple'].includes(this.field.type) && this.field.areas?.every((a) => !a.option_uuid))
|
||||||
},
|
},
|
||||||
showDescription () {
|
showDescription () {
|
||||||
return !['stamp', 'heading', 'strikethrough'].includes(this.field.type)
|
return !['stamp', 'heading', 'strikethrough'].includes(this.field.type)
|
||||||
|
|||||||
@@ -452,7 +452,7 @@
|
|||||||
class="pb-0.5 mt-0.5"
|
class="pb-0.5 mt-0.5"
|
||||||
>
|
>
|
||||||
<li
|
<li
|
||||||
v-if="['text', 'number', 'date', 'select', 'heading', 'cells'].includes(field.type)"
|
v-if="['text', 'number', 'date', 'select', 'heading', 'cells'].includes(field.type) || (['radio', 'multiple'].includes(field.type) && field.areas?.every((a) => !a.option_uuid))"
|
||||||
class="field-settings-font"
|
class="field-settings-font"
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
@@ -830,6 +830,7 @@ export default {
|
|||||||
} else if (format === 'percent') {
|
} else if (format === 'percent') {
|
||||||
return `${number}%`
|
return `${number}%`
|
||||||
} else if (format === 'percent_space') {
|
} else if (format === 'percent_space') {
|
||||||
|
// eslint-disable-next-line no-irregular-whitespace
|
||||||
return `${String(number).replace('.', ',')} %`
|
return `${String(number).replace('.', ',')} %`
|
||||||
} else {
|
} else {
|
||||||
return number
|
return number
|
||||||
|
|||||||
@@ -57,6 +57,7 @@
|
|||||||
@scroll-to="$emit('scroll-to', $event)"
|
@scroll-to="$emit('scroll-to', $event)"
|
||||||
@add-custom-field="$emit('add-custom-field', $event)"
|
@add-custom-field="$emit('add-custom-field', $event)"
|
||||||
@contextmenu="openAreaContextMenu($event, item.area, item.field)"
|
@contextmenu="openAreaContextMenu($event, item.area, item.field)"
|
||||||
|
@multi-select="openMultiSelectContextMenu"
|
||||||
@click-title="closeContextMenu"
|
@click-title="closeContextMenu"
|
||||||
/>
|
/>
|
||||||
<FieldArea
|
<FieldArea
|
||||||
@@ -408,6 +409,12 @@ export default {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.selectedAreasRef.value.length >= 2) {
|
||||||
|
this.openSelectionContextMenu(event)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
|
|
||||||
@@ -443,6 +450,13 @@ export default {
|
|||||||
areas: this.selectedAreasRef.value
|
areas: this.selectedAreasRef.value
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
openMultiSelectContextMenu (event) {
|
||||||
|
if (this.selectedAreasRef.value.length >= 2) {
|
||||||
|
this.openSelectionContextMenu(event)
|
||||||
|
} else {
|
||||||
|
this.closeContextMenu()
|
||||||
|
}
|
||||||
|
},
|
||||||
handleSelectionCopy () {
|
handleSelectionCopy () {
|
||||||
this.$emit('copy-selected-areas')
|
this.$emit('copy-selected-areas')
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,62 @@
|
|||||||
@mousedown.stop
|
@mousedown.stop
|
||||||
@pointerdown.stop
|
@pointerdown.stop
|
||||||
>
|
>
|
||||||
|
<label
|
||||||
|
v-if="requiredFields.length"
|
||||||
|
class="field-settings-required w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm cursor-pointer"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
:checked="isAllRequired"
|
||||||
|
:indeterminate="isMixedRequired"
|
||||||
|
type="checkbox"
|
||||||
|
class="toggle toggle-xs"
|
||||||
|
:disabled="!editable"
|
||||||
|
@change="handleToggleRequired($event.target.checked)"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<span>{{ t('required') }}</span>
|
||||||
|
</label>
|
||||||
|
<label
|
||||||
|
v-if="readOnlyFields.length"
|
||||||
|
class="field-settings-read-only w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm cursor-pointer"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
:checked="isAllReadOnly"
|
||||||
|
:indeterminate="isMixedReadOnly"
|
||||||
|
type="checkbox"
|
||||||
|
class="toggle toggle-xs"
|
||||||
|
:disabled="!editable"
|
||||||
|
@change="handleToggleReadOnly($event.target.checked)"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<span>{{ t('read_only') }}</span>
|
||||||
|
</label>
|
||||||
|
<hr
|
||||||
|
v-if="requiredFields.length || readOnlyFields.length"
|
||||||
|
class="my-1 border-neutral-200"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-if="showFont"
|
||||||
|
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
|
||||||
|
@click.stop="openFontModal"
|
||||||
|
>
|
||||||
|
<IconTypography class="w-4 h-4" />
|
||||||
|
<span>{{ t('font') }}</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="showCondition"
|
||||||
|
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
|
||||||
|
@click.stop="openConditionModal"
|
||||||
|
>
|
||||||
|
<IconRouteAltLeft class="w-4 h-4" />
|
||||||
|
<span>{{ t('condition') }}</span>
|
||||||
|
</button>
|
||||||
|
<hr
|
||||||
|
v-if="showFont || showCondition"
|
||||||
|
class="my-1 border-neutral-200"
|
||||||
|
>
|
||||||
<ContextSubmenu
|
<ContextSubmenu
|
||||||
:icon="IconLayoutAlignMiddle"
|
:icon="IconLayoutAlignMiddle"
|
||||||
:label="t('align')"
|
:label="t('align')"
|
||||||
@@ -61,26 +117,6 @@
|
|||||||
<span>{{ t('height') }}</span>
|
<span>{{ t('height') }}</span>
|
||||||
</button>
|
</button>
|
||||||
</ContextSubmenu>
|
</ContextSubmenu>
|
||||||
<hr
|
|
||||||
v-if="showFont || showCondition"
|
|
||||||
class="my-1 border-neutral-200"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
v-if="showFont"
|
|
||||||
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
|
|
||||||
@click.stop="openFontModal"
|
|
||||||
>
|
|
||||||
<IconTypography class="w-4 h-4" />
|
|
||||||
<span>{{ t('font') }}</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-if="showCondition"
|
|
||||||
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
|
|
||||||
@click.stop="openConditionModal"
|
|
||||||
>
|
|
||||||
<IconRouteAltLeft class="w-4 h-4" />
|
|
||||||
<span>{{ t('condition') }}</span>
|
|
||||||
</button>
|
|
||||||
<hr class="my-1 border-neutral-200">
|
<hr class="my-1 border-neutral-200">
|
||||||
<button
|
<button
|
||||||
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center justify-between text-sm"
|
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center justify-between text-sm"
|
||||||
@@ -192,6 +228,24 @@ export default {
|
|||||||
return this.template.fields.find((f) => f.areas?.includes(area))
|
return this.template.fields.find((f) => f.areas?.includes(area))
|
||||||
}).filter(Boolean)
|
}).filter(Boolean)
|
||||||
},
|
},
|
||||||
|
requiredFields () {
|
||||||
|
return this.selectedFields.filter((f) => !['phone', 'stamp', 'verification', 'strikethrough', 'heading'].includes(f.type))
|
||||||
|
},
|
||||||
|
readOnlyFields () {
|
||||||
|
return this.selectedFields.filter((f) => ['text', 'number', 'radio', 'multiple', 'select'].includes(f.type))
|
||||||
|
},
|
||||||
|
isAllRequired () {
|
||||||
|
return this.requiredFields.every((f) => f.required)
|
||||||
|
},
|
||||||
|
isMixedRequired () {
|
||||||
|
return !this.isAllRequired && this.requiredFields.some((f) => f.required)
|
||||||
|
},
|
||||||
|
isAllReadOnly () {
|
||||||
|
return this.readOnlyFields.every((f) => f.readonly)
|
||||||
|
},
|
||||||
|
isMixedReadOnly () {
|
||||||
|
return !this.isAllReadOnly && this.readOnlyFields.some((f) => f.readonly)
|
||||||
|
},
|
||||||
isMac () {
|
isMac () {
|
||||||
return (navigator.userAgentData?.platform || navigator.platform)?.toLowerCase()?.includes('mac')
|
return (navigator.userAgentData?.platform || navigator.platform)?.toLowerCase()?.includes('mac')
|
||||||
},
|
},
|
||||||
@@ -234,6 +288,16 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
handleToggleRequired (value) {
|
||||||
|
this.requiredFields.forEach((field) => { field.required = value })
|
||||||
|
|
||||||
|
this.save()
|
||||||
|
},
|
||||||
|
handleToggleReadOnly (value) {
|
||||||
|
this.readOnlyFields.forEach((field) => { field.readonly = value })
|
||||||
|
|
||||||
|
this.save()
|
||||||
|
},
|
||||||
onKeyDown (event) {
|
onKeyDown (event) {
|
||||||
if (event.key === 'Escape') {
|
if (event.key === 'Escape') {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ class ProcessSubmissionExpiredJob
|
|||||||
return if submission.archived_at?
|
return if submission.archived_at?
|
||||||
return if submission.template&.archived_at?
|
return if submission.template&.archived_at?
|
||||||
return if submission.submitters.where.not(declined_at: nil).exists?
|
return if submission.submitters.where.not(declined_at: nil).exists?
|
||||||
return unless submission.submitters.exists?(completed_at: nil)
|
return if submission.completed_at?
|
||||||
|
return if params['expire_at'] && submission.expire_at&.to_i != params['expire_at']
|
||||||
|
|
||||||
WebhookUrls.enqueue_events(submission, 'submission.expired')
|
WebhookUrls.enqueue_events(submission, 'submission.expired')
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -5,30 +5,45 @@ class ProcessSubmitterCompletionJob
|
|||||||
|
|
||||||
def perform(params = {})
|
def perform(params = {})
|
||||||
submitter = Submitter.find(params['submitter_id'])
|
submitter = Submitter.find(params['submitter_id'])
|
||||||
|
submission = submitter.submission
|
||||||
|
|
||||||
create_completed_submitter!(submitter)
|
create_completed_submitter!(submitter)
|
||||||
|
|
||||||
is_all_completed = !submitter.submission.submitters.exists?(completed_at: nil)
|
is_last =
|
||||||
|
if params.key?('is_last')
|
||||||
|
params['is_last']
|
||||||
|
else
|
||||||
|
viewer_uuids = submission.template_submitters.to_a.filter_map { |s| s['uuid'] if s['is_viewer'] }
|
||||||
|
|
||||||
|
incomplete = submission.submitters.where(completed_at: nil)
|
||||||
|
incomplete = incomplete.where.not(uuid: viewer_uuids) if viewer_uuids.present?
|
||||||
|
|
||||||
|
!incomplete.exists? &&
|
||||||
|
submitter.completed_at == submission.submitters.maximum(:completed_at)
|
||||||
|
end
|
||||||
|
|
||||||
Submissions::EnsureResultGenerated.call(submitter)
|
Submissions::EnsureResultGenerated.call(submitter)
|
||||||
|
|
||||||
if is_all_completed && submitter.completed_at == submitter.submission.submitters.maximum(:completed_at)
|
if is_last
|
||||||
if submitter.submission.account.account_configs.exists?(key: AccountConfig::COMBINE_PDF_RESULT_KEY, value: true)
|
if submission.account.account_configs.exists?(key: AccountConfig::COMBINE_PDF_RESULT_KEY, value: true)
|
||||||
Submissions::EnsureCombinedGenerated.call(submitter)
|
Submissions::EnsureCombinedGenerated.call(submitter)
|
||||||
end
|
end
|
||||||
|
|
||||||
Submissions::EnsureAuditGenerated.call(submitter.submission)
|
Submissions::EnsureAuditGenerated.call(submission)
|
||||||
|
|
||||||
enqueue_completed_emails(submitter)
|
enqueue_completed_emails(submitter)
|
||||||
end
|
end
|
||||||
|
|
||||||
create_completed_documents!(submitter)
|
create_completed_documents!(submitter)
|
||||||
|
|
||||||
if !is_all_completed && submitter.submission.submitters_order_preserved? && params['send_invitation_email'] != false
|
if !submission.completed_at && submission.submitters_order_preserved? && params['send_invitation_email'] != false &&
|
||||||
enqueue_next_submitter_request_notification(submitter)
|
Submission.exists?(id: submission.id, completed_at: nil)
|
||||||
|
next_submitters = enqueue_next_submitter_request_notification(submitter)
|
||||||
|
|
||||||
|
enqueue_next_submitter_viewer_notification(submission, next_submitters) unless is_last
|
||||||
end
|
end
|
||||||
|
|
||||||
enqueue_completed_webhooks(submitter, is_all_completed:)
|
enqueue_completed_webhooks(submitter, is_last:)
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_completed_submitter!(submitter)
|
def create_completed_submitter!(submitter)
|
||||||
@@ -77,7 +92,7 @@ class ProcessSubmitterCompletionJob
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def enqueue_completed_webhooks(submitter, is_all_completed: false)
|
def enqueue_completed_webhooks(submitter, is_last: false)
|
||||||
event_uuids = {}
|
event_uuids = {}
|
||||||
|
|
||||||
WebhookUrls.for_account_id(submitter.account_id, %w[form.completed submission.completed]).each do |webhook|
|
WebhookUrls.for_account_id(submitter.account_id, %w[form.completed submission.completed]).each do |webhook|
|
||||||
@@ -89,7 +104,7 @@ class ProcessSubmitterCompletionJob
|
|||||||
'webhook_url_id' => webhook.id)
|
'webhook_url_id' => webhook.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
next unless webhook.events.include?('submission.completed') && is_all_completed
|
next unless webhook.events.include?('submission.completed') && is_last
|
||||||
|
|
||||||
event_uuids['submission.completed'] ||= SecureRandom.uuid
|
event_uuids['submission.completed'] ||= SecureRandom.uuid
|
||||||
|
|
||||||
@@ -137,7 +152,7 @@ class ProcessSubmitterCompletionJob
|
|||||||
return if configs.value['enabled'] == false
|
return if configs.value['enabled'] == false
|
||||||
|
|
||||||
to = submitter.submission.submitters.reject { |e| e.preferences['send_email'] == false }
|
to = submitter.submission.submitters.reject { |e| e.preferences['send_email'] == false }
|
||||||
.sort_by(&:completed_at).select(&:email?).map(&:friendly_name)
|
.sort_by { |e| e.completed_at || Time.current }.select(&:email?).map(&:friendly_name)
|
||||||
|
|
||||||
return if to.blank?
|
return if to.blank?
|
||||||
|
|
||||||
@@ -157,9 +172,9 @@ class ProcessSubmitterCompletionJob
|
|||||||
bcc.to_s.scan(User::EMAIL_REGEXP)
|
bcc.to_s.scan(User::EMAIL_REGEXP)
|
||||||
end
|
end
|
||||||
|
|
||||||
def enqueue_next_submitter_request_notification(submitter)
|
def enqueue_next_submitter_request_notification(submitter) # rubocop:disable Metrics/PerceivedComplexity
|
||||||
submission = submitter.submission
|
submission = submitter.submission
|
||||||
submitters_index = submission.submitters.index_by(&:uuid)
|
submitters_index = submission.submitters.reject(&:viewer?).index_by(&:uuid)
|
||||||
|
|
||||||
next_submitter_items =
|
next_submitter_items =
|
||||||
if submission.template_submitters.any? { |s| s['order'] }
|
if submission.template_submitters.any? { |s| s['order'] }
|
||||||
@@ -188,5 +203,43 @@ class ProcessSubmitterCompletionJob
|
|||||||
next_submitters = submitters_index.values_at(*Array.wrap(next_submitter_items).pluck('uuid')).compact
|
next_submitters = submitters_index.values_at(*Array.wrap(next_submitter_items).pluck('uuid')).compact
|
||||||
|
|
||||||
Submitters.send_signature_requests(next_submitters)
|
Submitters.send_signature_requests(next_submitters)
|
||||||
|
|
||||||
|
next_submitters
|
||||||
|
end
|
||||||
|
|
||||||
|
def enqueue_next_submitter_viewer_notification(submission, next_submitters)
|
||||||
|
viewers = submission.submitters.select(&:viewer?)
|
||||||
|
|
||||||
|
return [] if viewers.blank?
|
||||||
|
|
||||||
|
next_submitter_uuids = next_submitters.to_set(&:uuid)
|
||||||
|
viewers_index = viewers.index_by(&:uuid)
|
||||||
|
|
||||||
|
next_viewers =
|
||||||
|
if submission.template_submitters.any? { |s| s['order'] }
|
||||||
|
next_orders = submission.template_submitters
|
||||||
|
.select { |s| next_submitter_uuids.include?(s['uuid']) }
|
||||||
|
.pluck('order')
|
||||||
|
|
||||||
|
submission.template_submitters.filter_map do |s|
|
||||||
|
viewers_index[s['uuid']] if next_orders.include?(s['order'])
|
||||||
|
end
|
||||||
|
else
|
||||||
|
preceding_submitter_uuid = nil
|
||||||
|
|
||||||
|
submission.template_submitters.filter_map do |template_submitter|
|
||||||
|
viewer = viewers_index[template_submitter['uuid']]
|
||||||
|
|
||||||
|
if viewer
|
||||||
|
viewer if next_submitter_uuids.include?(preceding_submitter_uuid)
|
||||||
|
else
|
||||||
|
preceding_submitter_uuid = template_submitter['uuid']
|
||||||
|
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Submitters.send_signature_requests(next_viewers)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -19,7 +19,12 @@ class SendSubmitterInvitationEmailJob
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
mail = SubmitterMailer.invitation_email(submitter)
|
mail =
|
||||||
|
if submitter.viewer?
|
||||||
|
SubmitterMailer.invitation_view_email(submitter)
|
||||||
|
else
|
||||||
|
SubmitterMailer.invitation_email(submitter)
|
||||||
|
end
|
||||||
|
|
||||||
Submitters::ValidateSending.call(submitter, mail)
|
Submitters::ValidateSending.call(submitter, mail)
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class SubmitterMailer < ApplicationMailer
|
|||||||
|
|
||||||
template_submitters_index = @email_message.blank? ? build_submitter_preferences_index(@submitter) : {}
|
template_submitters_index = @email_message.blank? ? build_submitter_preferences_index(@submitter) : {}
|
||||||
|
|
||||||
@body = @email_message&.body.presence ||
|
@body = @email_message&.normalized_body.presence ||
|
||||||
template_submitters_index.dig(@submitter.uuid, 'request_email_body').presence ||
|
template_submitters_index.dig(@submitter.uuid, 'request_email_body').presence ||
|
||||||
@submitter.template&.preferences&.dig('request_email_body').presence
|
@submitter.template&.preferences&.dig('request_email_body').presence
|
||||||
|
|
||||||
@@ -45,6 +45,45 @@ class SubmitterMailer < ApplicationMailer
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def invitation_view_email(submitter)
|
||||||
|
@current_account = submitter.submission.account
|
||||||
|
@submitter = submitter
|
||||||
|
|
||||||
|
if submitter.preferences['email_message_uuid']
|
||||||
|
@email_message = submitter.account.email_messages.find_by(uuid: submitter.preferences['email_message_uuid'])
|
||||||
|
end
|
||||||
|
|
||||||
|
template_submitters_index = @email_message.blank? ? build_submitter_preferences_index(@submitter) : {}
|
||||||
|
|
||||||
|
@body = @email_message&.normalized_body.presence ||
|
||||||
|
@submitter.template&.preferences&.dig('invitation_view_email_body').presence ||
|
||||||
|
template_submitters_index.dig(@submitter.uuid, 'request_email_body').presence
|
||||||
|
|
||||||
|
@subject = @email_message&.subject.presence ||
|
||||||
|
@submitter.template&.preferences&.dig('invitation_view_email_subject').presence ||
|
||||||
|
template_submitters_index.dig(@submitter.uuid, 'request_email_subject').presence
|
||||||
|
|
||||||
|
@email_config = AccountConfigs.find_for_account(@current_account, AccountConfig::SUBMITTER_VIEW_INVITATION_EMAIL_KEY)
|
||||||
|
@body ||= fetch_config_email_body(@email_config, @submitter)
|
||||||
|
|
||||||
|
assign_message_metadata('submitter_view_invitation', @submitter)
|
||||||
|
|
||||||
|
reply_to = build_submitter_reply_to(@submitter, email_config: @email_config)
|
||||||
|
|
||||||
|
maybe_set_custom_domain(@submitter)
|
||||||
|
|
||||||
|
I18n.with_locale(@current_account.locale) do
|
||||||
|
subject = build_invite_subject(@subject, @email_config, submitter)
|
||||||
|
|
||||||
|
mail(
|
||||||
|
to: @submitter.friendly_name,
|
||||||
|
from: from_address_for_submitter(submitter),
|
||||||
|
subject:,
|
||||||
|
reply_to:
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def completed_email(submitter, user, to: nil)
|
def completed_email(submitter, user, to: nil)
|
||||||
@current_account = submitter.submission.account
|
@current_account = submitter.submission.account
|
||||||
@submitter = submitter
|
@submitter = submitter
|
||||||
@@ -53,8 +92,6 @@ class SubmitterMailer < ApplicationMailer
|
|||||||
|
|
||||||
template_preferences = @submission.template&.preferences || {}
|
template_preferences = @submission.template&.preferences || {}
|
||||||
|
|
||||||
Submissions::EnsureResultGenerated.call(submitter)
|
|
||||||
|
|
||||||
@email_config = AccountConfigs.find_for_account(@current_account, AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY)
|
@email_config = AccountConfigs.find_for_account(@current_account, AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY)
|
||||||
|
|
||||||
add_completed_email_attachments!(
|
add_completed_email_attachments!(
|
||||||
@@ -97,7 +134,7 @@ class SubmitterMailer < ApplicationMailer
|
|||||||
to: user.role == 'integration' ? user.friendly_name.sub(/\+\w+@/, '@') : user.friendly_name,
|
to: user.role == 'integration' ? user.friendly_name.sub(/\+\w+@/, '@') : user.friendly_name,
|
||||||
reply_to: @submitter.friendly_name,
|
reply_to: @submitter.friendly_name,
|
||||||
subject: I18n.t(:name_declined_by_submitter,
|
subject: I18n.t(:name_declined_by_submitter,
|
||||||
name: (@submission.name || @submission.template.name).truncate(20),
|
name: (@submission.name || @submission.template&.name).to_s.truncate(20),
|
||||||
submitter: @submitter.name || @submitter.email || @submitter.phone))
|
submitter: @submitter.name || @submitter.email || @submitter.phone))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -109,8 +146,6 @@ class SubmitterMailer < ApplicationMailer
|
|||||||
|
|
||||||
template_preferences = @submitter.template&.preferences || {}
|
template_preferences = @submitter.template&.preferences || {}
|
||||||
|
|
||||||
Submissions::EnsureResultGenerated.call(@submitter)
|
|
||||||
|
|
||||||
@email_config = AccountConfigs.find_for_account(@current_account, AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY)
|
@email_config = AccountConfigs.find_for_account(@current_account, AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY)
|
||||||
|
|
||||||
add_completed_email_attachments!(
|
add_completed_email_attachments!(
|
||||||
@@ -144,6 +179,7 @@ class SubmitterMailer < ApplicationMailer
|
|||||||
end
|
end
|
||||||
|
|
||||||
def otp_verification_email(submitter, locale: nil)
|
def otp_verification_email(submitter, locale: nil)
|
||||||
|
@current_account = submitter.account
|
||||||
@submitter = submitter
|
@submitter = submitter
|
||||||
@otp_code = EmailVerificationCodes.generate([submitter.email.downcase.strip, submitter.slug].join(':'))
|
@otp_code = EmailVerificationCodes.generate([submitter.email.downcase.strip, submitter.slug].join(':'))
|
||||||
|
|
||||||
@@ -171,7 +207,7 @@ class SubmitterMailer < ApplicationMailer
|
|||||||
end
|
end
|
||||||
|
|
||||||
def add_completed_email_attachments!(submitter, with_audit_log: true, with_documents: true)
|
def add_completed_email_attachments!(submitter, with_audit_log: true, with_documents: true)
|
||||||
documents = with_documents ? Submitters.select_attachments_for_download(submitter) : []
|
documents = with_documents ? select_completed_documents(submitter) : []
|
||||||
|
|
||||||
filename_format = AccountConfig.find_or_initialize_by(account_id: submitter.account_id,
|
filename_format = AccountConfig.find_or_initialize_by(account_id: submitter.account_id,
|
||||||
key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value
|
key: AccountConfig::DOCUMENT_FILENAME_FORMAT_KEY)&.value
|
||||||
@@ -215,6 +251,8 @@ class SubmitterMailer < ApplicationMailer
|
|||||||
def build_invite_subject(subject, email_config, submitter)
|
def build_invite_subject(subject, email_config, submitter)
|
||||||
if email_config || subject
|
if email_config || subject
|
||||||
ReplaceEmailVariables.call(subject || email_config.value['subject'], submitter:)
|
ReplaceEmailVariables.call(subject || email_config.value['subject'], submitter:)
|
||||||
|
elsif submitter.viewer?
|
||||||
|
I18n.t(:you_are_invited_to_view_a_document)
|
||||||
elsif submitter.with_signature_fields?
|
elsif submitter.with_signature_fields?
|
||||||
I18n.t(:you_are_invited_to_sign_a_document)
|
I18n.t(:you_are_invited_to_sign_a_document)
|
||||||
else
|
else
|
||||||
@@ -226,6 +264,14 @@ class SubmitterMailer < ApplicationMailer
|
|||||||
submitter.template&.preferences&.dig('submitters').to_a.index_by { |e| e['uuid'] }
|
submitter.template&.preferences&.dig('submitters').to_a.index_by { |e| e['uuid'] }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def select_completed_documents(submitter)
|
||||||
|
last_submitter = Submitter.where(submission_id: submitter.submission_id).completed.order(:completed_at).last
|
||||||
|
|
||||||
|
Submissions::EnsureResultGenerated.call(last_submitter)
|
||||||
|
|
||||||
|
Submitters.select_attachments_for_download(last_submitter)
|
||||||
|
end
|
||||||
|
|
||||||
def add_attachments_with_size_limit(submitter, storage_attachments, current_size, filename_format = nil)
|
def add_attachments_with_size_limit(submitter, storage_attachments, current_size, filename_format = nil)
|
||||||
total_size = current_size
|
total_size = current_size
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ class Account < ApplicationRecord
|
|||||||
has_many :encrypted_configs, dependent: :destroy
|
has_many :encrypted_configs, dependent: :destroy
|
||||||
has_many :account_configs, dependent: :destroy
|
has_many :account_configs, dependent: :destroy
|
||||||
has_many :email_messages, dependent: :destroy
|
has_many :email_messages, dependent: :destroy
|
||||||
|
has_many :email_message_assets, dependent: :destroy
|
||||||
has_many :templates, dependent: :destroy
|
has_many :templates, dependent: :destroy
|
||||||
has_many :template_folders, dependent: :destroy
|
has_many :template_folders, dependent: :destroy
|
||||||
has_one :default_template_folder, -> { where(name: TemplateFolder::DEFAULT_NAME) },
|
has_one :default_template_folder, -> { where(name: TemplateFolder::DEFAULT_NAME) },
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
#
|
#
|
||||||
class AccountConfig < ApplicationRecord
|
class AccountConfig < ApplicationRecord
|
||||||
SUBMITTER_INVITATION_EMAIL_KEY = 'submitter_invitation_email'
|
SUBMITTER_INVITATION_EMAIL_KEY = 'submitter_invitation_email'
|
||||||
|
SUBMITTER_VIEW_INVITATION_EMAIL_KEY = 'submitter_view_invitation_email'
|
||||||
SUBMITTER_INVITATION_REMINDER_EMAIL_KEY = 'submitter_invitation_reminder_email'
|
SUBMITTER_INVITATION_REMINDER_EMAIL_KEY = 'submitter_invitation_reminder_email'
|
||||||
SUBMITTER_COMPLETED_EMAIL_KEY = 'submitter_completed_email'
|
SUBMITTER_COMPLETED_EMAIL_KEY = 'submitter_completed_email'
|
||||||
SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY = 'submitter_documents_copy_email'
|
SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY = 'submitter_documents_copy_email'
|
||||||
@@ -47,6 +48,7 @@ class AccountConfig < ApplicationRecord
|
|||||||
WITH_SIGNATURE_ID = 'with_signature_id'
|
WITH_SIGNATURE_ID = 'with_signature_id'
|
||||||
WITH_FILE_LINKS_KEY = 'with_file_links'
|
WITH_FILE_LINKS_KEY = 'with_file_links'
|
||||||
WITH_SIGNATURE_ID_REASON_KEY = 'with_signature_id_reason'
|
WITH_SIGNATURE_ID_REASON_KEY = 'with_signature_id_reason'
|
||||||
|
WITH_SIGNATURE_ID_COMPLETED_AT_KEY = 'with_signature_id_completed_at'
|
||||||
RECIPIENT_FORM_FIELDS_KEY = 'recipient_form_fields'
|
RECIPIENT_FORM_FIELDS_KEY = 'recipient_form_fields'
|
||||||
WITH_AUDIT_VALUES_KEY = 'with_audit_values'
|
WITH_AUDIT_VALUES_KEY = 'with_audit_values'
|
||||||
WITH_AUDIT_SENDER_KEY = 'with_audit_sender'
|
WITH_AUDIT_SENDER_KEY = 'with_audit_sender'
|
||||||
@@ -58,11 +60,13 @@ class AccountConfig < ApplicationRecord
|
|||||||
COMBINE_PDF_RESULT_KEY = 'combine_pdf_result_key'
|
COMBINE_PDF_RESULT_KEY = 'combine_pdf_result_key'
|
||||||
DOCUMENT_FILENAME_FORMAT_KEY = 'document_filename_format'
|
DOCUMENT_FILENAME_FORMAT_KEY = 'document_filename_format'
|
||||||
TEMPLATE_CUSTOM_FIELDS_KEY = 'template_custom_fields'
|
TEMPLATE_CUSTOM_FIELDS_KEY = 'template_custom_fields'
|
||||||
|
TEMPLATE_DATE_FORMATS_KEY = 'template_date_formats'
|
||||||
POLICY_LINKS_KEY = 'policy_links'
|
POLICY_LINKS_KEY = 'policy_links'
|
||||||
ENABLE_MCP_KEY = 'enable_mcp'
|
ENABLE_MCP_KEY = 'enable_mcp'
|
||||||
|
|
||||||
EMAIL_VARIABLES = {
|
EMAIL_VARIABLES = {
|
||||||
SUBMITTER_INVITATION_EMAIL_KEY => %w[template.name submitter.link account.name].freeze,
|
SUBMITTER_INVITATION_EMAIL_KEY => %w[template.name submitter.link account.name].freeze,
|
||||||
|
SUBMITTER_VIEW_INVITATION_EMAIL_KEY => %w[template.name submitter.link account.name].freeze,
|
||||||
SUBMITTER_COMPLETED_EMAIL_KEY => %w[template.name submission.submitters submission.link].freeze,
|
SUBMITTER_COMPLETED_EMAIL_KEY => %w[template.name submission.submitters submission.link].freeze,
|
||||||
SUBMITTER_INVITATION_REMINDER_EMAIL_KEY => %w[template.name submitter.link account.name].freeze,
|
SUBMITTER_INVITATION_REMINDER_EMAIL_KEY => %w[template.name submitter.link account.name].freeze,
|
||||||
SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY => %w[template.name documents.link account.name].freeze
|
SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY => %w[template.name documents.link account.name].freeze
|
||||||
@@ -75,6 +79,12 @@ class AccountConfig < ApplicationRecord
|
|||||||
'body' => I18n.t(:submitter_invitation_email_sign_body)
|
'body' => I18n.t(:submitter_invitation_email_sign_body)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
SUBMITTER_VIEW_INVITATION_EMAIL_KEY => lambda {
|
||||||
|
{
|
||||||
|
'subject' => I18n.t(:you_are_invited_to_view_a_document),
|
||||||
|
'body' => I18n.t(:submitter_invitation_email_view_body)
|
||||||
|
}
|
||||||
|
},
|
||||||
SUBMITTER_INVITATION_REMINDER_EMAIL_KEY => lambda {
|
SUBMITTER_INVITATION_REMINDER_EMAIL_KEY => lambda {
|
||||||
{
|
{
|
||||||
'subject' => I18n.t(:you_are_invited_to_sign_a_document),
|
'subject' => I18n.t(:you_are_invited_to_sign_a_document),
|
||||||
|
|||||||
@@ -33,6 +33,15 @@ class EmailMessage < ApplicationRecord
|
|||||||
|
|
||||||
before_validation :set_sha1, on: :create
|
before_validation :set_sha1, on: :create
|
||||||
|
|
||||||
|
def normalized_body
|
||||||
|
@normalized_body ||=
|
||||||
|
if body&.include?(EmailMessages::ASSET_PREFIX)
|
||||||
|
EmailMessages.rebuild_body_with_assets(account_id, body)
|
||||||
|
else
|
||||||
|
body
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def set_sha1
|
def set_sha1
|
||||||
self.sha1 = Digest::SHA1.hexdigest({ subject:, body: }.to_json)
|
self.sha1 = Digest::SHA1.hexdigest({ subject:, body: }.to_json)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: email_message_assets
|
||||||
|
#
|
||||||
|
# id :bigint not null, primary key
|
||||||
|
# data :text not null
|
||||||
|
# sha1 :string not null
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
# account_id :bigint not null
|
||||||
|
#
|
||||||
|
# Indexes
|
||||||
|
#
|
||||||
|
# index_email_message_assets_on_account_id_and_sha1 (account_id,sha1) UNIQUE
|
||||||
|
#
|
||||||
|
# Foreign Keys
|
||||||
|
#
|
||||||
|
# fk_rails_... (account_id => accounts.id)
|
||||||
|
#
|
||||||
|
class EmailMessageAsset < ApplicationRecord
|
||||||
|
belongs_to :account
|
||||||
|
|
||||||
|
before_validation :set_sha1, on: :create
|
||||||
|
|
||||||
|
def set_sha1
|
||||||
|
self.sha1 = Digest::SHA1.hexdigest(data.to_s)
|
||||||
|
end
|
||||||
|
end
|
||||||
+14
-15
@@ -6,6 +6,7 @@
|
|||||||
#
|
#
|
||||||
# id :bigint not null, primary key
|
# id :bigint not null, primary key
|
||||||
# archived_at :datetime
|
# archived_at :datetime
|
||||||
|
# completed_at :datetime
|
||||||
# expire_at :datetime
|
# expire_at :datetime
|
||||||
# name :text
|
# name :text
|
||||||
# preferences :text not null
|
# preferences :text not null
|
||||||
@@ -25,9 +26,12 @@
|
|||||||
#
|
#
|
||||||
# Indexes
|
# Indexes
|
||||||
#
|
#
|
||||||
|
# index_submissions_on_account_id_and_completed_at (account_id,completed_at) WHERE ((completed_at IS NOT NULL) AND (archived_at IS NULL))
|
||||||
# index_submissions_on_account_id_and_id (account_id,id)
|
# index_submissions_on_account_id_and_id (account_id,id)
|
||||||
|
# index_submissions_on_account_id_and_id_pending (account_id,id) WHERE ((completed_at IS NULL) AND (archived_at IS NULL))
|
||||||
# index_submissions_on_account_id_and_template_id_and_id (account_id,template_id,id) WHERE (archived_at IS NULL)
|
# index_submissions_on_account_id_and_template_id_and_id (account_id,template_id,id) WHERE (archived_at IS NULL)
|
||||||
# index_submissions_on_account_id_and_template_id_and_id_archived (account_id,template_id,id) WHERE (archived_at IS NOT NULL)
|
# index_submissions_on_account_id_and_template_id_and_id_archived (account_id,template_id,id) WHERE (archived_at IS NOT NULL)
|
||||||
|
# index_submissions_on_created_at (created_at)
|
||||||
# index_submissions_on_created_by_user_id (created_by_user_id)
|
# index_submissions_on_created_by_user_id (created_by_user_id)
|
||||||
# index_submissions_on_slug (slug) UNIQUE
|
# index_submissions_on_slug (slug) UNIQUE
|
||||||
# index_submissions_on_template_id (template_id)
|
# index_submissions_on_template_id (template_id)
|
||||||
@@ -88,23 +92,18 @@ class Submission < ApplicationRecord
|
|||||||
|
|
||||||
scope :active, -> { where(archived_at: nil) }
|
scope :active, -> { where(archived_at: nil) }
|
||||||
scope :archived, -> { where.not(archived_at: nil) }
|
scope :archived, -> { where.not(archived_at: nil) }
|
||||||
scope :pending, lambda {
|
scope :non_expired, -> { where(expire_at: nil).or(where(expire_at: Time.current..)) }
|
||||||
where(expire_at: nil).or(where(expire_at: Time.current..))
|
scope :pending, -> { non_expired.where(completed_at: nil) }
|
||||||
.where(Submitter.where(Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id])
|
scope :completed, -> { where.not(completed_at: nil) }
|
||||||
.and(Submitter.arel_table[:completed_at].eq(nil))).select(1).arel.exists)
|
|
||||||
}
|
|
||||||
scope :completed, lambda {
|
|
||||||
where.not(Submitter.where(Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id])
|
|
||||||
.and(Submitter.arel_table[:completed_at].eq(nil))).select(1).arel.exists)
|
|
||||||
}
|
|
||||||
scope :declined, lambda {
|
scope :declined, lambda {
|
||||||
where(Submitter.where(Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id])
|
where(Submitter.where(Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id]))
|
||||||
.and(Submitter.arel_table[:declined_at].not_eq(nil))).select(1).arel.exists)
|
.where.not(declined_at: nil).limit(1).arel.exists)
|
||||||
}
|
}
|
||||||
scope :expired, lambda {
|
scope :expired, -> { where(expire_at: ..Time.current).where(completed_at: nil) }
|
||||||
where(expire_at: ..Time.current)
|
|
||||||
.where(Submitter.where(Submitter.arel_table[:submission_id].eq(Submission.arel_table[:id])
|
scope :select_for_list, lambda {
|
||||||
.and(Submitter.arel_table[:completed_at].eq(nil))).select(1).arel.exists)
|
select(:id, :name, :created_by_user_id, :account_id, :completed_at,
|
||||||
|
:created_at, :archived_at, :expire_at, :template_id, :template_submitters)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum :source, {
|
enum :source, {
|
||||||
|
|||||||
@@ -28,8 +28,8 @@
|
|||||||
#
|
#
|
||||||
# Indexes
|
# Indexes
|
||||||
#
|
#
|
||||||
|
# index_submitters_on_account_id_and_completed_at (account_id,completed_at) WHERE (completed_at IS NOT NULL)
|
||||||
# index_submitters_on_account_id_and_id (account_id,id)
|
# index_submitters_on_account_id_and_id (account_id,id)
|
||||||
# index_submitters_on_completed_at_and_account_id (completed_at,account_id)
|
|
||||||
# index_submitters_on_email (email)
|
# index_submitters_on_email (email)
|
||||||
# index_submitters_on_external_id (external_id)
|
# index_submitters_on_external_id (external_id)
|
||||||
# index_submitters_on_slug (slug) UNIQUE
|
# index_submitters_on_slug (slug) UNIQUE
|
||||||
@@ -116,6 +116,12 @@ class Submitter < ApplicationRecord
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def viewer?
|
||||||
|
return false if submission.template_submitters.blank?
|
||||||
|
|
||||||
|
submission.template_submitters.any? { |s| s['uuid'] == uuid && s['is_viewer'] }
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def anonymize_email_events
|
def anonymize_email_events
|
||||||
|
|||||||
@@ -81,6 +81,8 @@ class Template < ApplicationRecord
|
|||||||
scope :active, -> { where(archived_at: nil) }
|
scope :active, -> { where(archived_at: nil) }
|
||||||
scope :archived, -> { where.not(archived_at: nil) }
|
scope :archived, -> { where.not(archived_at: nil) }
|
||||||
|
|
||||||
|
scope :select_for_list, -> { select(:id, :name, :author_id, :account_id, :created_at, :archived_at, :folder_id) }
|
||||||
|
|
||||||
def application_key
|
def application_key
|
||||||
external_id
|
external_id
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -57,6 +57,11 @@
|
|||||||
<%= f.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button' %>
|
<%= f.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button' %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
<% if Docuseal.multitenant? && @encrypted_config.persisted? %>
|
||||||
|
<div class="flex justify-center pt-2">
|
||||||
|
<%= button_to t('reset_default'), settings_email_path(@encrypted_config), method: :delete, class: 'link', data: { turbo_confirm: t('are_you_sure_') } %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-0 md:w-52"></div>
|
<div class="w-0 md:w-52"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -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="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 2" />
|
||||||
|
<path d="M9 15l2 2l4 -4" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 437 B |
@@ -0,0 +1,7 @@
|
|||||||
|
<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 2" />
|
||||||
|
<path d="M12 17v.01" />
|
||||||
|
<path d="M12 14a1.5 1.5 0 1 0 -1.14 -2.474" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 482 B |
@@ -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>
|
||||||
|
<path d="M10 19h-5a2 2 0 0 1 -2 -2v-11a2 2 0 0 1 2 -2h4l3 3h7a2 2 0 0 1 2 2v2.5"></path>
|
||||||
|
<path d="M17.8 20.817l-2.172 1.138a.392 .392 0 0 1 -.568 -.41l.415 -2.411l-1.757 -1.707a.389 .389 0 0 1 .217 -.665l2.428 -.352l1.086 -2.193a.392 .392 0 0 1 .702 0l1.086 2.193l2.428 .352a.39 .39 0 0 1 .217 .665l-1.757 1.707l.414 2.41a.39 .39 0 0 1 -.567 .411l-2.172 -1.138"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 662 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>
|
||||||
|
<path d="M12 19h-7a2 2 0 0 1 -2 -2v-11a2 2 0 0 1 2 -2h4l3 3h7a2 2 0 0 1 2 2v3.5"></path>
|
||||||
|
<path d="M19 22v-6"></path>
|
||||||
|
<path d="M22 19l-3 -3l-3 3"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 447 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="M8 9h8" />
|
||||||
|
<path d="M8 13h6" />
|
||||||
|
<path d="M18 4a3 3 0 0 1 3 3v8a3 3 0 0 1 -3 3h-5l-5 3v-3h-2a3 3 0 0 1 -3 -3v-8a3 3 0 0 1 3 -3h12" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 431 B |
@@ -0,0 +1,7 @@
|
|||||||
|
<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="M16 7h4" />
|
||||||
|
<path d="M16 16v1l2 2l.5 -.5m1.5 -2.5v-11c0 -1.121 -.879 -2 -2 -2s-2 .879 -2 2v7" />
|
||||||
|
<path d="M18 19h-13a2 2 0 1 1 0 -4h4a2 2 0 1 0 0 -4h-3" />
|
||||||
|
<path d="M3 3l18 18" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 480 B |
@@ -0,0 +1,53 @@
|
|||||||
|
<div class="flex items-center px-2 py-2 border-b" style="height: 42px;">
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('bold') %> (Ctrl+B)">
|
||||||
|
<button type="button" data-action="click:<%= editor_tag %>#bold" data-target="<%= editor_tag %>.boldButton" aria-label="<%= t('bold') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
|
||||||
|
<%= svg_icon('bold', class: 'w-4 h-4') %>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('italic') %> (Ctrl+I)">
|
||||||
|
<button type="button" data-action="click:<%= editor_tag %>#italic" data-target="<%= editor_tag %>.italicButton" aria-label="<%= t('italic') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
|
||||||
|
<%= svg_icon('italic', class: 'w-4 h-4') %>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('underline') %> (Ctrl+U)">
|
||||||
|
<button type="button" data-action="click:<%= editor_tag %>#underline" data-target="<%= editor_tag %>.underlineButton" aria-label="<%= t('underline') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
|
||||||
|
<%= svg_icon('underline', class: 'w-4 h-4') %>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('link') %> (Ctrl+K)">
|
||||||
|
<button type="button" data-action="click:<%= editor_tag %>#linkSelection" data-target="<%= editor_tag %>.linkButton" aria-label="<%= t('link') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
|
||||||
|
<%= svg_icon('link', class: 'w-4 h-4') %>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mx-2 h-5 border-l border-base-content/20"></div>
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('undo') %> (Ctrl+Z)">
|
||||||
|
<button type="button" data-action="click:<%= editor_tag %>#undo" data-target="<%= editor_tag %>.undoButton" aria-label="<%= t('undo') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
|
||||||
|
<%= svg_icon('arrow_back_up', class: 'w-4 h-4') %>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('redo') %> (Ctrl+Shift+Z)">
|
||||||
|
<button type="button" data-action="click:<%= editor_tag %>#redo" data-target="<%= editor_tag %>.redoButton" aria-label="<%= t('redo') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
|
||||||
|
<%= svg_icon('arrow_forward_up', class: 'w-4 h-4') %>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% if local_assigns[:variables]&.any? %>
|
||||||
|
<% variable_labels = { 'account.name' => t('variables.account_name'), 'submitter.link' => t('variables.submitter_link'), 'template.name' => t('variables.template_name'), 'submission.submitters' => t('variables.submission_submitters'), 'submission.link' => t('variables.submission_link'), 'documents.link' => t('variables.documents_link') } %>
|
||||||
|
<div class="dropdown dropdown-end ml-auto">
|
||||||
|
<label tabindex="0" class="flex items-center gap-1 text-sm px-2 py-1 rounded hover:bg-base-200 cursor-pointer">
|
||||||
|
<%= t('add_variable') %>
|
||||||
|
<%= svg_icon('chevron_down', class: 'w-3.5 h-3.5') %>
|
||||||
|
</label>
|
||||||
|
<div tabindex="0" class="dropdown-content right-0 top-full mt-1 p-1 bg-white border border-neutral-200 rounded-lg shadow-lg z-50">
|
||||||
|
<% local_assigns[:variables]&.each do |variable| %>
|
||||||
|
<button type="button" data-variable="<%= variable %>" data-action="click:<%= editor_tag %>#insertVariable" class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 text-left text-sm cursor-pointer whitespace-nowrap">
|
||||||
|
<%= variable_labels.fetch(variable, "{#{variable}}") %>
|
||||||
|
</button>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<%= render 'personalization_settings/markdown_editor', name:, value:, variables: local_assigns[:variables] %>
|
||||||
@@ -1,76 +1,18 @@
|
|||||||
<% if value.to_s.start_with?('<html') %>
|
<markdown-editor>
|
||||||
<autoresize-textarea>
|
<template data-target="markdown-editor.linkTooltipTemplate">
|
||||||
<%= text_area_tag name, value, required: true, class: 'base-input w-full py-2 !rounded-2xl', dir: 'auto', style: 'max-height: 400px' %>
|
<div class="hidden absolute flex bg-white border border-base-300 rounded-xl shadow p-1 gap-1 items-center z-50" contenteditable="false">
|
||||||
</autoresize-textarea>
|
<input type="text" placeholder="<%= t('enter_a_url_or_variable_name') %>" class="rounded-lg border border-base-300 px-2 py-1 text-sm outline-none" style="field-sizing: content; min-width: 205px; max-width: 320px;" autocomplete="off">
|
||||||
<% else %>
|
<button type="button" data-role="link-save" class="flex items-center px-1 w-6 h-6 rounded hover:bg-success/10 cursor-pointer">
|
||||||
<markdown-editor>
|
<%= svg_icon('check', class: 'w-4 h-4 text-success') %>
|
||||||
<template data-target="markdown-editor.linkTooltipTemplate">
|
</button>
|
||||||
<div class="hidden absolute flex bg-white border border-base-300 rounded-xl shadow p-1 gap-1 items-center z-50" contenteditable="false">
|
<button type="button" data-role="link-remove" class="flex items-center px-1 w-6 h-6 rounded hover:bg-error/10 cursor-pointer">
|
||||||
<input type="text" placeholder="<%= t('enter_a_url_or_variable_name') %>" class="rounded-lg border border-base-300 px-2 py-1 text-sm outline-none" style="field-sizing: content; min-width: 205px; max-width: 320px;" autocomplete="off">
|
<%= svg_icon('x', class: 'w-4 h-4 text-error') %>
|
||||||
<button type="button" data-role="link-save" class="flex items-center px-1 w-6 h-6 rounded hover:bg-success/10 cursor-pointer">
|
</button>
|
||||||
<%= svg_icon('check', class: 'w-4 h-4 text-success') %>
|
|
||||||
</button>
|
|
||||||
<button type="button" data-role="link-remove" class="flex items-center px-1 w-6 h-6 rounded hover:bg-error/10 cursor-pointer">
|
|
||||||
<%= svg_icon('x', class: 'w-4 h-4 text-error') %>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<div class="border border-base-content/20 rounded-2xl bg-white">
|
|
||||||
<div class="flex items-center px-2 py-2 border-b" style="height: 42px;">
|
|
||||||
<div class="flex items-center gap-1">
|
|
||||||
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('bold') %> (Ctrl+B)">
|
|
||||||
<button type="button" data-action="click:markdown-editor#bold" data-target="markdown-editor.boldButton" aria-label="<%= t('bold') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
|
|
||||||
<%= svg_icon('bold', class: 'w-4 h-4') %>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('italic') %> (Ctrl+I)">
|
|
||||||
<button type="button" data-action="click:markdown-editor#italic" data-target="markdown-editor.italicButton" aria-label="<%= t('italic') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
|
|
||||||
<%= svg_icon('italic', class: 'w-4 h-4') %>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('underline') %> (Ctrl+U)">
|
|
||||||
<button type="button" data-action="click:markdown-editor#underline" data-target="markdown-editor.underlineButton" aria-label="<%= t('underline') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
|
|
||||||
<%= svg_icon('underline', class: 'w-4 h-4') %>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('link') %> (Ctrl+K)">
|
|
||||||
<button type="button" data-action="click:markdown-editor#linkSelection" data-target="markdown-editor.linkButton" aria-label="<%= t('link') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
|
|
||||||
<%= svg_icon('link', class: 'w-4 h-4') %>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mx-2 h-5 border-l border-base-content/20"></div>
|
|
||||||
<div class="flex items-center gap-1">
|
|
||||||
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('undo') %> (Ctrl+Z)">
|
|
||||||
<button type="button" data-action="click:markdown-editor#undo" data-target="markdown-editor.undoButton" aria-label="<%= t('undo') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
|
|
||||||
<%= svg_icon('arrow_back_up', class: 'w-4 h-4') %>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="tooltip tooltip-top before:text-xs" data-tip="<%= t('redo') %> (Ctrl+Shift+Z)">
|
|
||||||
<button type="button" data-action="click:markdown-editor#redo" data-target="markdown-editor.redoButton" aria-label="<%= t('redo') %>" class="flex items-center px-1 w-6 h-6 rounded hover:bg-base-300">
|
|
||||||
<%= svg_icon('arrow_forward_up', class: 'w-4 h-4') %>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<% if local_assigns[:variables]&.any? %>
|
|
||||||
<% variable_labels = { 'account.name' => t('variables.account_name'), 'submitter.link' => t('variables.submitter_link'), 'template.name' => t('variables.template_name'), 'submission.submitters' => t('variables.submission_submitters'), 'submission.link' => t('variables.submission_link'), 'documents.link' => t('variables.documents_link') } %>
|
|
||||||
<div class="dropdown dropdown-end ml-auto">
|
|
||||||
<label tabindex="0" class="flex items-center gap-1 text-sm px-2 py-1 rounded hover:bg-base-200 cursor-pointer">
|
|
||||||
<%= t('add_variable') %>
|
|
||||||
<%= svg_icon('chevron_down', class: 'w-3.5 h-3.5') %>
|
|
||||||
</label>
|
|
||||||
<div tabindex="0" class="dropdown-content right-0 top-full mt-1 p-1 bg-white border border-neutral-200 rounded-lg shadow-lg z-50">
|
|
||||||
<% local_assigns[:variables]&.each do |variable| %>
|
|
||||||
<button type="button" data-variable="<%= variable %>" data-action="click:markdown-editor#insertVariable" class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 text-left text-sm cursor-pointer whitespace-nowrap">
|
|
||||||
<%= variable_labels.fetch(variable, "{#{variable}}") %>
|
|
||||||
</button>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
<div data-target="markdown-editor.editorElement"></div>
|
|
||||||
</div>
|
</div>
|
||||||
<%= hidden_field_tag name, value, required: true, data: { target: 'markdown-editor.textarea' } %>
|
</template>
|
||||||
</markdown-editor>
|
<div class="border border-base-content/20 rounded-2xl bg-white">
|
||||||
<% end %>
|
<%= render 'personalization_settings/editor_toolbar', editor_tag: 'markdown-editor', variables: local_assigns[:variables] %>
|
||||||
|
<div data-target="markdown-editor.editorElement"></div>
|
||||||
|
</div>
|
||||||
|
<%= hidden_field_tag name, value, required: true, data: { target: 'markdown-editor.textarea' } %>
|
||||||
|
</markdown-editor>
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<%= f.label :current_password, t('current_password'), class: 'label' %>
|
<%= f.label :current_password, t('current_password'), class: 'label' %>
|
||||||
<%= f.password_field :current_password, autocomplete: 'current-password', class: 'base-input' %>
|
<%= f.password_field :current_password, autocomplete: 'current-password', class: 'base-input' %>
|
||||||
<% if Accounts.can_send_emails?(current_account) %>
|
<% if Docuseal.multitenant? || Accounts.can_send_emails?(current_account) %>
|
||||||
<span class="label-text-alt mt-1">
|
<span class="label-text-alt mt-1">
|
||||||
<%= t('dont_remember_your_current_password_click_here_to_reset_it_html') %>
|
<%= t('dont_remember_your_current_password_click_here_to_reset_it_html') %>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -4,7 +4,9 @@
|
|||||||
<p>
|
<p>
|
||||||
<% if @current_account&.testing? %>
|
<% if @current_account&.testing? %>
|
||||||
<%= t('sent_using_product_name_in_testing_mode_html', product_url: "#{Docuseal::PRODUCT_EMAIL_URL}/start", product_name: Docuseal.product_name) %>
|
<%= t('sent_using_product_name_in_testing_mode_html', product_url: "#{Docuseal::PRODUCT_EMAIL_URL}/start", product_name: Docuseal.product_name) %>
|
||||||
<% else %>
|
<% elsif Docuseal.multitenant? %>
|
||||||
<%= t('sent_using_product_name_free_document_signing_html', product_url: "#{Docuseal::PRODUCT_EMAIL_URL}/start", product_name: Docuseal.product_name) %>
|
<%= t('sent_using_product_name_free_document_signing_html', product_url: "#{Docuseal::PRODUCT_EMAIL_URL}/start", product_name: Docuseal.product_name) %>
|
||||||
|
<% else %>
|
||||||
|
<%= t('sent_using_product_name_open_source_software_html', product_url: "#{Docuseal::PRODUCT_EMAIL_URL}/open", product_name: Docuseal.product_name) %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</p>
|
</p>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<a target="_blank" href="<%= Docuseal::GITHUB_URL %>" rel="noopener noreferrer nofollow" class="relative flex items-center rounded-full px-2 py-0.5 text-xs leading-4 mt-1 text-base-content border border-base-300 tooltip tooltip-bottom" data-tip="Give a star on GitHub">
|
<a target="_blank" href="<%= Docuseal::GITHUB_URL %>" rel="noopener noreferrer nofollow" class="relative flex items-center rounded-full px-2 py-0.5 text-xs leading-4 mt-1 text-base-content border border-base-300 tooltip tooltip-bottom" data-tip="Give a star on GitHub">
|
||||||
<span class="flex items-center justify-between space-x-0.5 font-medium">
|
<span class="flex items-center justify-between space-x-0.5 font-medium">
|
||||||
<%= svg_icon('start', class: 'h-3 w-3') %>
|
<%= svg_icon('start', class: 'h-3 w-3') %>
|
||||||
<span>17k</span>
|
<span>18k</span>
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
@@ -71,10 +71,11 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<li>
|
<li>
|
||||||
<%= button_to destroy_user_session_path, method: :delete, data: { turbo: false }, class: 'flex items-center' do %>
|
<button form="destroy_user_session_form">
|
||||||
<%= svg_icon('logout', class: 'w-5 h-5 flex-shrink-0 stroke-2 mr-2 inline') %>
|
<%= svg_icon('logout', class: 'w-5 h-5 flex-shrink-0 stroke-2 inline') %>
|
||||||
<span class="mr-1 whitespace-nowrap"><%= t('sign_out') %></span>
|
<span class="whitespace-nowrap"><%= t('sign_out') %></span>
|
||||||
<% end %>
|
</button>
|
||||||
|
<%= button_to '', destroy_user_session_path, method: :delete, form: { id: 'destroy_user_session_form', data: { turbo: false } }, form_class: 'hidden' %>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
<form action="<%= url_for %>" method="get" class="items-center flex">
|
<form action="<%= url_for %>" method="get" class="items-center flex max-md:focus-within:flex-1 <%= 'max-md:flex-1' if params[:q].present? %>">
|
||||||
<% Submissions::Filter::ALLOWED_PARAMS.each do |key| %>
|
<% Submissions::Filter::ALLOWED_PARAMS.each do |key| %>
|
||||||
<% if params[key].present? %>
|
<% if params[key].present? %>
|
||||||
<input name="<%= key %>" value="<%= params[key] %>" class="hidden">
|
<input name="<%= key %>" value="<%= params[key] %>" class="hidden">
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
<% if params[:archived].present? %>
|
||||||
|
<input name="archived" value="<%= params[:archived] %>" class="hidden">
|
||||||
|
<% end %>
|
||||||
<% if params[:q].present? %>
|
<% if params[:q].present? %>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<a href="<%= url_for(params: request.query_parameters.except('q')) %>" title="<%= t('clear') %>" class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-auto text-neutral text-2xl font-extralight">
|
<a href="<%= url_for(params: request.query_parameters.except('q')) %>" title="<%= t('clear') %>" class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-auto text-neutral text-2xl font-extralight">
|
||||||
@@ -11,8 +14,8 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<search-input data-title="<%= local_assigns[:title_selector] || 'h1' %>" class="flex items-center">
|
<search-input class="group flex items-center max-md:focus-within:flex-1 <%= 'max-md:flex-1' if params[:q].present? %>">
|
||||||
<input id="search" name="q" value="<%= params[:q] %>" class="input text-lg pr-10 -mr-12 w-0 md:w-60 <%= 'pl-8 input-outlined w-60' if params[:q].present? %>" placeholder="<%= local_assigns[:placeholder] %>">
|
<input id="search" name="q" value="<%= params[:q] %>" enterkeyhint="search" class="input text-lg pr-10 -mr-12 w-0 md:w-60 max-md:group-focus-within:w-full <%= 'pl-8 input-outlined max-md:w-full' if params[:q].present? %>" placeholder="<%= local_assigns[:placeholder] %>">
|
||||||
<button type="submit" title="<%= t('search') %>" class="btn btn-ghost btn-circle">
|
<button type="submit" title="<%= t('search') %>" class="btn btn-ghost btn-circle">
|
||||||
<span class="enabled">
|
<span class="enabled">
|
||||||
<%= svg_icon('search', class: 'w-6 h-6 stroke-2') %>
|
<%= svg_icon('search', class: 'w-6 h-6 stroke-2') %>
|
||||||
|
|||||||
@@ -12,12 +12,12 @@
|
|||||||
<li>
|
<li>
|
||||||
<%= link_to t('account'), settings_account_path, class: 'text-base hover:bg-base-300' %>
|
<%= link_to t('account'), settings_account_path, class: 'text-base hover:bg-base-300' %>
|
||||||
</li>
|
</li>
|
||||||
|
<% if (!Docuseal.multitenant? || EncryptedConfig.exists?(key: EncryptedConfig::EMAIL_SMTP_KEY, account: current_account)) && can?(:read, EncryptedConfig.new(key: EncryptedConfig::EMAIL_SMTP_KEY, account: current_account)) && ENV['SMTP_ADDRESS'].blank? && true_user == current_user %>
|
||||||
|
<li>
|
||||||
|
<%= link_to t('email'), settings_email_index_path, class: 'text-base hover:bg-base-300' %>
|
||||||
|
</li>
|
||||||
|
<% end %>
|
||||||
<% unless Docuseal.multitenant? %>
|
<% unless Docuseal.multitenant? %>
|
||||||
<% if can?(:read, EncryptedConfig.new(key: EncryptedConfig::EMAIL_SMTP_KEY, account: current_account)) && ENV['SMTP_ADDRESS'].blank? && true_user == current_user %>
|
|
||||||
<li>
|
|
||||||
<%= link_to t('email'), settings_email_index_path, class: 'text-base hover:bg-base-300' %>
|
|
||||||
</li>
|
|
||||||
<% end %>
|
|
||||||
<% if can?(:read, EncryptedConfig.new(key: EncryptedConfig::FILES_STORAGE_KEY, account: current_account)) && true_user == current_user && ENV['S3_ATTACHMENTS_BUCKET'].blank? && ENV['GCS_BUCKET'].blank? && ENV['AZURE_CONTAINER'].blank? %>
|
<% if can?(:read, EncryptedConfig.new(key: EncryptedConfig::FILES_STORAGE_KEY, account: current_account)) && true_user == current_user && ENV['S3_ATTACHMENTS_BUCKET'].blank? && ENV['GCS_BUCKET'].blank? && ENV['AZURE_CONTAINER'].blank? %>
|
||||||
<li>
|
<li>
|
||||||
<%= link_to t('storage'), settings_storage_index_path, class: 'text-base hover:bg-base-300' %>
|
<%= link_to t('storage'), settings_storage_index_path, class: 'text-base hover:bg-base-300' %>
|
||||||
@@ -132,7 +132,7 @@
|
|||||||
<%= capture do %>
|
<%= capture do %>
|
||||||
<div class="tooltip" data-tip="<%= t('ai_assistant') %>">
|
<div class="tooltip" data-tip="<%= t('ai_assistant') %>">
|
||||||
<a href="<%= Docuseal::CHATGPT_URL %>" target="_blank" class="btn btn-circle btn-primary btn-md">
|
<a href="<%= Docuseal::CHATGPT_URL %>" target="_blank" class="btn btn-circle btn-primary btn-md">
|
||||||
<%= svg_icon('brand_openai', class: 'w-8 h-8') %>
|
<%= svg_icon('message', class: 'w-8 h-8') %>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<%= svg_icon('writing_sign', class: 'w-10 h-10') %>
|
<%= svg_icon('writing_sign', class: 'w-10 h-10') %>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p dir="auto" class="text-lg font-bold mb-1"><%= @submitter.submission.template.name %></p>
|
<p dir="auto" class="text-lg font-bold mb-1"><%= @submitter.submission.template&.name %></p>
|
||||||
<p dir="auto" class="text-sm">
|
<p dir="auto" class="text-sm">
|
||||||
<%= t(@submitter.with_signature_fields? ? 'signed_on_time' : 'completed_on_time', time: l(@submitter.completed_at.to_date, format: :long)) %>
|
<%= t(@submitter.with_signature_fields? ? 'signed_on_time' : 'completed_on_time', time: l(@submitter.completed_at.to_date, format: :long)) %>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -91,8 +91,8 @@
|
|||||||
</dynamic-list>
|
</dynamic-list>
|
||||||
<%= local_assigns[:variables_form] %>
|
<%= local_assigns[:variables_form] %>
|
||||||
<div>
|
<div>
|
||||||
<%= render('submitters_order', f:, template:) if Accounts.can_send_emails?(current_account) %>
|
<%= render('submitters_order', f:, template:) if can_send_emails %>
|
||||||
<%= render 'send_email', f:, template: %>
|
<%= render 'send_email', f:, template:, can_send_emails:, viewer_submitter_uuids: local_assigns[:viewer_submitter_uuids] %>
|
||||||
<% if has_phone_field %>
|
<% if has_phone_field %>
|
||||||
<%= render 'send_sms', f: %>
|
<%= render 'send_sms', f: %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -69,8 +69,8 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
<%= local_assigns[:variables_form] %>
|
<%= local_assigns[:variables_form] %>
|
||||||
<div>
|
<div>
|
||||||
<%= render('submitters_order', f:, template:) if Accounts.can_send_emails?(current_account) %>
|
<%= render('submitters_order', f:, template:) if can_send_emails %>
|
||||||
<%= render 'send_email', f:, template: %>
|
<%= render 'send_email', f:, template:, can_send_emails:, viewer_submitter_uuids: local_assigns[:viewer_submitter_uuids] %>
|
||||||
<%= render 'extra_fields', f: %>
|
<%= render 'extra_fields', f: %>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
|
|||||||
@@ -56,7 +56,7 @@
|
|||||||
</dynamic-list>
|
</dynamic-list>
|
||||||
<%= local_assigns[:variables_form] %>
|
<%= local_assigns[:variables_form] %>
|
||||||
<div>
|
<div>
|
||||||
<%= render('submitters_order', f:, template:) if Accounts.can_send_emails?(current_account) %>
|
<%= render 'submitters_order', f:, template: %>
|
||||||
<%= render 'send_sms', f:, checked: true %>
|
<%= render 'send_sms', f:, checked: true %>
|
||||||
<%= render 'extra_phone_fields', f: %>
|
<%= render 'extra_phone_fields', f: %>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,15 +2,14 @@
|
|||||||
<% template_submitters = local_assigns[:submitter]&.submission&.template_submitters || template.submitters %>
|
<% template_submitters = local_assigns[:submitter]&.submission&.template_submitters || template.submitters %>
|
||||||
<% message_field_id = "message_field_#{SecureRandom.hex(3)}" %>
|
<% message_field_id = "message_field_#{SecureRandom.hex(3)}" %>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<% can_send_emails = Accounts.can_send_emails?(current_account) %>
|
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<%= f.label :send_email, for: uuid = SecureRandom.uuid, class: 'flex items-center cursor-pointer' do %>
|
<%= f.label :send_email, for: uuid = SecureRandom.uuid, class: 'flex items-center cursor-pointer' do %>
|
||||||
<%= f.check_box :send_email, id: uuid, class: 'base-checkbox', disabled: !can_send_emails || local_assigns[:disable_email], checked: can_send_emails && !local_assigns.key?(:resend_email) && !local_assigns[:disable_email] && template&.preferences&.dig('request_email_enabled') != false %>
|
<%= f.check_box :send_email, id: uuid, class: 'base-checkbox', disabled: !can_send_emails || local_assigns[:disable_email], checked: can_send_emails && !local_assigns.key?(:resend_email) && !local_assigns[:disable_email] && template&.preferences&.dig('request_email_enabled') != false %>
|
||||||
<span class="label"><%= local_assigns[:resend_email] ? t('re_send_email') : t('send_email') %></span>
|
<span class="label"><%= local_assigns[:resend_email] ? t('re_send_email') : t('send_email') %></span>
|
||||||
<% end %>
|
<% end %>
|
||||||
<div>
|
<div>
|
||||||
|
<%= render 'submissions/email_stats' %>
|
||||||
<% if can_send_emails %>
|
<% if can_send_emails %>
|
||||||
<%= render 'submissions/email_stats' %>
|
|
||||||
<%= content_for(:edit_button) || capture do %>
|
<%= content_for(:edit_button) || capture do %>
|
||||||
<toggle-visible data-element-ids="<%= [message_field_id].to_json %>" class="flex">
|
<toggle-visible data-element-ids="<%= [message_field_id].to_json %>" class="flex">
|
||||||
<label>
|
<label>
|
||||||
@@ -39,26 +38,38 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% config = AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY) %>
|
<% config = AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY) %>
|
||||||
|
<% view_config = AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_VIEW_INVITATION_EMAIL_KEY) %>
|
||||||
|
<% config_body = (config.value['body_type'] == 'html' && config.value['html_body'].presence) || config.value['body'] %>
|
||||||
|
<% view_template_subject = template&.preferences&.dig('invitation_view_email_subject').presence %>
|
||||||
|
<% view_template_body = template&.preferences&.dig('invitation_view_email_body').presence %>
|
||||||
|
<% default_subject = template&.preferences&.dig('request_email_subject').presence || config.value['subject'] %>
|
||||||
|
<% default_body = template&.preferences&.dig('request_email_body').presence || config_body %>
|
||||||
|
<% is_edit_viewer = local_assigns[:submitter] && local_assigns[:viewer_submitter_uuids].include?(local_assigns[:submitter].uuid) %>
|
||||||
<div id="<%= message_field_id %>" class="card card-compact bg-base-300/40 hidden">
|
<div id="<%= message_field_id %>" class="card card-compact bg-base-300/40 hidden">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<%= tag.input id: toggle_uuid = SecureRandom.uuid, value: '1', name: 'request_email_per_submitter', class: 'peer', type: 'checkbox', hidden: true, checked: local_assigns[:message_per_submitter] != false && template&.preferences&.dig('submitters').to_a.size > 1 %>
|
<%= tag.input id: toggle_uuid = SecureRandom.uuid, value: '1', name: 'request_email_per_submitter', class: 'peer', type: 'checkbox', hidden: true, checked: local_assigns[:message_per_submitter] != false && template_submitters.size < 11 && template&.preferences&.dig('submitters').to_a.size > 1 %>
|
||||||
|
<% if local_assigns[:viewer_submitter_uuids].present? && local_assigns[:submitter].blank? %>
|
||||||
|
<% (template_submitters.pluck('uuid') - local_assigns[:viewer_submitter_uuids].to_a).each do |signer_uuid| %>
|
||||||
|
<%= hidden_field_tag 'email_message_submitter_uuids[]', signer_uuid %>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
<div class="peer-checked:hidden form-control space-y-2">
|
<div class="peer-checked:hidden form-control space-y-2">
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<%= f.label :subject, t('subject'), class: 'label' %>
|
<%= f.label :subject, t('subject'), class: 'label' %>
|
||||||
<% if template_submitters.size > 1 && template_submitters.size < 5 && local_assigns[:message_per_submitter] != false %>
|
<% if template_submitters.size > 1 && template_submitters.size < 11 && local_assigns[:message_per_submitter] != false %>
|
||||||
<label for="<%= toggle_uuid %>" class="label underline">
|
<label for="<%= toggle_uuid %>" class="label underline">
|
||||||
<%= t('edit_per_party') %>
|
<%= t('edit_per_party') %>
|
||||||
</label>
|
</label>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<%= f.text_field :subject, value: local_assigns[:submitter_email_message]&.subject.presence || submitter_preferences_index.dig(local_assigns[:submitter]&.uuid, 'request_email_subject').presence || template&.preferences&.dig('request_email_subject').presence || config.value['subject'], required: true, class: '!text-sm base-input w-full', dir: 'auto' %>
|
<%= f.text_field :subject, value: local_assigns[:submitter_email_message]&.subject.presence || (is_edit_viewer ? view_template_subject : nil) || submitter_preferences_index.dig(local_assigns[:submitter]&.uuid, 'request_email_subject').presence || (is_edit_viewer ? view_config.value['subject'] : default_subject), required: true, class: '!text-sm base-input w-full', dir: 'auto' %>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<%= f.label :message, t('body'), class: 'label' %>
|
<%= f.label :message, t('body'), class: 'label' %>
|
||||||
<% body_variables = AccountConfig::EMAIL_VARIABLES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY] %>
|
<% body_variables = AccountConfig::EMAIL_VARIABLES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY] %>
|
||||||
<%= render 'personalization_settings/markdown_editor', name: f.field_name(:body), value: local_assigns[:submitter_email_message]&.body.presence || submitter_preferences_index.dig(local_assigns[:submitter]&.uuid, 'request_email_body').presence || template&.preferences&.dig('request_email_body').presence || config.value['body'], variables: body_variables %>
|
<%= render 'personalization_settings/email_body_editor', name: f.field_name(:body), value: local_assigns[:submitter_email_message]&.normalized_body.presence || (is_edit_viewer ? view_template_body : nil) || submitter_preferences_index.dig(local_assigns[:submitter]&.uuid, 'request_email_body').presence || (is_edit_viewer ? view_config.value['body'] : default_body), variables: body_variables %>
|
||||||
<% unless local_assigns.fetch(:disable_save_as_default_template_option, false) %>
|
<% if !local_assigns.fetch(:disable_save_as_default_template_option, false) && config.value['body_type'] != 'html' %>
|
||||||
<label for="<%= uuid = SecureRandom.uuid %>" class="flex items-center cursor-pointer">
|
<label for="<%= uuid = SecureRandom.uuid %>" class="flex items-center cursor-pointer">
|
||||||
<%= check_box_tag :save_message, id: uuid, class: 'base-checkbox', checked: false %>
|
<%= check_box_tag :save_message, id: uuid, class: 'base-checkbox', checked: false %>
|
||||||
<span class="label"><%= t('save_as_default_template_message') %></span>
|
<span class="label"><%= t('save_as_default_template_message') %></span>
|
||||||
@@ -67,32 +78,35 @@
|
|||||||
</div>
|
</div>
|
||||||
<%= render 'submissions/message_fields' %>
|
<%= render 'submissions/message_fields' %>
|
||||||
</div>
|
</div>
|
||||||
<% if template_submitters.size > 1 && template_submitters.size < 5 && local_assigns[:message_per_submitter] != false %>
|
<% if template_submitters.size > 1 && template_submitters.size < 11 && local_assigns[:message_per_submitter] != false %>
|
||||||
<div class="hidden peer-checked:block form-control space-y-2">
|
<div class="hidden peer-checked:block form-control space-y-2">
|
||||||
<% uuid = SecureRandom.uuid %>
|
<% uuid = SecureRandom.uuid %>
|
||||||
<% options = template_submitters.map { |e| [e['name'], "request_email_#{uuid}_#{e['uuid']}"] } %>
|
<% options = template_submitters.map { |e| [e['name'], "request_email_#{uuid}_#{e['uuid']}"] } %>
|
||||||
<toggle-visible data-element-ids="<%= options.map(&:last).to_json %>" class="flex relative px-1">
|
<toggle-visible data-element-ids="<%= options.map(&:last).to_json %>" class="flex relative px-1">
|
||||||
<ul class="tabs w-full flex flex-nowrap">
|
<ul class="tabs w-full min-w-0 flex flex-nowrap">
|
||||||
<% options.each_with_index do |(label, val), index| %>
|
<% options.each_with_index do |(label, val), index| %>
|
||||||
<div class="w-full">
|
<div class="w-full min-w-0 has-[:checked]:min-w-fit">
|
||||||
<%= f.radio_button :selected, val, checked: index.zero?, id: "#{val}_radio", data: { action: 'click:toggle-visible#trigger' }, class: 'hidden peer' %>
|
<%= f.radio_button :selected, val, checked: index.zero?, id: "#{val}_radio", data: { action: 'click:toggle-visible#trigger' }, class: 'hidden peer' %>
|
||||||
<%= f.label :selected, label, value: val, for: "#{val}_radio", class: 'tab w-full tab-lifted peer-checked:tab-active !bg-transparent' %>
|
<%= f.label :selected, value: val, for: "#{val}_radio", class: 'tab w-full tab-lifted peer-checked:tab-active !bg-transparent !px-2' do %>
|
||||||
|
<span class="truncate" title="<%= label %>"><%= label %></span>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</ul>
|
</ul>
|
||||||
</toggle-visible>
|
</toggle-visible>
|
||||||
<% template_submitters.each_with_index do |submitter, index| %>
|
<% template_submitters.each_with_index do |submitter, index| %>
|
||||||
|
<% is_viewer = local_assigns[:viewer_submitter_uuids].include?(submitter['uuid']) %>
|
||||||
<%= fields_for :submitter_preferences, nil, index: submitter['uuid'] do |ff| %>
|
<%= fields_for :submitter_preferences, nil, index: submitter['uuid'] do |ff| %>
|
||||||
<div id="request_email_<%= uuid %>_<%= submitter['uuid'] %>" class="<%= 'hidden' if index != 0 %>">
|
<div id="request_email_<%= uuid %>_<%= submitter['uuid'] %>" class="<%= 'hidden' if index != 0 %>">
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<%= ff.label :subject, t('subject'), class: 'label' %>
|
<%= ff.label :subject, t('subject'), class: 'label' %>
|
||||||
</div>
|
</div>
|
||||||
<%= ff.text_field :subject, value: local_assigns[:submitter_email_message]&.subject.presence || submitter_preferences_index.dig(submitter['uuid'], 'request_email_subject').presence || template&.preferences&.dig('request_email_subject').presence || config.value['subject'], required: true, class: '!text-sm base-input w-full', dir: 'auto' %>
|
<%= ff.text_field :subject, value: local_assigns[:submitter_email_message]&.subject.presence || (is_viewer ? view_template_subject : nil) || submitter_preferences_index.dig(submitter['uuid'], 'request_email_subject').presence || (is_viewer ? view_config.value['subject'] : default_subject), required: true, class: '!text-sm base-input w-full', dir: 'auto' %>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<%= ff.label :message, t('body'), class: 'label' %>
|
<%= ff.label :message, t('body'), class: 'label' %>
|
||||||
<%= render 'personalization_settings/markdown_editor', name: ff.field_name(:body), value: local_assigns[:submitter_email_message]&.body.presence || submitter_preferences_index.dig(submitter['uuid'], 'request_email_body').presence || template&.preferences&.dig('request_email_body').presence || config.value['body'], variables: body_variables %>
|
<%= render 'personalization_settings/email_body_editor', name: ff.field_name(:body), value: local_assigns[:submitter_email_message]&.normalized_body.presence || (is_viewer ? view_template_body : nil) || submitter_preferences_index.dig(submitter['uuid'], 'request_email_body').presence || (is_viewer ? view_config.value['body'] : default_body), variables: body_variables %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
<% if template.preferences['submitters_order'] == 'preserved' || template.submitters.any? { |s| s['order'] } || (template.submitters.size > 1 && template.fields.any? { |f| f['type'] == 'verification' }) %>
|
<% if template.preferences['submitters_order'] == 'preserved' || template.submitters.any? { |s| s['order'] } || (template.submitters.size > 1 && template.fields.any? { |f| f['type'] == 'verification' }) %>
|
||||||
<%= f.hidden_field :preserve_order, value: '1' %>
|
<%= f.hidden_field :preserve_order, value: '1' %>
|
||||||
<% elsif template.submitters.size > 1 %>
|
<% elsif template.submitters.size > 1 %>
|
||||||
|
<% account_submissions = template.submissions.where(account_id: current_account.id) %>
|
||||||
|
<% last_submission = account_submissions.active.last || account_submissions.archived.last %>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<%= f.label :preserve_order, for: uuid = SecureRandom.uuid, class: 'flex items-center cursor-pointer' do %>
|
<%= f.label :preserve_order, for: uuid = SecureRandom.uuid, class: 'flex items-center cursor-pointer' do %>
|
||||||
<%= f.check_box :preserve_order, id: uuid, class: 'base-checkbox', checked: template.submissions.last&.submitters_order.in?(['preserved', nil]) %>
|
<%= f.check_box :preserve_order, id: uuid, class: 'base-checkbox', checked: last_submission&.submitters_order.in?(['preserved', nil]) %>
|
||||||
<span class="label"><%= t('preserve_order') %></span>
|
<span class="label"><%= t('preserve_order') %></span>
|
||||||
<span class="tooltip" data-tip="<%= t('when_checked_notifications_will_be_sent_to_the_second_party_once_the_form_is_completed_by_the_previous_party_uncheck_this_option_to_send_notifications_to_all_parties_simultaneously_right_away') %>">
|
<span class="tooltip" data-tip="<%= t('when_checked_notifications_will_be_sent_to_the_second_party_once_the_form_is_completed_by_the_previous_party_uncheck_this_option_to_send_notifications_to_all_parties_simultaneously_right_away') %>">
|
||||||
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
|
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
|
||||||
|
|||||||
@@ -29,7 +29,8 @@
|
|||||||
<div>
|
<div>
|
||||||
<% timezone = local_assigns[:with_submitter_timezone] ? (submitter.timezone || local_assigns[:timezone]) : local_assigns[:timezone] %>
|
<% timezone = local_assigns[:with_submitter_timezone] ? (submitter.timezone || local_assigns[:timezone]) : local_assigns[:timezone] %>
|
||||||
<% time_format = local_assigns[:with_timestamp_seconds] ? :detailed : :long %>
|
<% time_format = local_assigns[:with_timestamp_seconds] ? :detailed : :long %>
|
||||||
<%= l(attachment.created_at.in_time_zone(timezone), format: time_format, locale: local_assigns[:locale]) %> <%= TimeUtils.timezone_abbr(timezone, attachment.created_at) %>
|
<% signature_timestamp = (local_assigns[:with_signature_id_completed_at] ? submitter.completed_at : nil) || attachment.created_at %>
|
||||||
|
<%= l(signature_timestamp.in_time_zone(timezone), format: time_format, locale: local_assigns[:locale]) %> <%= TimeUtils.timezone_abbr(timezone, signature_timestamp) %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
<% require_phone_2fa = @template.preferences['require_phone_2fa'] == true %>
|
<% require_phone_2fa = @template.preferences['require_phone_2fa'] == true %>
|
||||||
<% require_email_2fa = @template.preferences['require_email_2fa'] == true %>
|
<% require_email_2fa = @template.preferences['require_email_2fa'] == true %>
|
||||||
<% prefillable_fields = @template.fields.select { |f| f['prefillable'] } %>
|
<% prefillable_fields = @template.fields.select { |f| f['prefillable'] } %>
|
||||||
|
<% viewer_submitter_uuids = Set.new(@template.submitters.pluck('uuid')) - Set.new(@template.fields.to_a.pluck('submitter_uuid')) %>
|
||||||
<% default_tab = cookies.permanent[:add_recipients_tab].presence || 'email' %>
|
<% default_tab = cookies.permanent[:add_recipients_tab].presence || 'email' %>
|
||||||
<% recipient_form_fields = Accounts.load_recipient_form_fields(current_account) if prefillable_fields.blank? %>
|
<% recipient_form_fields = Accounts.load_recipient_form_fields(current_account) if prefillable_fields.blank? %>
|
||||||
|
<% can_send_emails = Accounts.can_send_emails?(current_account) %>
|
||||||
<% only_detailed = require_phone_2fa || require_email_2fa || prefillable_fields.present? || recipient_form_fields.present? %>
|
<% only_detailed = require_phone_2fa || require_email_2fa || prefillable_fields.present? || recipient_form_fields.present? %>
|
||||||
<% with_list = @template.variables_schema.blank? %>
|
<% with_list = @template.variables_schema.blank? %>
|
||||||
<% variables_form = render 'variables_form', schema: @template.variables_schema if @template.variables_schema.present? && @template.variables_schema.any? { |_, v| !v['disabled'] } %>
|
<% variables_form = render 'variables_form', schema: @template.variables_schema if @template.variables_schema.present? && @template.variables_schema.any? { |_, v| !v['disabled'] } %>
|
||||||
@@ -25,18 +27,18 @@
|
|||||||
<div class="px-5 mb-5 mt-4">
|
<div class="px-5 mb-5 mt-4">
|
||||||
<% unless only_detailed %>
|
<% unless only_detailed %>
|
||||||
<div id="email" class="<%= 'hidden' if default_tab != 'email' %>">
|
<div id="email" class="<%= 'hidden' if default_tab != 'email' %>">
|
||||||
<%= render 'email_form', template: @template, variables_form: %>
|
<%= render 'email_form', template: @template, variables_form:, can_send_emails:, viewer_submitter_uuids: %>
|
||||||
</div>
|
</div>
|
||||||
<div id="phone" class="<%= 'hidden' if default_tab != 'phone' %>">
|
<div id="phone" class="<%= 'hidden' if default_tab != 'phone' %>">
|
||||||
<%= render 'phone_form', template: @template, variables_form: %>
|
<%= render 'phone_form', template: @template, variables_form: %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<div id="detailed" class="<%= 'hidden' if !only_detailed && default_tab != 'detailed' %>">
|
<div id="detailed" class="<%= 'hidden' if !only_detailed && default_tab != 'detailed' %>">
|
||||||
<%= render 'detailed_form', template: @template, require_phone_2fa:, require_email_2fa:, prefillable_fields:, recipient_form_fields:, variables_form: %>
|
<%= render 'detailed_form', template: @template, require_phone_2fa:, require_email_2fa:, prefillable_fields:, recipient_form_fields:, variables_form:, can_send_emails:, viewer_submitter_uuids: %>
|
||||||
</div>
|
</div>
|
||||||
<% if with_list %>
|
<% if with_list %>
|
||||||
<div id="list" class="hidden">
|
<div id="list" class="hidden">
|
||||||
<%= render 'list_form', template: @template %>
|
<%= render 'list_form', template: @template, can_send_emails:, viewer_submitter_uuids: %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= render 'submissions/error' %>
|
<%= render 'submissions/error' %>
|
||||||
|
|||||||
@@ -2,21 +2,21 @@
|
|||||||
<%= render 'submissions/preview_tags' %>
|
<%= render 'submissions/preview_tags' %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% font_scale = 1040.0 / PdfUtils::US_LETTER_W %>
|
<% font_scale = 1040.0 / PdfUtils::US_LETTER_W %>
|
||||||
<% configs = AccountConfig.where(account_id: @submission.account_id, key: [AccountConfig::COMBINE_PDF_RESULT_KEY, AccountConfig::WITH_SIGNATURE_ID, AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY, AccountConfig::WITH_SIGNATURE_ID_REASON_KEY, AccountConfig::WITH_TIMESTAMP_SECONDS_KEY]) %>
|
<% configs = AccountConfig.where(account_id: @submission.account_id, key: [AccountConfig::COMBINE_PDF_RESULT_KEY, AccountConfig::WITH_SIGNATURE_ID, AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY, AccountConfig::WITH_SIGNATURE_ID_REASON_KEY, AccountConfig::WITH_TIMESTAMP_SECONDS_KEY, AccountConfig::WITH_SIGNATURE_ID_COMPLETED_AT_KEY]) %>
|
||||||
<% with_signature_id = configs.find { |e| e.key == AccountConfig::WITH_SIGNATURE_ID }&.value == true %>
|
<% with_signature_id = configs.find { |e| e.key == AccountConfig::WITH_SIGNATURE_ID }&.value == true %>
|
||||||
<% is_combined_enabled = configs.find { |e| e.key == AccountConfig::COMBINE_PDF_RESULT_KEY }&.value == true && !@submission.template_fields&.any? { |f| f['type'] == 'verification' } %>
|
<% is_combined_enabled = configs.find { |e| e.key == AccountConfig::COMBINE_PDF_RESULT_KEY }&.value == true && !@submission.template_fields&.any? { |f| f['type'] == 'verification' } %>
|
||||||
<% with_submitter_timezone = configs.find { |e| e.key == AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY }&.value == true %>
|
<% with_submitter_timezone = configs.find { |e| e.key == AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY }&.value == true %>
|
||||||
<% with_timestamp_seconds = configs.find { |e| e.key == AccountConfig::WITH_TIMESTAMP_SECONDS_KEY }&.value == true %>
|
<% with_timestamp_seconds = configs.find { |e| e.key == AccountConfig::WITH_TIMESTAMP_SECONDS_KEY }&.value == true %>
|
||||||
<% with_signature_id_reason = configs.find { |e| e.key == AccountConfig::WITH_SIGNATURE_ID_REASON_KEY }&.value != false %>
|
<% with_signature_id_reason = configs.find { |e| e.key == AccountConfig::WITH_SIGNATURE_ID_REASON_KEY }&.value != false %>
|
||||||
|
<% with_signature_id_completed_at = configs.find { |e| e.key == AccountConfig::WITH_SIGNATURE_ID_COMPLETED_AT_KEY }&.value == true %>
|
||||||
<main style="max-width: 1600px" class="mx-auto pl-4">
|
<main style="max-width: 1600px" class="mx-auto pl-4">
|
||||||
<div class="flex justify-between py-1.5 items-center pr-4 sticky top-0 md:relative z-10 bg-base-100">
|
<div class="flex justify-between py-1.5 items-center pr-4 sticky top-0 md:relative z-10 bg-base-100">
|
||||||
<a href="<%= signed_in? && @submission.account_id == current_account&.id && @submission.template ? template_path(@submission.template) : '/' %>" class="flex items-center space-x-3 py-1">
|
<a href="<%= signed_in? && @submission.account_id == current_account&.id && @submission.template ? template_path(@submission.template) : '/' %>" class="flex items-center space-x-3 py-1">
|
||||||
<span><%= render 'submissions/logo' %></span>
|
<span><%= render 'submissions/logo' %></span>
|
||||||
<h1 class="text-xl md:text-3xl font-semibold focus:text-clip" style="overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;"><% (@submission.name || @submission.template.name).split(/(_)/).each do |item| %><%= item %><wbr><% end %></h1>
|
<h1 class="text-xl md:text-3xl font-semibold focus:text-clip" style="overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;"><% (@submission.name || @submission.template&.name).to_s.split(/(_)/).each do |item| %><%= item %><wbr><% end %></h1>
|
||||||
</a>
|
</a>
|
||||||
<div class="space-x-3 flex items-center">
|
<div class="space-x-3 flex items-center">
|
||||||
<% is_all_completed = @submission.submitters.to_a.all?(&:completed_at?) %>
|
<% if signed_in? && can?(:destroy, @submission) && @submission.archived_at? && !@submission.completed_at? %>
|
||||||
<% if signed_in? && can?(:create, @submission) && @submission.archived_at? && !is_all_completed %>
|
|
||||||
<%= button_to button_title(title: t('unarchive'), disabled_with: t('unarchive')[0..-2], icon: svg_icon('rotate', class: 'w-6 h-6')), submission_unarchive_index_path(@submission), class: 'btn btn-primary btn-ghost text-base hidden md:flex' %>
|
<%= button_to button_title(title: t('unarchive'), disabled_with: t('unarchive')[0..-2], icon: svg_icon('rotate', class: 'w-6 h-6')), submission_unarchive_index_path(@submission), class: 'btn btn-primary btn-ghost text-base hidden md:flex' %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if @submission.audit_trail.present? %>
|
<% if @submission.audit_trail.present? %>
|
||||||
@@ -30,16 +30,19 @@
|
|||||||
<span class="hidden md:block"><%= t('event_log') %></span>
|
<span class="hidden md:block"><%= t('event_log') %></span>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if signed_in? && !is_all_completed && can?(:manage, :resend_all) && @submission.submitters.to_a.size > 3 && !@submission.archived_at? && !@submission.template&.archived_at? && !@submission.expired? && can?(:update, @submission) %>
|
<% if signed_in? && !@submission.completed_at? && can?(:manage, :resend_all) && @submission.submitters.to_a.size > 3 && !@submission.archived_at? && !@submission.template&.archived_at? && !@submission.expired? && can?(:update, @submission) %>
|
||||||
<% pending_submitters_count = @submission.submitters.to_a.count { |s| !s.completed_at? && s.email.present? && !s.declined_at? } %>
|
<% pending_submitters_count = @submission.submitters.to_a.count { |s| !s.completed_at? && s.email.present? && !s.declined_at? } %>
|
||||||
<% if pending_submitters_count.positive? %>
|
<% if pending_submitters_count.positive? %>
|
||||||
<%= button_to button_title(title: t('re_send_emails'), title_class: 'hidden md:inline', disabled_with: t('sending'), icon: svg_icon('mail_forward', class: 'w-6 h-6')), submission_resend_email_index_path(@submission), class: 'white-button', data: { turbo_confirm: t('are_you_sure_you_want_to_re_send_email_to_n_recipients', count: pending_submitters_count) } %>
|
<%= button_to button_title(title: t('re_send_emails'), title_class: 'hidden md:inline', disabled_with: t('sending'), icon: svg_icon('mail_forward', class: 'w-6 h-6')), submission_resend_email_index_path(@submission), class: 'white-button', data: { turbo_confirm: t('are_you_sure_you_want_to_re_send_email_to_n_recipients', count: pending_submitters_count) } %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if @submission.submitters.to_a.any?(&:completed_at?) %>
|
<% if @submission.submitters.to_a.any?(&:completed_at?) %>
|
||||||
<% if is_all_completed || !is_combined_enabled %>
|
<% show_combined_download = @submission.completed_at? && !is_combined_enabled %>
|
||||||
|
<% show_unarchive = signed_in? && can?(:destroy, @submission) && @submission.archived_at? && @submission.completed_at? %>
|
||||||
|
<% show_download_dropdown = show_combined_download || show_unarchive %>
|
||||||
|
<% if @submission.completed_at? || !is_combined_enabled %>
|
||||||
<div class="join relative">
|
<div class="join relative">
|
||||||
<download-button role="button" tabindex="0" aria-label="<%= t('download') %>" data-src="<%= local_assigns[:is_preview] ? (@sig_submitter ? submit_form_documents_path(@sig_submitter.slug, { sig: params[:sig], combined: is_combined_enabled }.compact_blank) : submissions_preview_download_index_path(@submission.slug, combined: is_combined_enabled.presence)) : submission_download_index_path(@submission, combined: is_combined_enabled.presence) %>" class="base-button <%= '!rounded-r-none !pr-2' if is_all_completed && !is_combined_enabled %>">
|
<download-button role="button" tabindex="0" aria-label="<%= t('download') %>" data-src="<%= local_assigns[:is_preview] ? (@sig_submitter ? submit_form_documents_path(@sig_submitter.slug, { sig: params[:sig], combined: is_combined_enabled }.compact_blank) : submissions_preview_download_index_path(@submission.slug, combined: is_combined_enabled.presence)) : submission_download_index_path(@submission, combined: is_combined_enabled.presence) %>" class="base-button <%= '!rounded-r-none !pr-2' if show_download_dropdown %>">
|
||||||
<span class="flex items-center justify-center space-x-2" data-target="download-button.defaultButton">
|
<span class="flex items-center justify-center space-x-2" data-target="download-button.defaultButton">
|
||||||
<%= svg_icon('download', class: 'w-6 h-6') %>
|
<%= svg_icon('download', class: 'w-6 h-6') %>
|
||||||
<span class="hidden md:inline"><%= t('download') %></span>
|
<span class="hidden md:inline"><%= t('download') %></span>
|
||||||
@@ -49,26 +52,36 @@
|
|||||||
<span class="hidden md:inline"><%= t('downloading') %></span>
|
<span class="hidden md:inline"><%= t('downloading') %></span>
|
||||||
</span>
|
</span>
|
||||||
</download-button>
|
</download-button>
|
||||||
<% if is_all_completed && !is_combined_enabled %>
|
<% if show_download_dropdown %>
|
||||||
<div class="dropdown dropdown-end">
|
<div class="dropdown dropdown-end has-[button:disabled]:dropdown-open">
|
||||||
<label tabindex="0" aria-label="<%= t('download') %>" class="base-button !rounded-l-none !pl-1 !pr-2 !border-l-neutral-500">
|
<label tabindex="0" aria-label="<%= t('download') %>" class="base-button !rounded-l-none !pl-1 !pr-2 !border-l-neutral-500">
|
||||||
<span class="text-sm align-text-top">
|
<span class="text-sm align-text-top">
|
||||||
<%= svg_icon('chevron_down', class: 'w-6 h-6 flex-shrink-0 stroke-2') %>
|
<%= svg_icon('chevron_down', class: 'w-6 h-6 flex-shrink-0 stroke-2') %>
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<ul class="z-10 dropdown-content p-2 mt-2 shadow menu text-base bg-base-100 rounded-box text-right">
|
<ul class="z-10 dropdown-content p-2 mt-2 shadow menu text-base bg-base-100 rounded-box text-right">
|
||||||
<li>
|
<% if show_combined_download %>
|
||||||
<download-button role="button" tabindex="0" data-src="<%= local_assigns[:is_preview] ? (@sig_submitter ? submit_form_documents_path(@sig_submitter.slug, { sig: params[:sig], combined: true }.compact) : submissions_preview_download_index_path(@submission.slug, combined: true)) : submission_download_index_path(@submission, combined: true) %>" class="flex items-center">
|
<li>
|
||||||
<span class="flex items-center justify-center space-x-2" data-target="download-button.defaultButton">
|
<download-button role="button" tabindex="0" data-src="<%= local_assigns[:is_preview] ? (@sig_submitter ? submit_form_documents_path(@sig_submitter.slug, { sig: params[:sig], combined: true }.compact) : submissions_preview_download_index_path(@submission.slug, combined: true)) : submission_download_index_path(@submission, combined: true) %>" class="flex items-center">
|
||||||
<%= svg_icon('download', class: 'w-6 h-6 flex-shrink-0') %>
|
<span class="flex items-center justify-center space-x-2" data-target="download-button.defaultButton">
|
||||||
<span class="whitespace-nowrap"><%= t('download_combined_pdf') %></span>
|
<%= svg_icon('download', class: 'w-6 h-6 flex-shrink-0') %>
|
||||||
</span>
|
<span class="whitespace-nowrap"><%= t('download_combined_pdf') %></span>
|
||||||
<span class="flex items-center justify-center space-x-2 hidden" data-target="download-button.loadingButton">
|
</span>
|
||||||
<%= svg_icon('loader', class: 'w-6 h-6 animate-spin') %>
|
<span class="flex items-center justify-center space-x-2 hidden" data-target="download-button.loadingButton">
|
||||||
<span><%= t('downloading') %></span>
|
<%= svg_icon('loader', class: 'w-6 h-6 animate-spin') %>
|
||||||
</span>
|
<span><%= t('downloading') %></span>
|
||||||
</download-button>
|
</span>
|
||||||
</li>
|
</download-button>
|
||||||
|
</li>
|
||||||
|
<% end %>
|
||||||
|
<% if show_unarchive %>
|
||||||
|
<li>
|
||||||
|
<button form="submission_unarchive_form">
|
||||||
|
<%= button_title(title: t('unarchive'), disabled_with: t('unarchive'), icon: svg_icon('rotate', class: 'w-6 h-6 flex-shrink-0')) %>
|
||||||
|
</button>
|
||||||
|
<%= button_to '', submission_unarchive_index_path(@submission), form: { id: 'submission_unarchive_form' }, form_class: 'hidden' %>
|
||||||
|
</li>
|
||||||
|
<% end %>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
@@ -132,7 +145,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<% else %>
|
<% else %>
|
||||||
<%= render 'submissions/value', page_width: width, page_height: height, font_scale:, area:, field:, attachments_index:, value: mask.present? ? Array.wrap(value).map { |e| TextUtils.mask_value(e, mask) }.join(', ') : value, locale: @submission.account.locale, timezone: @submission.account.timezone, submitter:, with_signature_id:, with_submitter_timezone:, with_timestamp_seconds:, with_signature_id_reason: %>
|
<%= render 'submissions/value', page_width: width, page_height: height, font_scale:, area:, field:, attachments_index:, value: mask.present? ? Array.wrap(value).map { |e| TextUtils.mask_value(e, mask) }.join(', ') : value, locale: @submission.account.locale, timezone: @submission.account.timezone, submitter:, with_signature_id:, with_submitter_timezone:, with_timestamp_seconds:, with_signature_id_reason:, with_signature_id_completed_at: %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% elsif field['readonly'] != true && submitter && !submitter.completed_at? %>
|
<% elsif field['readonly'] != true && submitter && !submitter.completed_at? %>
|
||||||
<% submitters_order_index ||= (@submission.template_submitters || @submission.template.submitters).each_with_index.to_h { |s, i| [s['uuid'], i] } %>
|
<% submitters_order_index ||= (@submission.template_submitters || @submission.template.submitters).each_with_index.to_h { |s, i| [s['uuid'], i] } %>
|
||||||
@@ -166,7 +179,7 @@
|
|||||||
<%= (@submission.template_submitters || @submission.template.submitters).find { |e| e['uuid'] == submitter&.uuid }&.dig('name') || "#{(index + 1).ordinalize} Submitter" %>
|
<%= (@submission.template_submitters || @submission.template.submitters).find { |e| e['uuid'] == submitter&.uuid }&.dig('name') || "#{(index + 1).ordinalize} Submitter" %>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<% if signed_in? && can?(:update, @submission) && submitter && !submitter.completed_at? && !submitter.declined_at? && !@submission.archived_at? && !@submission.template&.archived_at? && !@submission.expired? && !submitter.start_form_submission_events.any? %>
|
<% if signed_in? && can?(:update, @submission) && submitter && !submitter.completed_at? && !@submission.completed_at? && !submitter.declined_at? && !@submission.archived_at? && !@submission.template&.archived_at? && !@submission.expired? && !submitter.start_form_submission_events.any? %>
|
||||||
<span class="tooltip tooltip-left" data-tip="<%= t('edit') %>">
|
<span class="tooltip tooltip-left" data-tip="<%= t('edit') %>">
|
||||||
<%= link_to edit_submitter_path(submitter), class: 'shrink-0 inline md:hidden md:group-hover:inline', data: { turbo_frame: 'modal' } do %>
|
<%= link_to edit_submitter_path(submitter), class: 'shrink-0 inline md:hidden md:group-hover:inline', data: { turbo_frame: 'modal' } do %>
|
||||||
<%= svg_icon('pencil', class: 'w-5 h-5') %>
|
<%= svg_icon('pencil', class: 'w-5 h-5') %>
|
||||||
@@ -199,7 +212,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<div class="flex items-center space-x-1 mt-1">
|
<div class="flex items-center space-x-1 mt-1">
|
||||||
<% if @submission.expire_at? && submitter && !submitter.completed_at? %>
|
<% if submitter&.viewer? %>
|
||||||
|
<%= svg_icon(submitter.opened_at? ? 'file_check' : 'file_unknown', class: 'w-5 h-5') %>
|
||||||
|
<% elsif submitter && !submitter.completed_at? && @submission.completed_at? %>
|
||||||
|
<%= svg_icon('writing_off', class: 'w-5 h-5') %>
|
||||||
|
<% elsif @submission.expire_at? && submitter && !submitter.completed_at? %>
|
||||||
<%= svg_icon('clock_exclamation', class: 'w-5 h-5') %>
|
<%= svg_icon('clock_exclamation', class: 'w-5 h-5') %>
|
||||||
<% else %>
|
<% else %>
|
||||||
<%= svg_icon('writing', class: 'w-5 h-5') %>
|
<%= svg_icon('writing', class: 'w-5 h-5') %>
|
||||||
@@ -207,9 +224,19 @@
|
|||||||
<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&.viewer? %>
|
||||||
|
<% if submitter.opened_at? %>
|
||||||
|
<%= t('viewed_on_time', time: l(submitter.opened_at.in_time_zone(@submission.account.timezone), format: :short, locale: @submission.account.locale)) %>
|
||||||
|
<% elsif @submission.completed_at? %>
|
||||||
|
<%= t('not_viewed') %>
|
||||||
|
<% else %>
|
||||||
|
<%= t('not_viewed_yet') %>
|
||||||
|
<% end %>
|
||||||
<% elsif submitter %>
|
<% elsif submitter %>
|
||||||
<% if submitter.completed_at? %>
|
<% if submitter.completed_at? %>
|
||||||
<%= l(submitter.completed_at.in_time_zone(@submission.account.timezone), format: :long, locale: @submission.account.locale) %>
|
<%= l(submitter.completed_at.in_time_zone(@submission.account.timezone), format: :long, locale: @submission.account.locale) %>
|
||||||
|
<% elsif @submission.completed_at? %>
|
||||||
|
<%= t('not_completed') %>
|
||||||
<% elsif @submission.expire_at? %>
|
<% elsif @submission.expire_at? %>
|
||||||
<% if @submission.expired? %>
|
<% if @submission.expired? %>
|
||||||
<%= t(:expired) %>
|
<%= t(:expired) %>
|
||||||
@@ -232,15 +259,15 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if signed_in? && submitter && submitter.email && !submitter.completed_at && !@submission.archived_at? && !@submission.template&.archived_at? && can?(:update, @submission) && Accounts.can_send_emails?(current_account) && !@submission.expired? && !submitter.declined_at? %>
|
<% if signed_in? && submitter && submitter.email && !submitter.completed_at && !@submission.completed_at? && !@submission.archived_at? && !@submission.template&.archived_at? && can?(:update, @submission) && (Docuseal.multitenant? || Accounts.can_send_emails?(current_account)) && !@submission.expired? && !submitter.declined_at? %>
|
||||||
<div class="mt-2 mb-1">
|
<div class="mt-2 mb-1">
|
||||||
<%= button_to button_title(title: submitter.sent_at? ? t('re_send_email') : t('send_email'), disabled_with: t('sending')), submitter_send_email_index_path(submitter), class: 'btn btn-sm btn-primary w-full' %>
|
<%= button_to button_title(title: submitter.sent_at? ? t('re_send_email') : t('send_email'), disabled_with: t('sending')), submitter_send_email_index_path(submitter), class: 'btn btn-sm btn-primary w-full' %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if signed_in? && submitter && submitter.phone && !submitter.completed_at && !@submission.archived_at? && !@submission.template&.archived_at? && can?(:update, @submission) && !@submission.expired? && !submitter.declined_at? %>
|
<% if signed_in? && submitter && submitter.phone && !submitter.completed_at && !@submission.completed_at? && !@submission.archived_at? && !@submission.template&.archived_at? && can?(:update, @submission) && !@submission.expired? && !submitter.declined_at? %>
|
||||||
<%= render 'submissions/send_sms_button', submitter: %>
|
<%= render 'submissions/send_sms_button', submitter: %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if signed_in? && submitter && !submitter.completed_at? && !@submission.archived_at? && !@submission.template&.archived_at? && can?(:create, @submission) && !@submission.expired? && !submitter.declined_at? %>
|
<% if signed_in? && submitter && !submitter.viewer? && !submitter.completed_at? && !@submission.completed_at? && !@submission.archived_at? && !@submission.template&.archived_at? && can?(:create, @submission) && !@submission.expired? && !submitter.declined_at? %>
|
||||||
<div class="mt-2 mb-1">
|
<div class="mt-2 mb-1">
|
||||||
<a class="btn btn-sm btn-primary w-full" target="_blank" href="<%= submit_form_path(slug: submitter.slug) %>">
|
<a class="btn btn-sm btn-primary w-full" target="_blank" href="<%= submit_form_path(slug: submitter.slug) %>">
|
||||||
<%= t('sign_in_person') %>
|
<%= t('sign_in_person') %>
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
<% filter_params = params.permit(Submissions::Filter::ALLOWED_PARAMS).compact_blank %>
|
<% filter_params = params.permit(Submissions::Filter::ALLOWED_PARAMS).compact_blank %>
|
||||||
<div>
|
<div>
|
||||||
<%= link_to root_path, class: 'flex items-center' do %>
|
<%= link_to root_path, class: 'flex items-center mb-1 md:mb-0' do %>
|
||||||
<%= svg_icon('chevron_left', class: 'w-5 h-5') %>
|
<%= svg_icon('chevron_left', class: 'w-5 h-5') %>
|
||||||
<span style="margin-left: 3px"><%= t('back_to_active') %></span>
|
<span style="margin-left: 3px"><%= t('back_to_active') %></span>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col md:flex-row md:items-center mb-4 gap-3">
|
<div class="flex flex-col md:flex-row md:items-center mb-4 gap-3">
|
||||||
<div class="flex w-full justify-between items-center">
|
<div class="group/header flex w-full justify-between items-center">
|
||||||
<div>
|
<div class="max-md:group-has-[search-input:focus-within]/header:hidden">
|
||||||
<h1 class="text-2xl md:text-3xl font-bold md:block <%= 'hidden' if params[:q].present? %>"><%= t('submissions') %> <span class="badge badge-outline badge-lg align-middle"><%= t('archived') %></span></h1>
|
<h1 class="text-2xl md:text-3xl font-bold md:block <%= 'hidden' if params[:q].present? %>"><%= t('submissions') %> <span class="badge badge-outline badge-lg align-middle"><%= t('archived') %></span></h1>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="max-md:has-[search-input:focus-within]:grow <%= 'max-md:grow' if params[:q].present? %>">
|
||||||
<% if params[:q].present? || @pagy.pages > 1 || filter_params.present? %>
|
<% if params[:q].present? || @pagy.pages > 1 || filter_params.present? %>
|
||||||
<%= render 'shared/search_input', placeholder: "#{t('search')}..." %>
|
<%= render 'shared/search_input', placeholder: "#{t('search')}..." %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
Loaded 100 of 215 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user