mirror of
https://github.com/docusealco/docuseal
synced 2026-11-04 14:42:32 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c5c8779ec | ||
|
|
37ed110ad4 | ||
|
|
2ad5f8a316 | ||
|
|
e7594d81b6 | ||
|
|
e7c5f14860 | ||
|
|
c38830dd1c | ||
|
|
2cbf429293 | ||
|
|
517c292c17 | ||
|
|
14bd039a88 | ||
|
|
8c8c34cb75 | ||
|
|
0cc53066eb | ||
|
|
3d13d28e05 | ||
|
|
a80e20234f | ||
|
|
b1b46cf998 | ||
|
|
52ef0ae929 | ||
|
|
c4daf8ed49 | ||
|
|
c098e61947 | ||
|
|
ceb052b158 | ||
|
|
61b79d5b24 | ||
|
|
5a9cffc6fc | ||
|
|
08447ec4cf | ||
|
|
d3a5f36555 |
No files matched your search
@@ -20,6 +20,7 @@ gem 'oj'
|
||||
gem 'omniauth-google-oauth2'
|
||||
gem 'omniauth-rails_csrf_protection'
|
||||
gem 'pagy'
|
||||
gem 'pdf-reader'
|
||||
gem 'pg', require: false
|
||||
gem 'premailer-rails'
|
||||
gem 'puma'
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
Ascii85 (1.1.0)
|
||||
actioncable (7.0.7)
|
||||
actionpack (= 7.0.7)
|
||||
activesupport (= 7.0.7)
|
||||
@@ -68,6 +69,7 @@ GEM
|
||||
tzinfo (~> 2.0)
|
||||
addressable (2.8.5)
|
||||
public_suffix (>= 2.0.2, < 6.0)
|
||||
afm (0.2.2)
|
||||
annotate (3.2.0)
|
||||
activerecord (>= 3.2, < 8.0)
|
||||
rake (>= 10.4, < 14.0)
|
||||
@@ -233,6 +235,7 @@ GEM
|
||||
os (>= 0.9, < 2.0)
|
||||
signet (>= 0.16, < 2.a)
|
||||
hashdiff (1.0.1)
|
||||
hashery (2.1.2)
|
||||
hashie (5.0.0)
|
||||
hexapdf (0.33.0)
|
||||
cmdparse (~> 3.0, >= 3.0.3)
|
||||
@@ -335,6 +338,12 @@ GEM
|
||||
parser (3.2.2.3)
|
||||
ast (~> 2.4.1)
|
||||
racc
|
||||
pdf-reader (2.11.0)
|
||||
Ascii85 (~> 1.0)
|
||||
afm (~> 0.2.1)
|
||||
hashery (~> 2.0)
|
||||
ruby-rc4
|
||||
ttfunk
|
||||
pg (1.5.3)
|
||||
premailer (1.21.0)
|
||||
addressable
|
||||
@@ -461,6 +470,7 @@ GEM
|
||||
rubocop-capybara (~> 2.17)
|
||||
rubocop-factory_bot (~> 2.22)
|
||||
ruby-progressbar (1.13.0)
|
||||
ruby-rc4 (0.1.5)
|
||||
ruby-vips (2.1.4)
|
||||
ffi (~> 1.12)
|
||||
ruby2_keywords (0.0.5)
|
||||
@@ -502,6 +512,7 @@ GEM
|
||||
thor (1.2.2)
|
||||
timeout (0.4.0)
|
||||
trailblazer-option (0.1.2)
|
||||
ttfunk (1.7.0)
|
||||
turbo-rails (1.4.0)
|
||||
actionpack (>= 6.0.0)
|
||||
activejob (>= 6.0.0)
|
||||
@@ -564,6 +575,7 @@ DEPENDENCIES
|
||||
omniauth-google-oauth2
|
||||
omniauth-rails_csrf_protection
|
||||
pagy
|
||||
pdf-reader
|
||||
pg
|
||||
premailer-rails
|
||||
pry-rails
|
||||
|
||||
@@ -9,11 +9,11 @@ module Api
|
||||
template = current_account.templates.find(params[:template_id])
|
||||
|
||||
submissions =
|
||||
if params[:emails].present?
|
||||
if (params[:emails] || params[:email]).present?
|
||||
Submissions.create_from_emails(template:,
|
||||
user: current_user,
|
||||
source: :api,
|
||||
send_email: params[:send_email] != 'false',
|
||||
mark_as_sent: params[:send_email] != 'false',
|
||||
emails: params[:emails] || params[:email])
|
||||
else
|
||||
submissions_attrs = normalize_submissions_params!(submissions_params[:submission], template)
|
||||
@@ -21,13 +21,13 @@ module Api
|
||||
Submissions.create_from_submitters(template:,
|
||||
user: current_user,
|
||||
source: :api,
|
||||
send_email: params[:send_email] != 'false',
|
||||
mark_as_sent: params[:send_email] != 'false',
|
||||
submissions_attrs:)
|
||||
end
|
||||
|
||||
submitters = submissions.flat_map(&:submitters)
|
||||
|
||||
send_invitation_emails(submitters) if params[:send_email] != 'false'
|
||||
Submitters.send_signature_requests(submitters, send_email: params[:send_email] != 'false')
|
||||
|
||||
render json: submitters
|
||||
rescue UnknownFieldName, UnknownSubmitterName => e
|
||||
@@ -36,14 +36,8 @@ module Api
|
||||
|
||||
private
|
||||
|
||||
def send_invitation_emails(submitters)
|
||||
submitters.each do |submitter|
|
||||
SubmitterMailer.invitation_email(submitter, message: params[:message]).deliver_later!
|
||||
end
|
||||
end
|
||||
|
||||
def submissions_params
|
||||
params.permit(submission: [{ submitters: [[:uuid, :name, :email, { values: {} }]] }])
|
||||
params.permit(submission: [{ submitters: [[:uuid, :name, :email, :role, :phone, { values: {} }]] }])
|
||||
end
|
||||
|
||||
def normalize_submissions_params!(submissions_params, template)
|
||||
@@ -53,7 +47,8 @@ module Api
|
||||
|
||||
submitter[:values] =
|
||||
normalize_submitter_values(template,
|
||||
submitter[:values], submitter[:name] || template.submitters[index]['name'])
|
||||
submitter[:values],
|
||||
submitter[:role] || template.submitters[index]['name'])
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -7,12 +7,7 @@ module Api
|
||||
|
||||
@template = current_account.templates.find(params[:template_id])
|
||||
|
||||
documents =
|
||||
find_or_create_blobs.map do |blob|
|
||||
document = @template.documents.create!(blob:)
|
||||
|
||||
Templates::ProcessDocument.call(document)
|
||||
end
|
||||
documents = Templates::CreateAttachments.call(@template, params)
|
||||
|
||||
schema = documents.map do |doc|
|
||||
{ attachment_uuid: doc.uuid, name: doc.filename.base }
|
||||
@@ -27,19 +22,5 @@ module Api
|
||||
)
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_or_create_blobs
|
||||
blobs = params[:blobs]&.map do |attrs|
|
||||
ActiveStorage::Blob.find_signed(attrs[:signed_id])
|
||||
end
|
||||
|
||||
blobs || params[:files].map do |file|
|
||||
ActiveStorage::Blob.create_and_upload!(io: file.open,
|
||||
filename: file.original_filename,
|
||||
content_type: file.content_type)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -6,7 +6,7 @@ class EmailSettingsController < ApplicationController
|
||||
def index; end
|
||||
|
||||
def create
|
||||
if @encrypted_config.update(storage_configs)
|
||||
if @encrypted_config.update(email_configs)
|
||||
SettingsMailer.smtp_successful_setup(@encrypted_config.value['from_email']).deliver_now!
|
||||
|
||||
redirect_to settings_email_index_path, notice: 'Changes have been saved'
|
||||
@@ -26,7 +26,7 @@ class EmailSettingsController < ApplicationController
|
||||
EncryptedConfig.find_or_initialize_by(account: current_account, key: EncryptedConfig::EMAIL_SMTP_KEY)
|
||||
end
|
||||
|
||||
def storage_configs
|
||||
def email_configs
|
||||
params.require(:encrypted_config).permit(value: {}).tap do |e|
|
||||
e[:value].compact_blank!
|
||||
end
|
||||
|
||||
@@ -1,19 +1,111 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class EsignSettingsController < ApplicationController
|
||||
DEFAULT_CERT_NAME = 'DocuSeal Self-Host Autogenerated'
|
||||
|
||||
CertFormRecord = Struct.new(:name, :file, :password, keyword_init: true) do
|
||||
include ActiveModel::Validations
|
||||
|
||||
def to_key
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
def show
|
||||
cert_data = EncryptedConfig.find_by(account: current_account,
|
||||
key: EncryptedConfig::ESIGN_CERTS_KEY)&.value || {}
|
||||
|
||||
default_pkcs = GenerateCertificate.load_pkcs(cert_data) if cert_data['cert'].present?
|
||||
|
||||
custom_pkcs_list = (cert_data['custom'] || []).map do |e|
|
||||
{ 'pkcs' => OpenSSL::PKCS12.new(Base64.urlsafe_decode64(e['data']), e['password'].to_s),
|
||||
'name' => e['name'],
|
||||
'status' => e['status'] }
|
||||
end
|
||||
|
||||
@pkcs_list = [
|
||||
if default_pkcs
|
||||
{
|
||||
'pkcs' => default_pkcs,
|
||||
'name' => DEFAULT_CERT_NAME,
|
||||
'status' => custom_pkcs_list.any? { |e| e['status'] == 'default' } ? 'validate' : 'default'
|
||||
}
|
||||
end,
|
||||
*custom_pkcs_list
|
||||
].compact.reverse
|
||||
end
|
||||
|
||||
def new
|
||||
@cert_record = CertFormRecord.new
|
||||
end
|
||||
|
||||
def create
|
||||
pdfs =
|
||||
params[:files].map do |file|
|
||||
HexaPDF::Document.new(io: file.open)
|
||||
end
|
||||
@cert_record = CertFormRecord.new(**cert_params)
|
||||
|
||||
certs = Accounts.load_signing_certs(current_account)
|
||||
cert_configs = EncryptedConfig.find_or_initialize_by(account: current_account,
|
||||
key: EncryptedConfig::ESIGN_CERTS_KEY)
|
||||
|
||||
trusted_certs = [certs[:cert], certs[:sub_ca], certs[:root_ca]]
|
||||
if (cert_configs.value && cert_configs.value['custom']&.any? { |e| e['name'] == @cert_record.name }) ||
|
||||
@cert_record.name == DEFAULT_CERT_NAME
|
||||
|
||||
render turbo_stream: turbo_stream.replace('result', partial: 'result',
|
||||
locals: { pdfs:, files: params[:files], trusted_certs: })
|
||||
rescue HexaPDF::MalformedPDFError
|
||||
render turbo_stream: turbo_stream.replace('result', html: helpers.tag.div('Invalid PDF', id: 'result'))
|
||||
@cert_record.errors.add(:name, 'already exists')
|
||||
|
||||
return render turbo_stream: turbo_stream.replace(:modal, template: 'esign_settings/new'),
|
||||
status: :unprocessable_entity
|
||||
end
|
||||
|
||||
save_new_cert!(cert_configs, @cert_record)
|
||||
|
||||
redirect_to settings_esign_path, notice: 'Certificate has been successfully added!'
|
||||
rescue OpenSSL::PKCS12::PKCS12Error
|
||||
@cert_record.errors.add(:password, "is invalid. Make sure you're uploading a valid .p12 file")
|
||||
|
||||
render turbo_stream: turbo_stream.replace(:modal, template: 'esign_settings/new'), status: :unprocessable_entity
|
||||
end
|
||||
|
||||
def update
|
||||
cert_configs = EncryptedConfig.find_by(account: current_account, key: EncryptedConfig::ESIGN_CERTS_KEY)
|
||||
|
||||
cert_configs.value['custom'].each { |e| e['status'] = 'validate' }
|
||||
custom_cert_data = cert_configs.value['custom'].find { |e| e['name'] == params[:name] }
|
||||
custom_cert_data['status'] = 'default' if custom_cert_data
|
||||
|
||||
cert_configs.save!
|
||||
|
||||
redirect_to settings_esign_path, notice: 'Default certificate has been selected'
|
||||
end
|
||||
|
||||
def destroy
|
||||
cert_configs = EncryptedConfig.find_by(account: current_account, key: EncryptedConfig::ESIGN_CERTS_KEY)
|
||||
|
||||
cert_configs.value['custom'].reject! { |e| e['name'] == params[:name] }
|
||||
|
||||
cert_configs.save!
|
||||
|
||||
redirect_to settings_esign_path, notice: 'Certificate has been removed'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def save_new_cert!(cert_configs, cert_record)
|
||||
pkcs = OpenSSL::PKCS12.new(cert_record.file.read, cert_record.password)
|
||||
|
||||
cert_configs.value ||= {}
|
||||
cert_configs.value['custom'] ||= []
|
||||
cert_configs.value['custom'].each { |e| e['status'] = 'validate' }
|
||||
cert_configs.value['custom'] << {
|
||||
data: Base64.urlsafe_encode64(pkcs.to_der),
|
||||
password: cert_record.password,
|
||||
name: cert_record.name,
|
||||
status: 'default'
|
||||
}
|
||||
|
||||
cert_configs.save!
|
||||
end
|
||||
|
||||
def cert_params
|
||||
return {} if params[:esign_settings_controller_cert_form_record].blank?
|
||||
|
||||
params.require(:esign_settings_controller_cert_form_record).permit(:name, :file, :password)
|
||||
end
|
||||
end
|
||||
@@ -28,7 +28,7 @@ class RegistrationsController < Devise::RegistrationsController
|
||||
return unless signed_in?
|
||||
return if params[:redir].blank?
|
||||
|
||||
redirect_to after_sign_up_path_for(current_user)
|
||||
redirect_to after_sign_up_path_for(current_user), allow_other_host: true
|
||||
end
|
||||
|
||||
def require_no_authentication
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SmsSettingsController < ApplicationController
|
||||
def index; end
|
||||
end
|
||||
@@ -6,7 +6,7 @@ class SubmissionsController < ApplicationController
|
||||
def show
|
||||
@submission =
|
||||
Submission.joins(:template).where(template: { account_id: current_account.id })
|
||||
.preload(template: { documents_attachments: { preview_images_attachments: :blob } })
|
||||
.preload(:template, template_schema_documents: [:blob, { preview_images_attachments: :blob }])
|
||||
.find(params[:id])
|
||||
|
||||
render :show, layout: 'plain'
|
||||
@@ -20,23 +20,19 @@ class SubmissionsController < ApplicationController
|
||||
Submissions.create_from_emails(template: @template,
|
||||
user: current_user,
|
||||
source: :invite,
|
||||
send_email: params[:send_email] == '1',
|
||||
mark_as_sent: params[:send_email] == '1',
|
||||
emails: params[:emails])
|
||||
else
|
||||
Submissions.create_from_submitters(template: @template,
|
||||
user: current_user,
|
||||
source: :invite,
|
||||
send_email: params[:send_email] == '1',
|
||||
mark_as_sent: params[:send_email] == '1',
|
||||
submissions_attrs: submissions_params[:submission].to_h.values)
|
||||
end
|
||||
|
||||
submitters = submissions.flat_map(&:submitters)
|
||||
|
||||
if params[:send_email] == '1'
|
||||
submitters.each do |submitter|
|
||||
SubmitterMailer.invitation_email(submitter, message: params[:message]).deliver_later!
|
||||
end
|
||||
end
|
||||
Submitters.send_signature_requests(submitters, params)
|
||||
|
||||
redirect_to template_path(@template),
|
||||
notice: "#{submitters.size} #{'recipient'.pluralize(submitters.size)} added"
|
||||
@@ -54,7 +50,7 @@ class SubmissionsController < ApplicationController
|
||||
private
|
||||
|
||||
def submissions_params
|
||||
params.permit(submission: { submitters: [%i[uuid email]] })
|
||||
params.permit(submission: { submitters: [%i[uuid email phone name]] })
|
||||
end
|
||||
|
||||
def load_template
|
||||
|
||||
@@ -7,7 +7,9 @@ class SubmitFormController < ApplicationController
|
||||
|
||||
def show
|
||||
@submitter =
|
||||
Submitter.preload(submission: { template: { documents_attachments: { preview_images_attachments: :blob } } })
|
||||
Submitter.preload(submission: [
|
||||
:template, { template_schema_documents: [:blob, { preview_images_attachments: :blob }] }
|
||||
])
|
||||
.find_by!(slug: params[:slug])
|
||||
|
||||
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
|
||||
@@ -21,6 +23,8 @@ class SubmitFormController < ApplicationController
|
||||
Submitters::SubmitValues.call(submitter, params, request)
|
||||
|
||||
head :ok
|
||||
rescue Submitters::SubmitValues::ValidationError => e
|
||||
render json: { error: e.message }, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
def completed
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class SubmittersSendEmailController < ApplicationController
|
||||
def create
|
||||
submitter = Submitter.joins(:template)
|
||||
.where(template: { account_id: current_account.id })
|
||||
.find_by!(slug: params[:submitter_slug])
|
||||
|
||||
SubmitterMailer.invitation_email(submitter).deliver_later!
|
||||
|
||||
submitter.sent_at ||= Time.current
|
||||
submitter.save!
|
||||
|
||||
redirect_back(fallback_location: submission_path(submitter.submission), notice: 'Email has been sent')
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,32 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class VerifyPdfSignatureController < ApplicationController
|
||||
def create
|
||||
pdfs =
|
||||
params[:files].map do |file|
|
||||
HexaPDF::Document.new(io: file.open)
|
||||
end
|
||||
|
||||
cert_data = if Docuseal.multitenant?
|
||||
Docuseal::CERTS
|
||||
else
|
||||
EncryptedConfig.find_by(account: current_account, key: EncryptedConfig::ESIGN_CERTS_KEY)&.value || {}
|
||||
end
|
||||
|
||||
default_pkcs = GenerateCertificate.load_pkcs(cert_data)
|
||||
|
||||
custom_certs = cert_data.fetch('custom', []).map do |e|
|
||||
OpenSSL::PKCS12.new(Base64.urlsafe_decode64(e['data']), e['password'].to_s)
|
||||
end
|
||||
|
||||
trusted_certs = [default_pkcs.certificate,
|
||||
*default_pkcs.ca_certs,
|
||||
*custom_certs.map(&:certificate),
|
||||
*custom_certs.flat_map(&:ca_certs).compact]
|
||||
|
||||
render turbo_stream: turbo_stream.replace('result', partial: 'result',
|
||||
locals: { pdfs:, files: params[:files], trusted_certs: })
|
||||
rescue HexaPDF::MalformedPDFError
|
||||
render turbo_stream: turbo_stream.replace('result', html: helpers.tag.div('Invalid PDF', id: 'result'))
|
||||
end
|
||||
end
|
||||
@@ -74,6 +74,9 @@ window.customElements.define('template-builder', class extends HTMLElement {
|
||||
|
||||
this.app = createApp(TemplateBuilder, {
|
||||
template: reactive(JSON.parse(this.dataset.template)),
|
||||
backgroundColor: '#faf7f5',
|
||||
withPhone: this.dataset.withPhone === 'true',
|
||||
acceptFileTypes: this.dataset.acceptFileTypes,
|
||||
isDirectUpload: this.dataset.isDirectUpload === 'true'
|
||||
})
|
||||
|
||||
|
||||
@@ -38,7 +38,11 @@ export default actionable(targetable(class extends HTMLElement {
|
||||
this.uploadFiles(this.input.files)
|
||||
}
|
||||
|
||||
toggleLoading = () => {
|
||||
toggleLoading = (e) => {
|
||||
if (e && e.target && !e.target.contains(this)) {
|
||||
return
|
||||
}
|
||||
|
||||
this.loading.classList.toggle('hidden')
|
||||
this.icon.classList.toggle('hidden')
|
||||
this.classList.toggle('opacity-50')
|
||||
|
||||
@@ -9,12 +9,12 @@ window.customElements.define('submission-form', class extends HTMLElement {
|
||||
this.appElem = document.createElement('div')
|
||||
|
||||
this.app = createApp(Form, {
|
||||
submitterSlug: this.dataset.submitterSlug,
|
||||
submitterUuid: this.dataset.submitterUuid,
|
||||
submitter: JSON.parse(this.dataset.submitter),
|
||||
authenticityToken: this.dataset.authenticityToken,
|
||||
canSendEmail: this.dataset.canSendEmail === 'true',
|
||||
isDirectUpload: this.dataset.isDirectUpload === 'true',
|
||||
isDemo: this.dataset.isDemo === 'true',
|
||||
attribution: this.dataset.attribution !== 'false',
|
||||
withConfetti: true,
|
||||
values: reactive(JSON.parse(this.dataset.values)),
|
||||
attachments: reactive(JSON.parse(this.dataset.attachments)),
|
||||
|
||||
@@ -113,7 +113,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconCheck, IconColumns3 } from '@tabler/icons-vue'
|
||||
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconCheck, IconColumns3, IconPhoneCheck } from '@tabler/icons-vue'
|
||||
|
||||
export default {
|
||||
name: 'FieldArea',
|
||||
@@ -179,7 +179,8 @@ export default {
|
||||
select: 'Select',
|
||||
checkbox: 'Checkbox',
|
||||
radio: 'Radio',
|
||||
multiple: 'Multiple Select'
|
||||
multiple: 'Multiple Select',
|
||||
phone: 'Phone'
|
||||
}
|
||||
},
|
||||
fieldIcons () {
|
||||
@@ -193,7 +194,8 @@ export default {
|
||||
checkbox: IconCheckbox,
|
||||
radio: IconCircleDot,
|
||||
cells: IconColumns3,
|
||||
multiple: IconChecks
|
||||
multiple: IconChecks,
|
||||
phone: IconPhoneCheck
|
||||
}
|
||||
},
|
||||
image () {
|
||||
|
||||
@@ -7,13 +7,13 @@
|
||||
:height="30"
|
||||
/>
|
||||
<span>
|
||||
Form has been completed!
|
||||
{{ t('form_has_been_completed') }}
|
||||
</span>
|
||||
</p>
|
||||
<div class="space-y-3 mt-5">
|
||||
<button
|
||||
v-if="canSendEmail && !isDemo"
|
||||
class="white-button flex items-center space-x-1 w-full"
|
||||
class="white-button !h-auto flex items-center space-x-1 w-full"
|
||||
:disabled="isSendingCopy"
|
||||
@click.prevent="sendCopyToEmail"
|
||||
>
|
||||
@@ -23,7 +23,7 @@
|
||||
/>
|
||||
<IconMail v-else />
|
||||
<span>
|
||||
Send copy via email
|
||||
{{ t('send_copy_via_email') }}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
@@ -37,7 +37,7 @@
|
||||
/>
|
||||
<IconDownload v-else />
|
||||
<span>
|
||||
Download
|
||||
{{ t('download') }}
|
||||
</span>
|
||||
</button>
|
||||
<a
|
||||
@@ -58,17 +58,20 @@
|
||||
>
|
||||
<IconLogin />
|
||||
<span>
|
||||
Create a Free Account
|
||||
{{ t('create_a_free_account') }}
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="text-center mt-4">
|
||||
Signed with
|
||||
<div
|
||||
v-if="attribution"
|
||||
class="text-center mt-4"
|
||||
>
|
||||
{{ t('signed_with') }}
|
||||
<a
|
||||
href="https://www.docuseal.co"
|
||||
target="_blank"
|
||||
class="underline"
|
||||
>DocuSeal</a> - open source documents software
|
||||
>DocuSeal</a> - {{ t('open_source_documents_software') }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -86,7 +89,7 @@ export default {
|
||||
IconLogin,
|
||||
IconDownload
|
||||
},
|
||||
inject: ['baseUrl'],
|
||||
inject: ['baseUrl', 't'],
|
||||
props: {
|
||||
submitterSlug: {
|
||||
type: String,
|
||||
@@ -97,6 +100,11 @@ export default {
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
attribution: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
},
|
||||
withConfetti: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
{{ message }}
|
||||
</div>
|
||||
<div class="text-xs">
|
||||
<span class="font-medium">Click to upload</span> or drag and drop files
|
||||
<span class="font-medium">{{ t('click_to_upload') }}</span> {{ t('or_drag_and_drop_files') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -55,7 +55,7 @@ export default {
|
||||
IconCloudUpload,
|
||||
IconInnerShadowTop
|
||||
},
|
||||
inject: ['baseUrl'],
|
||||
inject: ['baseUrl', 't'],
|
||||
props: {
|
||||
message: {
|
||||
type: String,
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
class="btn btn-neutral flex text-white absolute rounded-none border-x-0 md:border md:rounded-full bottom-0 w-full md:mb-4 text-base"
|
||||
@click.prevent="isFormVisible = true"
|
||||
>
|
||||
Submit Form
|
||||
{{ t('submit_form') }}
|
||||
<IconArrowsDiagonal
|
||||
class="absolute right-0 mr-4"
|
||||
:width="20"
|
||||
@@ -24,6 +24,7 @@
|
||||
v-show="isFormVisible"
|
||||
id="form_container"
|
||||
class="shadow-md bg-base-100 absolute bottom-0 md:bottom-4 w-full border-base-200 border p-4 rounded"
|
||||
:style="{ backgroundColor: backgroundColor }"
|
||||
>
|
||||
<button
|
||||
v-if="!isCompleted"
|
||||
@@ -62,7 +63,7 @@
|
||||
:for="currentField.uuid"
|
||||
class="label text-2xl mb-2"
|
||||
>{{ currentField.name }}
|
||||
<template v-if="!currentField.required">(optional)</template>
|
||||
<template v-if="!currentField.required">({{ t('optional') }})</template>
|
||||
</label>
|
||||
<div
|
||||
v-else
|
||||
@@ -74,7 +75,7 @@
|
||||
v-model="values[currentField.uuid]"
|
||||
class="base-input !text-2xl w-full"
|
||||
:required="currentField.required"
|
||||
:placeholder="`Type here...${currentField.required ? '' : ' (optional)'}`"
|
||||
:placeholder="`${t('type_here')}...${currentField.required ? '' : ` (${t('optional')})`}`"
|
||||
type="text"
|
||||
:name="`values[${currentField.uuid}]`"
|
||||
@focus="$refs.areas.scrollIntoField(currentField)"
|
||||
@@ -87,7 +88,7 @@
|
||||
:for="currentField.uuid"
|
||||
class="label text-2xl mb-2"
|
||||
>{{ currentField.name }}
|
||||
<template v-if="!currentField.required">(optional)</template>
|
||||
<template v-if="!currentField.required">({{ t('optional') }})</template>
|
||||
</label>
|
||||
<div
|
||||
v-else
|
||||
@@ -111,7 +112,7 @@
|
||||
:for="currentField.uuid"
|
||||
class="label text-2xl mb-2"
|
||||
>{{ currentField.name }}
|
||||
<template v-if="!currentField.required">(optional)</template>
|
||||
<template v-if="!currentField.required">({{ t('optional') }})</template>
|
||||
</label>
|
||||
<div
|
||||
v-else
|
||||
@@ -129,7 +130,7 @@
|
||||
value=""
|
||||
:selected="!values[currentField.uuid]"
|
||||
>
|
||||
Select your option
|
||||
{{ t('select_your_option') }}
|
||||
</option>
|
||||
<option
|
||||
v-for="(option, index) in currentField.options"
|
||||
@@ -147,7 +148,7 @@
|
||||
:for="currentField.uuid"
|
||||
class="label text-2xl mb-2"
|
||||
>{{ currentField.name }}
|
||||
<template v-if="!currentField.required">(optional)</template>
|
||||
<template v-if="!currentField.required">({{ t('optional') }})</template>
|
||||
</label>
|
||||
<div class="flex w-full">
|
||||
<div class="space-y-3.5 mx-auto">
|
||||
@@ -195,7 +196,7 @@
|
||||
>
|
||||
<template v-if="isAnonymousChecboxes">
|
||||
<span class="text-xl">
|
||||
Complete hightlighted checkboxes and click <span class="font-semibold">{{ stepFields.length === currentStep + 1 ? 'submit' : 'next' }}</span>.
|
||||
{{ t('complete_hightlighted_checkboxes_and_click') }} <span class="font-semibold">{{ stepFields.length === currentStep + 1 ? t('submit') : t('next') }}</span>.
|
||||
</span>
|
||||
<input
|
||||
v-for="field in currentStepFields"
|
||||
@@ -264,6 +265,16 @@
|
||||
:submitter-slug="submitterSlug"
|
||||
@attached="[attachments.push($event), $refs.areas.scrollIntoField(currentField)]"
|
||||
/>
|
||||
<PhoneStep
|
||||
v-else-if="currentField.type === 'phone'"
|
||||
ref="currentStep"
|
||||
v-model="values[currentField.uuid]"
|
||||
:field="currentField"
|
||||
:default-value="submitter.phone"
|
||||
:submitter-slug="submitterSlug"
|
||||
@focus="$refs.areas.scrollIntoField(currentField)"
|
||||
@submit="submitStep"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-6 md:mt-8">
|
||||
<button
|
||||
@@ -277,10 +288,10 @@
|
||||
class="mr-1 animate-spin"
|
||||
/>
|
||||
<span v-if="stepFields.length === currentStep + 1">
|
||||
Submit
|
||||
{{ t('submit') }}
|
||||
</span>
|
||||
<span v-else>
|
||||
Next
|
||||
{{ t('next') }}
|
||||
</span><span
|
||||
v-if="isSubmitting"
|
||||
class="w-6 flex justify-start mr-1"
|
||||
@@ -292,8 +303,9 @@
|
||||
<FormCompleted
|
||||
v-else
|
||||
:is-demo="isDemo"
|
||||
:attribution="attribution"
|
||||
:with-confetti="withConfetti"
|
||||
:can-send-email="canSendEmail"
|
||||
:can-send-email="canSendEmail && !!submitter.email"
|
||||
:submitter-slug="submitterSlug"
|
||||
/>
|
||||
<div class="flex justify-center">
|
||||
@@ -318,8 +330,10 @@ import ImageStep from './image_step'
|
||||
import SignatureStep from './signature_step'
|
||||
import AttachmentStep from './attachment_step'
|
||||
import MultiSelectStep from './multi_select_step'
|
||||
import PhoneStep from './phone_step'
|
||||
import FormCompleted from './completed'
|
||||
import { IconInnerShadowTop, IconArrowsDiagonal, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
|
||||
import { t } from './i18n'
|
||||
|
||||
export default {
|
||||
name: 'SubmissionForm',
|
||||
@@ -331,17 +345,19 @@ export default {
|
||||
MultiSelectStep,
|
||||
IconInnerShadowTop,
|
||||
IconArrowsDiagonal,
|
||||
PhoneStep,
|
||||
IconArrowsDiagonalMinimize2,
|
||||
FormCompleted
|
||||
},
|
||||
provide () {
|
||||
return {
|
||||
baseUrl: this.baseUrl
|
||||
baseUrl: this.baseUrl,
|
||||
t: this.t
|
||||
}
|
||||
},
|
||||
props: {
|
||||
submitterSlug: {
|
||||
type: String,
|
||||
submitter: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
canSendEmail: {
|
||||
@@ -349,10 +365,6 @@ export default {
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
submitterUuid: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
attachments: {
|
||||
type: Array,
|
||||
required: false,
|
||||
@@ -378,6 +390,11 @@ export default {
|
||||
required: false,
|
||||
default: ''
|
||||
},
|
||||
backgroundColor: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: ''
|
||||
},
|
||||
isDirectUpload: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
@@ -388,6 +405,11 @@ export default {
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
attribution: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
},
|
||||
values: {
|
||||
type: Object,
|
||||
required: false,
|
||||
@@ -399,20 +421,28 @@ export default {
|
||||
isCompleted: false,
|
||||
isFormVisible: true,
|
||||
currentStep: 0,
|
||||
isSubmitting: false
|
||||
isSubmitting: false,
|
||||
recalculateButtonDisabledKey: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
currentStepFields () {
|
||||
return this.stepFields[this.currentStep]
|
||||
},
|
||||
submitterSlug () {
|
||||
return this.submitter.slug
|
||||
},
|
||||
isAnonymousChecboxes () {
|
||||
return this.currentField.type === 'checkbox' && this.currentStepFields.every((e) => !e.name) && this.currentStepFields.length > 4
|
||||
},
|
||||
isButtonDisabled () {
|
||||
return this.isSubmitting ||
|
||||
if (this.recalculateButtonDisabledKey) {
|
||||
return this.isSubmitting ||
|
||||
(this.currentField.required && ['image', 'file'].includes(this.currentField.type) && !this.values[this.currentField.uuid]?.length) ||
|
||||
(this.currentField.required && this.currentField.type === 'signature' && !this.values[this.currentField.uuid]?.length && this.$refs.currentStep && !this.$refs.currentStep.isSignatureStarted)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
},
|
||||
currentField () {
|
||||
return this.currentStepFields[0]
|
||||
@@ -460,12 +490,19 @@ export default {
|
||||
scrollbox.parentNode.style.maxHeight = '-webkit-fill-available'
|
||||
})
|
||||
}
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.recalculateButtonDisabledKey = Math.random()
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
t,
|
||||
goToStep (step, scrollToArea = false, clickUpload = false) {
|
||||
this.currentStep = this.stepFields.indexOf(step)
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.recalculateButtonDisabledKey = Math.random()
|
||||
|
||||
if (scrollToArea) {
|
||||
this.$refs.areas.scrollIntoField(step[0])
|
||||
}
|
||||
@@ -490,28 +527,40 @@ export default {
|
||||
async submitStep () {
|
||||
this.isSubmitting = true
|
||||
|
||||
const stepPromise = this.currentField.type === 'signature'
|
||||
const stepPromise = ['signature', 'phone'].includes(this.currentField.type)
|
||||
? this.$refs.currentStep.submit
|
||||
: () => Promise.resolve({})
|
||||
|
||||
await stepPromise()
|
||||
stepPromise().then(() => {
|
||||
const formData = new FormData(this.$refs.form)
|
||||
|
||||
const formData = new FormData(this.$refs.form)
|
||||
|
||||
if (this.currentStep === this.stepFields.length - 1) {
|
||||
formData.append('completed', 'true')
|
||||
}
|
||||
|
||||
this.saveStep(formData).then(response => {
|
||||
const nextStep = this.stepFields[this.currentStep + 1]
|
||||
|
||||
if (nextStep) {
|
||||
this.goToStep(this.stepFields[this.currentStep + 1], true)
|
||||
} else {
|
||||
this.isCompleted = true
|
||||
if (this.currentStep === this.stepFields.length - 1) {
|
||||
formData.append('completed', 'true')
|
||||
}
|
||||
|
||||
this.saveStep(formData).then(async (response) => {
|
||||
if (response.status === 422) {
|
||||
const data = await response.json()
|
||||
|
||||
alert(data.error || 'Value is invalid')
|
||||
|
||||
return Promise.reject(new Error(data.error))
|
||||
}
|
||||
|
||||
const nextStep = this.stepFields[this.currentStep + 1]
|
||||
|
||||
if (nextStep) {
|
||||
this.goToStep(this.stepFields[this.currentStep + 1], true)
|
||||
} else {
|
||||
this.isCompleted = true
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error('Error submitting form:', error)
|
||||
}).finally(() => {
|
||||
this.isSubmitting = false
|
||||
})
|
||||
}).catch(error => {
|
||||
console.error('Error submitting form:', error)
|
||||
console.log(error)
|
||||
}).finally(() => {
|
||||
this.isSubmitting = false
|
||||
})
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
const en = {
|
||||
submit_form: 'Submit Form',
|
||||
type_here: 'Type here',
|
||||
optional: 'optional',
|
||||
select_your_option: 'Select your option',
|
||||
complete_hightlighted_checkboxes_and_click: 'Complete hightlighted checkboxes and click',
|
||||
submit: 'submit',
|
||||
next: 'next',
|
||||
click_to_upload: 'Click to upload',
|
||||
or_drag_and_drop_files: 'or drag and drop files',
|
||||
send_copy_via_email: 'Send copy via email',
|
||||
download: 'Download',
|
||||
form_has_been_completed: 'Form has been completed!',
|
||||
create_a_free_account: 'Create a Free Account',
|
||||
signed_with: 'Signed with',
|
||||
open_source_documents_software: 'open source documents software',
|
||||
verified_phone_number: 'Verify Phone Number',
|
||||
use_international_format: 'Use internatioanl format: +1xxx',
|
||||
six_digits_code: '6-digit code',
|
||||
change_phone_number: 'Change phone number',
|
||||
sending: 'Sending...',
|
||||
resend_code: 'Re-send code',
|
||||
verification_code_has_been_resent: 'Verification code has been re-sent via SMS'
|
||||
}
|
||||
|
||||
const es = {
|
||||
submit_form: 'Enviar Formulario',
|
||||
type_here: 'Escribe aquí',
|
||||
optional: 'opcional',
|
||||
select_your_option: 'Selecciona tu opción',
|
||||
complete_hightlighted_checkboxes_and_click: 'Completa las casillas resaltadas y haz clic',
|
||||
submit: 'enviar',
|
||||
next: 'siguiente',
|
||||
click_to_upload: 'Haz clic para cargar',
|
||||
or_drag_and_drop_files: 'o arrastra y suelta archivos',
|
||||
send_copy_via_email: 'Enviar copia por correo electrónico',
|
||||
download: 'Descargar',
|
||||
form_has_been_completed: '¡El formulario ha sido completado!',
|
||||
create_a_free_account: 'Crear una Cuenta Gratuita',
|
||||
signed_with: 'Firmado con',
|
||||
open_source_documents_software: 'software de documentos de código abierto',
|
||||
verified_phone_number: 'Verificar número de teléfono',
|
||||
use_international_format: 'Usar formato internacional: +1xxx',
|
||||
six_digits_code: 'Código de 6 dígitos',
|
||||
change_phone_number: 'Cambiar número de teléfono',
|
||||
sending: 'Enviando...',
|
||||
resend_code: 'Reenviar código',
|
||||
verification_code_has_been_resent: 'El código de verificación ha sido reenviado por SMS'
|
||||
}
|
||||
|
||||
const it = {
|
||||
submit_form: 'Invia Modulo',
|
||||
type_here: 'Digita qui',
|
||||
optional: 'opzionale',
|
||||
select_your_option: 'Seleziona la tua opzione',
|
||||
complete_hightlighted_checkboxes_and_click: 'Completa le caselle evidenziate e fai clic',
|
||||
submit: 'invia',
|
||||
next: 'avanti',
|
||||
click_to_upload: 'Clicca per caricare',
|
||||
or_drag_and_drop_files: 'oppure trascina e rilascia i file',
|
||||
send_copy_via_email: 'Invia copia via email',
|
||||
download: 'Scarica',
|
||||
form_has_been_completed: 'Il modulo è stato completato!',
|
||||
create_a_free_account: 'Crea un Account Gratuito',
|
||||
signed_with: 'Firmato con',
|
||||
open_source_documents_software: 'software di documenti open source',
|
||||
verified_phone_number: 'Verifica numero di telefono',
|
||||
use_international_format: 'Usa formato internazionale: +1xxx',
|
||||
six_digits_code: 'Codice a 6 cifre',
|
||||
change_phone_number: 'Cambia numero di telefono',
|
||||
sending: 'Invio in corso...',
|
||||
resend_code: 'Rinvia codice',
|
||||
verification_code_has_been_resent: 'Il codice di verifica è stato rinviato tramite SMS'
|
||||
}
|
||||
|
||||
const de = {
|
||||
submit_form: 'Formular absenden',
|
||||
type_here: 'Hier eingeben',
|
||||
optional: 'optional',
|
||||
select_your_option: 'Wähle deine Option',
|
||||
complete_hightlighted_checkboxes_and_click: 'Markierte Kontrollkästchen ausfüllen und klicken',
|
||||
submit: 'absenden',
|
||||
next: 'weiter',
|
||||
click_to_upload: 'Klicken zum Hochladen',
|
||||
or_drag_and_drop_files: 'oder Dateien hierher ziehen und ablegen',
|
||||
send_copy_via_email: 'Kopie per E-Mail senden',
|
||||
download: 'Herunterladen',
|
||||
form_has_been_completed: 'Formular wurde ausgefüllt!',
|
||||
create_a_free_account: 'Kostenloses Konto erstellen',
|
||||
signed_with: 'Unterschrieben mit',
|
||||
open_source_documents_software: 'Open-Source-Dokumentensoftware',
|
||||
verified_phone_number: 'Telefonnummer überprüfen',
|
||||
use_international_format: 'Internationales Format verwenden: +1xxx',
|
||||
six_digits_code: '6-stelliger Code',
|
||||
change_phone_number: 'Telefonnummer ändern',
|
||||
sending: 'Senden...',
|
||||
resend_code: 'Code erneut senden',
|
||||
verification_code_has_been_resent: 'Die Verifizierungscode wurde erneut per SMS gesendet'
|
||||
}
|
||||
|
||||
const fr = {
|
||||
submit_form: 'Envoyer le Formulaire',
|
||||
type_here: 'Tapez ici',
|
||||
optional: 'facultatif',
|
||||
select_your_option: 'Sélectionnez votre option',
|
||||
complete_hightlighted_checkboxes_and_click: 'Complétez les cases à cocher en surbrillance et cliquez',
|
||||
submit: 'envoyer',
|
||||
next: 'suivant',
|
||||
click_to_upload: 'Cliquez pour télécharger',
|
||||
or_drag_and_drop_files: 'ou faites glisser-déposer les fichiers',
|
||||
send_copy_via_email: 'Envoyer une copie par e-mail',
|
||||
download: 'Télécharger',
|
||||
form_has_been_completed: 'Le formulaire a été complété !',
|
||||
create_a_free_account: 'Créer un Compte Gratuit',
|
||||
signed_with: 'Signé avec',
|
||||
open_source_documents_software: 'logiciel de documents open source',
|
||||
verified_phone_number: 'Vérifier le numéro de téléphone',
|
||||
use_international_format: 'Utiliser le format international : +1xxx',
|
||||
six_digits_code: 'Code à 6 chiffres',
|
||||
change_phone_number: 'Changer le numéro de téléphone',
|
||||
sending: 'Envoi en cours...',
|
||||
resend_code: 'Renvoyer le code',
|
||||
verification_code_has_been_resent: 'Le code de vérification a été renvoyé par SMS'
|
||||
}
|
||||
|
||||
const pl = {
|
||||
submit_form: 'Wyślij Formularz',
|
||||
type_here: 'Wpisz tutaj',
|
||||
optional: 'opcjonalny',
|
||||
select_your_option: 'Wybierz swoją opcję',
|
||||
complete_hightlighted_checkboxes_and_click: 'Wypełnij zaznaczone pola wyboru i kliknij',
|
||||
submit: 'wyślij',
|
||||
next: 'dalej',
|
||||
click_to_upload: 'Kliknij, aby przesłać',
|
||||
or_drag_and_drop_files: 'lub przeciągnij i upuść pliki',
|
||||
send_copy_via_email: 'Wyślij kopię drogą mailową',
|
||||
download: 'Pobierz',
|
||||
form_has_been_completed: 'Formularz został wypełniony!',
|
||||
create_a_free_account: 'Utwórz darmowe konto',
|
||||
signed_with: 'Podpisane za pomocą',
|
||||
open_source_documents_software: 'oprogramowanie do dokumentów open source',
|
||||
verified_phone_number: 'Zweryfikuj numer telefonu',
|
||||
use_international_format: 'Użyj międzynarodowego formatu: +1xxx',
|
||||
six_digits_code: '6-cyfrowy kod',
|
||||
change_phone_number: 'Zmień numer telefonu',
|
||||
sending: 'Wysyłanie...',
|
||||
resend_code: 'Ponownie wyślij kod',
|
||||
verification_code_has_been_resent: 'Kod weryfikacyjny został ponownie wysłany'
|
||||
}
|
||||
|
||||
const uk = {
|
||||
submit_form: 'Надіслати Форму',
|
||||
type_here: 'Введіть тут',
|
||||
optional: 'необов’язково',
|
||||
select_your_option: 'Виберіть варіант',
|
||||
complete_hightlighted_checkboxes_and_click: 'Заповніть позначені прапорці та натисніть',
|
||||
submit: 'надіслати',
|
||||
next: 'далі',
|
||||
click_to_upload: 'Клацніть, щоб завантажити',
|
||||
or_drag_and_drop_files: 'або перетягніть файли сюди',
|
||||
send_copy_via_email: 'Надіслати копію електронною поштою',
|
||||
download: 'Завантажити',
|
||||
form_has_been_completed: 'Форму заповнено!',
|
||||
create_a_free_account: 'Створити безкоштовний обліковий запис',
|
||||
signed_with: 'Підписано за допомогою',
|
||||
open_source_documents_software: 'відкритий програмний засіб для документів',
|
||||
verified_phone_number: 'Підтвердіть номер телефону',
|
||||
use_international_format: 'Використовуйте міжнародний формат: +1xxx',
|
||||
six_digits_code: '6-значний код',
|
||||
change_phone_number: 'Змінити номер телефону',
|
||||
sending: 'Надсилаю...',
|
||||
resend_code: 'Повторно відправити код',
|
||||
verification_code_has_been_resent: 'Код підтвердження був повторно надісланий'
|
||||
}
|
||||
|
||||
const cs = {
|
||||
submit_form: 'Odeslat formulář',
|
||||
type_here: 'Zadejte zde',
|
||||
optional: 'volitelné',
|
||||
select_your_option: 'Vyberte svou volbu',
|
||||
complete_hightlighted_checkboxes_and_click: 'Označte zvýrazněné zaškrtávací políčka a klikněte na',
|
||||
submit: 'odeslat',
|
||||
next: 'další',
|
||||
click_to_upload: 'Klikněte pro nahrání',
|
||||
or_drag_and_drop_files: 'nebo přetáhněte soubory sem',
|
||||
send_copy_via_email: 'Odeslat kopii e-mailem',
|
||||
download: 'Stáhnout',
|
||||
form_has_been_completed: 'Formulář byl dokončen!',
|
||||
create_a_free_account: 'Vytvořit bezplatný účet',
|
||||
signed_with: 'Podepsáno pomocí',
|
||||
open_source_documents_software: 'open source software pro dokumenty',
|
||||
verified_phone_number: 'Ověřte telefonní číslo',
|
||||
use_international_format: 'Použijte mezinárodní formát: +1xxx',
|
||||
six_digits_code: '6-místný kód',
|
||||
change_phone_number: 'Změnit telefonní číslo',
|
||||
sending: 'Odesílání...',
|
||||
resend_code: 'Znovu odeslat kód',
|
||||
verification_code_has_been_resent: 'Ověřovací kód byl znovu odeslán'
|
||||
}
|
||||
|
||||
const i18n = { en, es, it, de, fr, pl, uk, cs }
|
||||
|
||||
const browserLanguage = (navigator.language || navigator.userLanguage || 'en').split('-')[0]
|
||||
|
||||
const t = (key) => i18n[browserLanguage]?.[key] || i18n.en[key] || key
|
||||
|
||||
export default i18n
|
||||
export { t }
|
||||
@@ -0,0 +1,155 @@
|
||||
<template>
|
||||
<div>
|
||||
<label
|
||||
:for="isCodeSent ? 'one_time_code' : field.uuid"
|
||||
class="label text-2xl mb-2"
|
||||
>{{ field.name || t('verified_phone_number') }}
|
||||
<template v-if="!field.required">({{ t('optional') }})</template>
|
||||
</label>
|
||||
<div>
|
||||
<input
|
||||
type="hidden"
|
||||
name="normalize_phone"
|
||||
value="true"
|
||||
>
|
||||
<div v-if="isCodeSent">
|
||||
<input
|
||||
id="one_time_code"
|
||||
class="base-input !text-2xl w-full text-center"
|
||||
name="one_time_code"
|
||||
type="text"
|
||||
autocomplete="one-time-code"
|
||||
:placeholder="t('six_digits_code')"
|
||||
required
|
||||
maxlength="6"
|
||||
autofocus
|
||||
inputmode="decimal"
|
||||
@input="onInputCode"
|
||||
>
|
||||
<div class="flex justify-between mt-2 -mb-2 md:-mb-4">
|
||||
<a
|
||||
href="#"
|
||||
class="link"
|
||||
@click.prevent="isCodeSent = false"
|
||||
>
|
||||
{{ t('change_phone_number') }}
|
||||
</a>
|
||||
<a
|
||||
href="#"
|
||||
class="link"
|
||||
@click.prevent="resendCode"
|
||||
>
|
||||
{{ isResendLoading ? t('sending') : t('resend_code') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
v-show="!isCodeSent"
|
||||
:id="field.uuid"
|
||||
ref="phone"
|
||||
:value="modelValue || defaultValue"
|
||||
class="base-input !text-2xl w-full"
|
||||
autocomplete="tel"
|
||||
pattern="^\+[0-9\s\-]+$"
|
||||
:oninvalid="`this.value ? this.setCustomValidity('${t('use_international_format')}...') : ''`"
|
||||
oninput="this.setCustomValidity('')"
|
||||
type="tel"
|
||||
inputmode="tel"
|
||||
:required="field.required"
|
||||
placeholder="+1 234 567-8900"
|
||||
:name="`values[${field.uuid}]`"
|
||||
@input="$emit('update:model-value', $event.target.value)"
|
||||
@focus="$emit('focus')"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
function throttle (func, delay) {
|
||||
let lastCallTime = 0
|
||||
|
||||
return function (...args) {
|
||||
const now = Date.now()
|
||||
|
||||
if (now - lastCallTime >= delay) {
|
||||
func.apply(this, args)
|
||||
lastCallTime = now
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'PhoneStep',
|
||||
inject: ['t', 'baseUrl'],
|
||||
props: {
|
||||
field: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
submitterSlug: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
modelValue: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: ''
|
||||
},
|
||||
defaultValue: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
emits: ['update:model-value', 'focus', 'submit'],
|
||||
data () {
|
||||
return {
|
||||
isCodeSent: false,
|
||||
isResendLoading: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
emitSubmit: throttle(function (e) {
|
||||
this.$emit('submit')
|
||||
}, 1000),
|
||||
onInputCode (e) {
|
||||
if (e.target.value.length === 6) {
|
||||
this.emitSubmit()
|
||||
}
|
||||
},
|
||||
resendCode () {
|
||||
this.isResendLoading = true
|
||||
|
||||
this.sendVerificationCode().finally(() => {
|
||||
alert(this.t('verification_code_has_been_resent'))
|
||||
|
||||
this.isResendLoading = false
|
||||
})
|
||||
},
|
||||
sendVerificationCode () {
|
||||
return fetch(this.baseUrl + '/api/send_phone_verification_code', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
submitter_slug: this.submitterSlug,
|
||||
phone: this.$refs.phone.value
|
||||
}),
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
},
|
||||
async submit () {
|
||||
if (!this.isCodeSent) {
|
||||
this.sendVerificationCode()
|
||||
|
||||
this.$emit('update:model-value', this.$refs.phone.value)
|
||||
|
||||
this.isCodeSent = true
|
||||
|
||||
return Promise.reject(new Error('verify with code'))
|
||||
} else {
|
||||
return Promise.resolve({})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -75,6 +75,7 @@
|
||||
:with-arrows="template.schema.length > 1"
|
||||
:item="item"
|
||||
:document="sortedDocuments[index]"
|
||||
:accept-file-types="acceptFileTypes"
|
||||
:template="template"
|
||||
:is-direct-upload="isDirectUpload"
|
||||
@scroll-to="scrollIntoDocument(item)"
|
||||
@@ -84,9 +85,13 @@
|
||||
@down="moveDocument(item, 1)"
|
||||
@change="save"
|
||||
/>
|
||||
<div class="sticky bottom-0 bg-base-100 py-2">
|
||||
<div
|
||||
class="sticky bottom-0 py-2"
|
||||
:class="{ 'bg-base-100': withStickySubmitters }"
|
||||
>
|
||||
<Upload
|
||||
v-if="sortedDocuments.length"
|
||||
:accept-file-types="acceptFileTypes"
|
||||
:template-id="template.id"
|
||||
:is-direct-upload="isDirectUpload"
|
||||
@success="updateFromUpload"
|
||||
@@ -101,6 +106,7 @@
|
||||
<Dropzone
|
||||
v-if="!sortedDocuments.length"
|
||||
:template-id="template.id"
|
||||
:accept-file-types="acceptFileTypes"
|
||||
:is-direct-upload="isDirectUpload"
|
||||
@success="updateFromUpload"
|
||||
/>
|
||||
@@ -233,6 +239,8 @@ export default {
|
||||
template: this.template,
|
||||
save: this.save,
|
||||
baseFetch: this.baseFetch,
|
||||
backgroundColor: this.backgroundColor,
|
||||
withPhone: this.withPhone,
|
||||
selectedAreaRef: computed(() => this.selectedAreaRef)
|
||||
}
|
||||
},
|
||||
@@ -246,6 +254,16 @@ export default {
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
backgroundColor: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: ''
|
||||
},
|
||||
acceptFileTypes: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'image/*, application/pdf'
|
||||
},
|
||||
baseUrl: {
|
||||
type: String,
|
||||
required: false,
|
||||
@@ -261,6 +279,11 @@ export default {
|
||||
required: false,
|
||||
default: true
|
||||
},
|
||||
withPhone: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
fetchOptions: {
|
||||
type: Object,
|
||||
required: false,
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
ref="input"
|
||||
type="file"
|
||||
name="files[]"
|
||||
accept="image/*, application/pdf"
|
||||
:accept="acceptFileTypes"
|
||||
multiple
|
||||
@change="upload"
|
||||
>
|
||||
@@ -67,6 +67,11 @@ export default {
|
||||
type: [Number, String],
|
||||
required: true
|
||||
},
|
||||
acceptFileTypes: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'image/*, application/pdf'
|
||||
},
|
||||
isDirectUpload: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
@@ -89,8 +94,10 @@ export default {
|
||||
return 'Uploading...'
|
||||
} else if (this.isProcessing) {
|
||||
return 'Processing...'
|
||||
} else {
|
||||
} else if (this.acceptFileTypes === 'image/*, application/pdf') {
|
||||
return 'Add PDF documents or images'
|
||||
} else {
|
||||
return 'Add documents or images'
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -102,7 +109,7 @@ export default {
|
||||
methods: {
|
||||
upload: Upload.methods.upload,
|
||||
onDropFiles (e) {
|
||||
if ([...e.dataTransfer.files].every((f) => f.type.match(/(?:image\/)|(?:application\/pdf)/))) {
|
||||
if (this.acceptFileTypes !== 'image/*, application/pdf' || [...e.dataTransfer.files].every((f) => f.type.match(/(?:image\/)|(?:application\/pdf)/))) {
|
||||
this.$refs.input.files = e.dataTransfer.files
|
||||
|
||||
this.upload()
|
||||
|
||||
@@ -29,8 +29,11 @@
|
||||
@update:model-value="$emit('name-change', selectedSubmitter)"
|
||||
/>
|
||||
</div>
|
||||
<span>
|
||||
▼
|
||||
<span class="flex items-center">
|
||||
<IconPlus
|
||||
width="18"
|
||||
height="18"
|
||||
/>
|
||||
</span>
|
||||
</label>
|
||||
<ul
|
||||
@@ -86,7 +89,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { IconUserPlus, IconTrashX } from '@tabler/icons-vue'
|
||||
import { IconUserPlus, IconTrashX, IconPlus } from '@tabler/icons-vue'
|
||||
import Contenteditable from './contenteditable'
|
||||
import { v4 } from 'uuid'
|
||||
|
||||
@@ -95,6 +98,7 @@ export default {
|
||||
components: {
|
||||
IconUserPlus,
|
||||
Contenteditable,
|
||||
IconPlus,
|
||||
IconTrashX
|
||||
},
|
||||
props: {
|
||||
|
||||
@@ -18,33 +18,36 @@
|
||||
:class="menuClasses"
|
||||
@click="closeDropdown"
|
||||
>
|
||||
<li
|
||||
<template
|
||||
v-for="(icon, type) in fieldIcons"
|
||||
:key="type"
|
||||
>
|
||||
<a
|
||||
href="#"
|
||||
class="text-sm py-1 px-2"
|
||||
:class="{ 'active': type === modelValue }"
|
||||
@click.prevent="$emit('update:model-value', type)"
|
||||
>
|
||||
<component
|
||||
:is="icon"
|
||||
:stroke-width="1.6"
|
||||
:width="20"
|
||||
/>
|
||||
{{ fieldNames[type] }}
|
||||
</a>
|
||||
</li>
|
||||
<li v-if="withPhone || type !== 'phone'">
|
||||
<a
|
||||
href="#"
|
||||
class="text-sm py-1 px-2"
|
||||
:class="{ 'active': type === modelValue }"
|
||||
@click.prevent="$emit('update:model-value', type)"
|
||||
>
|
||||
<component
|
||||
:is="icon"
|
||||
:stroke-width="1.6"
|
||||
:width="20"
|
||||
/>
|
||||
{{ fieldNames[type] }}
|
||||
</a>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconColumns3 } from '@tabler/icons-vue'
|
||||
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconColumns3, IconPhoneCheck } from '@tabler/icons-vue'
|
||||
|
||||
export default {
|
||||
name: 'FiledTypeDropdown',
|
||||
inject: ['withPhone'],
|
||||
props: {
|
||||
modelValue: {
|
||||
type: String,
|
||||
@@ -79,7 +82,8 @@ export default {
|
||||
checkbox: 'Checkbox',
|
||||
multiple: 'Multiple',
|
||||
radio: 'Radio',
|
||||
cells: 'Cells'
|
||||
cells: 'Cells',
|
||||
phone: 'Phone'
|
||||
}
|
||||
},
|
||||
fieldIcons () {
|
||||
@@ -93,7 +97,8 @@ export default {
|
||||
checkbox: IconCheckbox,
|
||||
cells: IconColumns3,
|
||||
multiple: IconChecks,
|
||||
radio: IconCircleDot
|
||||
radio: IconCircleDot,
|
||||
phone: IconPhoneCheck
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -25,48 +25,79 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-1 pb-2">
|
||||
<button
|
||||
<template
|
||||
v-for="(icon, type) in fieldIcons"
|
||||
:key="type"
|
||||
draggable="true"
|
||||
class="flex items-center justify-center border border-dashed border-base-300 w-full rounded relative"
|
||||
@dragstart="onDragstart(type)"
|
||||
@dragend="$emit('drag-end')"
|
||||
@click="addField(type)"
|
||||
>
|
||||
<div class="w-0 absolute left-0">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="cursor-grab"
|
||||
width="18"
|
||||
height="18"
|
||||
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"
|
||||
<button
|
||||
v-if="withPhone || type != 'phone'"
|
||||
draggable="true"
|
||||
class="flex items-center justify-center border border-dashed border-base-300 w-full rounded relative"
|
||||
:style="{ backgroundColor: backgroundColor }"
|
||||
@dragstart="onDragstart(type)"
|
||||
@dragend="$emit('drag-end')"
|
||||
@click="addField(type)"
|
||||
>
|
||||
<div class="w-0 absolute left-0">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="cursor-grab"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
/>
|
||||
<path d="M9 5m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
||||
<path d="M9 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
||||
<path d="M9 19m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
||||
<path d="M15 5m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
||||
<path d="M15 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
||||
<path d="M15 19m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
||||
</svg>
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path
|
||||
stroke="none"
|
||||
d="M0 0h24v24H0z"
|
||||
fill="none"
|
||||
/>
|
||||
<path d="M9 5m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
||||
<path d="M9 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
||||
<path d="M9 19m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
||||
<path d="M15 5m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
||||
<path d="M15 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
||||
<path d="M15 19m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex items-center flex-col px-2 py-2">
|
||||
<component :is="icon" />
|
||||
<span class="text-xs mt-1">
|
||||
{{ fieldNames[type] }}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
<div
|
||||
v-else
|
||||
class="tooltip flex"
|
||||
data-tip="Unlock SMS-verified phone number field with paid plan. Use text field for phone numbers without verification."
|
||||
>
|
||||
<a
|
||||
href="https://www.docuseal.co/pricing"
|
||||
target="_blank"
|
||||
class="opacity-50 flex items-center justify-center border border-dashed border-base-300 w-full rounded relative"
|
||||
:style="{ backgroundColor: backgroundColor }"
|
||||
>
|
||||
<div class="w-0 absolute left-0">
|
||||
<IconLock
|
||||
width="18"
|
||||
height="18"
|
||||
stroke-width="1.5"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center flex-col px-2 py-2">
|
||||
<component :is="icon" />
|
||||
<span class="text-xs mt-1">
|
||||
{{ fieldNames[type] }}
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex items-center flex-col px-2 py-2">
|
||||
<component :is="icon" />
|
||||
<span class="text-xs mt-1">
|
||||
{{ fieldNames[type] }}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
<div
|
||||
v-if="fields.length < 4"
|
||||
@@ -94,14 +125,16 @@ import Field from './field'
|
||||
import { v4 } from 'uuid'
|
||||
import FieldType from './field_type'
|
||||
import FieldSubmitter from './field_submitter'
|
||||
import { IconLock } from '@tabler/icons-vue'
|
||||
|
||||
export default {
|
||||
name: 'TemplateFields',
|
||||
components: {
|
||||
Field,
|
||||
FieldSubmitter
|
||||
FieldSubmitter,
|
||||
IconLock
|
||||
},
|
||||
inject: ['save'],
|
||||
inject: ['save', 'backgroundColor', 'withPhone'],
|
||||
props: {
|
||||
fields: {
|
||||
type: Array,
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
<ReplaceButton
|
||||
:is-direct-upload="isDirectUpload"
|
||||
:template-id="template.id"
|
||||
:accept-file-types="acceptFileTypes"
|
||||
class="opacity-0 group-hover:opacity-100"
|
||||
@click.stop
|
||||
@success="$emit('replace', { replaceSchemaItem: item, ...$event })"
|
||||
@@ -94,6 +95,11 @@ export default {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
acceptFileTypes: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'image/*, application/pdf'
|
||||
},
|
||||
isDirectUpload: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
ref="input"
|
||||
name="files[]"
|
||||
type="file"
|
||||
accept="image/*, application/pdf"
|
||||
:accept="acceptFileTypes"
|
||||
@change="upload"
|
||||
>
|
||||
</form>
|
||||
@@ -32,6 +32,11 @@ export default {
|
||||
type: [Number, String],
|
||||
required: true
|
||||
},
|
||||
acceptFileTypes: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'image/*, application/pdf'
|
||||
},
|
||||
isDirectUpload: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
ref="input"
|
||||
name="files[]"
|
||||
type="file"
|
||||
accept="image/*, application/pdf"
|
||||
:accept="acceptFileTypes"
|
||||
multiple
|
||||
@change="upload"
|
||||
>
|
||||
@@ -56,6 +56,11 @@ export default {
|
||||
type: [Number, String],
|
||||
required: true
|
||||
},
|
||||
acceptFileTypes: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'image/*, application/pdf'
|
||||
},
|
||||
isDirectUpload: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
|
||||
@@ -36,9 +36,9 @@ class AccountConfig < ApplicationRecord
|
||||
'{{account.name}}'
|
||||
},
|
||||
SUBMITTER_COMPLETED_EMAIL_KEY => {
|
||||
'subject' => '{{submitter.email}} has completed the "{{template.name}}" form',
|
||||
'subject' => '{{submitter.name}} has completed the "{{template.name}}" form',
|
||||
'body' => "Hi,\n\n" \
|
||||
"{{submitter.email}} has completed the \"{{template.name}}\" form:\n\n" \
|
||||
"{{submitter.name}} has completed the \"{{template.name}}\" form:\n\n" \
|
||||
"{{submission.link}}\n\n" \
|
||||
"Thanks,\n" \
|
||||
'{{account.name}}'
|
||||
|
||||
@@ -38,6 +38,10 @@ class Submission < ApplicationRecord
|
||||
|
||||
attribute :source, :string, default: 'link'
|
||||
|
||||
has_many :template_schema_documents,
|
||||
->(e) { where(uuid: (e.template_schema.presence || e.template.schema).pluck('attachment_uuid')) },
|
||||
through: :template, source: :documents_attachments
|
||||
|
||||
scope :active, -> { where(deleted_at: nil) }
|
||||
|
||||
enum :source, {
|
||||
|
||||
@@ -6,9 +6,11 @@
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# completed_at :datetime
|
||||
# email :string not null
|
||||
# email :string
|
||||
# ip :string
|
||||
# name :string
|
||||
# opened_at :datetime
|
||||
# phone :string
|
||||
# sent_at :datetime
|
||||
# slug :string not null
|
||||
# ua :string
|
||||
|
||||
@@ -59,7 +59,8 @@
|
||||
{
|
||||
"submitters": [
|
||||
{
|
||||
"name": "<%= current_account.templates.last ? current_account.templates.last.submitters.first['name'] : 'First Submitter' %>",
|
||||
"name": "John Doe",
|
||||
"role": "<%= current_account.templates.last ? current_account.templates.last.submitters.first['name'] : 'First Submitter' %>",
|
||||
"email": "<%= current_user.email.sub('@', '+test@') %>",
|
||||
"values": {
|
||||
"Form Text Field Name": "Default Value"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<div class="max-w-xl mx-auto px-2">
|
||||
<%= render 'devise/shared/select_server' if Docuseal.multitenant? %>
|
||||
<h1 class="text-3xl font-bold text-center my-8">Create Free Account</h1>
|
||||
<%= form_for(User.new, html: { class: 'space-y-6' }, url: new_registration_path, data: { turbo: params[:redir].blank? }, method: :get) do |f| %>
|
||||
<% if params[:redir].present? %>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<div class="max-w-xl mx-auto px-2">
|
||||
<%= render 'devise/shared/select_server' if Docuseal.multitenant? %>
|
||||
<h1 class="text-4xl font-bold text-center my-8">Log In</h1>
|
||||
<%= form_for(resource, as: resource_name, html: { class: 'space-y-6' }, data: { turbo: params[:redir].blank? }, url: session_path(resource_name)) do |f| %>
|
||||
<% if params[:redir].present? %>
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<div class="text-center">
|
||||
<div class="join">
|
||||
<a href="https://docuseal.co<%= request.fullpath.gsub('docuseal.eu', 'docuseal.co') %>" class="btn bg-base-200 join-item w-32 <%= 'bg-base-300' if request.host == 'docuseal.co' %>">
|
||||
🌎 Global
|
||||
</a>
|
||||
<a href="https://docuseal.eu<%= request.fullpath.gsub('docuseal.co', 'docuseal.eu') %>" class="btn bg-base-200 join-item w-32 <%= 'bg-base-300' if request.host == 'docuseal.eu' %>">
|
||||
🇪🇺 Europe
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
Whitespace-only changes.
@@ -1,41 +0,0 @@
|
||||
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0">
|
||||
<%= render 'shared/settings_nav' %>
|
||||
<div class="flex-grow max-w-xl mx-auto">
|
||||
<h1 class="text-4xl font-bold mb-4">PDF Signature</h1>
|
||||
<div id="result">
|
||||
<p class="mb-2">
|
||||
Upload signed PDF file to validate its signature:
|
||||
</p>
|
||||
</div>
|
||||
<%= form_for '', url: settings_esign_index_path, method: :post, html: { enctype: 'multipart/form-data' } do |f| %>
|
||||
<%= f.button type: 'submit', class: 'flex' do %>
|
||||
<div class="disabled mb-3">
|
||||
<%= svg_icon('loader', class: 'w-5 h-5 animate-spin inline') %>
|
||||
Analyzing...
|
||||
</div>
|
||||
<% end %>
|
||||
<file-dropzone data-is-direct-upload="false" data-name="verify_attachments" data-submit-on-upload="true" class="w-full">
|
||||
<label for="file" class="w-full block h-32 relative bg-base-200 hover:bg-base-200/70 rounded-md border border-base-content border-dashed">
|
||||
<div class="absolute top-0 right-0 left-0 bottom-0 flex items-center justify-center">
|
||||
<div class="flex flex-col items-center">
|
||||
<span data-target="file-dropzone.icon">
|
||||
<%= svg_icon('cloud_upload', class: 'w-10 h-10') %>
|
||||
</span>
|
||||
<span data-target="file-dropzone.loading" class="hidden">
|
||||
<%= svg_icon('loader', class: 'w-10 h-10 animate-spin') %>
|
||||
</span>
|
||||
<div class="font-medium mb-1">
|
||||
Verify Signed PDF
|
||||
</div>
|
||||
<div class="text-xs">
|
||||
<span class="font-medium">Click to upload</span> or drag and drop files
|
||||
</div>
|
||||
</div>
|
||||
<input id="file" name="files[]" class="hidden" data-action="change:file-dropzone#onSelectFiles" data-target="file-dropzone.input" type="file" accept="application/pdf" multiple>
|
||||
</div>
|
||||
</label>
|
||||
</file-dropzone>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="w-0 md:w-52"></div>
|
||||
</div>
|
||||
@@ -0,0 +1,24 @@
|
||||
<%= render 'shared/turbo_modal', title: 'Upload Certificate' do %>
|
||||
<%= form_for @cert_record, url: settings_esign_path, html: { class: 'space-y-4', enctype: 'multipart/form-data' }, data: { turbo_frame: :_top } do |f| %>
|
||||
<div class="space-y-2">
|
||||
<div class="form-control">
|
||||
<%= f.label :name, class: 'label' %>
|
||||
<%= f.text_field :name, required: true, class: 'base-input' %>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<%= f.label :file, class: 'label' %>
|
||||
<%= f.file_field :file, required: true %>
|
||||
<label class="label">
|
||||
<span class="label-text-alt">Use a valid .der, .p12 or .pfx file.</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<%= f.label :password, 'Password (optional)', class: 'label' %>
|
||||
<%= f.text_field :password, class: 'base-input' %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-control pt-2">
|
||||
<%= f.button button_title, class: 'base-button' %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
@@ -0,0 +1,99 @@
|
||||
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0 md:space-x-10">
|
||||
<%= render 'shared/settings_nav' %>
|
||||
<div class="flex-grow">
|
||||
<div class="max-w-xl">
|
||||
<h1 class="text-4xl font-bold mb-4">PDF Signature</h1>
|
||||
<div id="result">
|
||||
<p class="mb-2">
|
||||
Upload signed PDF file to validate its signature:
|
||||
</p>
|
||||
</div>
|
||||
<%= form_for '', url: verify_pdf_signature_index_path, method: :post, html: { enctype: 'multipart/form-data' } do |f| %>
|
||||
<%= f.button type: 'submit', class: 'flex' do %>
|
||||
<div class="disabled mb-3">
|
||||
<%= svg_icon('loader', class: 'w-5 h-5 animate-spin inline') %>
|
||||
Analyzing...
|
||||
</div>
|
||||
<% end %>
|
||||
<file-dropzone data-is-direct-upload="false" data-name="verify_attachments" data-submit-on-upload="true" class="w-full">
|
||||
<label for="file" class="w-full block h-32 relative bg-base-200 hover:bg-base-200/70 rounded-md border border-base-content border-dashed">
|
||||
<div class="absolute top-0 right-0 left-0 bottom-0 flex items-center justify-center">
|
||||
<div class="flex flex-col items-center">
|
||||
<span data-target="file-dropzone.icon">
|
||||
<%= svg_icon('cloud_upload', class: 'w-10 h-10') %>
|
||||
</span>
|
||||
<span data-target="file-dropzone.loading" class="hidden">
|
||||
<%= svg_icon('loader', class: 'w-10 h-10 animate-spin') %>
|
||||
</span>
|
||||
<div class="font-medium mb-1">
|
||||
Verify Signed PDF
|
||||
</div>
|
||||
<div class="text-xs">
|
||||
<span class="font-medium">Click to upload</span> or drag and drop files
|
||||
</div>
|
||||
</div>
|
||||
<input id="file" name="files[]" class="hidden" data-action="change:file-dropzone#onSelectFiles" data-target="file-dropzone.input" type="file" accept="application/pdf" multiple>
|
||||
</div>
|
||||
</label>
|
||||
</file-dropzone>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="flex justify-between items-end mb-4 mt-8">
|
||||
<h2 class="text-3xl font-bold">Signing Certificates</h2>
|
||||
<%= link_to new_settings_esign_path, class: 'btn btn-primary btn-md', data: { turbo_frame: 'modal' } do %>
|
||||
<%= svg_icon('plus', class: 'w-6 h-6') %>
|
||||
<span>Upload Cert</span>
|
||||
<% end %>
|
||||
</div>
|
||||
<%= render 'alert' %>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table w-full table-lg rounded-b-none overflow-hidden">
|
||||
<thead class="bg-base-200">
|
||||
<tr class="text-neutral uppercase">
|
||||
<th>
|
||||
Name
|
||||
</th>
|
||||
<th>
|
||||
Valid To
|
||||
</th>
|
||||
<th>
|
||||
Status
|
||||
</th>
|
||||
<th class="text-right" width="1px">
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% @pkcs_list.each do |item| %>
|
||||
<tr scope="row" class="group">
|
||||
<td>
|
||||
<%= item['name'] %>
|
||||
</td>
|
||||
<td>
|
||||
<%= l(item['pkcs'].certificate.not_after.to_date, format: :long, locale: current_account.locale) %>
|
||||
</td>
|
||||
<td>
|
||||
<% if item['status'] == 'default' %>
|
||||
<span class="badge badge-lg badge-info badge-outline">
|
||||
<%= item['status'] %>
|
||||
</span>
|
||||
<% else %>
|
||||
<%= button_to settings_esign_path, method: :put, params: { name: item['name'] }, class: 'btn btn-outline btn-neutral btn-xs whitespace-nowrap', title: 'Delete', data: { turbo_confirm: 'Are you sure?' } do %>
|
||||
Make Default
|
||||
<% end %>
|
||||
<% end %>
|
||||
</td>
|
||||
<td>
|
||||
<% if item['name'] != EsignSettingsController::DEFAULT_CERT_NAME && item['status'] != 'default' %>
|
||||
<%= button_to settings_esign_path, params: { name: item['name'] }, method: :delete, class: 'btn btn-outline btn-error btn-xs', title: 'Delete', data: { turbo_confirm: 'Are you sure?' } do %>
|
||||
Remove
|
||||
<% end %>
|
||||
<% end %>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</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>
|
||||
<path d="M12 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0"></path>
|
||||
<path d="M12 19m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0"></path>
|
||||
<path d="M12 5m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 470 B |
@@ -0,0 +1,4 @@
|
||||
<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="M5 4h4l2 5l-2.5 1.5a11 11 0 0 0 5 5l1.5 -2.5l5 2v4a2 2 0 0 1 -2 2a16 16 0 0 1 -15 -15a2 2 0 0 1 2 -2"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 409 B |
@@ -0,0 +1,28 @@
|
||||
<div class="collapse collapse-plus bg-base-200">
|
||||
<input type="checkbox">
|
||||
<div class="collapse-title text-xl font-medium">
|
||||
<div>
|
||||
Documents Copy Email
|
||||
</div>
|
||||
</div>
|
||||
<div class="collapse-content">
|
||||
<%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %>
|
||||
<%= f.hidden_field :key %>
|
||||
<%= f.fields_for :value, Struct.new(:subject, :body).new(*f.object.value.values_at('subject', 'body')) do |ff| %>
|
||||
<div class="form-control">
|
||||
<%= ff.label :subject, class: 'label' %>
|
||||
<%= ff.text_field :subject, required: true, class: 'base-input' %>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<%= ff.label :body, class: 'label' %>
|
||||
<autoresize-textarea>
|
||||
<%= ff.text_area :body, required: true, class: 'base-input w-full py-2' %>
|
||||
</autoresize-textarea>
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="form-control pt-2">
|
||||
<%= f.button button_title(title: 'Save', disabled_with: 'Saving'), class: 'base-button' %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,28 @@
|
||||
<div class="collapse collapse-plus bg-base-200">
|
||||
<input type="checkbox">
|
||||
<div class="collapse-title text-xl font-medium">
|
||||
<div>
|
||||
Signature Request Email
|
||||
</div>
|
||||
</div>
|
||||
<div class="collapse-content">
|
||||
<%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %>
|
||||
<%= f.hidden_field :key %>
|
||||
<%= f.fields_for :value, Struct.new(:subject, :body).new(*f.object.value.values_at('subject', 'body')) do |ff| %>
|
||||
<div class="form-control">
|
||||
<%= ff.label :subject, class: 'label' %>
|
||||
<%= ff.text_field :subject, required: true, class: 'base-input' %>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<%= ff.label :body, class: 'label' %>
|
||||
<autoresize-textarea>
|
||||
<%= ff.text_area :body, required: true, class: 'base-input w-full py-2' %>
|
||||
</autoresize-textarea>
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="form-control pt-2">
|
||||
<%= f.button button_title(title: 'Save', disabled_with: 'Saving'), class: 'base-button' %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,28 @@
|
||||
<div class="collapse collapse-plus bg-base-200">
|
||||
<input type="checkbox">
|
||||
<div class="collapse-title text-xl font-medium">
|
||||
<div>
|
||||
Form Completed Email
|
||||
</div>
|
||||
</div>
|
||||
<div class="collapse-content">
|
||||
<%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %>
|
||||
<%= f.hidden_field :key %>
|
||||
<%= f.fields_for :value, Struct.new(:subject, :body).new(*f.object.value.values_at('subject', 'body')) do |ff| %>
|
||||
<div class="form-control">
|
||||
<%= ff.label :subject, class: 'label' %>
|
||||
<%= ff.text_field :subject, required: true, class: 'base-input' %>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<%= ff.label :body, class: 'label' %>
|
||||
<autoresize-textarea>
|
||||
<%= ff.text_area :body, required: true, class: 'base-input w-full py-2' %>
|
||||
</autoresize-textarea>
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="form-control pt-2">
|
||||
<%= f.button button_title(title: 'Save', disabled_with: 'Saving'), class: 'base-button' %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2,89 +2,10 @@
|
||||
<%= render 'shared/settings_nav' %>
|
||||
<div class="flex-grow max-w-xl mx-auto">
|
||||
<p class="text-4xl font-bold mb-4">Email Templates</p>
|
||||
<div class="collapse collapse-plus bg-base-200 px-1">
|
||||
<input type="checkbox">
|
||||
<div class="collapse-title text-xl font-medium">
|
||||
<div>
|
||||
Signature Request Email
|
||||
</div>
|
||||
</div>
|
||||
<div class="collapse-content">
|
||||
<%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %>
|
||||
<%= f.hidden_field :key %>
|
||||
<%= f.fields_for :value, Struct.new(:subject, :body).new(*f.object.value.values_at('subject', 'body')) do |ff| %>
|
||||
<div class="form-control">
|
||||
<%= ff.label :subject, class: 'label' %>
|
||||
<%= ff.text_field :subject, required: true, class: 'base-input' %>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<%= ff.label :body, class: 'label' %>
|
||||
<autoresize-textarea>
|
||||
<%= ff.text_area :body, required: true, class: 'base-input w-full py-2' %>
|
||||
</autoresize-textarea>
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="form-control pt-2">
|
||||
<%= f.button button_title(title: 'Save', disabled_with: 'Saving'), class: 'base-button' %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="collapse collapse-plus bg-base-200 px-1 mt-4">
|
||||
<input type="checkbox">
|
||||
<div class="collapse-title text-xl font-medium">
|
||||
<div>
|
||||
Form Completed Email
|
||||
</div>
|
||||
</div>
|
||||
<div class="collapse-content">
|
||||
<%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %>
|
||||
<%= f.hidden_field :key %>
|
||||
<%= f.fields_for :value, Struct.new(:subject, :body).new(*f.object.value.values_at('subject', 'body')) do |ff| %>
|
||||
<div class="form-control">
|
||||
<%= ff.label :subject, class: 'label' %>
|
||||
<%= ff.text_field :subject, required: true, class: 'base-input' %>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<%= ff.label :body, class: 'label' %>
|
||||
<autoresize-textarea>
|
||||
<%= ff.text_area :body, required: true, class: 'base-input w-full py-2' %>
|
||||
</autoresize-textarea>
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="form-control pt-2">
|
||||
<%= f.button button_title(title: 'Save', disabled_with: 'Saving'), class: 'base-button' %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="collapse collapse-plus bg-base-200 px-1 mt-4">
|
||||
<input type="checkbox">
|
||||
<div class="collapse-title text-xl font-medium">
|
||||
<div>
|
||||
Documents Copy Email
|
||||
</div>
|
||||
</div>
|
||||
<div class="collapse-content">
|
||||
<%= form_for AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY), url: settings_personalization_path, method: :post, html: { autocomplete: 'off', class: 'space-y-4' } do |f| %>
|
||||
<%= f.hidden_field :key %>
|
||||
<%= f.fields_for :value, Struct.new(:subject, :body).new(*f.object.value.values_at('subject', 'body')) do |ff| %>
|
||||
<div class="form-control">
|
||||
<%= ff.label :subject, class: 'label' %>
|
||||
<%= ff.text_field :subject, required: true, class: 'base-input' %>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<%= ff.label :body, class: 'label' %>
|
||||
<autoresize-textarea>
|
||||
<%= ff.text_area :body, required: true, class: 'base-input w-full py-2' %>
|
||||
</autoresize-textarea>
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="form-control pt-2">
|
||||
<%= f.button button_title(title: 'Save', disabled_with: 'Saving'), class: 'base-button' %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<%= render 'signature_request_email_form' %>
|
||||
<%= render 'submitter_completed_email_form' %>
|
||||
<%= render 'documents_copy_email_form' %>
|
||||
</div>
|
||||
<p class="text-4xl font-bold mb-4 mt-8">Company Logo</p>
|
||||
<%= render 'logo_form' %>
|
||||
|
||||
@@ -1,4 +1 @@
|
||||
<div class="text-center px-2">
|
||||
Powered by
|
||||
<a href="<%= Docuseal::PRODUCT_URL %>" class="underline"><%= Docuseal::PRODUCT_NAME %></a> - open source documents software
|
||||
</div>
|
||||
<%= render 'shared/powered_by' %>
|
||||
@@ -8,7 +8,7 @@
|
||||
</span>
|
||||
<span class="disabled">
|
||||
<span class="flex items-center justify-center space-x-2">
|
||||
<%= local_assigns[:icon_disabled] || svg_icon('loader', class: 'w-6 h-6 animate-spin') %>
|
||||
<%= local_assigns[:icon_disabled] || svg_icon('loader', class: 'w-5 h-5 animate-spin') %>
|
||||
<span><%= disabled_with %>...</span>
|
||||
</span>
|
||||
</span>
|
||||
@@ -1,4 +1,4 @@
|
||||
<div id="flash" class="absolute top-0 w-full h-0">
|
||||
<div id="flash" class="absolute top-0 w-full h-0 z-20">
|
||||
<div class="max-w-xl mx-auto mt-1.5">
|
||||
<div class="px-4 py-3 rounded-2xl bg-base-200 flex items-center justify-between mx-4 md:mx-0">
|
||||
<div class="flex items-center space-x-3">
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
</li>
|
||||
<% end %>
|
||||
<li>
|
||||
<%= link_to settings_esign_index_path, class: 'flex items-center' do %>
|
||||
<%= link_to settings_esign_path, class: 'flex items-center' do %>
|
||||
<%= svg_icon('zoom_check', class: 'w-5 h-5 stroke-2') %>
|
||||
<span class="mr-1">Verify PDF</span>
|
||||
<% end %>
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<div class="text-center px-2">
|
||||
Powered by
|
||||
<a href="<%= Docuseal::PRODUCT_URL %>" class="underline"><%= Docuseal::PRODUCT_NAME %></a> - open source documents software
|
||||
</div>
|
||||
@@ -16,9 +16,12 @@
|
||||
<li>
|
||||
<%= link_to 'Storage', settings_storage_index_path, class: 'text-base hover:bg-base-300' %>
|
||||
</li>
|
||||
<li>
|
||||
<%= link_to 'SMS', settings_sms_path, class: 'text-base hover:bg-base-300' %>
|
||||
</li>
|
||||
<% end %>
|
||||
<li>
|
||||
<%= link_to 'Signature', settings_esign_index_path, class: 'text-base hover:bg-base-300' %>
|
||||
<%= link_to 'E-Signature', settings_esign_path, class: 'text-base hover:bg-base-300' %>
|
||||
</li>
|
||||
<li>
|
||||
<%= link_to 'Team', settings_users_path, class: 'text-base hover:bg-base-300' %>
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<div class="alert">
|
||||
<%= svg_icon('info_circle', class: 'w-6 h-6') %>
|
||||
<div>
|
||||
<p class="font-bold">Send signature requests via SMS</p>
|
||||
<p class="text-gray-700">
|
||||
Unlock with DocuSeal Enterprise
|
||||
<br>
|
||||
<a class="link font-medium" target="_blank" href="<%= "#{Docuseal::CONSOLE_URL}/#{Docuseal.multitenant? ? 'plans' : 'on_premise'}" %>">Learn More</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,8 @@
|
||||
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0">
|
||||
<%= render 'shared/settings_nav' %>
|
||||
<div class="flex-grow max-w-xl mx-auto">
|
||||
<h1 class="text-4xl font-bold mb-4">SMS</h1>
|
||||
<%= render 'placeholder' %>
|
||||
</div>
|
||||
<div class="w-0 md:w-52"></div>
|
||||
</div>
|
||||
@@ -0,0 +1 @@
|
||||
<a href="<%= annot['value'] %>" rel="noopener noreferrer nofollow" target="_blank" class="flex absolute" style="width: <%= annot['w'] * 100 %>%; height: <%= annot['h'] * 100 %>%; left: <%= annot['x'] * 100 %>%; top: <%= annot['y'] * 100 %>%"></a>
|
||||
@@ -0,0 +1,43 @@
|
||||
<%= form_for '', url: template_submissions_path(template), html: { class: 'space-y-4', autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %>
|
||||
<dynamic-list class="space-y-4">
|
||||
<div class="space-y-4">
|
||||
<div class="card card-compact bg-base-200" data-targets="dynamic-list.items">
|
||||
<div class="card-body">
|
||||
<div class="absolute right-4 top-5">
|
||||
<a href="#" data-action="click:dynamic-list#removeItem" class="<%= template.submitters.size == 1 ? 'right-2' : '-top-3' %> relative block w-6 h-6 rounded-lg text-neutral-700 text-center bg-base-300 p-1 hidden hover:bg-neutral hover:text-white">
|
||||
<%= svg_icon('trash', class: 'w-4 h-4') %>
|
||||
</a>
|
||||
</div>
|
||||
<div class="grid <%= 'md:grid-cols-2' if template.submitters.size > 1 %> gap-4">
|
||||
<% template.submitters.each do |item| %>
|
||||
<div class="form-control">
|
||||
<% if template.submitters.size > 1 %>
|
||||
<label class="label pt-0 pb-1 text-xs">
|
||||
<span class="label-text"> <%= item['name'] %></span>
|
||||
</label>
|
||||
<% end %>
|
||||
<input type="hidden" name="submission[1][submitters][][uuid]" value="<%= item['uuid'] %>">
|
||||
<input type="text" name="submission[1][submitters][][name]" autocomplete="off" class="input input-sm input-bordered" placeholder="Name" required>
|
||||
<div class="grid <%= 'md:grid-cols-2 gap-1' if template.submitters.size == 1 %>">
|
||||
<input type="email" name="submission[1][submitters][][email]" autocomplete="off" class="input input-sm input-bordered mt-1.5" placeholder="Email (optional)">
|
||||
<input type="tel" pattern="^\+[0-9\s\-]+$" oninvalid="this.value ? this.setCustomValidity('Use internatioanl format: +1xxx...') : ''" oninput="this.setCustomValidity('')" name="submission[1][submitters][][phone]" autocomplete="off" class="input input-sm input-bordered mt-1.5" placeholder="Phone (optional)">
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a href="#" class="btn btn-primary btn-sm w-full flex items-center justify-center" data-action="click:dynamic-list#addItem">
|
||||
<%= svg_icon('user_plus', class: 'w-4 h-4 stroke-2') %>
|
||||
<span>Add Recipient</span>
|
||||
</a>
|
||||
</dynamic-list>
|
||||
<div>
|
||||
<%= render 'send_email', f:, template: %>
|
||||
<%= render 'send_sms', f: %>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<%= f.button button_title(title: 'Add Recipients'), class: 'base-button' %>
|
||||
</div>
|
||||
<% end %>
|
||||
@@ -0,0 +1,42 @@
|
||||
<%= form_for '', url: template_submissions_path(template), html: { class: 'space-y-4', autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %>
|
||||
<% if template.submitters.size == 1 %>
|
||||
<div class="form-control">
|
||||
<autoresize-textarea>
|
||||
<%= f.text_area :emails, required: true, class: 'base-textarea w-full', placeholder: 'Type emails here...' %>
|
||||
</autoresize-textarea>
|
||||
</div>
|
||||
<% else %>
|
||||
<dynamic-list class="space-y-4">
|
||||
<div class="space-y-4">
|
||||
<div class="card card-compact bg-base-200" data-targets="dynamic-list.items">
|
||||
<div class="card-body">
|
||||
<div class="absolute right-4 top-5">
|
||||
<a href="#" data-action="click:dynamic-list#removeItem" class="-top-3 relative block w-6 h-6 rounded-lg text-neutral-700 text-center bg-base-300 p-1 hidden hover:bg-neutral hover:text-white">
|
||||
<%= svg_icon('trash', class: 'w-4 h-4') %>
|
||||
</a>
|
||||
</div>
|
||||
<div class="grid md:grid-cols-2 gap-4">
|
||||
<% template.submitters.each do |item| %>
|
||||
<div class="form-control">
|
||||
<label class="label pt-0 pb-1 text-xs">
|
||||
<span class="label-text"> <%= item['name'] %></span>
|
||||
</label>
|
||||
<input type="hidden" name="submission[1][submitters][][uuid]" value="<%= item['uuid'] %>">
|
||||
<input type="email" name="submission[1][submitters][][email]" autocomplete="off" class="input input-sm input-bordered" placeholder="Email" required>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a href="#" class="btn btn-primary btn-sm w-full flex items-center justify-center" data-action="click:dynamic-list#addItem">
|
||||
<%= svg_icon('user_plus', class: 'w-4 h-4 stroke-2') %>
|
||||
<span>Add Recipient</span>
|
||||
</a>
|
||||
</dynamic-list>
|
||||
<% end %>
|
||||
<%= render 'send_email', f:, template: %>
|
||||
<div class="form-control">
|
||||
<%= f.button button_title(title: 'Add Recipients'), class: 'base-button' %>
|
||||
</div>
|
||||
<% end %>
|
||||
@@ -0,0 +1,44 @@
|
||||
<%= form_for '', url: template_submissions_path(template), html: { class: 'space-y-4', autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %>
|
||||
<dynamic-list class="space-y-4">
|
||||
<div class="space-y-4">
|
||||
<div class="card card-compact bg-base-200" data-targets="dynamic-list.items">
|
||||
<div class="card-body">
|
||||
<div class="absolute right-4 top-5">
|
||||
<a href="#" data-action="click:dynamic-list#removeItem" class="<%= template.submitters.size == 1 ? 'right-2' : '-top-3' %> relative block w-6 h-6 rounded-lg text-neutral-700 text-center bg-base-300 p-1 hidden hover:bg-neutral hover:text-white">
|
||||
<%= svg_icon('trash', class: 'w-4 h-4') %>
|
||||
</a>
|
||||
</div>
|
||||
<div class="grid md:grid-cols-2 <%= template.submitters.size > 1 ? 'gap-4' : 'gap-1' %>">
|
||||
<% template.submitters.each do |item| %>
|
||||
<div class="form-control">
|
||||
<% if template.submitters.size > 1 %>
|
||||
<label class="label pt-0 pb-1 text-xs">
|
||||
<span class="label-text"> <%= item['name'] %></span>
|
||||
</label>
|
||||
<% end %>
|
||||
<input type="hidden" name="submission[1][submitters][][uuid]" value="<%= item['uuid'] %>">
|
||||
<input type="tel" pattern="^\+[0-9\s\-]+$" oninvalid="this.value ? this.setCustomValidity('Use internatioanl format: +1xxx...') : ''" oninput="this.setCustomValidity('')" name="submission[1][submitters][][phone]" autocomplete="off" class="input input-sm input-bordered" placeholder="Phone" required>
|
||||
<% if template.submitters.size > 1 %>
|
||||
<input type="text" name="submission[1][submitters][][name]" autocomplete="off" class="input input-sm input-bordered mt-1.5" placeholder="Name (optional)">
|
||||
<% end %>
|
||||
</div>
|
||||
<% if template.submitters.size == 1 %>
|
||||
<div class="form-control flex">
|
||||
<input type="text" name="submission[1][submitters][][name]" autocomplete="off" class="input input-sm input-bordered" placeholder="Name (optional)">
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a href="#" class="btn btn-primary btn-sm w-full flex items-center justify-center" data-action="click:dynamic-list#addItem">
|
||||
<%= svg_icon('user_plus', class: 'w-4 h-4 stroke-2') %>
|
||||
<span>Add Recipient</span>
|
||||
</a>
|
||||
</dynamic-list>
|
||||
<%= render 'send_sms', f: %>
|
||||
<div class="form-control">
|
||||
<%= f.button button_title(title: 'Add Recipients'), class: 'base-button' %>
|
||||
</div>
|
||||
<% end %>
|
||||
@@ -0,0 +1,37 @@
|
||||
<div class="form-control">
|
||||
<% is_smtp_configured = Accounts.can_send_emails?(current_account) %>
|
||||
<%= 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: !is_smtp_configured, onchange: "window.message_field && message_field.classList.toggle('hidden', !event.currentTarget.checked)" %>
|
||||
<span class="label">Send Email</span>
|
||||
<% end %>
|
||||
<% unless is_smtp_configured %>
|
||||
<div class="alert my-4">
|
||||
<%= svg_icon('info_circle', class: 'w-6 h-6') %>
|
||||
<div>
|
||||
<p class="font-bold">SMTP not Configured</p>
|
||||
<p class="text-gray-700">
|
||||
Configure SMTP settings in order to send emails:
|
||||
<br>
|
||||
<a class="link font-medium" data-turbo-frame="_top" href="<%= settings_email_index_path %>">Go to SMTP settings</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% unless AccountConfig.exists?(key: AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY) %>
|
||||
<div id="message_field" class="card card-compact bg-base-200 hidden">
|
||||
<div class="card-body">
|
||||
<div class="form-control space-y-2">
|
||||
<span class="label-text">Hi there,</span>
|
||||
<autoresize-textarea>
|
||||
<%= f.text_area :message, value: format(SubmitterMailer::DEFAULT_MESSAGE, name: template.name), required: true, class: 'base-textarea !rounded-lg w-full' %>
|
||||
</autoresize-textarea>
|
||||
<span class="label-text">
|
||||
Thanks,
|
||||
<br>
|
||||
<%= current_account.name %>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
@@ -0,0 +1,3 @@
|
||||
<div class="form-control">
|
||||
<%= render 'sms_settings/placeholder' %>
|
||||
</div>
|
||||
@@ -0,0 +1,5 @@
|
||||
<div class="mt-2 mb-1">
|
||||
<div class="tooltip w-full" data-tip="Unlock with Enterpise">
|
||||
<%= link_to submitter.sent_at? ? 'Re-send SMS' : 'Send SMS', "#{Docuseal::CONSOLE_URL}/#{Docuseal.multitenant? ? 'plans' : 'on_premise'}", class: 'btn btn-sm btn-primary text-gray-400 w-full' %>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,81 +1,26 @@
|
||||
<%= render 'shared/turbo_modal', title: 'New Recipients' do %>
|
||||
<%= form_for '', url: template_submissions_path(@template), html: { class: 'space-y-4', autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %>
|
||||
<% if @template.submitters.size == 1 %>
|
||||
<div class="form-control">
|
||||
<%= f.label :emails, class: 'label' %>
|
||||
<autoresize-textarea>
|
||||
<%= f.text_area :emails, required: true, class: 'base-textarea w-full' %>
|
||||
</autoresize-textarea>
|
||||
</div>
|
||||
<% else %>
|
||||
<dynamic-list class="space-y-4">
|
||||
<div class="space-y-4">
|
||||
<div class="card card-compact bg-base-200" data-targets="dynamic-list.items">
|
||||
<div class="card-body">
|
||||
<div class="absolute right-4 top-5">
|
||||
<a href="#" data-action="click:dynamic-list#removeItem" class="-top-3 relative block w-6 h-6 rounded-lg text-neutral-700 text-center bg-base-300 p-1 hidden hover:bg-neutral hover:text-white">
|
||||
<%= svg_icon('trash', class: 'w-4 h-4') %>
|
||||
</a>
|
||||
</div>
|
||||
<div class="grid md:grid-cols-2 gap-4">
|
||||
<% @template.submitters.each do |item| %>
|
||||
<div class="form-control">
|
||||
<label class="label pt-0 pb-1 text-xs">
|
||||
<span class="label-text"> <%= item['name'] %></span>
|
||||
</label>
|
||||
<input type="hidden" name="submission[1][submitters][][uuid]" value="<%= item['uuid'] %>">
|
||||
<input type="email" name="submission[1][submitters][][email]" value="<%= item['email'] %>" autocomplete="off" class="input input-sm input-bordered" placeholder="Email" required>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a href="#" class="btn btn-primary btn-sm w-full flex items-center justify-center" data-action="click:dynamic-list#addItem">
|
||||
<%= svg_icon('user_plus', class: 'w-4 h-4 stroke-2') %>
|
||||
<span>Add Recipient</span>
|
||||
</a>
|
||||
</dynamic-list>
|
||||
<% end %>
|
||||
<div class="form-control">
|
||||
<% is_smtp_configured = Accounts.can_send_emails?(current_account) %>
|
||||
<%= f.label :send_email, class: 'flex items-center cursor-pointer' do %>
|
||||
<%= f.check_box :send_email, class: 'base-checkbox', disabled: !is_smtp_configured, onchange: "window.message_field && message_field.classList.toggle('hidden', !event.currentTarget.checked)" %>
|
||||
<span class="label">Send Email</span>
|
||||
<% end %>
|
||||
<% unless is_smtp_configured %>
|
||||
<div class="alert my-4">
|
||||
<%= svg_icon('info_circle', class: 'w-6 h-6') %>
|
||||
<div>
|
||||
<p class="font-bold">SMTP not Configured</p>
|
||||
<p class="text-gray-700">
|
||||
Configure SMTP settings in order to send emails:
|
||||
<br>
|
||||
<a class="link font-medium" data-turbo-frame="_top" href="<%= settings_email_index_path %>">Go to SMTP settings</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<%= render 'shared/turbo_modal', title: 'Add New Recipients' do %>
|
||||
<% options = [['via Email', 'email'], ['via Phone', 'phone'], ['Detailed', 'detailed']] %>
|
||||
<toggle-visible data-element-ids="<%= options.map(&:last).to_json %>" class="relative text-center mt-4 block">
|
||||
<div class="join">
|
||||
<% options.each_with_index do |(label, value), index| %>
|
||||
<span>
|
||||
<%= radio_button_tag 'option', value, value == 'email', class: 'peer hidden', data: { action: 'change:toggle-visible#trigger' } %>
|
||||
<label for="option_<%= value %>" class="<%= '!rounded-s-full' if index.zero? %> btn btn-focus btn-sm join-item w-28 peer-checked:btn-active normal-case">
|
||||
<%= label %>
|
||||
</label>
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
<% unless AccountConfig.exists?(key: AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY) %>
|
||||
<div id="message_field" class="card card-compact bg-base-200 hidden">
|
||||
<div class="card-body">
|
||||
<div class="form-control space-y-2">
|
||||
<span class="label-text">Hi there,</span>
|
||||
<autoresize-textarea>
|
||||
<%= f.text_area :message, value: format(SubmitterMailer::DEFAULT_MESSAGE, name: @template.name), required: true, class: 'base-textarea !rounded-lg w-full' %>
|
||||
</autoresize-textarea>
|
||||
<span class="label-text">
|
||||
Thanks,
|
||||
<br>
|
||||
<%= current_account.name %>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="form-control">
|
||||
<%= f.button button_title(title: 'Add Recipients'), class: 'base-button' %>
|
||||
</toggle-visible>
|
||||
<div class="mt-4">
|
||||
<div id="email">
|
||||
<%= render 'email_form', template: @template %>
|
||||
</div>
|
||||
<% end %>
|
||||
<div id="phone" class="hidden">
|
||||
<%= render 'phone_form', template: @template %>
|
||||
</div>
|
||||
<div id="detailed" class="hidden">
|
||||
<%= render 'detailed_form', template: @template %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
@@ -24,7 +24,7 @@
|
||||
<div class="flex md:max-h-[calc(100vh-60px)]">
|
||||
<div class="overflow-y-auto overflow-x-hidden hidden lg:block w-52 flex-none pr-3 mt-0.5 pt-0.5">
|
||||
<% (@submission.template_schema || @submission.template.schema).each do |item| %>
|
||||
<% document = @submission.template.documents.find { |a| item['attachment_uuid'] == a.uuid } %>
|
||||
<% document = @submission.template_schema_documents.find { |a| item['attachment_uuid'] == a.uuid } %>
|
||||
<a href="#<%= "page-#{document.uuid}-0" %>" onclick="[event.preventDefault(), window[event.target.closest('a').href.split('#')[1]].scrollIntoView({ behavior: 'smooth', block: 'start' })]" class="block cursor-pointer">
|
||||
<img src="<%= document.preview_images.first.url %>" width="<%= document.preview_images.first.metadata['width'] %>" height="<%= document.preview_images.first.metadata['height'] %>" class="rounded border" loading="lazy">
|
||||
<div class="pb-2 pt-1.5 text-center">
|
||||
@@ -39,11 +39,15 @@
|
||||
<% values = @submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) } %>
|
||||
<% attachments_index = ActiveStorage::Attachment.where(record: @submission.submitters, name: :attachments).preload(:blob).index_by(&:uuid) %>
|
||||
<% (@submission.template_schema || @submission.template.schema).each do |item| %>
|
||||
<% document = @submission.template.documents.find { |e| e.uuid == item['attachment_uuid'] } %>
|
||||
<% document = @submission.template_schema_documents.find { |e| e.uuid == item['attachment_uuid'] } %>
|
||||
<% document_annots_index = document.metadata.dig('pdf', 'annotations')&.group_by { |e| e['page'] } || {} %>
|
||||
<% document.preview_images.sort_by { |a| a.filename.base.to_i }.each_with_index do |page, index| %>
|
||||
<div id="<%= "page-#{document.uuid}-#{index}" %>" class="relative">
|
||||
<img src="<%= page.url %>" width="<%= page.metadata['width'] %>" class="shadow-md mb-4" height="<%= page.metadata['height'] %>" loading="lazy">
|
||||
<div class="top-0 bottom-0 left-0 right-0 absolute">
|
||||
<% document_annots_index[index]&.each do |annot| %>
|
||||
<%= render 'submissions/annotation', annot: %>
|
||||
<% end %>
|
||||
<% fields_index.dig(document.uuid, index)&.each do |(area, field)| %>
|
||||
<% value = values[field['uuid']] %>
|
||||
<% next if value.blank? %>
|
||||
@@ -68,18 +72,44 @@
|
||||
<%= (@submission.template_submitters || @submission.template.submitters).find { |e| e['uuid'] == submitter&.uuid }&.dig('name') || "#{(index + 1).ordinalize} Submitter" %>
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-1 mt-1">
|
||||
<%= svg_icon('mail', class: 'w-5 h-5') %>
|
||||
<span>
|
||||
<%= submitter&.email || 'N/A' %>
|
||||
</span>
|
||||
</div>
|
||||
<% if submitter&.name.present? %>
|
||||
<div class="flex items-center space-x-1 mt-1">
|
||||
<%= svg_icon('user', class: 'w-5 h-5') %>
|
||||
<span>
|
||||
<%= submitter&.name %>
|
||||
</span>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if submitter&.email.present? %>
|
||||
<div class="flex items-center space-x-1 mt-1">
|
||||
<%= svg_icon('mail', class: 'w-5 h-5') %>
|
||||
<span>
|
||||
<%= submitter.email || 'N/A' %>
|
||||
</span>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if submitter&.phone.present? %>
|
||||
<div class="flex items-center space-x-1 mt-1">
|
||||
<%= svg_icon('phone', class: 'w-5 h-5') %>
|
||||
<span>
|
||||
<%= submitter.phone %>
|
||||
</span>
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="flex items-center space-x-1 mt-1">
|
||||
<%= svg_icon('writing', class: 'w-5 h-5') %>
|
||||
<span>
|
||||
<%= submitter&.completed_at? ? l(submitter.completed_at.in_time_zone(current_account.timezone), format: :long, locale: current_account.locale) : 'Not completed yet' %>
|
||||
</span>
|
||||
</div>
|
||||
<% if submitter && submitter.email && !submitter.completed_at %>
|
||||
<div class="mt-2 mb-1">
|
||||
<%= button_to button_title(title: submitter.sent_at? ? 'Re-send Email' : 'Send Email', disabled_with: 'Sending'), submitter_send_email_index_path(submitter_slug: submitter.slug), class: 'btn btn-sm btn-primary w-full' %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if submitter && submitter.phone && !submitter.completed_at %>
|
||||
<%= render 'send_sms_button', submitter: %>
|
||||
<% end %>
|
||||
<% if submitter && !submitter.completed_at? %>
|
||||
<div class="mt-2 mb-1">
|
||||
<a class="btn btn-sm btn-primary w-full" target="_blank" href="<%= submit_form_path(slug: submitter.slug) %>">
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<% data_attachments = attachments_index.values.select { |e| e.record_id == submitter.id }.to_json(only: %i[uuid], methods: %i[url filename content_type]) %>
|
||||
<% data_fields = (submitter.submission.template_fields || submitter.submission.template.fields).select { |f| f['submitter_uuid'] == submitter.uuid }.to_json %>
|
||||
<submission-form data-is-demo="<%= Docuseal.demo? %>" data-is-direct-upload="<%= Docuseal.active_storage_public? %>" data-submitter="<%= submitter.to_json(only: %i[uuid slug name phone email]) %>" data-can-send-email="<%= Accounts.can_send_emails?(Struct.new(:id).new(@submitter.submission.template.account_id)) %>" data-attachments="<%= data_attachments %>" data-fields="<%= data_fields %>" data-authenticity-token="<%= form_authenticity_token %>" data-values="<%= submitter.values.to_json %>"></submission-form>
|
||||
@@ -17,7 +17,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<% if Accounts.can_send_emails?(@submitter.submission.template.account) %>
|
||||
<% if Accounts.can_send_emails?(@submitter.submission.template.account) && @submitter.email.present? %>
|
||||
<%= button_to button_title(title: 'Send copy to Email', disabled_with: 'Sending', icon: svg_icon('mail_forward', class: 'w-6 h-6')), send_submission_email_index_path, params: { submitter_slug: @submitter.slug }, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'white-button w-full' %>
|
||||
<div class="divider">OR</div>
|
||||
<% end %>
|
||||
|
||||
@@ -8,11 +8,15 @@
|
||||
<%= render 'banner' %>
|
||||
</div>
|
||||
<% (@submitter.submission.template_schema || @submitter.submission.template.schema).each do |item| %>
|
||||
<% document = @submitter.submission.template.documents.find { |a| a.uuid == item['attachment_uuid'] } %>
|
||||
<% document = @submitter.submission.template_schema_documents.find { |a| a.uuid == item['attachment_uuid'] } %>
|
||||
<% document_annots_index = document.metadata.dig('pdf', 'annotations')&.group_by { |e| e['page'] } || {} %>
|
||||
<% document.preview_images.sort_by { |a| a.filename.base.to_i }.each_with_index do |page, index| %>
|
||||
<div class="relative my-4 shadow-md">
|
||||
<img src="<%= page.url %>" width="<%= page.metadata['width'] %>" height="<%= page.metadata['height'] %>" loading="lazy">
|
||||
<div id="page-<%= [document.uuid, index].join('-') %>" class="top-0 bottom-0 left-0 right-0 absolute">
|
||||
<% document_annots_index[index]&.each do |annot| %>
|
||||
<%= render 'submissions/annotation', annot: %>
|
||||
<% end %>
|
||||
<% fields_index.dig(document.uuid, index)&.each do |(area, field)| %>
|
||||
<% value = values[field['uuid']] %>
|
||||
<% next if value.blank? %>
|
||||
@@ -29,7 +33,7 @@
|
||||
<div class="fixed bottom-0 w-full h-0 z-20">
|
||||
<div class="mx-auto" style="max-width: 1000px">
|
||||
<div class="relative md:mx-32">
|
||||
<submission-form data-is-demo="<%= Docuseal.demo? %>" data-is-direct-upload="<%= Docuseal.active_storage_public? %>" data-submitter-uuid="<%= @submitter.uuid %>" data-submitter-slug="<%= @submitter.slug %>" data-can-send-email="<%= Accounts.can_send_emails?(Struct.new(:id).new(@submitter.submission.template.account_id)) %>" data-attachments="<%= attachments_index.values.select { |e| e.record_id == @submitter.id }.to_json(only: %i[uuid], methods: %i[url filename content_type]) %>" data-fields="<%= (@submitter.submission.template_fields || @submitter.submission.template.fields).select { |f| f['submitter_uuid'] == @submitter.uuid }.to_json %>" data-values="<%= @submitter.values.to_json %>" data-authenticity-token="<%= form_authenticity_token %>"></submission-form>
|
||||
<%= render 'submission_form', attachments_index:, submitter: @submitter %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,116 @@
|
||||
<% status_badges = { 'awaiting' => 'badge-info', 'sent' => 'badge-info', 'completed' => 'badge-success', 'opened' => 'badge-warning' } %>
|
||||
<a href="<%= submission_path(submission) %>" class="bg-base-200 w-full flex flex-col md:flex-row space-y-4 md:space-y-0 md:justify-between rounded-2xl px-6 py-5 md:items-center">
|
||||
<% submitters = (submission.template_submitters || submission.template.submitters).filter_map { |item| submission.submitters.find { |e| e.uuid == item['uuid'] } } %>
|
||||
<% is_submission_completed = submitters.all?(&:completed_at?) %>
|
||||
<% if submitters.size == 1 %>
|
||||
<div>
|
||||
<% submitter = submitters.first %>
|
||||
<div class="flex items-center space-x-4">
|
||||
<span class="flex items-center space-x-3">
|
||||
<div class="tooltip flex" data-tip="<%= l(submitter.status_event_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>">
|
||||
<span class="badge <%= status_badges[submitter.status] %> md:w-32 badge-lg bg-opacity-50 uppercase text-sm font-semibold">
|
||||
<%= submitter.status %>
|
||||
</span>
|
||||
</div>
|
||||
<span class="text-lg break-all flex items-center">
|
||||
<%= submitter.name || submitter.email || submitter.phone %>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex space-x-2 items-center">
|
||||
<% if submitter.completed_at? %>
|
||||
<form onsubmit="event.preventDefault()">
|
||||
<button onclick="event.stopPropagation()">
|
||||
<download-button data-src="<%= submitter_download_index_path(submitter.slug) %>" class="btn btn-sm btn-neutral text-white md:w-36">
|
||||
<span class="flex items-center justify-center space-x-2" data-target="download-button.defaultButton">
|
||||
<%= svg_icon('download', class: 'w-5 h-5 stroke-2') %>
|
||||
<span class="inline">Download</span>
|
||||
</span>
|
||||
<span class="flex items-center justify-center space-x-2 hidden" data-target="download-button.loadingButton">
|
||||
<%= svg_icon('loader', class: 'w-5 h-5 animate-spin') %>
|
||||
<span class="inline">Downloa...</span>
|
||||
</span>
|
||||
</download-button>
|
||||
</button>
|
||||
</form>
|
||||
<% else %>
|
||||
<%= render 'shared/clipboard_copy', text: submit_form_url(slug: submitter.slug), class: 'btn btn-sm btn-neutral text-white md:w-36 flex', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy Link', copy_title_md: 'Copy', copied_title_md: 'Copied' %>
|
||||
<% end %>
|
||||
<span class="btn btn-outline btn-sm w-20 md:w-24">View</span>
|
||||
<%= button_to button_title(title: nil, disabled_with: 'Remov', icon: svg_icon('trash', class: 'w-6 h-6')), submission_path(submission), class: 'btn btn-outline btn-sm', title: 'Delete', method: :delete, data: { turbo_confirm: 'Are you sure?' }, onclick: 'event.stopPropagation()' %>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="space-y-1 w-full md:mr-2">
|
||||
<div class="flex items-center space-x-3">
|
||||
<% if is_submission_completed %>
|
||||
<% latest_submitter = submitters.select(&:completed_at?).max_by(&:completed_at) %>
|
||||
<div class="tooltip flex" data-tip="<%= l(latest_submitter.status_event_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>">
|
||||
<span class="badge <%= status_badges[latest_submitter.status] %> md:w-32 bg-opacity-50 badge-lg uppercase text-sm font-semibold">
|
||||
<%= latest_submitter.status %>
|
||||
</span>
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="w-full">
|
||||
<% submitters.each_with_index do |submitter, index| %>
|
||||
<div class="flex justify-between items-center space-x-3">
|
||||
<span class="flex items-center space-x-3">
|
||||
<% unless is_submission_completed %>
|
||||
<div class="tooltip flex" data-tip="<%= l(submitter.status_event_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>">
|
||||
<span class="badge md:w-24 <%= status_badges[submitter.status] %> bg-opacity-50 uppercase text-xs font-semibold">
|
||||
<%= submitter.status %>
|
||||
</span>
|
||||
</div>
|
||||
<% end %>
|
||||
<span class="text-lg break-all">
|
||||
<%= submitter.name || submitter.email || submitter.phone %>
|
||||
</span>
|
||||
</span>
|
||||
<% if submitter.completed_at? && !is_submission_completed %>
|
||||
<form onsubmit="event.preventDefault()">
|
||||
<button onclick="event.stopPropagation()">
|
||||
<download-button data-src="<%= submitter_download_index_path(submitter.slug) %>" class="btn btn-xs btn-neutral text-white md:w-36">
|
||||
<span class="flex items-center justify-center space-x-2" data-target="download-button.defaultButton">
|
||||
<%= svg_icon('download', class: 'w-4 h-4 stroke-2') %>
|
||||
<span class="hidden md:inline">Download</span>
|
||||
</span>
|
||||
<span class="flex items-center justify-center space-x-2 hidden" data-target="download-button.loadingButton">
|
||||
<%= svg_icon('loader', class: 'w-4 h-4 animate-spin') %>
|
||||
<span class="hidden md:inline">Downloa...</span>
|
||||
</span>
|
||||
</download-button>
|
||||
</button>
|
||||
</form>
|
||||
<% elsif !is_submission_completed %>
|
||||
<div class="flex items-center space-x-3">
|
||||
<%= render 'shared/clipboard_copy', text: submit_form_url(slug: submitter.slug), class: 'btn btn-xs text-xs btn-neutral text-white md:w-36 flex', icon_class: 'w-4 h-4 text-white', copy_title: 'Copy Link' %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex space-x-2 items-center">
|
||||
<% if is_submission_completed %>
|
||||
<% latest_submitter = submitters.select(&:completed_at?).max_by(&:completed_at) %>
|
||||
<form onsubmit="event.preventDefault()">
|
||||
<button onclick="event.stopPropagation()">
|
||||
<download-button data-src="<%= submitter_download_index_path(latest_submitter.slug) %>" class="btn btn-sm btn-neutral text-white md:w-36">
|
||||
<span class="flex items-center justify-center space-x-2" data-target="download-button.defaultButton">
|
||||
<%= svg_icon('download', class: 'w-5 h-5 stroke-2') %>
|
||||
<span class="inline">Download</span>
|
||||
</span>
|
||||
<span class="flex items-center justify-center space-x-2 hidden" data-target="download-button.loadingButton">
|
||||
<%= svg_icon('loader', class: 'w-5 h-5 animate-spin') %>
|
||||
<span class="inline">Downloa...</span>
|
||||
</span>
|
||||
</download-button>
|
||||
</button>
|
||||
</form>
|
||||
<% end %>
|
||||
<span class="btn btn-outline btn-sm w-20 md:w-24">View</span>
|
||||
<%= button_to button_title(title: nil, disabled_with: 'Remov', icon: svg_icon('trash', class: 'w-6 h-6')), submission_path(submission), class: 'btn btn-outline btn-sm', title: 'Delete', method: :delete, data: { turbo_confirm: 'Are you sure?' }, onclick: 'event.stopPropagation()' %>
|
||||
</div>
|
||||
<% end %>
|
||||
</a>
|
||||
@@ -38,126 +38,9 @@
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<% status_badges = { 'awaiting' => 'badge-info', 'sent' => 'badge-info', 'completed' => 'badge-success', 'opened' => 'badge-warning' } %>
|
||||
<% if @submissions.present? %>
|
||||
<div class="space-y-4">
|
||||
<% @submissions.each do |submission| %>
|
||||
<a href="<%= submission_path(submission) %>" class="bg-base-200 w-full flex flex-col md:flex-row space-y-4 md:space-y-0 md:justify-between rounded-2xl px-6 py-5 md:items-center">
|
||||
<% submitters = (submission.template_submitters || submission.template.submitters).filter_map { |item| submission.submitters.find { |e| e.uuid == item['uuid'] } } %>
|
||||
<% is_submission_completed = submitters.all?(&:completed_at?) %>
|
||||
<% if submitters.size == 1 %>
|
||||
<div>
|
||||
<% submitter = submitters.first %>
|
||||
<div class="flex items-center space-x-4">
|
||||
<span class="flex items-center space-x-3">
|
||||
<div class="tooltip flex" data-tip="<%= l(submitter.status_event_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>">
|
||||
<span class="badge <%= status_badges[submitter.status] %> md:w-32 badge-lg bg-opacity-50 uppercase text-sm font-semibold">
|
||||
<%= submitter.status %>
|
||||
</span>
|
||||
</div>
|
||||
<span class="text-lg break-all">
|
||||
<%= submitter.email %>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex space-x-2 items-center">
|
||||
<% if submitter.completed_at? %>
|
||||
<form onsubmit="event.preventDefault()">
|
||||
<button onclick="event.stopPropagation()">
|
||||
<download-button data-src="<%= submitter_download_index_path(submitter.slug) %>" class="btn btn-sm btn-neutral text-white md:w-36">
|
||||
<span class="flex items-center justify-center space-x-2" data-target="download-button.defaultButton">
|
||||
<%= svg_icon('download', class: 'w-5 h-5 stroke-2') %>
|
||||
<span class="inline">Download</span>
|
||||
</span>
|
||||
<span class="flex items-center justify-center space-x-2 hidden" data-target="download-button.loadingButton">
|
||||
<%= svg_icon('loader', class: 'w-5 h-5 animate-spin') %>
|
||||
<span class="inline">Downloa...</span>
|
||||
</span>
|
||||
</download-button>
|
||||
</button>
|
||||
</form>
|
||||
<% else %>
|
||||
<%= render 'shared/clipboard_copy', text: submit_form_url(slug: submitter.slug), class: 'btn btn-sm btn-neutral text-white md:w-36 flex', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy Link' %>
|
||||
<% end %>
|
||||
<span class="btn btn-outline btn-sm w-20 md:w-24">View</span>
|
||||
<%= button_to button_title(title: nil, disabled_with: 'Remov', icon: svg_icon('trash', class: 'w-6 h-6')), submission_path(submission), class: 'btn btn-outline btn-sm', title: 'Delete', method: :delete, data: { turbo_confirm: 'Are you sure?' }, onclick: 'event.stopPropagation()' %>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="space-y-1 w-full md:mr-2">
|
||||
<div class="flex items-center space-x-3">
|
||||
<% if is_submission_completed %>
|
||||
<% latest_submitter = submitters.select(&:completed_at?).max_by(&:completed_at) %>
|
||||
<div class="tooltip flex" data-tip="<%= l(latest_submitter.status_event_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>">
|
||||
<span class="badge <%= status_badges[latest_submitter.status] %> md:w-32 bg-opacity-50 badge-lg uppercase text-sm font-semibold">
|
||||
<%= latest_submitter.status %>
|
||||
</span>
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="w-full">
|
||||
<% submitters.each_with_index do |submitter, index| %>
|
||||
<div class="flex justify-between items-center space-x-3">
|
||||
<span class="flex items-center space-x-3">
|
||||
<% unless is_submission_completed %>
|
||||
<div class="tooltip flex" data-tip="<%= l(submitter.status_event_at.in_time_zone(current_account.timezone), format: :short, locale: current_account.locale) %>">
|
||||
<span class="badge md:w-24 <%= status_badges[submitter.status] %> bg-opacity-50 uppercase text-xs font-semibold">
|
||||
<%= submitter.status %>
|
||||
</span>
|
||||
</div>
|
||||
<% end %>
|
||||
<span class="text-lg break-all">
|
||||
<%= submitter.email %>
|
||||
</span>
|
||||
</span>
|
||||
<% if submitter.completed_at? && !is_submission_completed %>
|
||||
<form onsubmit="event.preventDefault()">
|
||||
<button onclick="event.stopPropagation()">
|
||||
<download-button data-src="<%= submitter_download_index_path(submitter.slug) %>" class="btn btn-xs btn-neutral text-white md:w-36">
|
||||
<span class="flex items-center justify-center space-x-2" data-target="download-button.defaultButton">
|
||||
<%= svg_icon('download', class: 'w-4 h-4 stroke-2') %>
|
||||
<span class="hidden md:inline">Download</span>
|
||||
</span>
|
||||
<span class="flex items-center justify-center space-x-2 hidden" data-target="download-button.loadingButton">
|
||||
<%= svg_icon('loader', class: 'w-4 h-4 animate-spin') %>
|
||||
<span class="hidden md:inline">Downloa...</span>
|
||||
</span>
|
||||
</download-button>
|
||||
</button>
|
||||
</form>
|
||||
<% elsif !is_submission_completed %>
|
||||
<div class="flex items-center space-x-3">
|
||||
<%= render 'shared/clipboard_copy', text: submit_form_url(slug: submitter.slug), class: 'btn btn-xs text-xs btn-neutral text-white md:w-36 flex', icon_class: 'w-4 h-4 text-white', copy_title: 'Copy Link' %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex space-x-2 items-center">
|
||||
<% if is_submission_completed %>
|
||||
<% latest_submitter = submitters.select(&:completed_at?).max_by(&:completed_at) %>
|
||||
<form onsubmit="event.preventDefault()">
|
||||
<button onclick="event.stopPropagation()">
|
||||
<download-button data-src="<%= submitter_download_index_path(latest_submitter.slug) %>" class="btn btn-sm btn-neutral text-white md:w-36">
|
||||
<span class="flex items-center justify-center space-x-2" data-target="download-button.defaultButton">
|
||||
<%= svg_icon('download', class: 'w-5 h-5 stroke-2') %>
|
||||
<span class="inline">Download</span>
|
||||
</span>
|
||||
<span class="flex items-center justify-center space-x-2 hidden" data-target="download-button.loadingButton">
|
||||
<%= svg_icon('loader', class: 'w-5 h-5 animate-spin') %>
|
||||
<span class="inline">Downloa...</span>
|
||||
</span>
|
||||
</download-button>
|
||||
</button>
|
||||
</form>
|
||||
<% end %>
|
||||
<span class="btn btn-outline btn-sm w-20 md:w-24">View</span>
|
||||
<%= button_to button_title(title: nil, disabled_with: 'Remov', icon: svg_icon('trash', class: 'w-6 h-6')), submission_path(submission), class: 'btn btn-outline btn-sm', title: 'Delete', method: :delete, data: { turbo_confirm: 'Are you sure?' }, onclick: 'event.stopPropagation()' %>
|
||||
</div>
|
||||
<% end %>
|
||||
</a>
|
||||
<% end %>
|
||||
<%= render partial: 'submission', collection: @submissions %>
|
||||
</div>
|
||||
<%= render 'shared/pagination', pagy: @pagy, items_name: 'submissions' %>
|
||||
<% else %>
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
<%= link_to edit_user_path(user), class: 'btn btn-outline btn-xs', title: 'Edit', data: { turbo_frame: 'modal' } do %>
|
||||
Edit
|
||||
<% end %>
|
||||
<%= link_to user_path(user), class: 'btn btn-outline btn-error btn-xs', title: 'Delete', data: { turbo_method: :delete, turbo_confirm: 'Are you sure?' } do %>
|
||||
<%= button_to user_path(user), method: :delete, class: 'btn btn-outline btn-error btn-xs', title: 'Delete', data: { turbo_confirm: 'Are you sure?' } do %>
|
||||
Remove
|
||||
<% end %>
|
||||
</td>
|
||||
|
||||
+3
-3
@@ -29,10 +29,10 @@
|
||||
</p>
|
||||
<% if message == 'Signature valid' %>
|
||||
<p class="flex space-x-1 items-center">
|
||||
<% if signature.signature_handler.signer_certificate.public_key.to_der == trusted_certs.first.public_key.to_der %>
|
||||
<% if trusted_certs.any? { |e| e.public_key.to_der == signature.signature_handler.signer_certificate.public_key.to_der } %>
|
||||
<%= svg_icon('circle_check', class: 'w-6 h-6 text-green-500') %>
|
||||
<span>
|
||||
Signed with DocuSeal certificate
|
||||
Signed with trusted certificate
|
||||
</span>
|
||||
<% else %>
|
||||
<%= svg_icon('x_circle', class: 'w-6 h-6 text-red-500') %>
|
||||
@@ -54,7 +54,7 @@
|
||||
</div>
|
||||
<div class="flex items-center space-x-1">
|
||||
<%= svg_icon('certificate', class: 'w-5 h-5 inline') %>
|
||||
<span><%= signature.signer_name %></span>
|
||||
<span><%= signature.signer_name.force_encoding('UTF-8') %></span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-1">
|
||||
<%= svg_icon('lock_access', class: 'w-5 h-5 inline') %>
|
||||
+4
-1
@@ -38,6 +38,7 @@ Rails.application.routes.draw do
|
||||
end
|
||||
end
|
||||
|
||||
resources :verify_pdf_signature, only: %i[create]
|
||||
resources :dashboard, only: %i[index]
|
||||
resources :setup, only: %i[index create]
|
||||
resource :newsletter, only: %i[show update]
|
||||
@@ -63,6 +64,7 @@ Rails.application.routes.draw do
|
||||
|
||||
resources :submitters, only: %i[], param: 'slug' do
|
||||
resources :download, only: %i[index], controller: 'submissions_download'
|
||||
resources :send_email, only: %i[create], controller: 'submitters_send_email'
|
||||
resources :debug, only: %i[index], controller: 'submissions_debug' if Rails.env.development?
|
||||
end
|
||||
|
||||
@@ -70,8 +72,9 @@ Rails.application.routes.draw do
|
||||
unless Docuseal.multitenant?
|
||||
resources :storage, only: %i[index create], controller: 'storage_settings'
|
||||
resources :email, only: %i[index create], controller: 'email_settings'
|
||||
resources :sms, only: %i[index], controller: 'sms_settings'
|
||||
end
|
||||
resources :esign, only: %i[index create], controller: 'esign_settings'
|
||||
resource :esign, only: %i[show create new update destroy], controller: 'esign_settings'
|
||||
resources :users, only: %i[index]
|
||||
resource :personalization, only: %i[show create], controller: 'personalization_settings'
|
||||
if !Docuseal.multitenant? || Docuseal.demo?
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class AddPhoneAndNameToSubmitters < ActiveRecord::Migration[7.0]
|
||||
def change
|
||||
add_column :submitters, :name, :string
|
||||
add_column :submitters, :phone, :string
|
||||
|
||||
change_column_null :submitters, :email, true
|
||||
end
|
||||
end
|
||||
+4
-2
@@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[7.0].define(version: 2023_08_19_190316) do
|
||||
ActiveRecord::Schema[7.0].define(version: 2023_09_02_171216) do
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
|
||||
@@ -108,7 +108,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_08_19_190316) do
|
||||
create_table "submitters", force: :cascade do |t|
|
||||
t.bigint "submission_id", null: false
|
||||
t.string "uuid", null: false
|
||||
t.string "email", null: false
|
||||
t.string "email"
|
||||
t.string "slug", null: false
|
||||
t.text "values", null: false
|
||||
t.string "ua"
|
||||
@@ -118,6 +118,8 @@ ActiveRecord::Schema[7.0].define(version: 2023_08_19_190316) do
|
||||
t.datetime "completed_at"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.string "name"
|
||||
t.string "phone"
|
||||
t.index ["email"], name: "index_submitters_on_email"
|
||||
t.index ["slug"], name: "index_submitters_on_slug", unique: true
|
||||
t.index ["submission_id"], name: "index_submitters_on_submission_id"
|
||||
|
||||
+7
-10
@@ -42,22 +42,19 @@ module Accounts
|
||||
new_template
|
||||
end
|
||||
|
||||
def load_signing_certs(account)
|
||||
certs =
|
||||
def load_signing_pkcs(account)
|
||||
cert_data =
|
||||
if Docuseal.multitenant?
|
||||
Docuseal::CERTS
|
||||
else
|
||||
EncryptedConfig.find_by(account:, key: EncryptedConfig::ESIGN_CERTS_KEY).value
|
||||
end
|
||||
|
||||
{
|
||||
cert: OpenSSL::X509::Certificate.new(certs['cert']),
|
||||
key: OpenSSL::PKey::RSA.new(certs['key']),
|
||||
sub_ca: OpenSSL::X509::Certificate.new(certs['sub_ca']),
|
||||
sub_key: OpenSSL::PKey::RSA.new(certs['sub_key']),
|
||||
root_ca: OpenSSL::X509::Certificate.new(certs['root_ca']),
|
||||
root_key: OpenSSL::PKey::RSA.new(certs['root_key'])
|
||||
}
|
||||
if (default_cert = cert_data['custom']&.find { |e| e['status'] == 'default' })
|
||||
OpenSSL::PKCS12.new(Base64.urlsafe_decode64(default_cert['data']), default_cert['password'].to_s)
|
||||
else
|
||||
GenerateCertificate.load_pkcs(cert_data)
|
||||
end
|
||||
end
|
||||
|
||||
def can_send_emails?(account)
|
||||
|
||||
+6
-1
@@ -10,13 +10,18 @@ module Docuseal
|
||||
TWITTER_URL = 'https://twitter.com/docusealco'
|
||||
TWITTER_HANDLE = '@docusealco'
|
||||
SUPPORT_EMAIL = 'support@docuseal.co'
|
||||
HOST = ENV.fetch('HOST', 'localhost')
|
||||
CONSOLE_URL = if Rails.env.development?
|
||||
'http://console.localhost.io:3001'
|
||||
elsif ENV['MULTITENANT'] == 'true'
|
||||
"https://console.#{HOST}"
|
||||
else
|
||||
'https://console.docuseal.co'
|
||||
end
|
||||
CDN_URL = if Rails.env.development?
|
||||
'http://localhost:3000'
|
||||
elsif ENV['MULTITENANT'] == 'true'
|
||||
"https://cdn.#{HOST}"
|
||||
else
|
||||
'https://cdn.docuseal.co'
|
||||
end
|
||||
@@ -24,7 +29,7 @@ module Docuseal
|
||||
CERTS = JSON.parse(ENV.fetch('CERTS', '{}'))
|
||||
|
||||
DEFAULT_URL_OPTIONS = {
|
||||
host: ENV.fetch('HOST', 'localhost'),
|
||||
host: HOST,
|
||||
protocol: ENV['FORCE_SSL'] == 'true' ? 'https' : 'http'
|
||||
}.freeze
|
||||
|
||||
|
||||
@@ -87,4 +87,19 @@ module GenerateCertificate
|
||||
|
||||
[cert, key]
|
||||
end
|
||||
|
||||
def load_pkcs(cert_data)
|
||||
cert = OpenSSL::X509::Certificate.new(cert_data['cert'])
|
||||
key = OpenSSL::PKey::RSA.new(cert_data['key'])
|
||||
sub_ca = OpenSSL::X509::Certificate.new(cert_data['sub_ca'])
|
||||
root_ca = OpenSSL::X509::Certificate.new(cert_data['root_ca'])
|
||||
|
||||
OpenSSL::PKCS12.create(
|
||||
'',
|
||||
'',
|
||||
key,
|
||||
cert,
|
||||
[sub_ca, root_ca]
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -5,6 +5,7 @@ module ReplaceEmailVariables
|
||||
SUBMITTER_LINK = '{{submitter.link}}'
|
||||
ACCOUNT_NAME = '{{account.name}}'
|
||||
SUBMITTER_EMAIL = '{{submitter.email}}'
|
||||
SUBMITTER_NAME = '{{submitter.name}}'
|
||||
SUBMISSION_LINK = '{{submission.link}}'
|
||||
DOCUMENTS_LINKS = '{{documents.links}}'
|
||||
|
||||
@@ -16,18 +17,23 @@ module ReplaceEmailVariables
|
||||
slug: submitter.slug, **Docuseal.default_url_options
|
||||
)
|
||||
|
||||
submission_link =
|
||||
Rails.application.routes.url_helpers.submission_url(
|
||||
submitter.submission, **Docuseal.default_url_options
|
||||
)
|
||||
if submitter.submission
|
||||
submission_link =
|
||||
Rails.application.routes.url_helpers.submission_url(
|
||||
submitter.submission, **Docuseal.default_url_options
|
||||
)
|
||||
end
|
||||
|
||||
text = text.gsub(TEMAPLTE_NAME, submitter.template.name)
|
||||
text = text.gsub(SUBMITTER_EMAIL, submitter.email)
|
||||
text = text.gsub(TEMAPLTE_NAME, submitter.template.name) if submitter.template
|
||||
text = text.gsub(SUBMITTER_EMAIL, submitter.email) if submitter.email
|
||||
text = text.gsub(SUBMITTER_NAME, submitter.name || submitter.email || submitter.phone)
|
||||
text = text.gsub(SUBMITTER_LINK, submitter_link)
|
||||
text = text.gsub(SUBMISSION_LINK, submission_link)
|
||||
text = text.gsub(SUBMISSION_LINK, submission_link) if submission_link
|
||||
text = text.gsub(DOCUMENTS_LINKS, build_documents_links_text(submitter))
|
||||
|
||||
text.gsub(ACCOUNT_NAME, submitter.template.account.name)
|
||||
text = text.gsub(ACCOUNT_NAME, submitter.template.account.name) if submitter.template
|
||||
|
||||
text
|
||||
end
|
||||
|
||||
def build_documents_links_text(submitter)
|
||||
|
||||
+7
-5
@@ -11,33 +11,35 @@ module Submissions
|
||||
submission.save!
|
||||
end
|
||||
|
||||
def create_from_emails(template:, user:, emails:, source:, send_email: false)
|
||||
def create_from_emails(template:, user:, emails:, source:, mark_as_sent: false)
|
||||
emails = emails.to_s.scan(User::EMAIL_REGEXP) unless emails.is_a?(Array)
|
||||
|
||||
emails.map do |email|
|
||||
submission = template.submissions.new(created_by_user: user, source:, template_submitters: template.submitters)
|
||||
submission.submitters.new(email:,
|
||||
uuid: template.submitters.first['uuid'],
|
||||
sent_at: send_email ? Time.current : nil)
|
||||
sent_at: mark_as_sent ? Time.current : nil)
|
||||
|
||||
submission.tap(&:save!)
|
||||
end
|
||||
end
|
||||
|
||||
def create_from_submitters(template:, user:, submissions_attrs:, source:, send_email: false)
|
||||
def create_from_submitters(template:, user:, submissions_attrs:, source:, mark_as_sent: false)
|
||||
submissions_attrs.map do |attrs|
|
||||
submission = template.submissions.new(created_by_user: user, source:, template_submitters: template.submitters)
|
||||
|
||||
attrs[:submitters].each_with_index do |submitter_attrs, index|
|
||||
uuid =
|
||||
submitter_attrs[:uuid].presence ||
|
||||
template.submitters.find { |e| e['name'] == submitter_attrs[:name] }&.dig('uuid') ||
|
||||
template.submitters.find { |e| e['name'] == submitter_attrs[:role] }&.dig('uuid') ||
|
||||
template.submitters[index]&.dig('uuid')
|
||||
|
||||
next if uuid.blank?
|
||||
|
||||
submission.submitters.new(email: submitter_attrs[:email],
|
||||
sent_at: send_email ? Time.current : nil,
|
||||
phone: submitter_attrs[:phone].to_s.gsub(/[^0-9+]/, ''),
|
||||
name: submitter_attrs[:name],
|
||||
sent_at: mark_as_sent && submitter_attrs[:email].present? ? Time.current : nil,
|
||||
values: submitter_attrs[:values] || {},
|
||||
uuid:)
|
||||
end
|
||||
|
||||
@@ -91,15 +91,23 @@ module Submissions
|
||||
|
||||
def build_submission_data(submitter, submitter_name, submitters_count)
|
||||
[
|
||||
{
|
||||
name: column_name('Name', submitter_name, submitters_count),
|
||||
value: submitter.name
|
||||
},
|
||||
{
|
||||
name: column_name('Email', submitter_name, submitters_count),
|
||||
value: submitter.email
|
||||
},
|
||||
{
|
||||
name: column_name('Phone', submitter_name, submitters_count),
|
||||
value: submitter.phone
|
||||
},
|
||||
{
|
||||
name: column_name('Completed At', submitter_name, submitters_count),
|
||||
value: submitter.completed_at
|
||||
}
|
||||
]
|
||||
].reject { |e| e[:value].blank? }
|
||||
end
|
||||
|
||||
def column_name(name, submitter_name, submitters_count = 1)
|
||||
|
||||
@@ -28,7 +28,7 @@ module Submissions
|
||||
|
||||
template = submitter.submission.template
|
||||
|
||||
certs = Accounts.load_signing_certs(submitter.submission.template.account)
|
||||
pkcs = Accounts.load_signing_pkcs(submitter.submission.template.account)
|
||||
|
||||
pdfs_index = build_pdfs_index(submitter)
|
||||
|
||||
@@ -171,7 +171,7 @@ module Submissions
|
||||
submitter.submission.template_schema.map do |item|
|
||||
pdf = pdfs_index[item['attachment_uuid']]
|
||||
|
||||
attachment = save_signed_pdf(pdf:, submitter:, certs:, uuid: item['attachment_uuid'], name: item['name'])
|
||||
attachment = save_signed_pdf(pdf:, submitter:, pkcs:, uuid: item['attachment_uuid'], name: item['name'])
|
||||
|
||||
image_pdfs << pdf if original_documents.find { |a| a.uuid == item['attachment_uuid'] }.image?
|
||||
|
||||
@@ -189,7 +189,7 @@ module Submissions
|
||||
save_signed_pdf(
|
||||
pdf: images_pdf,
|
||||
submitter:,
|
||||
certs:,
|
||||
pkcs:,
|
||||
uuid: images_pdf_uuid(original_documents.select(&:image?)),
|
||||
name: template.name
|
||||
)
|
||||
@@ -198,15 +198,15 @@ module Submissions
|
||||
end
|
||||
# rubocop:enable Metrics
|
||||
|
||||
def save_signed_pdf(pdf:, submitter:, certs:, uuid:, name:)
|
||||
def save_signed_pdf(pdf:, submitter:, pkcs:, uuid:, name:)
|
||||
io = StringIO.new
|
||||
|
||||
pdf.trailer.info[:Creator] = INFO_CREATOR
|
||||
|
||||
pdf.sign(io, reason: format(SIGN_REASON, email: submitter.email),
|
||||
certificate: certs[:cert],
|
||||
key: certs[:key],
|
||||
certificate_chain: [certs[:sub_ca], certs[:root_ca]])
|
||||
certificate: pkcs.certificate,
|
||||
key: pkcs.key,
|
||||
certificate_chain: pkcs.ca_certs || [])
|
||||
|
||||
ActiveStorage::Attachment.create!(
|
||||
uuid:,
|
||||
|
||||
@@ -28,4 +28,14 @@ module Submitters
|
||||
record: submitter
|
||||
)
|
||||
end
|
||||
|
||||
def send_signature_requests(submitters, params)
|
||||
return if params[:send_email] != true && params[:send_email] != '1'
|
||||
|
||||
submitters.each do |submitter|
|
||||
next if submitter.email.blank?
|
||||
|
||||
SubmitterMailer.invitation_email(submitter, message: params[:message]).deliver_later!
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -14,7 +14,7 @@ module Submitters
|
||||
.except('uuid', 'values', 'slug')
|
||||
.merge('values' => values,
|
||||
'documents' => documents,
|
||||
'submitter_name' => submitter_name)
|
||||
'role' => submitter_name)
|
||||
end
|
||||
|
||||
def build_values_array(submitter)
|
||||
@@ -37,18 +37,24 @@ module Submitters
|
||||
|
||||
def build_documents_array(submitter)
|
||||
submitter.documents.preload(:blob).map do |attachment|
|
||||
{ name: attachment.filename.base, url: attachment.url }
|
||||
{ name: attachment.filename.base, url: rails_storage_proxy_url(attachment) }
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_field_value(field, value, attachments_index)
|
||||
if field['type'].in?(%w[image signature])
|
||||
attachments_index[value]&.url
|
||||
rails_storage_proxy_url(attachments_index[value])
|
||||
elsif field['type'] == 'file'
|
||||
Array.wrap(value).compact_blank.filter_map { |e| attachments_index[e]&.url }
|
||||
Array.wrap(value).compact_blank.filter_map { |e| rails_storage_proxy_url(attachments_index[e]) }
|
||||
else
|
||||
value
|
||||
end
|
||||
end
|
||||
|
||||
def rails_storage_proxy_url(attachment)
|
||||
return if attachment.blank?
|
||||
|
||||
Rails.application.routes.url_helpers.rails_storage_proxy_url(attachment, **Docuseal.default_url_options)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -2,13 +2,15 @@
|
||||
|
||||
module Submitters
|
||||
module SubmitValues
|
||||
ValidationError = Class.new(StandardError)
|
||||
|
||||
module_function
|
||||
|
||||
def call(submitter, params, request)
|
||||
update_submitter!(submitter, params, request)
|
||||
|
||||
Submissions.update_template_fields!(submitter.submission) if submitter.submission.template_fields.blank?
|
||||
|
||||
update_submitter!(submitter, params, request)
|
||||
|
||||
submitter.submission.save!
|
||||
|
||||
return unless submitter.completed_at?
|
||||
@@ -27,7 +29,11 @@ module Submitters
|
||||
end
|
||||
|
||||
def update_submitter!(submitter, params, request)
|
||||
submitter.values.merge!(normalized_values(params))
|
||||
values = normalized_values(params)
|
||||
|
||||
validate_values!(values, submitter, params)
|
||||
|
||||
submitter.values.merge!(values)
|
||||
submitter.opened_at ||= Time.current
|
||||
|
||||
if params[:completed] == 'true'
|
||||
@@ -45,10 +51,24 @@ module Submitters
|
||||
params.fetch(:values, {}).to_unsafe_h.transform_values do |v|
|
||||
if params[:cast_boolean] == 'true'
|
||||
v == 'true'
|
||||
elsif params[:normalize_phone] == 'true'
|
||||
v.to_s.gsub(/[^0-9+]/, '')
|
||||
else
|
||||
v.is_a?(Array) ? v.compact_blank : v
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def validate_values!(values, submitter, params)
|
||||
values.each do |key, value|
|
||||
field = submitter.submission.template_fields.find { |e| e['uuid'] == key }
|
||||
|
||||
validate_value!(value, field, params)
|
||||
end
|
||||
end
|
||||
|
||||
def validate_value!(_value, _field, _params)
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,35 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Templates
|
||||
module BuildAnnotations
|
||||
module_function
|
||||
|
||||
def call(data)
|
||||
pdf = PDF::Reader.new(StringIO.new(data))
|
||||
|
||||
pdf.pages.flat_map.with_index do |page, index|
|
||||
annotations = page.objects.deref!(page.attributes[:Annots]) || []
|
||||
annotations.filter_map do |annot|
|
||||
next if annot[:A].blank? || annot[:A][:URI].blank?
|
||||
next unless annot[:Subtype] == :Link
|
||||
next if !annot[:A][:URI].starts_with?('https://') && !annot[:A][:URI].starts_with?('http://')
|
||||
|
||||
build_external_link_hash(page, annot).merge('page' => index)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def build_external_link_hash(page, annot)
|
||||
left, bottom, right, top = annot[:Rect]
|
||||
|
||||
{
|
||||
'type' => 'external_link',
|
||||
'value' => annot[:A][:URI],
|
||||
'x' => left / page.width,
|
||||
'y' => (page.height - top) / page.height,
|
||||
'w' => (right - left) / page.width,
|
||||
'h' => (top - bottom) / page.height
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,56 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Templates
|
||||
module CreateAttachments
|
||||
PDF_CONTENT_TYPE = 'application/pdf'
|
||||
InvalidFileType = Class.new(StandardError)
|
||||
|
||||
module_function
|
||||
|
||||
def call(template, params)
|
||||
find_or_create_blobs(params).map do |blob|
|
||||
document_data = blob.download
|
||||
|
||||
if !blob.image? && blob.content_type != PDF_CONTENT_TYPE
|
||||
blob, document_data = handle_file_types(blob, document_data)
|
||||
end
|
||||
|
||||
document = template.documents.create!(blob:)
|
||||
|
||||
if blob.content_type == PDF_CONTENT_TYPE && blob.metadata['pdf'].nil?
|
||||
blob.metadata['pdf'] = { 'annotations' => Templates::BuildAnnotations.call(document_data) }
|
||||
end
|
||||
|
||||
blob.save!
|
||||
|
||||
Templates::ProcessDocument.call(document, document_data)
|
||||
end
|
||||
end
|
||||
|
||||
def find_or_create_blobs(params)
|
||||
blobs = params[:blobs]&.map do |attrs|
|
||||
ActiveStorage::Blob.find_signed(attrs[:signed_id])
|
||||
end
|
||||
|
||||
blobs || params[:files].map do |file|
|
||||
data = file.read
|
||||
|
||||
if file.content_type == PDF_CONTENT_TYPE
|
||||
metadata = { 'identified' => true, 'analyzed' => true,
|
||||
'pdf' => { 'annotations' => Templates::BuildAnnotations.call(data) } }
|
||||
end
|
||||
|
||||
ActiveStorage::Blob.create_and_upload!(
|
||||
io: StringIO.new(data),
|
||||
filename: file.original_filename,
|
||||
metadata:,
|
||||
content_type: file.content_type
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def handle_file_types(_document_data, blob)
|
||||
raise InvalidFileType, blob.content_type
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -12,22 +12,20 @@ module Templates
|
||||
|
||||
module_function
|
||||
|
||||
def call(attachment)
|
||||
def call(attachment, data)
|
||||
if attachment.content_type == PDF_CONTENT_TYPE
|
||||
generate_pdf_preview_images(attachment)
|
||||
generate_pdf_preview_images(attachment, data)
|
||||
elsif attachment.image?
|
||||
generate_preview_image(attachment)
|
||||
generate_preview_image(attachment, data)
|
||||
end
|
||||
|
||||
attachment
|
||||
end
|
||||
|
||||
def generate_preview_image(attachment)
|
||||
binary = attachment.download
|
||||
|
||||
def generate_preview_image(attachment, data)
|
||||
ActiveStorage::Attachment.where(name: ATTACHMENT_NAME, record: attachment).destroy_all
|
||||
|
||||
image = Vips::Image.new_from_buffer(binary, '')
|
||||
image = Vips::Image.new_from_buffer(data, '')
|
||||
image = image.autorot.resize(MAX_WIDTH / image.width.to_f)
|
||||
|
||||
io = StringIO.new(image.write_to_buffer(FORMAT, Q: Q, interlace: true))
|
||||
@@ -42,14 +40,12 @@ module Templates
|
||||
)
|
||||
end
|
||||
|
||||
def generate_pdf_preview_images(attachment)
|
||||
binary = attachment.download
|
||||
|
||||
def generate_pdf_preview_images(attachment, data)
|
||||
ActiveStorage::Attachment.where(name: ATTACHMENT_NAME, record: attachment).destroy_all
|
||||
number_of_pages = HexaPDF::Document.new(io: StringIO.new(binary)).pages.size - 1
|
||||
number_of_pages = PDF::Reader.new(StringIO.new(data)).pages.size - 1
|
||||
|
||||
(0..number_of_pages).each do |page_number|
|
||||
page = Vips::Image.new_from_buffer(binary, '', dpi: DPI, page: page_number)
|
||||
page = Vips::Image.new_from_buffer(data, '', dpi: DPI, page: page_number)
|
||||
page = page.resize(MAX_WIDTH / page.width.to_f)
|
||||
|
||||
io = StringIO.new(page.write_to_buffer(FORMAT, Q: Q, interlace: true))
|
||||
|
||||
@@ -19,7 +19,7 @@ FactoryBot.define do
|
||||
record: template
|
||||
)
|
||||
|
||||
Templates::ProcessDocument.call(attachment)
|
||||
Templates::ProcessDocument.call(attachment, attachment.download)
|
||||
|
||||
template.schema = [{ attachment_uuid: attachment.uuid, name: 'sample-document' }]
|
||||
template.submitters = [
|
||||
|
||||
@@ -8,7 +8,7 @@ RSpec.describe 'PDF Signature Settings' do
|
||||
|
||||
before do
|
||||
sign_in(user)
|
||||
visit settings_esign_index_path
|
||||
visit settings_esign_path
|
||||
end
|
||||
|
||||
it 'shows verify signed PDF page' do
|
||||
|
||||
@@ -27,12 +27,12 @@ RSpec.describe 'Submit Form' do
|
||||
click_button 'Start'
|
||||
|
||||
fill_in 'First Name', with: 'Adam'
|
||||
click_on 'Next'
|
||||
click_on 'next'
|
||||
click_on 'type_text_button'
|
||||
fill_in 'signature_text_input', with: 'Adam'
|
||||
|
||||
expect do
|
||||
click_on 'Submit'
|
||||
click_on 'submit'
|
||||
end.not_to(change(Submitter, :count))
|
||||
|
||||
submitter = Submitter.find_by(email: 'john.dou@example.com')
|
||||
@@ -58,10 +58,10 @@ RSpec.describe 'Submit Form' do
|
||||
|
||||
it 'completes the form' do
|
||||
fill_in 'First Name', with: 'Sally'
|
||||
click_on 'Next'
|
||||
click_on 'next'
|
||||
click_on 'type_text_button'
|
||||
fill_in 'signature_text_input', with: 'Sally'
|
||||
click_on 'Submit'
|
||||
click_on 'submit'
|
||||
|
||||
submitter.reload
|
||||
|
||||
@@ -75,12 +75,12 @@ RSpec.describe 'Submit Form' do
|
||||
|
||||
it 'sends completed email' do
|
||||
fill_in 'First Name', with: 'Adam'
|
||||
click_on 'Next'
|
||||
click_on 'next'
|
||||
click_on 'type_text_button'
|
||||
fill_in 'signature_text_input', with: 'Adam'
|
||||
|
||||
expect do
|
||||
click_on 'Submit'
|
||||
click_on 'submit'
|
||||
end.to change(enqueued_jobs, :size).by(3)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -70,7 +70,7 @@ RSpec.describe 'Team Settings' do
|
||||
it 'removes a user' do
|
||||
expect do
|
||||
accept_confirm('Are you sure?') do
|
||||
first(:link, 'Delete').click
|
||||
first(:button, 'Delete').click
|
||||
end
|
||||
end.to change { User.active.count }.by(-1)
|
||||
|
||||
@@ -86,7 +86,7 @@ RSpec.describe 'Team Settings' do
|
||||
it 'does not allow to remove the current user' do
|
||||
expect do
|
||||
accept_confirm('Are you sure?') do
|
||||
first(:link, 'Delete').click
|
||||
first(:button, 'Delete').click
|
||||
end
|
||||
end.not_to(change { User.admins.count })
|
||||
|
||||
|
||||
Reference in new issue
Block a user