Compare commits

...

15 Commits

Author SHA1 Message Date
Pete Matsyburka 13f76c9944 adjust submission title link 2023-11-26 00:33:05 +02:00
Pete Matsyburka ce4e55f387 autoadjust field value font size 2023-11-26 00:33:02 +02:00
Pete Matsyburka 712107ba5a limit manual email sending in cloud 2023-11-25 13:36:37 +02:00
Pete Matsyburka 1aa867893e fix bcc address removal 2023-11-24 23:22:04 +02:00
Pete Matsyburka 4f6142f9c0 adjust form builder 2023-11-24 23:09:39 +02:00
Pete Matsyburka c303349666 fix default builder fields 2023-11-24 17:34:57 +02:00
Pete Matsyburka e85df170f2 fix connection to DB URL with underscores 2023-11-24 17:17:56 +02:00
Pete Matsyburka 01333e5b2e preserve submitter preferences 2023-11-23 20:03:52 +02:00
Pete Matsyburka 514987138a adjust readme 2023-11-23 16:00:08 +02:00
Pete Matsyburka 0814c6d065 adjust submissions logo 2023-11-22 22:15:10 +02:00
Pete Matsyburka 542a443fb8 add custom exceptions handling controller 2023-11-22 01:41:39 +02:00
Pete Matsyburka f2c6bb1c3d add submission order param 2023-11-22 01:04:47 +02:00
Alex Turchyn b85c9cfdb7 adjust form builder 2023-11-21 23:16:34 +02:00
Pete Matsyburka e07b50c22d fix undefined method 'field_value' 2023-11-21 13:08:40 +02:00
Pete Matsyburka 4dde9890e0 update deps 2023-11-20 17:28:31 +02:00
46 changed files with 415 additions and 110 deletions
+2 -2
View File
@@ -246,7 +246,7 @@ GEM
hashdiff (1.0.1)
hashery (2.1.2)
hashie (5.0.0)
hexapdf (0.33.0)
hexapdf (0.34.1)
cmdparse (~> 3.0, >= 3.0.3)
geom2d (~> 0.4, >= 0.4.1)
openssl (>= 2.2.1)
@@ -339,7 +339,7 @@ GEM
omniauth-rails_csrf_protection (1.0.1)
actionpack (>= 4.2)
omniauth (~> 2.0)
openssl (3.1.0)
openssl (3.2.0)
orm_adapter (0.5.0)
os (1.1.4)
pagy (6.0.4)
+3 -3
View File
@@ -34,10 +34,10 @@ DocuSeal is an open source platform that provides secure and efficient digital d
## Features
- [x] PDF form fields builder (WYSIWYG)
- [x] 10 field types available (Signature, Date, File, Checkbox etc.)
- [x] 11 field types available (Signature, Date, File, Checkbox etc.)
- [x] Multiple submitters per document
- [x] Automated emails via SMTP
- [x] Files storage on AWS S3, Google Storage, or Azure
- [x] Files storage on disk or AWS S3, Google Storage, Azure Cloud
- [x] Automatic PDF eSignature
- [x] PDF signature verification
- [x] Users management
@@ -76,7 +76,7 @@ Run the app under a custom domain over https using docker compose (make sure you
HOST=your-domain-name.com docker-compose up
```
## For Companies
## For Businesses
### Integrate seamless document signing into your web or mobile apps with DocuSeal!
At DocuSeal we have expertise and technologies to make documents creation, filling, signing and processing seamlessly integrated with your product. We specialize in working with various industries, including **Banking, Healthcare, Transport, Real Estate, eCommerce, KYC, CRM, and other software products** that require bulk document signing. By leveraging DocuSeal, we can assist in reducing the overall cost of developing and processing electronic documents while ensuring security and compliance with local electronic document laws.
+47 -31
View File
@@ -50,35 +50,14 @@ module Api
end
def create
is_send_email = !params[:send_email].in?(['false', false])
params[:send_email] = true unless params.key?(:send_email)
params[:send_sms] = false unless params.key?(:send_sms)
submissions =
if (emails = (params[:emails] || params[:email]).presence) && params[:submission].blank?
Submissions.create_from_emails(template: @template,
user: current_user,
source: :api,
mark_as_sent: is_send_email,
emails:)
else
submissions_attrs, attachments = normalize_submissions_params!(submissions_params[:submission], @template)
submissions = create_submissions(@template, params)
Submissions.create_from_submitters(
template: @template,
user: current_user,
source: :api,
mark_as_sent: is_send_email,
submitters_order: params[:submitters_order] || 'preserved',
submissions_attrs:
)
end
Submissions.send_signature_requests(submissions)
Submissions.send_signature_requests(submissions, send_email: is_send_email)
submitters = submissions.flat_map(&:submitters)
save_default_value_attachments!(attachments, submitters)
render json: submitters
render json: submissions.flat_map(&:submitters)
rescue Submitters::NormalizeValues::UnknownFieldName, Submitters::NormalizeValues::UnknownSubmitterName => e
render json: { error: e.message }, status: :unprocessable_entity
end
@@ -91,6 +70,35 @@ module Api
private
def create_submissions(template, params)
is_send_email = !params[:send_email].in?(['false', false])
if (emails = (params[:emails] || params[:email]).presence) && params[:submission].blank?
Submissions.create_from_emails(template:,
user: current_user,
source: :api,
mark_as_sent: is_send_email,
emails:,
params:)
else
submissions_attrs, attachments = normalize_submissions_params!(submissions_params, template)
submissions = Submissions.create_from_submitters(
template:,
user: current_user,
source: :api,
mark_as_sent: is_send_email,
submitters_order: params[:submitters_order] || params[:order] || 'preserved',
submissions_attrs:,
params:
)
save_default_value_attachments!(attachments, submissions.flat_map(&:submitters))
submissions
end
end
def serialize_params
{
only: %i[id source submitters_order created_at updated_at],
@@ -107,11 +115,19 @@ module Api
end
def submissions_params
params.permit(submission: [{
submitters: [[:uuid, :name, :email, :role, :completed, :phone, :application_key,
{ values: {}, readonly_fields: [],
fields: [%i[name default_value readonly validation_pattern invalid_message]] }]]
}])
key = params.key?(:submission) ? :submission : :submissions
params.permit(
key => [
[:send_email, :send_sms, {
message: %i[subject body],
submitters: [[:send_email, :send_sms, :uuid, :name, :email, :role,
:completed, :phone, :application_key,
{ values: {}, readonly_fields: [], message: %i[subject body],
fields: [%i[name default_value readonly validation_pattern invalid_message]] }]]
}]
]
).fetch(key, [])
end
def normalize_submissions_params!(submissions_params, template)
+35
View File
@@ -0,0 +1,35 @@
# frozen_string_literal: true
class ErrorsController < ActionController::Base
ENTERPRISE_FEATURE_MESSAGE =
'This feature is available in Enterprise Edition: https://www.docuseal.co/pricing'
ENTERPRISE_PATHS = [
'/templates/html',
'/api/templates/html',
'/templates/pdf',
'/api/templates/pdf'
].freeze
def show
if request.original_fullpath.in?(ENTERPRISE_PATHS)
return render json: { status: 404, message: ENTERPRISE_FEATURE_MESSAGE }, status: :not_found
end
respond_to do |f|
f.json do
render json: { status: error_status_code }, status: error_status_code
end
f.html { render error_status_code.to_s, status: error_status_code }
end
end
private
def error_status_code
@error_status_code ||=
ActionDispatch::ExceptionWrapper.new(request.env,
request.env['action_dispatch.exception']).status_code
end
end
@@ -12,7 +12,7 @@ class NotificationsSettingsController < ApplicationController
def index; end
def create
if @account_config.save
if @account_config.value.present? ? @account_config.save : @account_config.delete
redirect_back fallback_location: settings_notifications_path, notice: 'Changes have been saved'
else
redirect_back fallback_location: settings_notifications_path, alert: 'Unable to save'
+2 -1
View File
@@ -23,7 +23,8 @@ class StartFormController < ApplicationController
@submitter.assign_attributes(
uuid: @template.submitters.first['uuid'],
ip: request.remote_ip,
ua: request.user_agent
ua: request.user_agent,
preferences: { 'send_email' => true }
)
@submitter.submission ||= Submission.new(template: @template,
+10 -3
View File
@@ -34,23 +34,30 @@ class SubmissionsController < ApplicationController
def create
authorize!(:create, Submission)
if params[:is_custom_message] != '1'
params.delete(:subject)
params.delete(:body)
end
submissions =
if params[:emails].present?
Submissions.create_from_emails(template: @template,
user: current_user,
source: :invite,
mark_as_sent: params[:send_email] == '1',
emails: params[:emails])
emails: params[:emails],
params:)
else
Submissions.create_from_submitters(template: @template,
user: current_user,
source: :invite,
submitters_order: params[:preserve_order] == '1' ? 'preserved' : 'random',
mark_as_sent: params[:send_email] == '1',
submissions_attrs: submissions_params[:submission].to_h.values)
submissions_attrs: submissions_params[:submission].to_h.values,
params:)
end
Submissions.send_signature_requests(submissions, params)
Submissions.send_signature_requests(submissions)
redirect_to template_path(@template), notice: 'New recipients have been added'
end
@@ -4,6 +4,13 @@ class SubmittersSendEmailController < ApplicationController
load_and_authorize_resource :submitter, id_param: :submitter_slug, find_by: :slug
def create
if Docuseal.multitenant? && SubmissionEvent.exists?(submitter: @submitter,
event_type: 'send_email',
created_at: 24.hours.ago..Time.current)
return redirect_back(fallback_location: submission_path(@submitter.submission),
alert: 'Email has been sent already.')
end
SubmitterMailer.invitation_email(@submitter).deliver_later!
SubmissionEvent.create!(submitter: @submitter, event_type: 'send_email')
+2
View File
@@ -79,6 +79,8 @@ window.customElements.define('template-builder', class extends HTMLElement {
connectedCallback () {
this.appElem = document.createElement('div')
this.appElem.classList.add('md:h-screen')
this.app = createApp(TemplateBuilder, {
template: reactive(JSON.parse(this.dataset.template)),
backgroundColor: '#faf7f5',
+14 -3
View File
@@ -4,6 +4,7 @@
class="mx-auto pl-3 md:pl-4 h-full"
>
<div
v-if="$slots.buttons || withTitle"
class="flex justify-between py-1.5 items-center pr-4 sticky top-0 z-10"
:style="{ backgroundColor }"
>
@@ -15,6 +16,7 @@
<Logo />
</a>
<Contenteditable
v-if="withTitle"
:model-value="template.name"
:editable="editable"
class="text-3xl font-semibold focus:text-clip"
@@ -64,7 +66,10 @@
</template>
</div>
</div>
<div class="flex md:max-h-[calc(100vh-60px)]">
<div
class="flex"
:class="$slots.buttons || withTitle ? 'md:max-h-[calc(100%_-_60px)]' : 'md:max-h-[100%]'"
>
<div
ref="previews"
:style="{ 'display': isBreakpointLg ? 'none' : 'initial' }"
@@ -90,7 +95,7 @@
/>
<div
class="sticky bottom-0 py-2"
:class="{ 'bg-base-100': withStickySubmitters }"
:style="withStickySubmitters ? { backgroundColor } : {}"
>
<Upload
v-if="sortedDocuments.length && editable && withUploadButton"
@@ -200,7 +205,8 @@
>
<div
v-if="drawField"
class="sticky inset-0 bg-base-100 h-full"
class="sticky inset-0 h-full"
:style="{ backgroundColor }"
>
<div class="bg-base-300 rounded-lg p-5 text-center space-y-4">
<p>
@@ -336,6 +342,11 @@ export default {
required: false,
default: true
},
withTitle: {
type: Boolean,
required: false,
default: true
},
withPhone: {
type: Boolean,
required: false,
@@ -24,11 +24,10 @@
*
</span>
<IconPencil
v-if="editable"
class="cursor-pointer flex-none opacity-0 group-hover/contenteditable-container:opacity-100 group-hover/contenteditable:opacity-100 align-middle peer-focus:hidden"
:style="iconInline ? {} : { right: -(1.1 * iconWidth) + 'px' }"
title="Edit"
:class="{ 'ml-1': !withRequired, 'absolute': !iconInline, 'inline align-bottom': iconInline }"
:class="{ invisible: !editable, 'ml-1': !withRequired, 'absolute': !iconInline, 'inline align-bottom': iconInline }"
:width="iconWidth"
:stroke-width="iconStrokeWidth"
@click="[focusContenteditable(), selectOnEditClick && selectContent()]"
@@ -74,7 +74,7 @@
:stroke-width="1.6"
/>
<span class="py-1">
Add Submitter
Add {{ names[submitters.length] }}
</span>
</a>
</li>
@@ -100,7 +100,7 @@
<label
v-else
tabindex="0"
class="cursor-pointer group/contenteditable-container rounded-md p-2 border border-base-300 w-full flex justify-between"
class="group cursor-pointer group/contenteditable-container rounded-md p-2 border border-base-300 hover:border-content w-full flex justify-between"
>
<div class="flex items-center space-x-2">
<span
@@ -117,7 +117,7 @@
@update:model-value="$emit('name-change', selectedSubmitter)"
/>
</div>
<span class="flex items-center">
<span class="flex items-center transition-all duration-75 group-hover:border border-base-content/20 border-dashed w-6 h-6 flex justify-center items-center rounded">
<IconPlus
width="18"
height="18"
@@ -169,7 +169,7 @@
:stroke-width="1.6"
/>
<span class="py-1">
Add Submitter
Add {{ names[submitters.length] }}
</span>
</a>
</li>
@@ -239,16 +239,16 @@ export default {
},
names () {
return [
'First Submitter',
'Second Submitter',
'Third Submitter',
'Fourth Submitter',
'Fifth Submitter',
'Sixth Submitter',
'Seventh Submitter',
'Eighth Submitter',
'Ninth Submitter',
'Tenth Submitter'
'First Party',
'Second Party',
'Third Party',
'Fourth Party',
'Fifth Party',
'Sixth Party',
'Seventh Party',
'Eighth Party',
'Ninth Party',
'Tenth Party'
]
},
selectedSubmitter () {
+8 -4
View File
@@ -72,14 +72,14 @@
<button
v-if="withPhone || type != 'phone'"
draggable="true"
class="flex items-center justify-center border border-dashed border-base-300 w-full rounded relative"
class="group flex items-center justify-center border border-dashed border-base-300 hover:border-base-content/20 w-full rounded relative"
:style="{ backgroundColor: backgroundColor }"
@dragstart="onDragstart({ type: type })"
@dragend="$emit('drag-end')"
@click="addField(type)"
>
<div class="w-0 absolute left-0">
<IconDrag class="cursor-grab" />
<div class="flex items-console group-hover:bg-base-200/50 transition-all cursor-grab h-full absolute left-0">
<IconDrag class=" my-auto" />
</div>
<div class="flex items-center flex-col px-2 py-2">
<component :is="icon" />
@@ -195,7 +195,7 @@ export default {
},
submitterDefaultFields () {
return this.defaultFields.filter((f) => {
return !this.fields.find((field) => field.name === f.name) && (!f.role || f.role === this.selectedSubmitter.name)
return !this.submitterFields.find((field) => field.name === f.name) && (!f.role || f.role === this.selectedSubmitter.name)
})
}
},
@@ -257,6 +257,10 @@ export default {
this.fields.push(field)
if (['signature', 'initials', 'cells'].includes(type)) {
this.$emit('set-draw', { field })
}
this.save()
}
}
+3 -2
View File
@@ -31,7 +31,8 @@ class ProcessSubmitterCompletionJob < ApplicationJob
SubmitterMailer.completed_email(submitter, user, bcc:).deliver_later!
end
to = submitter.submission.submitters.sort_by(&:completed_at).select(&:email?).map(&:friendly_name).join(', ')
to = submitter.submission.submitters.reject { |e| e.preferences['send_email'] == false }
.sort_by(&:completed_at).select(&:email?).map(&:friendly_name).join(', ')
SubmitterMailer.documents_copy_email(submitter, to:).deliver_later! if to.present?
end
@@ -48,6 +49,6 @@ class ProcessSubmitterCompletionJob < ApplicationJob
next_submitter = submitter.submission.submitters.find { |s| s.uuid == next_submitter_item['uuid'] }
Submitters.send_signature_requests([next_submitter], send_email: true)
Submitters.send_signature_requests([next_submitter])
end
end
@@ -4,7 +4,7 @@ class SendSubmitterInvitationEmailJob < ApplicationJob
def perform(params = {})
submitter = Submitter.find(params['submitter_id'])
SubmitterMailer.invitation_email(submitter, subject: params['subject'], body: params['body']).deliver_now!
SubmitterMailer.invitation_email(submitter).deliver_now!
SubmissionEvent.create!(submitter:, event_type: 'send_email')
+13 -5
View File
@@ -3,18 +3,26 @@
class SubmitterMailer < ApplicationMailer
MAX_ATTACHMENTS_SIZE = 10.megabytes
def invitation_email(submitter, body: nil, subject: nil)
DEFAULT_INVITATION_SUBJECT = 'You are invited to submit a form'
def invitation_email(submitter)
@current_account = submitter.submission.template.account
@submitter = submitter
@body = body.presence
if submitter.preferences['email_message_uuid']
@email_message = submitter.account.email_messages.find_by(uuid: submitter.preferences['email_message_uuid'])
end
@body = @email_message&.body.presence
@subject = @email_message&.subject.presence
@email_config = AccountConfigs.find_for_account(@current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY)
subject =
if @email_config || subject.present?
ReplaceEmailVariables.call(subject.presence || @email_config.value['subject'], submitter:)
if @email_config || @subject
ReplaceEmailVariables.call(@subject || @email_config.value['subject'], submitter:)
else
'You are invited to submit a form'
DEFAULT_INVITATION_SUBJECT
end
mail(to: @submitter.friendly_name,
+1
View File
@@ -15,6 +15,7 @@ class Account < ApplicationRecord
has_many :users, dependent: :destroy
has_many :encrypted_configs, dependent: :destroy
has_many :account_configs, dependent: :destroy
has_many :email_messages, dependent: :destroy
has_many :templates, dependent: :destroy
has_many :template_folders, dependent: :destroy
has_one :default_template_folder, -> { where(name: TemplateFolder::DEFAULT_NAME) },
+39
View File
@@ -0,0 +1,39 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: email_messages
#
# id :bigint not null, primary key
# body :text not null
# sha1 :string not null
# subject :text not null
# uuid :string not null
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint not null
# author_id :bigint not null
#
# Indexes
#
# index_email_messages_on_account_id (account_id)
# index_email_messages_on_sha1 (sha1)
# index_email_messages_on_uuid (uuid)
#
# Foreign Keys
#
# fk_rails_... (account_id => accounts.id)
# fk_rails_... (author_id => users.id)
#
class EmailMessage < ApplicationRecord
belongs_to :author, class_name: 'User'
belongs_to :account
attribute :uuid, :string, default: -> { SecureRandom.uuid }
before_validation :set_sha1, on: :create
def set_sha1
self.sha1 = Digest::SHA1.hexdigest({ subject:, body: }.to_json)
end
end
+4
View File
@@ -6,6 +6,7 @@
#
# id :bigint not null, primary key
# deleted_at :datetime
# preferences :text not null
# slug :string not null
# source :text not null
# submitters_order :string not null
@@ -36,9 +37,12 @@ class Submission < ApplicationRecord
has_many :submitters, dependent: :destroy
has_many :submission_events, dependent: :destroy
attribute :preferences, :string, default: -> { {} }
serialize :template_fields, JSON
serialize :template_schema, JSON
serialize :template_submitters, JSON
serialize :preferences, JSON
attribute :source, :string, default: 'link'
attribute :submitters_order, :string, default: 'random'
+3
View File
@@ -12,6 +12,7 @@
# name :string
# opened_at :datetime
# phone :string
# preferences :text not null
# sent_at :datetime
# slug :string not null
# ua :string
@@ -37,9 +38,11 @@ class Submitter < ApplicationRecord
has_one :account, through: :template
attribute :values, :string, default: -> { {} }
attribute :preferences, :string, default: -> { {} }
attribute :slug, :string, default: -> { SecureRandom.base58(14) }
serialize :values, JSON
serialize :preferences, JSON
has_many_attached :documents
has_many_attached :attachments
+1 -1
View File
@@ -33,7 +33,7 @@
# fk_rails_... (folder_id => template_folders.id)
#
class Template < ApplicationRecord
DEFAULT_SUBMITTER_NAME = 'First Submitter'
DEFAULT_SUBMITTER_NAME = 'First Party'
belongs_to :author, class_name: 'User'
belongs_to :account
+1
View File
@@ -55,6 +55,7 @@ class User < ApplicationRecord
has_many :template_folders, dependent: :destroy, foreign_key: :author_id, inverse_of: :author
has_many :user_configs, dependent: :destroy
has_many :encrypted_configs, dependent: :destroy, class_name: 'EncryptedUserConfig'
has_many :email_messages, dependent: :destroy
devise :two_factor_authenticatable, :recoverable, :rememberable, :validatable, :trackable
devise :registerable, :omniauthable, omniauth_providers: [:google_oauth2] if Docuseal.multitenant?
+1 -1
View File
@@ -34,7 +34,7 @@
"submitters": [
{
"name": "John Doe",
"role": "<%= current_account.templates.last ? current_account.templates.last.submitters.first['name'] : 'First Submitter' %>",
"role": "<%= current_account.templates.last ? current_account.templates.last.submitters.first['name'] : 'First Party' %>",
"email": "<%= current_user.email.sub('@', '+test@') %>",
"values": {
"Form Text Field Name": "Default Value"
@@ -0,0 +1,15 @@
<script>
if (!window.customElements.get('autosize-field')) {
window.customElements.define('autosize-field', class extends HTMLElement {
connectedCallback() {
if (this.field.scrollHeight > this.field.clientHeight) {
this.field.classList.remove('text-[1.5vw]', 'lg:text-base');
this.field.classList.add('text-[1.0vw]', 'lg:text-[0.70rem]');
}
}
get field() {
return this.closest('field-value');
}
});
}
</script>
+1
View File
@@ -0,0 +1 @@
<%= render 'shared/logo', width: 40, height: 40 %>
@@ -2,8 +2,8 @@
<div class="form-control">
<%= f.label :preserve_order, for: uuid = SecureRandom.uuid, class: 'flex items-center cursor-pointer' do %>
<%= f.check_box :preserve_order, id: uuid, class: 'base-checkbox', checked: template.submissions.last&.submitters_order.in?(['preserved', nil]) %>
<span class="label">Preserve submitters order</span>
<span class="tooltip" data-tip="When checked, notifications will be sent to the second submitter once the form is completed by the previous submitter. Uncheck this option to send notifications to all submitters simultaneously right away.">
<span class="label">Preserve order</span>
<span class="tooltip" data-tip="When checked, notifications will be sent to the second party once the form is completed by the previous party. Uncheck this option to send notifications to all parties simultaneously right away.">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
<% end %>
+5 -2
View File
@@ -1,7 +1,8 @@
<div class="flex absolute <%= field['readonly'] ? 'text-[1.5vw] lg:text-xs' : 'text-[1.5vw] lg:text-base' %>" style="width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%">
<field-value class="flex absolute text-[1.5vw] lg:text-base" style="width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%">
<% if field['type'].in?(['signature', 'image', 'initials']) %>
<img class="object-contain mx-auto" src="<%= attachments_index[value].url %>" loading="lazy">
<% elsif field['type'] == 'file' %>
<autosize-field></autosize-field>
<div class="px-0.5 flex flex-col justify-center">
<% Array.wrap(value).each do |val| %>
<a target="_blank" href="<%= attachments_index[val].url %>">
@@ -31,10 +32,12 @@
<% end %>
</div>
<% elsif field['type'] == 'date' %>
<autosize-field></autosize-field>
<div class="flex items-center px-0.5">
<%= l(Date.parse(value), format: :long, locale: local_assigns[:locale]) %>
</div>
<% else %>
<autosize-field></autosize-field>
<div class="flex items-center px-0.5 whitespace-pre-wrap"><%= Array.wrap(value).join(', ') %></div>
<% end %>
</div>
</field-value>
+9 -6
View File
@@ -1,25 +1,27 @@
<div style="max-width: 1600px" class="mx-auto pl-4">
<div class="flex justify-between py-1.5 items-center pr-4 sticky top-0 md:relative z-10 bg-base-100">
<a href="<%= template_path(@submission.template) %>" class="flex space-x-3 py-1">
<span><%= render 'shared/logo', width: 40, height: 40 %></span>
<span class="text-3xl font-semibold focus:text-clip"><%= @submission.template.name %></span>
<a href="<%= signed_in? && @submission.template.account_id == current_account&.id ? template_path(@submission.template) : '/' %>" class="flex items-center space-x-3 py-1">
<span><%= render 'submissions/logo' %></span>
<span class="text-xl md:text-3xl font-semibold focus:text-clip" style="overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;"><%= @submission.template.name %></span>
</a>
<div class="space-x-3 flex items-center">
<% if @submission.audit_trail.present? %>
<a href="<%= rails_blob_path(@submission.audit_trail) %>" class="white-button" target="_blank">
<%= svg_icon('external_link', class: 'w-6 h-6') %>
Audit Log
<span class="hidden md:inline">
Audit Log
</span>
</a>
<% end %>
<% if last_submitter = @submission.submitters.to_a.select(&:completed_at?).max_by(&:completed_at) %>
<download-button data-src="<%= submitter_download_index_path(last_submitter.slug) %>" class="base-button">
<span class="flex items-center justify-center space-x-2" data-target="download-button.defaultButton">
<%= svg_icon('download', class: 'w-6 h-6') %>
<span>Download</span>
<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-6 h-6 animate-spin') %>
<span>Downloading</span>
<span class="hidden md:inline">Downloading</span>
</span>
</download-button>
<% elsif @submission.submitters.to_a.size == 1 %>
@@ -172,3 +174,4 @@
</div>
</div>
</div>
<%= render 'scripts/autosize_field' %>
+1
View File
@@ -41,3 +41,4 @@
</div>
</div>
</div>
<%= render 'scripts/autosize_field' %>
+2
View File
@@ -25,6 +25,8 @@ module DocuSeal
config.i18n.available_locales = %i[en en-US en-GB es-ES fr-FR pt-PT de-DE]
config.i18n.fallbacks = [:en]
config.exceptions_app = ->(env) { ErrorsController.action(:show).call(env) }
config.action_view.frozen_string_literal = true
config.middleware.insert_before ActionDispatch::Static, Rack::Deflater
+14
View File
@@ -24,5 +24,19 @@ if ENV['RAILS_ENV'] == 'production' && ENV['SECRET_KEY_BASE'].to_s.empty?
ENV['DATABASE_URL'] = ENV['DATABASE_URL'].to_s.empty? ? database_url : ENV.fetch('DATABASE_URL', nil)
end
if ENV['DATABASE_URL'].to_s.split('@').last.to_s.split('/').first.to_s.include?('_')
require 'addressable'
url = Addressable::URI.parse(ENV.fetch('DATABASE_URL', ''))
ENV['DATABASE_HOST'] = url.host
ENV['DATABASE_PORT'] = (url.port || 5432).to_s
ENV['DATABASE_USER'] = url.user
ENV['DATABASE_PASSWORD'] = url.password
ENV['DATABASE_NAME'] = url.path.to_s.delete_prefix('/')
ENV.delete('DATABASE_URL')
end
require 'bundler/setup' # Set up gems listed in the Gemfile.
require 'bootsnap/setup' # Speed up boot time by caching expensive operations.
+8 -1
View File
@@ -12,7 +12,14 @@ test:
database: docuseal_test
production:
<% if ENV['DATABASE_URL'].to_s.empty? %>
<% if !ENV['DATABASE_HOST'].to_s.empty? %>
<<: *default
host: <%= ENV['DATABASE_HOST'] %>
port: <%= ENV['DATABASE_PORT'] %>
username: <%= ENV['DATABASE_USER'] %>
password: <%= ENV['DATABASE_PASSWORD'] %>
database: <%= ENV['DATABASE_NAME'] %>
<% elsif ENV['DATABASE_URL'].to_s.empty? %>
adapter: sqlite3
database: <%= ENV['WORKDIR'] || '.' %>/db.sqlite3
<% elsif ENV['DATABASE_URL'].match?(/\Apostgres/) %>
+14
View File
@@ -0,0 +1,14 @@
# frozen_string_literal: true
# fix NoMethodError: undefined method `field_value' for #<HexaPDF::Type::AcroForm::Field
module HexaPDF
module Type
module AcroForm
class Field
def field_value
''
end
end
end
end
end
@@ -0,0 +1,15 @@
# frozen_string_literal: true
class AddPreferencesToSubmitters < ActiveRecord::Migration[7.0]
class MigrationSubmitter < ApplicationRecord
self.table_name = 'submitters'
end
def change
add_column :submitters, :preferences, :text
MigrationSubmitter.where(preferences: nil).update_all(preferences: '{}')
change_column_null :submitters, :preferences, false
end
end
@@ -0,0 +1,15 @@
# frozen_string_literal: true
class AddPreferencesToSubmissions < ActiveRecord::Migration[7.0]
class MigrationSubmission < ApplicationRecord
self.table_name = 'submissions'
end
def change
add_column :submissions, :preferences, :text
MigrationSubmission.where(preferences: nil).update_all(preferences: '{}')
change_column_null :submissions, :preferences, false
end
end
@@ -0,0 +1,16 @@
# frozen_string_literal: true
class CreateEmailMessages < ActiveRecord::Migration[7.0]
def change
create_table :email_messages do |t|
t.string :uuid, null: false, index: true
t.references :author, null: false, foreign_key: { to_table: :users }, index: false
t.references :account, null: false, foreign_key: true, index: true
t.text :subject, null: false
t.text :body, null: false
t.string :sha1, null: false, index: true
t.timestamps
end
end
end
+19 -1
View File
@@ -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_11_19_222105) do
ActiveRecord::Schema[7.0].define(version: 2023_11_22_212612) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -81,6 +81,20 @@ ActiveRecord::Schema[7.0].define(version: 2023_11_19_222105) do
t.index ["submitter_id"], name: "index_document_generation_events_on_submitter_id"
end
create_table "email_messages", force: :cascade do |t|
t.string "uuid", null: false
t.bigint "author_id", null: false
t.bigint "account_id", null: false
t.text "subject", null: false
t.text "body", null: false
t.string "sha1", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["account_id"], name: "index_email_messages_on_account_id"
t.index ["sha1"], name: "index_email_messages_on_sha1"
t.index ["uuid"], name: "index_email_messages_on_uuid"
end
create_table "encrypted_configs", force: :cascade do |t|
t.bigint "account_id", null: false
t.string "key", null: false
@@ -125,6 +139,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_11_19_222105) do
t.text "source", null: false
t.string "submitters_order", null: false
t.string "slug", null: false
t.text "preferences", null: false
t.index ["created_by_user_id"], name: "index_submissions_on_created_by_user_id"
t.index ["slug"], name: "index_submissions_on_slug", unique: true
t.index ["template_id"], name: "index_submissions_on_template_id"
@@ -146,6 +161,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_11_19_222105) do
t.string "name"
t.string "phone"
t.string "application_key"
t.text "preferences", null: false
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"
@@ -229,6 +245,8 @@ ActiveRecord::Schema[7.0].define(version: 2023_11_19_222105) do
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
add_foreign_key "document_generation_events", "submitters"
add_foreign_key "email_messages", "accounts"
add_foreign_key "email_messages", "users", column: "author_id"
add_foreign_key "encrypted_configs", "accounts"
add_foreign_key "encrypted_user_configs", "users"
add_foreign_key "submission_events", "submissions"
+17
View File
@@ -0,0 +1,17 @@
# frozen_string_literal: true
module EmailMessages
module_function
def find_or_create_for_account_user(account, user, subject, body)
subject = SubmitterMailer::DEFAULT_INVITATION_SUBJECT if subject.blank?
sha1 = Digest::SHA1.hexdigest({ subject:, body: }.to_json)
message = account.email_messages.find_by(sha1:)
message ||= account.email_messages.create!(author: user, subject:, body:)
message
end
end
+13 -7
View File
@@ -27,11 +27,17 @@ module Submissions
submission.save!
end
def create_from_emails(template:, user:, emails:, source:, mark_as_sent: false)
def create_from_emails(template:, user:, emails:, source:, mark_as_sent: false, params: {})
preferences = Submitters.normalize_preferences(template.account, user, params)
parse_emails(emails).uniq.map do |email|
submission = template.submissions.new(created_by_user: user, source:, template_submitters: template.submitters)
submission = template.submissions.new(created_by_user: user,
source:,
template_submitters: template.submitters)
submission.submitters.new(email: normalize_email(email),
uuid: template.submitters.first['uuid'],
preferences:,
sent_at: mark_as_sent ? Time.current : nil)
submission.tap(&:save!)
@@ -45,13 +51,13 @@ module Submissions
end
def create_from_submitters(template:, user:, submissions_attrs:, source:, mark_as_sent: false,
submitters_order: DEFAULT_SUBMITTERS_ORDER)
submitters_order: DEFAULT_SUBMITTERS_ORDER, params: {})
Submissions::CreateFromSubmitters.call(
template:, user:, submissions_attrs:, source:, mark_as_sent:, submitters_order:
template:, user:, submissions_attrs:, source:, mark_as_sent:, submitters_order:, params:
)
end
def send_signature_requests(submissions, params)
def send_signature_requests(submissions)
submissions.each do |submission|
submitters = submission.submitters.reject(&:completed_at?)
@@ -59,9 +65,9 @@ module Submissions
first_submitter =
submission.template_submitters.filter_map { |s| submitters.find { |e| e.uuid == s['uuid'] } }.first
Submitters.send_signature_requests([first_submitter], params)
Submitters.send_signature_requests([first_submitter])
else
Submitters.send_signature_requests(submitters, params)
Submitters.send_signature_requests(submitters)
end
end
end
+11 -3
View File
@@ -4,7 +4,9 @@ module Submissions
module CreateFromSubmitters
module_function
def call(template:, user:, submissions_attrs:, source:, submitters_order:, mark_as_sent: false)
def call(template:, user:, submissions_attrs:, source:, submitters_order:, mark_as_sent: false, params: {})
preferences = Submitters.normalize_preferences(template.account, user, params)
Array.wrap(submissions_attrs).map do |attrs|
submission = template.submissions.new(created_by_user: user, source:,
template_submitters: template.submitters, submitters_order:)
@@ -18,7 +20,11 @@ module Submissions
is_order_sent = submitters_order == 'random' || index.zero?
build_submitter(submission:, attrs: submitter_attrs, uuid:, is_order_sent:, mark_as_sent:)
submission_preferences = Submitters.normalize_preferences(template.account, user, attrs)
build_submitter(submission:, attrs: submitter_attrs, uuid:,
is_order_sent:, mark_as_sent:, user:,
preferences: preferences.merge(submission_preferences))
end
submission.tap(&:save!)
@@ -85,8 +91,9 @@ module Submissions
template.submitters[index]&.dig('uuid')
end
def build_submitter(submission:, attrs:, uuid:, is_order_sent:, mark_as_sent:)
def build_submitter(submission:, attrs:, uuid:, is_order_sent:, mark_as_sent:, user:, preferences:)
email = Submissions.normalize_email(attrs[:email])
submitter_preferences = Submitters.normalize_preferences(submission.account, user, attrs)
submission.submitters.new(
email:,
@@ -96,6 +103,7 @@ module Submissions
completed_at: attrs[:completed] ? Time.current : nil,
sent_at: mark_as_sent && email.present? && is_order_sent ? Time.current : nil,
values: attrs[:values] || {},
preferences: preferences.merge(submitter_preferences),
uuid:
)
end
+2 -2
View File
@@ -63,7 +63,7 @@ module Submissions
composer.new_page
composer.column(columns: 1) do |column|
add_logo(column)
add_logo(column, account)
column.text('Audit Log',
font_size: 16,
@@ -268,7 +268,7 @@ module Submissions
)
end
def add_logo(column)
def add_logo(column, _account = nil)
column.image(PdfIcons.logo_io, width: 40, height: 40, position: :float)
column.formatted_text([{ text: 'DocuSeal',
+22 -11
View File
@@ -1,6 +1,8 @@
# frozen_string_literal: true
module Submitters
TRUE_VALUES = ['1', 'true', true].freeze
module_function
def search(submitters, keyword)
@@ -43,21 +45,30 @@ module Submitters
)
end
def send_signature_requests(submitters, params)
return if params[:send_email] != true && params[:send_email] != '1'
def normalize_preferences(account, user, params)
preferences = {}
message_params = params['message'].presence || params.slice('subject', 'body').presence
if message_params.present?
email_message = EmailMessages.find_or_create_for_account_user(account, user,
message_params['subject'],
message_params['body'])
end
preferences['email_message_uuid'] = email_message.uuid if email_message
preferences['send_email'] = params['send_email'].in?(TRUE_VALUES) if params.key?('send_email')
preferences['send_sms'] = params['send_sms'].in?(TRUE_VALUES) if params.key?('send_sms')
preferences
end
def send_signature_requests(submitters)
submitters.each do |submitter|
next if submitter.email.blank?
next if submitter.preferences['send_email'] == false
enqueue_invitation_email(submitter, params)
SendSubmitterInvitationEmailJob.perform_later('submitter_id' => submitter.id)
end
end
def enqueue_invitation_email(submitter, params)
subject, body = params.values_at(:subject, :body) if params[:is_custom_message] == '1'
SendSubmitterInvitationEmailJob.perform_later('submitter_id' => submitter.id,
'body' => body,
'subject' => subject)
end
end
+1 -1
View File
@@ -24,7 +24,7 @@ FactoryBot.define do
template.schema = [{ attachment_uuid: attachment.uuid, name: 'sample-document' }]
template.submitters = [
{
'name' => 'First Submitter',
'name' => 'First Party',
'uuid' => '513848eb-1096-4abc-a743-68596b5aaa4c'
}
]