mirror of
https://github.com/docusealco/docuseal
synced 2026-11-04 22:42:32 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
077c55f7e8 | ||
|
|
e5c5620b20 | ||
|
|
187b354605 | ||
|
|
545b18086a | ||
|
|
130c91178f | ||
|
|
47c57158f4 | ||
|
|
3b745a650d | ||
|
|
6871a3f840 | ||
|
|
c57148eada |
No files matched your search
@@ -1,5 +1,6 @@
|
|||||||
/db/*.sqlite3
|
/db/*.sqlite3
|
||||||
|
|
||||||
|
**/node_modules
|
||||||
/node_modules
|
/node_modules
|
||||||
/coverage
|
/coverage
|
||||||
/doc
|
/doc
|
||||||
|
|||||||
+9
-1
@@ -37,7 +37,15 @@ COPY ./Gemfile ./Gemfile.lock ./
|
|||||||
|
|
||||||
RUN bundle update --bundler && bundle install && rm -rf ~/.bundle
|
RUN bundle update --bundler && bundle install && rm -rf ~/.bundle
|
||||||
|
|
||||||
COPY . ./
|
COPY ./bin ./bin
|
||||||
|
COPY ./app ./app
|
||||||
|
COPY ./config ./config
|
||||||
|
COPY ./db ./db
|
||||||
|
COPY ./log ./log
|
||||||
|
COPY ./lib ./lib
|
||||||
|
COPY ./public ./public
|
||||||
|
COPY ./tmp ./tmp
|
||||||
|
COPY LICENSE README.md Rakefile config.ru ./
|
||||||
|
|
||||||
COPY --from=webpack /app/public/packs ./public/packs
|
COPY --from=webpack /app/public/packs ./public/packs
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ gem 'pg', require: false
|
|||||||
gem 'premailer-rails'
|
gem 'premailer-rails'
|
||||||
gem 'puma'
|
gem 'puma'
|
||||||
gem 'rails'
|
gem 'rails'
|
||||||
|
gem 'rails_autolink'
|
||||||
gem 'rails-i18n'
|
gem 'rails-i18n'
|
||||||
gem 'rollbar', require: ENV.key?('ROLLBAR_ACCESS_TOKEN')
|
gem 'rollbar', require: ENV.key?('ROLLBAR_ACCESS_TOKEN')
|
||||||
gem 'ruby-vips'
|
gem 'ruby-vips'
|
||||||
|
|||||||
@@ -379,6 +379,10 @@ GEM
|
|||||||
rails-i18n (7.0.7)
|
rails-i18n (7.0.7)
|
||||||
i18n (>= 0.7, < 2)
|
i18n (>= 0.7, < 2)
|
||||||
railties (>= 6.0.0, < 8)
|
railties (>= 6.0.0, < 8)
|
||||||
|
rails_autolink (1.1.8)
|
||||||
|
actionview (> 3.1)
|
||||||
|
activesupport (> 3.1)
|
||||||
|
railties (> 3.1)
|
||||||
railties (7.0.5)
|
railties (7.0.5)
|
||||||
actionpack (= 7.0.5)
|
actionpack (= 7.0.5)
|
||||||
activesupport (= 7.0.5)
|
activesupport (= 7.0.5)
|
||||||
@@ -554,6 +558,7 @@ DEPENDENCIES
|
|||||||
puma
|
puma
|
||||||
rails
|
rails
|
||||||
rails-i18n
|
rails-i18n
|
||||||
|
rails_autolink
|
||||||
rollbar
|
rollbar
|
||||||
rspec-rails
|
rspec-rails
|
||||||
rubocop
|
rubocop
|
||||||
|
|||||||
@@ -7,20 +7,7 @@ module Api
|
|||||||
def create
|
def create
|
||||||
submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
submitter = Submitter.find_by!(slug: params[:submitter_slug])
|
||||||
|
|
||||||
blob =
|
attachment = Submitters.create_attachment!(submitter, params)
|
||||||
if (file = params[:file])
|
|
||||||
ActiveStorage::Blob.create_and_upload!(io: file.open,
|
|
||||||
filename: file.original_filename,
|
|
||||||
content_type: file.content_type)
|
|
||||||
else
|
|
||||||
ActiveStorage::Blob.find_signed(params[:blob_signed_id])
|
|
||||||
end
|
|
||||||
|
|
||||||
attachment = ActiveStorage::Attachment.create!(
|
|
||||||
blob:,
|
|
||||||
name: params[:name],
|
|
||||||
record: submitter
|
|
||||||
)
|
|
||||||
|
|
||||||
render json: attachment.as_json(only: %i[uuid], methods: %i[url filename content_type])
|
render json: attachment.as_json(only: %i[uuid], methods: %i[url filename content_type])
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
module Api
|
module Api
|
||||||
class SubmissionsController < ApiBaseController
|
class SubmissionsController < ApiBaseController
|
||||||
|
UnknownFieldName = Class.new(StandardError)
|
||||||
|
UnknownSubmitterName = Class.new(StandardError)
|
||||||
|
|
||||||
def create
|
def create
|
||||||
template = current_account.templates.find(params[:template_id])
|
template = current_account.templates.find(params[:template_id])
|
||||||
|
|
||||||
@@ -13,28 +16,65 @@ module Api
|
|||||||
send_email: params[:send_email] != 'false',
|
send_email: params[:send_email] != 'false',
|
||||||
emails: params[:emails] || params[:email])
|
emails: params[:emails] || params[:email])
|
||||||
else
|
else
|
||||||
|
submissions_attrs = normalize_submissions_params!(submissions_params[:submission], template)
|
||||||
|
|
||||||
Submissions.create_from_submitters(template:,
|
Submissions.create_from_submitters(template:,
|
||||||
user: current_user,
|
user: current_user,
|
||||||
source: :api,
|
source: :api,
|
||||||
send_email: params[:send_email] != 'false',
|
send_email: params[:send_email] != 'false',
|
||||||
submissions_attrs: submissions_params[:submission])
|
submissions_attrs:)
|
||||||
end
|
end
|
||||||
|
|
||||||
submitters = submissions.flat_map(&:submitters)
|
submitters = submissions.flat_map(&:submitters)
|
||||||
|
|
||||||
if params[:send_email] != 'false'
|
send_invitation_emails(submitters) if params[:send_email] != 'false'
|
||||||
submitters.each do |submitter|
|
|
||||||
SubmitterMailer.invitation_email(submitter, message: params[:message]).deliver_later!
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
render json: submitters
|
render json: submitters
|
||||||
|
rescue UnknownFieldName, UnknownSubmitterName => e
|
||||||
|
render json: { error: e.message }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def send_invitation_emails(submitters)
|
||||||
|
submitters.each do |submitter|
|
||||||
|
SubmitterMailer.invitation_email(submitter, message: params[:message]).deliver_later!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def submissions_params
|
def submissions_params
|
||||||
params.permit(submission: [{ submitters: [%i[uuid name email]] }])
|
params.permit(submission: [{ submitters: [[:uuid, :name, :email, { values: {} }]] }])
|
||||||
|
end
|
||||||
|
|
||||||
|
def normalize_submissions_params!(submissions_params, template)
|
||||||
|
submissions_params.each do |submission|
|
||||||
|
submission[:submitters].each_with_index do |submitter, index|
|
||||||
|
next if submitter[:values].blank?
|
||||||
|
|
||||||
|
submitter[:values] =
|
||||||
|
normalize_submitter_values(template,
|
||||||
|
submitter[:values], submitter[:name] || template.submitters[index]['name'])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
submissions_params
|
||||||
|
end
|
||||||
|
|
||||||
|
def normalize_submitter_values(template, values, submitter_name)
|
||||||
|
submitter =
|
||||||
|
template.submitters.find { |e| e['name'] == submitter_name } ||
|
||||||
|
raise(UnknownSubmitterName, "Unknown submitter: #{submitter_name}")
|
||||||
|
|
||||||
|
fields = template.fields.select { |e| e['submitter_uuid'] == submitter['uuid'] }
|
||||||
|
|
||||||
|
fields_uuid_index = fields.index_by { |e| e['uuid'] }
|
||||||
|
fields_name_index = fields.index_by { |e| e['name'] }
|
||||||
|
|
||||||
|
values.transform_keys do |key|
|
||||||
|
next key if fields_uuid_index[key].present?
|
||||||
|
|
||||||
|
fields_name_index[key]&.dig('uuid') || raise(UnknownFieldName, "Unknown field: #{key}")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -2,13 +2,18 @@
|
|||||||
|
|
||||||
module Api
|
module Api
|
||||||
class TemplatesController < ApiBaseController
|
class TemplatesController < ApiBaseController
|
||||||
|
before_action :load_template, only: %i[show update]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
render json: current_account.templates
|
render json: current_account.templates
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def show
|
||||||
@template = current_account.templates.find(params[:id])
|
render json: @template.as_json(include: { author: { only: %i[id email first_name last_name] },
|
||||||
|
documents: { only: %i[id uuid], methods: %i[url filename] } })
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
@template.update!(template_params)
|
@template.update!(template_params)
|
||||||
|
|
||||||
render :ok
|
render :ok
|
||||||
@@ -23,5 +28,9 @@ module Api
|
|||||||
fields: [[:uuid, :submitter_uuid, :name, :type, :required,
|
fields: [[:uuid, :submitter_uuid, :name, :type, :required,
|
||||||
{ options: [], areas: [%i[x y w h cell_w attachment_uuid page]] }]])
|
{ options: [], areas: [%i[x y w h cell_w attachment_uuid page]] }]])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def load_template
|
||||||
|
@template = current_account.templates.find(params[:id])
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -13,7 +13,7 @@ class EmailSettingsController < ApplicationController
|
|||||||
else
|
else
|
||||||
render :index, status: :unprocessable_entity
|
render :index, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
rescue Net::SMTPError, OpenSSL::SSL::SSLError, Net::ReadTimeout => e
|
rescue StandardError => e
|
||||||
flash[:alert] = e.message
|
flash[:alert] = e.message
|
||||||
|
|
||||||
render :index, status: :unprocessable_entity
|
render :index, status: :unprocessable_entity
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class PersonalizationSettingsController < ApplicationController
|
||||||
|
def show; end
|
||||||
|
|
||||||
|
def create
|
||||||
|
account_config =
|
||||||
|
current_account.account_configs.find_or_initialize_by(key: encrypted_config_params[:key])
|
||||||
|
|
||||||
|
account_config.update!(encrypted_config_params)
|
||||||
|
|
||||||
|
redirect_back(fallback_location: settings_personalization_path, notice: 'Settings have been saved.')
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def encrypted_config_params
|
||||||
|
params.require(:account_config).permit!
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -36,7 +36,9 @@ class StartFormController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def completed
|
def completed
|
||||||
@submitter = Submitter.where(submission: @template.submissions).find_by!(email: params[:email])
|
@submitter = Submitter.where(submission: @template.submissions)
|
||||||
|
.where.not(completed_at: nil)
|
||||||
|
.find_by!(email: params[:email])
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|||||||
@@ -18,23 +18,7 @@ class SubmitFormController < ApplicationController
|
|||||||
def update
|
def update
|
||||||
submitter = Submitter.find_by!(slug: params[:slug])
|
submitter = Submitter.find_by!(slug: params[:slug])
|
||||||
|
|
||||||
update_submitter!(submitter)
|
Submitters::SubmitValues.call(submitter, params, request)
|
||||||
|
|
||||||
Submissions.update_template_fields!(submitter.submission) if submitter.submission.template_fields.blank?
|
|
||||||
|
|
||||||
submitter.submission.save!
|
|
||||||
|
|
||||||
if submitter.completed_at?
|
|
||||||
GenerateSubmitterResultAttachmentsJob.perform_later(submitter)
|
|
||||||
|
|
||||||
if submitter.account.encrypted_configs.exists?(key: EncryptedConfig::WEBHOOK_URL_KEY)
|
|
||||||
SendWebhookRequestJob.perform_later(submitter)
|
|
||||||
end
|
|
||||||
|
|
||||||
submitter.submission.template.account.users.active.each do |user|
|
|
||||||
SubmitterMailer.completed_email(submitter, user).deliver_later!
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
@@ -42,26 +26,4 @@ class SubmitFormController < ApplicationController
|
|||||||
def completed
|
def completed
|
||||||
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
@submitter = Submitter.find_by!(slug: params[:submit_form_slug])
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def update_submitter!(submitter)
|
|
||||||
submitter.values.merge!(normalized_values)
|
|
||||||
submitter.completed_at = Time.current if params[:completed] == 'true'
|
|
||||||
submitter.opened_at ||= Time.current
|
|
||||||
|
|
||||||
submitter.save!
|
|
||||||
|
|
||||||
submitter
|
|
||||||
end
|
|
||||||
|
|
||||||
def normalized_values
|
|
||||||
params.fetch(:values, {}).to_unsafe_h.transform_values do |v|
|
|
||||||
if params[:cast_boolean] == 'true'
|
|
||||||
v == 'true'
|
|
||||||
else
|
|
||||||
v.is_a?(Array) ? v.compact_blank : v
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
@@ -15,6 +15,7 @@ window.customElements.define('submission-form', class extends HTMLElement {
|
|||||||
canSendEmail: this.dataset.canSendEmail === 'true',
|
canSendEmail: this.dataset.canSendEmail === 'true',
|
||||||
isDirectUpload: this.dataset.isDirectUpload === 'true',
|
isDirectUpload: this.dataset.isDirectUpload === 'true',
|
||||||
isDemo: this.dataset.isDemo === 'true',
|
isDemo: this.dataset.isDemo === 'true',
|
||||||
|
withConfetti: true,
|
||||||
values: reactive(JSON.parse(this.dataset.values)),
|
values: reactive(JSON.parse(this.dataset.values)),
|
||||||
attachments: reactive(JSON.parse(this.dataset.attachments)),
|
attachments: reactive(JSON.parse(this.dataset.attachments)),
|
||||||
fields: JSON.parse(this.dataset.fields)
|
fields: JSON.parse(this.dataset.fields)
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
:key="areaIndex"
|
:key="areaIndex"
|
||||||
>
|
>
|
||||||
<Teleport
|
<Teleport
|
||||||
:to="`#page-${area.attachment_uuid}-${area.page}`"
|
:to="findPageElementForArea(area)"
|
||||||
>
|
>
|
||||||
<FieldArea
|
<FieldArea
|
||||||
:ref="setAreaRef"
|
:ref="setAreaRef"
|
||||||
@@ -78,10 +78,16 @@ export default {
|
|||||||
this.areaRefs = []
|
this.areaRefs = []
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
findPageElementForArea (area) {
|
||||||
|
return (this.$root.$el?.parentNode?.getRootNode() || document).getElementById(`page-${area.attachment_uuid}-${area.page}`)
|
||||||
|
},
|
||||||
scrollIntoField (field) {
|
scrollIntoField (field) {
|
||||||
this.areaRefs.find((area) => {
|
this.areaRefs.find((area) => {
|
||||||
if (area.field === field) {
|
if (area.field === field) {
|
||||||
if (document.body.style.overflow === 'hidden') {
|
const root = this.$root.$el.parentNode.getRootNode()
|
||||||
|
const container = root.body || root.querySelector('div')
|
||||||
|
|
||||||
|
if (container.style.overflow === 'hidden') {
|
||||||
this.scrollInContainer(area.$el)
|
this.scrollInContainer(area.$el)
|
||||||
} else {
|
} else {
|
||||||
area.$refs.scrollToElem.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
area.$refs.scrollToElem.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||||
@@ -94,13 +100,19 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
scrollInContainer (target) {
|
scrollInContainer (target) {
|
||||||
|
const root = this.$root.$el.parentNode.getRootNode()
|
||||||
|
|
||||||
|
const scrollbox = root.getElementById('scrollbox')
|
||||||
|
const formContainer = root.getElementById('form_container')
|
||||||
|
const container = root.body || root.querySelector('div')
|
||||||
|
|
||||||
const padding = 64
|
const padding = 64
|
||||||
const boxRect = window.scrollbox.children[0].getBoundingClientRect()
|
const boxRect = scrollbox.children[0].getBoundingClientRect()
|
||||||
const targetRect = target.getBoundingClientRect()
|
const targetRect = target.getBoundingClientRect()
|
||||||
|
|
||||||
const targetTopRelativeToBox = targetRect.top - boxRect.top
|
const targetTopRelativeToBox = targetRect.top - boxRect.top
|
||||||
|
|
||||||
window.scrollbox.scrollTop = targetTopRelativeToBox - document.body.offsetHeight + window.form_container.offsetHeight + target.offsetHeight + padding
|
scrollbox.scrollTop = targetTopRelativeToBox - container.offsetHeight + formContainer.offsetHeight + target.offsetHeight + padding
|
||||||
},
|
},
|
||||||
setAreaRef (el) {
|
setAreaRef (el) {
|
||||||
if (el) {
|
if (el) {
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ export default {
|
|||||||
IconLogin,
|
IconLogin,
|
||||||
IconDownload
|
IconDownload
|
||||||
},
|
},
|
||||||
|
inject: ['baseUrl'],
|
||||||
props: {
|
props: {
|
||||||
submitterSlug: {
|
submitterSlug: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -96,6 +97,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
withConfetti: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
canSendEmail: {
|
canSendEmail: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -109,19 +115,21 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async mounted () {
|
async mounted () {
|
||||||
const { default: confetti } = await import('canvas-confetti')
|
if (this.withConfetti) {
|
||||||
|
const { default: confetti } = await import('canvas-confetti')
|
||||||
|
|
||||||
confetti({
|
confetti({
|
||||||
particleCount: 50,
|
particleCount: 50,
|
||||||
startVelocity: 30,
|
startVelocity: 30,
|
||||||
spread: 140
|
spread: 140
|
||||||
})
|
})
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
sendCopyToEmail () {
|
sendCopyToEmail () {
|
||||||
this.isSendingCopy = true
|
this.isSendingCopy = true
|
||||||
|
|
||||||
fetch(`/send_submission_email.json?submitter_slug=${this.submitterSlug}`, {
|
fetch(this.baseUrl + `/send_submission_email.json?submitter_slug=${this.submitterSlug}`, {
|
||||||
method: 'POST'
|
method: 'POST'
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
alert('Email has been sent')
|
alert('Email has been sent')
|
||||||
@@ -132,7 +140,7 @@ export default {
|
|||||||
download () {
|
download () {
|
||||||
this.isDownloading = true
|
this.isDownloading = true
|
||||||
|
|
||||||
fetch(`/submitters/${this.submitterSlug}/download`).then((response) => response.json()).then((urls) => {
|
fetch(this.baseUrl + `/submitters/${this.submitterSlug}/download`).then((response) => response.json()).then((urls) => {
|
||||||
const fileRequests = urls.map((url) => {
|
const fileRequests = urls.map((url) => {
|
||||||
return () => {
|
return () => {
|
||||||
return fetch(url).then(async (resp) => {
|
return fetch(url).then(async (resp) => {
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ export default {
|
|||||||
IconCloudUpload,
|
IconCloudUpload,
|
||||||
IconInnerShadowTop
|
IconInnerShadowTop
|
||||||
},
|
},
|
||||||
|
inject: ['baseUrl'],
|
||||||
props: {
|
props: {
|
||||||
message: {
|
message: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -141,7 +142,7 @@ export default {
|
|||||||
|
|
||||||
return await Promise.all(
|
return await Promise.all(
|
||||||
blobs.map((blob) => {
|
blobs.map((blob) => {
|
||||||
return fetch('/api/attachments', {
|
return fetch(this.baseUrl + '/api/attachments', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
name: 'attachments',
|
name: 'attachments',
|
||||||
@@ -166,7 +167,7 @@ export default {
|
|||||||
formData.append('submitter_slug', this.submitterSlug)
|
formData.append('submitter_slug', this.submitterSlug)
|
||||||
formData.append('name', 'attachments')
|
formData.append('name', 'attachments')
|
||||||
|
|
||||||
return fetch('/api/attachments', {
|
return fetch(this.baseUrl + '/api/attachments', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData
|
body: formData
|
||||||
}).then(resp => resp.json()).then((data) => {
|
}).then(resp => resp.json()).then((data) => {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
v-if="!isFormVisible"
|
v-if="!isFormVisible"
|
||||||
class="btn btn-neutral text-white absolute rounded-none border-x-0 md:border md:rounded-full bottom-0 w-full md:mb-4 text-base"
|
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"
|
@click.prevent="isFormVisible = true"
|
||||||
>
|
>
|
||||||
Submit Form
|
Submit Form
|
||||||
@@ -194,7 +194,9 @@
|
|||||||
class="space-y-3.5 mx-auto"
|
class="space-y-3.5 mx-auto"
|
||||||
>
|
>
|
||||||
<template v-if="isAnonymousChecboxes">
|
<template v-if="isAnonymousChecboxes">
|
||||||
Complete hightlighted checkboxes and click <span class="font-semibold">{{ stepFields.length === currentStep + 1 ? 'submit' : 'next' }}</span>.
|
<span class="text-xl">
|
||||||
|
Complete hightlighted checkboxes and click <span class="font-semibold">{{ stepFields.length === currentStep + 1 ? 'submit' : 'next' }}</span>.
|
||||||
|
</span>
|
||||||
<input
|
<input
|
||||||
v-for="field in currentStepFields"
|
v-for="field in currentStepFields"
|
||||||
:key="field.uuid"
|
:key="field.uuid"
|
||||||
@@ -290,6 +292,7 @@
|
|||||||
<FormCompleted
|
<FormCompleted
|
||||||
v-else
|
v-else
|
||||||
:is-demo="isDemo"
|
:is-demo="isDemo"
|
||||||
|
:with-confetti="withConfetti"
|
||||||
:can-send-email="canSendEmail"
|
:can-send-email="canSendEmail"
|
||||||
:submitter-slug="submitterSlug"
|
:submitter-slug="submitterSlug"
|
||||||
/>
|
/>
|
||||||
@@ -331,6 +334,11 @@ export default {
|
|||||||
IconArrowsDiagonalMinimize2,
|
IconArrowsDiagonalMinimize2,
|
||||||
FormCompleted
|
FormCompleted
|
||||||
},
|
},
|
||||||
|
provide () {
|
||||||
|
return {
|
||||||
|
baseUrl: this.baseUrl
|
||||||
|
}
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
submitterSlug: {
|
submitterSlug: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -350,6 +358,16 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
|
withConfetti: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
baseUrl: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
fields: {
|
fields: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -357,16 +375,17 @@ export default {
|
|||||||
},
|
},
|
||||||
authenticityToken: {
|
authenticityToken: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: false,
|
||||||
|
default: ''
|
||||||
},
|
},
|
||||||
isDirectUpload: {
|
isDirectUpload: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: true,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
isDemo: {
|
isDemo: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: true,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
values: {
|
values: {
|
||||||
@@ -429,10 +448,17 @@ export default {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (/iPhone|iPad|iPod/i.test(navigator.userAgent)) {
|
if (/iPhone|iPad|iPod/i.test(navigator.userAgent)) {
|
||||||
document.body.style.overflow = 'hidden'
|
this.$nextTick(() => {
|
||||||
|
const root = this.$root.$el.parentNode.getRootNode()
|
||||||
|
const scrollbox = root.getElementById('scrollbox')
|
||||||
|
const parent = root.body || root.querySelector('div')
|
||||||
|
|
||||||
window.scrollbox.classList.add('h-full', 'overflow-y-auto')
|
parent.style.overflow = 'hidden'
|
||||||
window.scrollbox.parentNode.classList.add('h-screen', 'overflow-y-auto')
|
|
||||||
|
scrollbox.classList.add('h-full', 'overflow-y-auto')
|
||||||
|
scrollbox.parentNode.classList.add('h-screen', 'overflow-y-auto')
|
||||||
|
scrollbox.parentNode.style.maxHeight = '-webkit-fill-available'
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -455,7 +481,7 @@ export default {
|
|||||||
if (this.isCompleted) {
|
if (this.isCompleted) {
|
||||||
return Promise.resolve({})
|
return Promise.resolve({})
|
||||||
} else {
|
} else {
|
||||||
return fetch(this.submitPath, {
|
return fetch(this.baseUrl + this.submitPath, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData || new FormData(this.$refs.form)
|
body: formData || new FormData(this.$refs.form)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ export default {
|
|||||||
IconTextSize,
|
IconTextSize,
|
||||||
IconArrowsDiagonalMinimize2
|
IconArrowsDiagonalMinimize2
|
||||||
},
|
},
|
||||||
|
inject: ['baseUrl'],
|
||||||
props: {
|
props: {
|
||||||
field: {
|
field: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -304,7 +305,7 @@ export default {
|
|||||||
file,
|
file,
|
||||||
'/direct_uploads'
|
'/direct_uploads'
|
||||||
).create((_error, data) => {
|
).create((_error, data) => {
|
||||||
fetch('/api/attachments', {
|
fetch(this.baseUrl + '/api/attachments', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
submitter_slug: this.submitterSlug,
|
submitter_slug: this.submitterSlug,
|
||||||
@@ -326,12 +327,12 @@ export default {
|
|||||||
formData.append('submitter_slug', this.submitterSlug)
|
formData.append('submitter_slug', this.submitterSlug)
|
||||||
formData.append('name', 'attachments')
|
formData.append('name', 'attachments')
|
||||||
|
|
||||||
return fetch('/api/attachments', {
|
return fetch(this.baseUrl + '/api/attachments', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData
|
body: formData
|
||||||
}).then((resp) => resp.json()).then((attachment) => {
|
}).then((resp) => resp.json()).then((attachment) => {
|
||||||
this.$emit('update:model-value', attachment.uuid)
|
|
||||||
this.$emit('attached', attachment)
|
this.$emit('attached', attachment)
|
||||||
|
this.$emit('update:model-value', attachment.uuid)
|
||||||
|
|
||||||
return resolve(attachment)
|
return resolve(attachment)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ class ApplicationMailer < ActionMailer::Base
|
|||||||
|
|
||||||
register_interceptor ActionMailerConfigsInterceptor
|
register_interceptor ActionMailerConfigsInterceptor
|
||||||
|
|
||||||
|
before_action do
|
||||||
|
ActiveStorage::Current.url_options = Docuseal.default_url_options
|
||||||
|
end
|
||||||
|
|
||||||
def default_url_options
|
def default_url_options
|
||||||
Docuseal.default_url_options
|
Docuseal.default_url_options
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -4,15 +4,26 @@ class SubmitterMailer < ApplicationMailer
|
|||||||
DEFAULT_MESSAGE = %(You have been invited to submit the "%<name>s" form:)
|
DEFAULT_MESSAGE = %(You have been invited to submit the "%<name>s" form:)
|
||||||
|
|
||||||
def invitation_email(submitter, message: '')
|
def invitation_email(submitter, message: '')
|
||||||
|
@current_account = submitter.submission.template.account
|
||||||
@submitter = submitter
|
@submitter = submitter
|
||||||
@message = message.presence || format(DEFAULT_MESSAGE, name: submitter.submission.template.name)
|
@message = message.presence || format(DEFAULT_MESSAGE, name: submitter.submission.template.name)
|
||||||
|
|
||||||
|
@email_config = @current_account.account_configs.find_by(key: AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY)
|
||||||
|
|
||||||
|
subject =
|
||||||
|
if @email_config
|
||||||
|
ReplaceEmailVariables.call(@email_config.value['subject'], submitter:)
|
||||||
|
else
|
||||||
|
'You have been invited to submit a form'
|
||||||
|
end
|
||||||
|
|
||||||
mail(to: @submitter.email,
|
mail(to: @submitter.email,
|
||||||
subject: 'You have been invited to submit a form',
|
subject:,
|
||||||
reply_to: submitter.submission.created_by_user&.friendly_name)
|
reply_to: submitter.submission.created_by_user&.friendly_name)
|
||||||
end
|
end
|
||||||
|
|
||||||
def completed_email(submitter, user)
|
def completed_email(submitter, user)
|
||||||
|
@current_account = submitter.submission.template.account
|
||||||
@submitter = submitter
|
@submitter = submitter
|
||||||
@user = user
|
@user = user
|
||||||
|
|
||||||
@@ -21,6 +32,7 @@ class SubmitterMailer < ApplicationMailer
|
|||||||
end
|
end
|
||||||
|
|
||||||
def documents_copy_email(submitter)
|
def documents_copy_email(submitter)
|
||||||
|
@current_account = submitter.submission.template.account
|
||||||
@submitter = submitter
|
@submitter = submitter
|
||||||
|
|
||||||
Submissions::EnsureResultGenerated.call(@submitter)
|
Submissions::EnsureResultGenerated.call(@submitter)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
class UserMailer < ApplicationMailer
|
class UserMailer < ApplicationMailer
|
||||||
def invitation_email(user)
|
def invitation_email(user)
|
||||||
|
@current_account = user.account
|
||||||
@user = user
|
@user = user
|
||||||
@token = @user.send(:set_reset_password_token)
|
@token = @user.send(:set_reset_password_token)
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
class Account < ApplicationRecord
|
class Account < ApplicationRecord
|
||||||
has_many :users, dependent: :destroy
|
has_many :users, dependent: :destroy
|
||||||
has_many :encrypted_configs, dependent: :destroy
|
has_many :encrypted_configs, dependent: :destroy
|
||||||
|
has_many :account_configs, dependent: :destroy
|
||||||
has_many :templates, dependent: :destroy
|
has_many :templates, dependent: :destroy
|
||||||
has_many :submissions, through: :templates
|
has_many :submissions, through: :templates
|
||||||
has_many :submitters, through: :submissions
|
has_many :submitters, through: :submissions
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: account_configs
|
||||||
|
#
|
||||||
|
# id :bigint not null, primary key
|
||||||
|
# key :string not null
|
||||||
|
# value :text not null
|
||||||
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
|
# account_id :bigint not null
|
||||||
|
#
|
||||||
|
# Indexes
|
||||||
|
#
|
||||||
|
# index_account_configs_on_account_id (account_id)
|
||||||
|
# index_account_configs_on_account_id_and_key (account_id,key) UNIQUE
|
||||||
|
#
|
||||||
|
# Foreign Keys
|
||||||
|
#
|
||||||
|
# fk_rails_... (account_id => accounts.id)
|
||||||
|
#
|
||||||
|
class AccountConfig < ApplicationRecord
|
||||||
|
SUBMITTER_INVITATION_EMAIL_KEY = 'submitter_invitation_email'
|
||||||
|
|
||||||
|
DEFAULT_VALUES = {
|
||||||
|
SUBMITTER_INVITATION_EMAIL_KEY => {
|
||||||
|
'subject' => 'You have been invited to submit a form',
|
||||||
|
'body' => "Hi there,\n\n" \
|
||||||
|
"You have been invited to submit the \"{{template.name}}\" form:\n\n" \
|
||||||
|
"{{submitter.link}}\n\n" \
|
||||||
|
"Please contact us by replying to this email if you didn't request this.\n\n" \
|
||||||
|
"Thanks,\n" \
|
||||||
|
'{{account.name}}'
|
||||||
|
}
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
belongs_to :account
|
||||||
|
|
||||||
|
serialize :value, JSON
|
||||||
|
end
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
<input type="checkbox">
|
<input type="checkbox">
|
||||||
<div class="collapse-title text-xl font-medium">
|
<div class="collapse-title text-xl font-medium">
|
||||||
<div>
|
<div>
|
||||||
Request signature, multiple submitters
|
Request signature, multiple submitters with default values
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-1">
|
<div class="mt-1">
|
||||||
<div class="badge badge-warning badge-lg">POST</div>
|
<div class="badge badge-warning badge-lg">POST</div>
|
||||||
@@ -58,7 +58,13 @@
|
|||||||
"submission": [
|
"submission": [
|
||||||
{
|
{
|
||||||
"submitters": [
|
"submitters": [
|
||||||
{ "name": "<%= current_account.templates.last ? current_account.templates.last.submitters.first['name'] : 'First Submitter' %>", "email": "<%= current_user.email.sub('@', '+test@') %>" },
|
{
|
||||||
|
"name": "<%= 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"
|
||||||
|
}
|
||||||
|
},
|
||||||
{ "name": "Second Submitter", "email": "<%= current_user.email.sub('@', '+test2@') %>" }
|
{ "name": "Second Submitter", "email": "<%= current_user.email.sub('@', '+test2@') %>" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -71,6 +77,28 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="collapse collapse-plus bg-base-200 px-1">
|
||||||
|
<input type="checkbox">
|
||||||
|
<div class="collapse-title text-xl font-medium">
|
||||||
|
<div>
|
||||||
|
Template details
|
||||||
|
</div>
|
||||||
|
<div class="mt-1">
|
||||||
|
<div class="badge badge-info badge-lg">GET</div>
|
||||||
|
<div class="badge badge-primary badge-lg"><%= api_template_path(':id') %></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="collapse-content" style="display: inherit">
|
||||||
|
<div class="mockup-code overflow-hidden">
|
||||||
|
<% text = capture do %>curl '<%= api_template_url(current_account.templates&.last || 1) %>' \
|
||||||
|
--header 'X-Auth-Token: <%= current_user.access_token.token %>'<% end.to_str %>
|
||||||
|
<span class="top-0 right-0 absolute">
|
||||||
|
<%= render 'shared/clipboard_copy', icon: 'copy', text:, class: 'btn btn-ghost text-white', icon_class: 'w-6 h-6 text-white', copy_title: 'Copy', copied_title: 'Copied' %>
|
||||||
|
</span>
|
||||||
|
<pre data-prefix="$"><code class="overflow-hidden w-full"><%= text %></code></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -8,11 +8,6 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<%= yield %>
|
<%= yield %>
|
||||||
<p>
|
<%= render partial: 'shared/mailer_attribution' %>
|
||||||
---
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Sent using <a href="<%= Docuseal::PRODUCT_URL %>"><%= Docuseal::PRODUCT_NAME %></a> free document signing.
|
|
||||||
</p>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<%= render 'logo_placeholder' %>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<div class="alert my-4">
|
||||||
|
<%= svg_icon('info_circle', class: 'w-6 h-6') %>
|
||||||
|
<div>
|
||||||
|
<p class="font-bold">Unlock with DocuSeal Enterprise</p>
|
||||||
|
<p>
|
||||||
|
Display your company name and logo when signing documents.
|
||||||
|
<br>
|
||||||
|
<a class="link font-medium" target="_blank" href="<%= "#{Docuseal::PRODUCT_URL}/pricing" %>">Learn More</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<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">
|
||||||
|
<p class="text-4xl font-bold mb-4">Signature Request Email</p>
|
||||||
|
<%= 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 %>
|
||||||
|
<p class="text-4xl font-bold mb-4 mt-8">Company Logo</p>
|
||||||
|
<%= render 'logo_form' %>
|
||||||
|
</div>
|
||||||
|
<div class="w-0 md:w-52"></div>
|
||||||
|
</div>
|
||||||
@@ -2,12 +2,7 @@
|
|||||||
<div class="space-y-6 mx-auto">
|
<div class="space-y-6 mx-auto">
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<div class="flex items-center justify-center">
|
<div class="flex items-center justify-center">
|
||||||
<a href="/" class="flex items-center">
|
<%= render 'start_form/docuseal_logo' %>
|
||||||
<div class="mr-3">
|
|
||||||
<%= render 'shared/logo', width: '50px', height: '50px' %>
|
|
||||||
</div>
|
|
||||||
<h1 class="text-5xl font-bold text-center">DocuSeal</h1>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="text-center text-4xl font-bold">
|
<div class="text-center text-4xl font-bold">
|
||||||
Email has been sent
|
Email has been sent
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<p>
|
||||||
|
---
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Sent using <a href="<%= Docuseal::PRODUCT_URL %>"><%= Docuseal::PRODUCT_NAME %></a> free document signing.
|
||||||
|
</p>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<%= render 'shared/email_attribution' %>
|
||||||
@@ -31,9 +31,12 @@
|
|||||||
<%= link_to 'Webhooks', settings_webhooks_path, class: 'text-base hover:bg-base-300' %>
|
<%= link_to 'Webhooks', settings_webhooks_path, class: 'text-base hover:bg-base-300' %>
|
||||||
</li>
|
</li>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
<li>
|
||||||
|
<%= link_to 'Personalization', settings_personalization_path, class: 'text-base hover:bg-base-300' %>
|
||||||
|
</li>
|
||||||
<% unless Docuseal.demo? %>
|
<% unless Docuseal.demo? %>
|
||||||
<li>
|
<li>
|
||||||
<%= link_to Docuseal.multitenant? ? console_redirect_index_path : Docuseal::CONSOLE_URL, class: 'text-base hover:bg-base-300' do %>
|
<%= link_to Docuseal.multitenant? ? console_redirect_index_path : Docuseal::CONSOLE_URL, class: 'text-base hover:bg-base-300', data: { prefetch: false } do %>
|
||||||
Console
|
Console
|
||||||
<span class="badge badge-warning">New</span>
|
<span class="badge badge-warning">New</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
<%= render 'docuseal_logo' %>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<a href="/" class="flex justify-center items-center">
|
||||||
|
<span class="mr-3">
|
||||||
|
<%= render 'shared/logo', width: '50px', height: '50px' %>
|
||||||
|
</span>
|
||||||
|
<h1 class="text-5xl font-bold text-center">DocuSeal</h1>
|
||||||
|
</a>
|
||||||
@@ -2,12 +2,7 @@
|
|||||||
<div class="space-y-6 mx-auto">
|
<div class="space-y-6 mx-auto">
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<div class="flex items-center justify-center">
|
<div class="flex items-center justify-center">
|
||||||
<a href="/" class="flex items-center">
|
<%= render 'banner' %>
|
||||||
<div class="mr-3">
|
|
||||||
<%= render 'shared/logo', width: '50px', height: '50px' %>
|
|
||||||
</div>
|
|
||||||
<h1 class="text-5xl font-bold text-center">DocuSeal</h1>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center bg-base-200 rounded-xl p-4 mb-4">
|
<div class="flex items-center bg-base-200 rounded-xl p-4 mb-4">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
|
|||||||
@@ -2,12 +2,7 @@
|
|||||||
<div class="space-y-6 mx-auto">
|
<div class="space-y-6 mx-auto">
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<div class="text-center w-full space-y-6">
|
<div class="text-center w-full space-y-6">
|
||||||
<a href="/" class="flex justify-center items-center">
|
<%= render 'banner' %>
|
||||||
<span class="mr-3">
|
|
||||||
<%= render 'shared/logo', width: '50px', height: '50px' %>
|
|
||||||
</span>
|
|
||||||
<h1 class="text-5xl font-bold text-center">DocuSeal</h1>
|
|
||||||
</a>
|
|
||||||
<p class="text-xl font-semibold text-center">You have been invited to submit a form</p>
|
<p class="text-xl font-semibold text-center">You have been invited to submit a form</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center bg-base-200 rounded-xl p-4 mb-4">
|
<div class="flex items-center bg-base-200 rounded-xl p-4 mb-4">
|
||||||
|
|||||||
@@ -40,7 +40,7 @@
|
|||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<% is_smtp_configured = Accounts.can_send_emails?(current_account) %>
|
<% is_smtp_configured = Accounts.can_send_emails?(current_account) %>
|
||||||
<%= f.label :send_email, class: 'flex items-center cursor-pointer' do %>
|
<%= f.label :send_email, class: 'flex items-center cursor-pointer' do %>
|
||||||
<%= f.check_box :send_email, class: 'base-checkbox', disabled: !is_smtp_configured, onchange: "message_field.classList.toggle('hidden', !event.currentTarget.checked)" %>
|
<%= 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>
|
<span class="label">Send Email</span>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% unless is_smtp_configured %>
|
<% unless is_smtp_configured %>
|
||||||
@@ -57,21 +57,23 @@
|
|||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<div id="message_field" class="card card-compact bg-base-200 hidden">
|
<% unless AccountConfig.exists?(key: AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY) %>
|
||||||
<div class="card-body">
|
<div id="message_field" class="card card-compact bg-base-200 hidden">
|
||||||
<div class="form-control space-y-2">
|
<div class="card-body">
|
||||||
<span class="label-text">Hi there,</span>
|
<div class="form-control space-y-2">
|
||||||
<autoresize-textarea>
|
<span class="label-text">Hi there,</span>
|
||||||
<%= f.text_area :message, value: format(SubmitterMailer::DEFAULT_MESSAGE, name: @template.name), required: true, class: 'base-textarea !rounded-lg w-full' %>
|
<autoresize-textarea>
|
||||||
</autoresize-textarea>
|
<%= f.text_area :message, value: format(SubmitterMailer::DEFAULT_MESSAGE, name: @template.name), required: true, class: 'base-textarea !rounded-lg w-full' %>
|
||||||
<span class="label-text">
|
</autoresize-textarea>
|
||||||
Thanks,
|
<span class="label-text">
|
||||||
<br>
|
Thanks,
|
||||||
<%= current_account.name %>
|
<br>
|
||||||
</span>
|
<%= current_account.name %>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<% end %>
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<%= f.button button_title(title: 'Add Recipients'), class: 'base-button' %>
|
<%= f.button button_title(title: 'Add Recipients'), class: 'base-button' %>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
<%= render 'docuseal_logo' %>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<a href="<%= root_path %>" class="mx-auto text-2xl md:text-3xl font-bold items-center flex space-x-3">
|
||||||
|
<%= render 'shared/logo', class: 'w-9 h-9 md:w-12 md:h-12' %>
|
||||||
|
<span><%= Docuseal::PRODUCT_NAME %></span>
|
||||||
|
</a>
|
||||||
@@ -2,12 +2,7 @@
|
|||||||
<div class="space-y-6 mx-auto">
|
<div class="space-y-6 mx-auto">
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<div class="flex items-center justify-center">
|
<div class="flex items-center justify-center">
|
||||||
<a href="/" class="flex items-center">
|
<%= render 'start_form/banner' %>
|
||||||
<div class="mr-3">
|
|
||||||
<%= render 'shared/logo', width: '50px', height: '50px' %>
|
|
||||||
</div>
|
|
||||||
<h1 class="text-5xl font-bold text-center">DocuSeal</h1>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center bg-base-200 rounded-xl p-4 mb-4">
|
<div class="flex items-center bg-base-200 rounded-xl p-4 mb-4">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
|
|||||||
@@ -5,10 +5,7 @@
|
|||||||
<div id="scrollbox">
|
<div id="scrollbox">
|
||||||
<div class="mx-auto block pb-72" style="max-width: 1000px">
|
<div class="mx-auto block pb-72" style="max-width: 1000px">
|
||||||
<div class="mt-4 flex">
|
<div class="mt-4 flex">
|
||||||
<a href="<%= root_path %>" class="mx-auto text-2xl md:text-3xl font-bold items-center flex space-x-3">
|
<%= render 'banner' %>
|
||||||
<%= render 'shared/logo', class: 'w-9 h-9 md:w-12 md:h-12' %>
|
|
||||||
<span>DocuSeal</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
<% (@submitter.submission.template_schema || @submitter.submission.template.schema).each do |item| %>
|
<% (@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.documents.find { |a| a.uuid == item['attachment_uuid'] } %>
|
||||||
|
|||||||
@@ -9,5 +9,5 @@
|
|||||||
</ul>
|
</ul>
|
||||||
<% end %>
|
<% end %>
|
||||||
<p>
|
<p>
|
||||||
Thanks,<br><%= @submitter.submission.template.account.name %>
|
Thanks,<br><%= @current_account.name %>
|
||||||
</p>
|
</p>
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
<p>Hi there,</p>
|
<% if @email_config %>
|
||||||
<%= simple_format(@message) %>
|
<%= auto_link(simple_format(h(ReplaceEmailVariables.call(@email_config.value['body'], submitter: @submitter)))) %>
|
||||||
<p><%= link_to 'Submit Form', submit_form_url(slug: @submitter.slug) %></p>
|
<% else %>
|
||||||
<p>Please contact us by replying to this email if you didn't request this.</p>
|
<p>Hi there,</p>
|
||||||
<p>
|
<%= simple_format(@message) %>
|
||||||
Thanks,<br><%= @submitter.submission.template.account.name %>
|
<p><%= link_to 'Submit Form', submit_form_url(slug: @submitter.slug) %></p>
|
||||||
</p>
|
<p>Please contact us by replying to this email if you didn't request this.</p>
|
||||||
|
<p>
|
||||||
|
Thanks,<br><%= @current_account.name %>
|
||||||
|
</p>
|
||||||
|
<% end %>
|
||||||
@@ -3,5 +3,5 @@
|
|||||||
<p><%= link_to 'Sign up', invitation_url(reset_password_token: @token) %></p>
|
<p><%= link_to 'Sign up', invitation_url(reset_password_token: @token) %></p>
|
||||||
<p>Please contact us by replying to this email if you didn't request this.</p>
|
<p>Please contact us by replying to this email if you didn't request this.</p>
|
||||||
<p>
|
<p>
|
||||||
Thanks,<br><%= @user.account.name %>
|
Thanks,<br><%= @current_account.name %>
|
||||||
</p>
|
</p>
|
||||||
@@ -29,5 +29,7 @@ module DocuSeal
|
|||||||
|
|
||||||
config.middleware.insert_before ActionDispatch::Static, Rack::Deflater
|
config.middleware.insert_before ActionDispatch::Static, Rack::Deflater
|
||||||
config.middleware.insert_before ActionDispatch::Static, ApiPathConsiderJsonMiddleware
|
config.middleware.insert_before ActionDispatch::Static, ApiPathConsiderJsonMiddleware
|
||||||
|
|
||||||
|
ActiveSupport.run_load_hooks(:application_config, self)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -13,6 +13,20 @@ Rails.configuration.to_prepare do
|
|||||||
response.set_header('Cache-Control', 'public, max-age=31536000') if action_name == 'show'
|
response.set_header('Cache-Control', 'public, max-age=31536000') if action_name == 'show'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
ActiveStorage::Blobs::ProxyController.before_action do
|
||||||
|
response.set_header('Access-Control-Allow-Origin', '*')
|
||||||
|
response.set_header('Access-Control-Allow-Methods', 'GET')
|
||||||
|
response.set_header('Access-Control-Allow-Headers', '*')
|
||||||
|
response.set_header('Access-Control-Max-Age', '1728000')
|
||||||
|
end
|
||||||
|
|
||||||
|
ActiveStorage::Blobs::RedirectController.before_action do
|
||||||
|
response.set_header('Access-Control-Allow-Origin', '*')
|
||||||
|
response.set_header('Access-Control-Allow-Methods', 'GET')
|
||||||
|
response.set_header('Access-Control-Allow-Headers', '*')
|
||||||
|
response.set_header('Access-Control-Max-Age', '1728000')
|
||||||
|
end
|
||||||
|
|
||||||
ActiveStorage::DirectUploadsController.before_action do
|
ActiveStorage::DirectUploadsController.before_action do
|
||||||
next if current_user
|
next if current_user
|
||||||
next if Submitter.find_signed(cookies[:submitter_sid])
|
next if Submitter.find_signed(cookies[:submitter_sid])
|
||||||
|
|||||||
+4
-1
@@ -30,7 +30,7 @@ Rails.application.routes.draw do
|
|||||||
namespace :api, defaults: { format: :json } do
|
namespace :api, defaults: { format: :json } do
|
||||||
resources :attachments, only: %i[create]
|
resources :attachments, only: %i[create]
|
||||||
resources :submissions, only: %i[create]
|
resources :submissions, only: %i[create]
|
||||||
resources :templates, only: %i[update index] do
|
resources :templates, only: %i[update show index] do
|
||||||
resources :submissions, only: %i[create]
|
resources :submissions, only: %i[create]
|
||||||
resources :documents, only: %i[create destroy], controller: 'templates_documents'
|
resources :documents, only: %i[create destroy], controller: 'templates_documents'
|
||||||
end
|
end
|
||||||
@@ -70,6 +70,7 @@ Rails.application.routes.draw do
|
|||||||
end
|
end
|
||||||
resources :esign, only: %i[index create], controller: 'esign_settings'
|
resources :esign, only: %i[index create], controller: 'esign_settings'
|
||||||
resources :users, only: %i[index]
|
resources :users, only: %i[index]
|
||||||
|
resource :personalization, only: %i[show create], controller: 'personalization_settings'
|
||||||
if !Docuseal.multitenant? || Docuseal.demo?
|
if !Docuseal.multitenant? || Docuseal.demo?
|
||||||
resources :api, only: %i[index], controller: 'api_settings'
|
resources :api, only: %i[index], controller: 'api_settings'
|
||||||
resource :webhooks, only: %i[show create update], controller: 'webhook_settings'
|
resource :webhooks, only: %i[show create update], controller: 'webhook_settings'
|
||||||
@@ -83,4 +84,6 @@ Rails.application.routes.draw do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
ActiveSupport.run_load_hooks(:routes, self)
|
||||||
end
|
end
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class CreateAccountConfigs < ActiveRecord::Migration[7.0]
|
||||||
|
def change
|
||||||
|
create_table :account_configs do |t|
|
||||||
|
t.references :account, null: false, foreign_key: true, index: true
|
||||||
|
t.string :key, null: false
|
||||||
|
t.text :value, null: false
|
||||||
|
|
||||||
|
t.index %i[account_id key], unique: true
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
+12
-1
@@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[7.0].define(version: 2023_08_06_140534) do
|
ActiveRecord::Schema[7.0].define(version: 2023_08_15_190540) do
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
|
|
||||||
@@ -24,6 +24,16 @@ ActiveRecord::Schema[7.0].define(version: 2023_08_06_140534) do
|
|||||||
t.index ["user_id"], name: "index_access_tokens_on_user_id"
|
t.index ["user_id"], name: "index_access_tokens_on_user_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
create_table "account_configs", force: :cascade do |t|
|
||||||
|
t.bigint "account_id", null: false
|
||||||
|
t.string "key", null: false
|
||||||
|
t.text "value", null: false
|
||||||
|
t.datetime "created_at", null: false
|
||||||
|
t.datetime "updated_at", null: false
|
||||||
|
t.index ["account_id", "key"], name: "index_account_configs_on_account_id_and_key", unique: true
|
||||||
|
t.index ["account_id"], name: "index_account_configs_on_account_id"
|
||||||
|
end
|
||||||
|
|
||||||
create_table "accounts", force: :cascade do |t|
|
create_table "accounts", force: :cascade do |t|
|
||||||
t.string "name", null: false
|
t.string "name", null: false
|
||||||
t.string "timezone", null: false
|
t.string "timezone", null: false
|
||||||
@@ -159,6 +169,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_08_06_140534) do
|
|||||||
end
|
end
|
||||||
|
|
||||||
add_foreign_key "access_tokens", "users"
|
add_foreign_key "access_tokens", "users"
|
||||||
|
add_foreign_key "account_configs", "accounts"
|
||||||
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
|
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 "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
|
||||||
add_foreign_key "document_generation_events", "submitters"
|
add_foreign_key "document_generation_events", "submitters"
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module AccountConfigs
|
||||||
|
module_function
|
||||||
|
|
||||||
|
def find_or_initialize_for_key(account, key)
|
||||||
|
account.account_configs.find_by(key:) ||
|
||||||
|
account.account_configs.new(key:, value: AccountConfig::DEFAULT_VALUES[key])
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -62,6 +62,7 @@ module Accounts
|
|||||||
|
|
||||||
def can_send_emails?(account)
|
def can_send_emails?(account)
|
||||||
return true if Docuseal.multitenant?
|
return true if Docuseal.multitenant?
|
||||||
|
return true if ENV['SMTP_ADDRESS'].present?
|
||||||
|
|
||||||
EncryptedConfig.exists?(account_id: account.id, key: EncryptedConfig::EMAIL_SMTP_KEY)
|
EncryptedConfig.exists?(account_id: account.id, key: EncryptedConfig::EMAIL_SMTP_KEY)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module ReplaceEmailVariables
|
||||||
|
TEMAPLTE_NAME = '{{template.name}}'
|
||||||
|
SUBMITTER_LINK = '{{submitter.link}}'
|
||||||
|
ACCOUNT_NAME = '{{account.name}}'
|
||||||
|
|
||||||
|
module_function
|
||||||
|
|
||||||
|
def call(text, submitter:)
|
||||||
|
link =
|
||||||
|
Rails.application.routes.url_helpers.submit_form_url(
|
||||||
|
slug: submitter.slug, **Docuseal.default_url_options
|
||||||
|
)
|
||||||
|
|
||||||
|
text = text.gsub(TEMAPLTE_NAME, submitter.template.name)
|
||||||
|
text = text.gsub(SUBMITTER_LINK, link)
|
||||||
|
|
||||||
|
text.gsub(ACCOUNT_NAME, submitter.template.account.name)
|
||||||
|
end
|
||||||
|
end
|
||||||
+2
-1
@@ -12,7 +12,7 @@ module Submissions
|
|||||||
end
|
end
|
||||||
|
|
||||||
def create_from_emails(template:, user:, emails:, source:, send_email: false)
|
def create_from_emails(template:, user:, emails:, source:, send_email: false)
|
||||||
emails = emails.to_s.scan(User::EMAIL_REGEXP)
|
emails = emails.to_s.scan(User::EMAIL_REGEXP) unless emails.is_a?(Array)
|
||||||
|
|
||||||
emails.map do |email|
|
emails.map do |email|
|
||||||
submission = template.submissions.new(created_by_user: user, source:)
|
submission = template.submissions.new(created_by_user: user, source:)
|
||||||
@@ -38,6 +38,7 @@ module Submissions
|
|||||||
|
|
||||||
submission.submitters.new(email: submitter_attrs[:email],
|
submission.submitters.new(email: submitter_attrs[:email],
|
||||||
sent_at: send_email ? Time.current : nil,
|
sent_at: send_email ? Time.current : nil,
|
||||||
|
values: submitter_attrs[:values] || {},
|
||||||
uuid:)
|
uuid:)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,9 @@ module Submissions
|
|||||||
|
|
||||||
page = pdf.pages[area['page']]
|
page = pdf.pages[area['page']]
|
||||||
|
|
||||||
|
page[:Annots] ||= []
|
||||||
|
page[:Annots] = page[:Annots].reject { |e| e[:A] && e[:A][:URI].to_s.starts_with?('file:///docuseal_field') }
|
||||||
|
|
||||||
width = page.box.width
|
width = page.box.width
|
||||||
height = page.box.height
|
height = page.box.height
|
||||||
font_size = ((page.box.width / A4_SIZE[0].to_f) * FONT_SIZE).to_i
|
font_size = ((page.box.width / A4_SIZE[0].to_f) * FONT_SIZE).to_i
|
||||||
@@ -66,8 +69,6 @@ module Submissions
|
|||||||
height: image.height * scale
|
height: image.height * scale
|
||||||
)
|
)
|
||||||
when 'file'
|
when 'file'
|
||||||
page[:Annots] ||= []
|
|
||||||
|
|
||||||
items = Array.wrap(value).each_with_object([]) do |uuid, acc|
|
items = Array.wrap(value).each_with_object([]) do |uuid, acc|
|
||||||
attachment = submitter.attachments.find { |a| a.uuid == uuid }
|
attachment = submitter.attachments.find { |a| a.uuid == uuid }
|
||||||
|
|
||||||
|
|||||||
@@ -11,4 +11,21 @@ module Submitters
|
|||||||
is_more_than_two_images && original_documents.find { |a| a.uuid == attachment.uuid }&.image?
|
is_more_than_two_images && original_documents.find { |a| a.uuid == attachment.uuid }&.image?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def create_attachment!(submitter, params)
|
||||||
|
blob =
|
||||||
|
if (file = params[:file])
|
||||||
|
ActiveStorage::Blob.create_and_upload!(io: file.open,
|
||||||
|
filename: file.original_filename,
|
||||||
|
content_type: file.content_type)
|
||||||
|
else
|
||||||
|
ActiveStorage::Blob.find_signed(params[:blob_signed_id])
|
||||||
|
end
|
||||||
|
|
||||||
|
ActiveStorage::Attachment.create!(
|
||||||
|
blob:,
|
||||||
|
name: params[:name],
|
||||||
|
record: submitter
|
||||||
|
)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Submitters
|
||||||
|
module SubmitValues
|
||||||
|
module_function
|
||||||
|
|
||||||
|
def call(submitter, params, request)
|
||||||
|
update_submitter!(submitter, params, request)
|
||||||
|
|
||||||
|
Submissions.update_template_fields!(submitter.submission) if submitter.submission.template_fields.blank?
|
||||||
|
|
||||||
|
submitter.submission.save!
|
||||||
|
|
||||||
|
return unless submitter.completed_at?
|
||||||
|
|
||||||
|
GenerateSubmitterResultAttachmentsJob.perform_later(submitter)
|
||||||
|
|
||||||
|
if submitter.account.encrypted_configs.exists?(key: EncryptedConfig::WEBHOOK_URL_KEY)
|
||||||
|
SendWebhookRequestJob.perform_later(submitter)
|
||||||
|
end
|
||||||
|
|
||||||
|
submitter.submission.template.account.users.active.each do |user|
|
||||||
|
SubmitterMailer.completed_email(submitter, user).deliver_later!
|
||||||
|
end
|
||||||
|
|
||||||
|
submitter
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_submitter!(submitter, params, request)
|
||||||
|
submitter.values.merge!(normalized_values(params))
|
||||||
|
submitter.opened_at ||= Time.current
|
||||||
|
|
||||||
|
if params[:completed] == 'true'
|
||||||
|
submitter.completed_at = Time.current
|
||||||
|
submitter.ip = request.remote_ip
|
||||||
|
submitter.ua = request.user_agent
|
||||||
|
end
|
||||||
|
|
||||||
|
submitter.save!
|
||||||
|
|
||||||
|
submitter
|
||||||
|
end
|
||||||
|
|
||||||
|
def normalized_values(params)
|
||||||
|
params.fetch(:values, {}).to_unsafe_h.transform_values do |v|
|
||||||
|
if params[:cast_boolean] == 'true'
|
||||||
|
v == 'true'
|
||||||
|
else
|
||||||
|
v.is_a?(Array) ? v.compact_blank : v
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Reference in new issue
Block a user