Compare commits

...
20 Commits
Author SHA1 Message Date
DocuSeal df561a6211 update rubocop 2023-10-30 00:56:44 +02:00
DocuSeal 50d3404f3b force ssl only if host passed 2023-10-29 21:36:30 +02:00
DocuSeal 607fc8d338 allow to permanently remove templates 2023-10-29 21:36:30 +02:00
DocuSeal 8c366dfc38 use account timezone in audit trail 2023-10-29 21:36:30 +02:00
DocuSeal 5ed08fa9ab add sso settings placeholder 2023-10-29 21:36:30 +02:00
DocuSeal ddb557ebef upload template from URL 2023-10-29 21:36:30 +02:00
DocuSeal b48615df3f use handwritten font for typed signature 2023-10-29 21:36:29 +02:00
DocuSeal 86c7b92675 apply previous signature and initials on the next field 2023-10-28 01:56:22 +03:00
DocuSeal 041f362cc4 do not allow to edit phone if already specified 2023-10-28 00:53:16 +03:00
DocuSeal 65f491325e adjust configs loading 2023-10-28 00:42:33 +03:00
DocuSeal 57aef5f5e5 refactor date step 2023-10-26 21:26:08 +03:00
Alex Turchyn e704e6fa04 show edit icon on hover the outer box 2023-10-25 20:09:34 +03:00
DocuSeal 4c99bbb58a add onComplete form callback 2023-10-25 02:06:47 +03:00
DocuSeal c0647eef2c process 15 pages after pdf upload 2023-10-24 23:48:26 +03:00
DocuSeal c619b9be93 move plans link 2023-10-24 23:16:02 +03:00
DocuSeal abab3f9688 do not use libvips pdf on large files 2023-10-24 22:31:09 +03:00
DocuSeal 7b15111e8f use temp file for puma pid 2023-10-24 13:36:50 +03:00
DocuSeal 2c7f38c503 optimize mailer attachments 2023-10-24 12:41:12 +03:00
Alex Turchyn ed80df1372 fix audit trail images generation 2023-10-24 00:46:47 +03:00
DocuSeal 3614d1796c do not split signature pad js in form 2023-10-22 23:26:43 +03:00
37 changed files with 474 additions and 184 deletions

No files matched your search

+3
View File
@@ -75,3 +75,6 @@ Rails/SkipsModelValidations:
Rails/ApplicationController: Rails/ApplicationController:
Enabled: false Enabled: false
Capybara/ClickLinkOrButtonStyle:
Enabled: false
+2 -1
View File
@@ -31,7 +31,7 @@ ENV BUNDLE_WITHOUT="development:test"
WORKDIR /app WORKDIR /app
RUN apk add --no-cache build-base sqlite-dev libpq-dev mariadb-dev vips-dev vips-poppler vips-heif libc6-compat ttf-freefont ttf-liberation && cp /usr/share/fonts/liberation/LiberationSans-Regular.ttf /usr/share/fonts/liberation/LiberationSans-Bold.ttf / && apk del ttf-liberation RUN apk add --no-cache build-base sqlite-dev libpq-dev mariadb-dev vips-dev vips-poppler poppler-utils vips-heif libc6-compat ttf-freefont ttf-liberation && mkdir /fonts && cp /usr/share/fonts/liberation/LiberationSans-Regular.ttf /usr/share/fonts/liberation/LiberationSans-Bold.ttf /fonts && apk del ttf-liberation && wget -O /fonts/DancingScript.otf "https://github.com/impallari/DancingScript/raw/master/fonts/DancingScript-Regular.otf" && wget -O /fonts/DancingScript-License.txt https://github.com/impallari/DancingScript/blob/master/OFL.txt
COPY ./Gemfile ./Gemfile.lock ./ COPY ./Gemfile ./Gemfile.lock ./
@@ -49,6 +49,7 @@ COPY LICENSE README.md Rakefile config.ru ./
COPY --from=webpack /app/public/packs ./public/packs COPY --from=webpack /app/public/packs ./public/packs
RUN ln -s /fonts /app/public/fonts
RUN bundle exec bootsnap precompile --gemfile app/ lib/ RUN bundle exec bootsnap precompile --gemfile app/ lib/
WORKDIR /data/docuseal WORKDIR /data/docuseal
+13 -15
View File
@@ -98,7 +98,6 @@ GEM
faraday_middleware (~> 1.0, >= 1.0.0.rc1) faraday_middleware (~> 1.0, >= 1.0.0.rc1)
net-http-persistent (~> 4.0) net-http-persistent (~> 4.0)
nokogiri (~> 1, >= 1.10.8) nokogiri (~> 1, >= 1.10.8)
base64 (0.1.1)
bcrypt (3.1.19) bcrypt (3.1.19)
better_html (2.0.2) better_html (2.0.2)
actionview (>= 6.0) actionview (>= 6.0)
@@ -294,7 +293,7 @@ GEM
mini_magick (4.12.0) mini_magick (4.12.0)
mini_mime (1.1.5) mini_mime (1.1.5)
mini_portile2 (2.8.4) mini_portile2 (2.8.4)
minitest (5.19.0) minitest (5.20.0)
msgpack (1.7.2) msgpack (1.7.2)
multi_json (1.15.0) multi_json (1.15.0)
multi_xml (0.6.0) multi_xml (0.6.0)
@@ -345,7 +344,7 @@ GEM
os (1.1.4) os (1.1.4)
pagy (6.0.4) pagy (6.0.4)
parallel (1.23.0) parallel (1.23.0)
parser (3.2.2.3) parser (3.2.2.4)
ast (~> 2.4.1) ast (~> 2.4.1)
racc racc
pdf-reader (2.11.0) pdf-reader (2.11.0)
@@ -418,7 +417,7 @@ GEM
rake (13.0.6) rake (13.0.6)
redis-client (0.16.0) redis-client (0.16.0)
connection_pool connection_pool
regexp_parser (2.8.1) regexp_parser (2.8.2)
reline (0.3.8) reline (0.3.8)
io-console (~> 0.5) io-console (~> 0.5)
representable (3.2.0) representable (3.2.0)
@@ -455,33 +454,32 @@ GEM
rspec-mocks (~> 3.12) rspec-mocks (~> 3.12)
rspec-support (~> 3.12) rspec-support (~> 3.12)
rspec-support (3.12.1) rspec-support (3.12.1)
rubocop (1.56.1) rubocop (1.57.2)
base64 (~> 0.1.1)
json (~> 2.3) json (~> 2.3)
language_server-protocol (>= 3.17.0) language_server-protocol (>= 3.17.0)
parallel (~> 1.10) parallel (~> 1.10)
parser (>= 3.2.2.3) parser (>= 3.2.2.4)
rainbow (>= 2.2.2, < 4.0) rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0) regexp_parser (>= 1.8, < 3.0)
rexml (>= 3.2.5, < 4.0) rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.28.1, < 2.0) rubocop-ast (>= 1.28.1, < 2.0)
ruby-progressbar (~> 1.7) ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0) unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.29.0) rubocop-ast (1.30.0)
parser (>= 3.2.1.0) parser (>= 3.2.1.0)
rubocop-capybara (2.18.0) rubocop-capybara (2.19.0)
rubocop (~> 1.41) rubocop (~> 1.41)
rubocop-factory_bot (2.23.1) rubocop-factory_bot (2.24.0)
rubocop (~> 1.33) rubocop (~> 1.33)
rubocop-performance (1.19.0) rubocop-performance (1.19.1)
rubocop (>= 1.7.0, < 2.0) rubocop (>= 1.7.0, < 2.0)
rubocop-ast (>= 0.4.0) rubocop-ast (>= 0.4.0)
rubocop-rails (2.20.2) rubocop-rails (2.22.1)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
rack (>= 1.1) rack (>= 1.1)
rubocop (>= 1.33.0, < 2.0) rubocop (>= 1.33.0, < 2.0)
rubocop-rspec (2.23.2) rubocop-rspec (2.25.0)
rubocop (~> 1.33) rubocop (~> 1.40)
rubocop-capybara (~> 2.17) rubocop-capybara (~> 2.17)
rubocop-factory_bot (~> 2.22) rubocop-factory_bot (~> 2.22)
ruby-progressbar (1.13.0) ruby-progressbar (1.13.0)
@@ -537,7 +535,7 @@ GEM
tzinfo-data (1.2023.3) tzinfo-data (1.2023.3)
tzinfo (>= 1.0.0) tzinfo (>= 1.0.0)
uber (0.1.0) uber (0.1.0)
unicode-display_width (2.4.2) unicode-display_width (2.5.0)
uniform_notifier (1.16.0) uniform_notifier (1.16.0)
version_gem (1.1.3) version_gem (1.1.3)
warden (1.2.9) warden (1.2.9)
@@ -6,12 +6,6 @@ class PreviewDocumentPageController < ActionController::API
FORMAT = Templates::ProcessDocument::FORMAT FORMAT = Templates::ProcessDocument::FORMAT
def show def show
if Docuseal.multitenant?
Rollbar.warning('load page')
return head :not_found
end
attachment = ActiveStorage::Attachment.find_by(uuid: params[:attachment_uuid]) attachment = ActiveStorage::Attachment.find_by(uuid: params[:attachment_uuid])
return head :not_found unless attachment return head :not_found unless attachment
@@ -0,0 +1,16 @@
# frozen_string_literal: true
class SsoSettingsController < ApplicationController
before_action :load_encrypted_config
authorize_resource :encrypted_config, only: :index
authorize_resource :encrypted_config, parent: false, except: :index
def index; end
private
def load_encrypted_config
@encrypted_config =
EncryptedConfig.find_or_initialize_by(account: current_account, key: 'saml_configs')
end
end
+11 -2
View File
@@ -57,9 +57,18 @@ class TemplatesController < ApplicationController
end end
def destroy def destroy
@template.update!(deleted_at: Time.current) notice =
if !Docuseal.multitenant? && params[:permanently].present?
@template.destroy!
redirect_back(fallback_location: root_path, notice: 'Template has been archived.') 'Template has been removed.'
else
@template.update!(deleted_at: Time.current)
'Template has been archived.'
end
redirect_back(fallback_location: root_path, notice:)
end end
private private
@@ -3,20 +3,55 @@
class TemplatesUploadsController < ApplicationController class TemplatesUploadsController < ApplicationController
load_and_authorize_resource :template, parent: false load_and_authorize_resource :template, parent: false
layout 'plain'
def show; end
def create def create
url_params = create_file_params_from_url if params[:url].present?
@template.account = current_account @template.account = current_account
@template.author = current_user @template.author = current_user
@template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name]) @template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name])
@template.name = File.basename(params[:files].first.original_filename, '.*') @template.name = File.basename((url_params || params)[:files].first.original_filename, '.*')
@template.save! @template.save!
documents = Templates::CreateAttachments.call(@template, params) documents = Templates::CreateAttachments.call(@template, url_params || params)
schema = documents.map { |doc| { attachment_uuid: doc.uuid, name: doc.filename.base } } schema = documents.map { |doc| { attachment_uuid: doc.uuid, name: doc.filename.base } }
@template.update!(schema:) @template.update!(schema:)
redirect_to edit_template_path(@template) redirect_to edit_template_path(@template)
rescue StandardError => e
Rollbar.error(e) if defined?(Rollbar)
redirect_to root_path, alert: 'Unable to upload file'
end
private
def create_file_params_from_url
tempfile = Tempfile.new
tempfile.binmode
tempfile.write(conn.get(params[:url]).body)
tempfile.rewind
file = ActionDispatch::Http::UploadedFile.new(
tempfile:,
filename: File.basename(
URI.decode_www_form_component(params[:filename].presence || params[:url])
),
type: Marcel::MimeType.for(tempfile)
)
{ files: [file] }
end
def conn
Faraday.new do |faraday|
faraday.response :follow_redirects
end
end end
end end
@@ -12,7 +12,7 @@ class VerifyPdfSignatureController < ApplicationController
cert_data = if Docuseal.multitenant? cert_data = if Docuseal.multitenant?
Docuseal::CERTS Docuseal::CERTS
else else
EncryptedConfig.find_by(account: current_account, key: EncryptedConfig::ESIGN_CERTS_KEY)&.value || {} EncryptedConfig.find_by(key: EncryptedConfig::ESIGN_CERTS_KEY)&.value || {}
end end
default_pkcs = GenerateCertificate.load_pkcs(cert_data) default_pkcs = GenerateCertificate.load_pkcs(cert_data)
+2
View File
@@ -18,6 +18,7 @@ import AutoresizeTextarea from './elements/autoresize_textarea'
import SubmittersAutocomplete from './elements/submitter_autocomplete' import SubmittersAutocomplete from './elements/submitter_autocomplete'
import FolderAutocomplete from './elements/folder_autocomplete' import FolderAutocomplete from './elements/folder_autocomplete'
import SignatureForm from './elements/signature_form' import SignatureForm from './elements/signature_form'
import SubmitForm from './elements/submit_form'
import * as TurboInstantClick from './lib/turbo_instant_click' import * as TurboInstantClick from './lib/turbo_instant_click'
@@ -49,6 +50,7 @@ window.customElements.define('autoresize-textarea', AutoresizeTextarea)
window.customElements.define('submitters-autocomplete', SubmittersAutocomplete) window.customElements.define('submitters-autocomplete', SubmittersAutocomplete)
window.customElements.define('folder-autocomplete', FolderAutocomplete) window.customElements.define('folder-autocomplete', FolderAutocomplete)
window.customElements.define('signature-form', SignatureForm) window.customElements.define('signature-form', SignatureForm)
window.customElements.define('submit-form', SubmitForm)
document.addEventListener('turbo:before-fetch-request', encodeMethodIntoRequestBody) document.addEventListener('turbo:before-fetch-request', encodeMethodIntoRequestBody)
document.addEventListener('turbo:submit-end', async (event) => { document.addEventListener('turbo:submit-end', async (event) => {
+5
View File
@@ -0,0 +1,5 @@
export default class extends HTMLElement {
connectedCallback () {
this.querySelector('form').requestSubmit()
}
}
@@ -0,0 +1,73 @@
<template>
<div>
<div class="flex justify-between items-center w-full mb-2">
<label
:for="field.uuid"
class="label text-2xl"
>{{ field.name || t('date') }}
<template v-if="!field.required">({{ t('optional') }})</template>
</label>
<button
class="btn btn-outline btn-sm !normal-case font-normal"
@click.prevent="setCurrentDate"
>
<IconCalendarCheck :width="16" />
{{ t('set_today') }}
</button>
</div>
<div class="text-center">
<input
ref="input"
v-model="value"
class="base-input !text-2xl text-center w-full"
:required="field.required"
type="date"
:name="`values[${field.uuid}]`"
@focus="$emit('focus')"
>
</div>
</div>
</template>
<script>
import { IconCalendarCheck } from '@tabler/icons-vue'
export default {
name: 'DateStep',
components: {
IconCalendarCheck
},
inject: ['t'],
props: {
field: {
type: Object,
required: true
},
modelValue: {
type: String,
required: false,
default: ''
}
},
emits: ['update:model-value', 'focus'],
computed: {
value: {
set (value) {
this.$emit('update:model-value', value)
},
get () {
return this.modelValue
}
}
},
methods: {
setCurrentDate () {
const inputEl = this.$refs.input
inputEl.valueAsDate = new Date(new Date().getTime() - new Date().getTimezoneOffset() * 60000)
inputEl.dispatchEvent(new Event('input', { bubbles: true }))
}
}
}
</script>
+35 -37
View File
@@ -65,34 +65,13 @@
@focus="$refs.areas.scrollIntoField(currentField)" @focus="$refs.areas.scrollIntoField(currentField)"
/> />
</div> </div>
<div v-else-if="currentField.type === 'date'"> <DateStep
<div class="flex justify-between items-center w-full mb-2"> v-else-if="currentField.type === 'date'"
<label :key="currentField.uuid"
:for="currentField.uuid" v-model="values[currentField.uuid]"
class="label text-2xl" :field="currentField"
>{{ currentField.name || t('date') }} @focus="$refs.areas.scrollIntoField(currentField)"
<template v-if="!currentField.required">({{ t('optional') }})</template> />
</label>
<button
class="btn btn-outline btn-sm !normal-case font-normal"
@click.prevent="setCurrentDate"
>
<IconCalendarCheck :width="16" />
{{ t('set_today') }}
</button>
</div>
<div class="text-center">
<input
:id="currentField.uuid"
v-model="values[currentField.uuid]"
class="base-input !text-2xl text-center w-full"
:required="currentField.required"
type="date"
:name="`values[${currentField.uuid}]`"
@focus="$refs.areas.scrollIntoField(currentField)"
>
</div>
</div>
<div v-else-if="currentField.type === 'select'"> <div v-else-if="currentField.type === 'select'">
<label <label
v-if="currentField.name" v-if="currentField.name"
@@ -242,6 +221,7 @@
:key="currentField.uuid" :key="currentField.uuid"
v-model="values[currentField.uuid]" v-model="values[currentField.uuid]"
:field="currentField" :field="currentField"
:previous-value="previousSignatureValue"
:is-direct-upload="isDirectUpload" :is-direct-upload="isDirectUpload"
:attachments-index="attachmentsIndex" :attachments-index="attachmentsIndex"
:submitter-slug="submitterSlug" :submitter-slug="submitterSlug"
@@ -255,6 +235,7 @@
:key="currentField.uuid" :key="currentField.uuid"
v-model="values[currentField.uuid]" v-model="values[currentField.uuid]"
:field="currentField" :field="currentField"
:previous-value="previousInitialsValue"
:is-direct-upload="isDirectUpload" :is-direct-upload="isDirectUpload"
:attachments-index="attachmentsIndex" :attachments-index="attachmentsIndex"
:submitter-slug="submitterSlug" :submitter-slug="submitterSlug"
@@ -350,8 +331,9 @@ import AttachmentStep from './attachment_step'
import MultiSelectStep from './multi_select_step' import MultiSelectStep from './multi_select_step'
import PhoneStep from './phone_step' import PhoneStep from './phone_step'
import TextStep from './text_step' import TextStep from './text_step'
import DateStep from './date_step'
import FormCompleted from './completed' import FormCompleted from './completed'
import { IconInnerShadowTop, IconArrowsDiagonal, IconArrowsDiagonalMinimize2, IconCalendarCheck } from '@tabler/icons-vue' import { IconInnerShadowTop, IconArrowsDiagonal, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
import { t } from './i18n' import { t } from './i18n'
export default { export default {
@@ -364,10 +346,10 @@ export default {
InitialsStep, InitialsStep,
MultiSelectStep, MultiSelectStep,
IconInnerShadowTop, IconInnerShadowTop,
DateStep,
IconArrowsDiagonal, IconArrowsDiagonal,
TextStep, TextStep,
PhoneStep, PhoneStep,
IconCalendarCheck,
IconArrowsDiagonalMinimize2, IconArrowsDiagonalMinimize2,
FormCompleted FormCompleted
}, },
@@ -392,6 +374,13 @@ export default {
required: false, required: false,
default: () => [] default: () => []
}, },
onComplete: {
type: Function,
required: false,
default () {
return () => {}
}
},
withConfetti: { withConfetti: {
type: Boolean, type: Boolean,
required: false, required: false,
@@ -471,6 +460,16 @@ export default {
submitterSlug () { submitterSlug () {
return this.submitter.slug return this.submitter.slug
}, },
previousSignatureValue () {
const signatureField = [...this.fields].reverse().find((field) => field.type === 'signature' && !!this.values[field.uuid])
return this.values[signatureField?.uuid]
},
previousInitialsValue () {
const initialsField = [...this.fields].reverse().find((field) => field.type === 'initials' && !!this.values[field.uuid])
return this.values[initialsField?.uuid]
},
isAnonymousChecboxes () { isAnonymousChecboxes () {
return this.currentField.type === 'checkbox' && this.currentStepFields.every((e) => !e.name) && this.currentStepFields.length > 4 return this.currentField.type === 'checkbox' && this.currentStepFields.every((e) => !e.name) && this.currentStepFields.length > 4
}, },
@@ -635,13 +634,6 @@ export default {
} }
}) })
}, },
setCurrentDate () {
const inputEl = document.getElementById(this.currentField.uuid)
inputEl.valueAsDate = new Date(new Date().getTime() - new Date().getTimezoneOffset() * 60000)
inputEl.dispatchEvent(new Event('input', { bubbles: true }))
},
saveStep (formData) { saveStep (formData) {
if (this.isCompleted) { if (this.isCompleted) {
return Promise.resolve({}) return Promise.resolve({})
@@ -696,6 +688,12 @@ export default {
} }
} else { } else {
this.isCompleted = true this.isCompleted = true
const respData = await response.text()
if (respData) {
this.onComplete(JSON.parse(respData))
}
} }
}).catch(error => { }).catch(error => {
console.error('Error submitting form:', error) console.error('Error submitting form:', error)
@@ -20,7 +20,7 @@
</a> </a>
</span> </span>
<a <a
v-if="modelValue" v-if="modelValue || computedPreviousValue"
href="#" href="#"
class="btn btn-outline btn-sm" class="btn btn-outline btn-sm"
@click.prevent="remove" @click.prevent="remove"
@@ -51,26 +51,26 @@
</div> </div>
</div> </div>
<input <input
:value="modelValue" :value="modelValue || computedPreviousValue"
type="hidden" type="hidden"
:name="`values[${field.uuid}]`" :name="`values[${field.uuid}]`"
> >
<img <img
v-if="modelValue" v-if="modelValue || computedPreviousValue"
:src="attachmentsIndex[modelValue].url" :src="attachmentsIndex[modelValue || computedPreviousValue].url"
class="mx-auto bg-white border border-base-300 rounded max-h-72" class="mx-auto bg-white border border-base-300 rounded max-h-72"
> >
<canvas <canvas
v-show="!modelValue" v-show="!modelValue && !computedPreviousValue"
ref="canvas" ref="canvas"
class="bg-white border border-base-300 rounded" class="bg-white border border-base-300 rounded"
/> />
<input <input
v-if="!isDrawInitials" v-if="!isDrawInitials && !modelValue && !computedPreviousValue"
id="initials_text_input" id="initials_text_input"
ref="textInput" ref="textInput"
class="base-input !text-2xl w-full mt-6 text-center" class="base-input !text-2xl w-full mt-6 text-center"
:required="field.required && !isInitialsStarted && !modelValue" :required="field.required && !isInitialsStarted"
:placeholder="`${t('type_initial_here')}...`" :placeholder="`${t('type_initial_here')}...`"
type="text" type="text"
@focus="$emit('focus')" @focus="$emit('focus')"
@@ -82,6 +82,7 @@
<script> <script>
import { cropCanvasAndExportToPNG } from './crop_canvas' import { cropCanvasAndExportToPNG } from './crop_canvas'
import { IconReload, IconSignature, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue' import { IconReload, IconSignature, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
import SignaturePad from 'signature_pad'
export default { export default {
name: 'InitialsStep', name: 'InitialsStep',
@@ -110,6 +111,11 @@ export default {
required: false, required: false,
default: () => ({}) default: () => ({})
}, },
previousValue: {
type: String,
required: false,
default: ''
},
modelValue: { modelValue: {
type: String, type: String,
required: false, required: false,
@@ -119,10 +125,20 @@ export default {
emits: ['attached', 'update:model-value', 'start', 'minimize', 'focus'], emits: ['attached', 'update:model-value', 'start', 'minimize', 'focus'],
data () { data () {
return { return {
isInitialsStarted: false, isInitialsStarted: !!this.previousValue,
isUsePreviousValue: true,
isDrawInitials: false isDrawInitials: false
} }
}, },
computed: {
computedPreviousValue () {
if (this.isUsePreviousValue) {
return this.previousValue
} else {
return null
}
}
},
async mounted () { async mounted () {
this.$nextTick(() => { this.$nextTick(() => {
if (this.$refs.canvas) { if (this.$refs.canvas) {
@@ -137,8 +153,6 @@ export default {
import('@rails/activestorage') import('@rails/activestorage')
} }
const { default: SignaturePad } = await import('signature_pad')
if (this.$refs.canvas) { if (this.$refs.canvas) {
this.pad = new SignaturePad(this.$refs.canvas) this.pad = new SignaturePad(this.$refs.canvas)
@@ -152,6 +166,9 @@ export default {
methods: { methods: {
remove () { remove () {
this.$emit('update:model-value', '') this.$emit('update:model-value', '')
this.isUsePreviousValue = false
this.isInitialsStarted = false
}, },
clear () { clear () {
this.pad.clear() this.pad.clear()
@@ -193,7 +210,11 @@ export default {
} }
}, },
async submit () { async submit () {
if (this.modelValue) { if (this.modelValue || this.computedPreviousValue) {
if (this.computedPreviousValue) {
this.$emit('update:model-value', this.computedPreviousValue)
}
return Promise.resolve({}) return Promise.resolve({})
} }
@@ -28,6 +28,7 @@
> >
<div class="flex justify-between mt-2 -mb-2 md:-mb-4"> <div class="flex justify-between mt-2 -mb-2 md:-mb-4">
<a <a
v-if="!defaultValue"
href="#" href="#"
class="link" class="link"
@click.prevent="isCodeSent = false" @click.prevent="isCodeSent = false"
@@ -48,6 +49,7 @@
:id="field.uuid" :id="field.uuid"
ref="phone" ref="phone"
:value="modelValue || defaultValue" :value="modelValue || defaultValue"
:readonly="!!defaultValue"
class="base-input !text-2xl w-full" class="base-input !text-2xl w-full"
autocomplete="tel" autocomplete="tel"
pattern="^\+[0-9\s\-]+$" pattern="^\+[0-9\s\-]+$"
@@ -9,14 +9,15 @@
class="tooltip" class="tooltip"
data-tip="Type text" data-tip="Type text"
> >
<button <a
id="type_text_button" id="type_text_button"
href="#"
class="btn btn-sm btn-circle" class="btn btn-sm btn-circle"
:class="{ 'btn-neutral': isTextSignature, 'btn-outline': !isTextSignature }" :class="{ 'btn-neutral': isTextSignature, 'btn-outline': !isTextSignature }"
@click.prevent="toggleTextInput" @click.prevent="toggleTextInput"
> >
<IconTextSize :width="16" /> <IconTextSize :width="16" />
</button> </a>
</span> </span>
<span <span
class="tooltip" class="tooltip"
@@ -34,23 +35,26 @@
> >
</label> </label>
</span> </span>
<button <a
v-if="modelValue" v-if="modelValue || computedPreviousValue"
href="#"
class="btn btn-outline btn-sm" class="btn btn-outline btn-sm"
@click.prevent="remove" @click.prevent="remove"
> >
<IconReload :width="16" /> <IconReload :width="16" />
{{ t('redraw') }} {{ t('redraw') }}
</button> </a>
<button <a
v-else v-else
href="#"
class="btn btn-outline btn-sm" class="btn btn-outline btn-sm"
@click.prevent="clear" @click.prevent="clear"
> >
<IconReload :width="16" /> <IconReload :width="16" />
{{ t('clear') }} {{ t('clear') }}
</button> </a>
<button <a
href="#"
title="Minimize" title="Minimize"
class="py-1.5 inline md:hidden" class="py-1.5 inline md:hidden"
@click.prevent="$emit('minimize')" @click.prevent="$emit('minimize')"
@@ -59,21 +63,21 @@
:width="20" :width="20"
:height="20" :height="20"
/> />
</button> </a>
</div> </div>
</div> </div>
<input <input
:value="modelValue" :value="modelValue || computedPreviousValue"
type="hidden" type="hidden"
:name="`values[${field.uuid}]`" :name="`values[${field.uuid}]`"
> >
<img <img
v-if="modelValue" v-if="modelValue || computedPreviousValue"
:src="attachmentsIndex[modelValue].url" :src="attachmentsIndex[modelValue || computedPreviousValue].url"
class="mx-auto bg-white border border-base-300 rounded max-h-72" class="mx-auto bg-white border border-base-300 rounded max-h-72"
> >
<canvas <canvas
v-show="!modelValue" v-show="!modelValue && !computedPreviousValue"
ref="canvas" ref="canvas"
class="bg-white border border-base-300 rounded" class="bg-white border border-base-300 rounded"
/> />
@@ -93,6 +97,9 @@
<script> <script>
import { IconReload, IconCamera, IconTextSize, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue' import { IconReload, IconCamera, IconTextSize, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
import { cropCanvasAndExportToPNG } from './crop_canvas' import { cropCanvasAndExportToPNG } from './crop_canvas'
import SignaturePad from 'signature_pad'
let isFontLoaded = false
export default { export default {
name: 'SignatureStep', name: 'SignatureStep',
@@ -122,6 +129,11 @@ export default {
required: false, required: false,
default: () => ({}) default: () => ({})
}, },
previousValue: {
type: String,
required: false,
default: ''
},
modelValue: { modelValue: {
type: String, type: String,
required: false, required: false,
@@ -131,10 +143,20 @@ export default {
emits: ['attached', 'update:model-value', 'start', 'minimize'], emits: ['attached', 'update:model-value', 'start', 'minimize'],
data () { data () {
return { return {
isSignatureStarted: false, isSignatureStarted: !!this.previousValue,
isUsePreviousValue: true,
isTextSignature: false isTextSignature: false
} }
}, },
computed: {
computedPreviousValue () {
if (this.isUsePreviousValue) {
return this.previousValue
} else {
return null
}
}
},
async mounted () { async mounted () {
this.$nextTick(() => { this.$nextTick(() => {
if (this.$refs.canvas) { if (this.$refs.canvas) {
@@ -147,8 +169,6 @@ export default {
import('@rails/activestorage') import('@rails/activestorage')
} }
const { default: SignaturePad } = await import('signature_pad')
if (this.$refs.canvas) { if (this.$refs.canvas) {
this.pad = new SignaturePad(this.$refs.canvas) this.pad = new SignaturePad(this.$refs.canvas)
@@ -162,6 +182,22 @@ export default {
methods: { methods: {
remove () { remove () {
this.$emit('update:model-value', '') this.$emit('update:model-value', '')
this.isUsePreviousValue = false
this.isSignatureStarted = false
},
loadFont () {
if (!isFontLoaded) {
const font = new FontFace('Dancing Script', `url(${this.baseUrl}/fonts/DancingScript.otf) format("opentype")`)
font.load().then((loadedFont) => {
document.fonts.add(loadedFont)
isFontLoaded = true
}).catch((error) => {
console.error('Font loading failed:', error)
})
}
}, },
clear () { clear () {
this.pad.clear() this.pad.clear()
@@ -178,8 +214,8 @@ export default {
const canvas = this.$refs.canvas const canvas = this.$refs.canvas
const context = canvas.getContext('2d') const context = canvas.getContext('2d')
const fontFamily = 'Arial' const fontFamily = 'Dancing Script'
const fontSize = '44px' const fontSize = '38px'
const fontStyle = 'italic' const fontStyle = 'italic'
const fontWeight = '' const fontWeight = ''
@@ -197,6 +233,8 @@ export default {
this.$nextTick(() => { this.$nextTick(() => {
this.$refs.textInput.focus() this.$refs.textInput.focus()
this.loadFont()
this.$emit('start') this.$emit('start')
}) })
} }
@@ -250,7 +288,11 @@ export default {
} }
}, },
async submit () { async submit () {
if (this.modelValue) { if (this.modelValue || this.computedPreviousValue) {
if (this.computedPreviousValue) {
this.$emit('update:model-value', this.computedPreviousValue)
}
return Promise.resolve({}) return Promise.resolve({})
} }
+19 -7
View File
@@ -31,6 +31,7 @@
:href="`/templates/${template.id}/submissions/new`" :href="`/templates/${template.id}/submissions/new`"
data-turbo-frame="modal" data-turbo-frame="modal"
class="btn btn-primary text-base" class="btn btn-primary text-base"
@click="maybeShowEmptyTemplateAlert"
> >
<IconUsersPlus <IconUsersPlus
width="20" width="20"
@@ -691,14 +692,25 @@ export default {
this.save() this.save()
}, },
onSaveClick () { maybeShowEmptyTemplateAlert (e) {
this.isSaving = true if (!this.template.fields.length) {
e.preventDefault()
this.save().then(() => { alert('Please draw fields to prepare the document.')
window.Turbo.visit(`/templates/${this.template.id}`) }
}).finally(() => { },
this.isSaving = false onSaveClick () {
}) if (this.template.fields.length) {
this.isSaving = true
this.save().then(() => {
window.Turbo.visit(`/templates/${this.template.id}`)
}).finally(() => {
this.isSaving = false
})
} else {
alert('Please draw fields to prepare the document.')
}
}, },
scrollToArea (area) { scrollToArea (area) {
const documentRef = this.documentRefs.find((a) => a.document.uuid === area.attachment_uuid) const documentRef = this.documentRefs.find((a) => a.document.uuid === area.attachment_uuid)
@@ -24,7 +24,7 @@
* *
</span> </span>
<IconPencil <IconPencil
class="cursor-pointer flex-none opacity-0 group-hover/contenteditable:opacity-100 align-middle peer-focus:hidden" 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' }" :style="iconInline ? {} : { right: -(1.1 * iconWidth) + 'px' }"
title="Edit" title="Edit"
:class="{ 'ml-1': !withRequired, 'absolute': !iconInline, 'inline align-bottom': iconInline }" :class="{ 'ml-1': !withRequired, 'absolute': !iconInline, 'inline align-bottom': iconInline }"
+1 -1
View File
@@ -5,7 +5,7 @@
<div <div
class="border border-base-300 rounded rounded-tr-none relative group" class="border border-base-300 rounded rounded-tr-none relative group"
> >
<div class="flex items-center justify-between relative"> <div class="flex items-center justify-between relative group/contenteditable-container">
<div <div
class="absolute top-0 bottom-0 right-0 left-0 cursor-pointer" class="absolute top-0 bottom-0 right-0 left-0 cursor-pointer"
@click="scrollToFirstArea" @click="scrollToFirstArea"
@@ -14,7 +14,7 @@
<label <label
v-else v-else
tabindex="0" tabindex="0"
class="cursor-pointer rounded-md p-2 border border-base-300 w-full flex justify-between" class="cursor-pointer group/contenteditable-container rounded-md p-2 border border-base-300 w-full flex justify-between"
> >
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<span <span
+2 -3
View File
@@ -87,12 +87,11 @@ class SubmitterMailer < ApplicationMailer
end end
documents.each do |attachment| documents.each do |attachment|
data = attachment.download total_size += attachment.byte_size
total_size += data.size
break if total_size >= MAX_ATTACHMENTS_SIZE break if total_size >= MAX_ATTACHMENTS_SIZE
attachments[attachment.filename.to_s] = data attachments[attachment.filename.to_s] = attachment.download
end end
attachments[submitter.submission.audit_trail.filename.to_s] = audit_trail_data if audit_trail_data attachments[submitter.submission.audit_trail.filename.to_s] = audit_trail_data if audit_trail_data
+13 -6
View File
@@ -41,12 +41,12 @@
<%= link_to 'Team', settings_users_path, class: 'text-base hover:bg-base-300' %> <%= link_to 'Team', settings_users_path, class: 'text-base hover:bg-base-300' %>
</li> </li>
<% end %> <% end %>
<% if !Docuseal.demo? && can?(:manage, EncryptedConfig) %> <% unless Docuseal.multitenant? %>
<li> <% if can?(:read, EncryptedConfig.new(key: 'saml_configs', account: current_account)) %>
<%= link_to Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CONSOLE_URL}/on_premise", class: 'text-base hover:bg-base-300', data: { prefetch: false } do %> <li>
Plans <%= link_to 'SSO', settings_sso_index_path, class: 'text-base hover:bg-base-300' %>
<% end %> </li>
</li> <% end %>
<% end %> <% end %>
<% if Docuseal.demo? || !Docuseal.multitenant? %> <% if Docuseal.demo? || !Docuseal.multitenant? %>
<% if can?(:read, AccessToken) %> <% if can?(:read, AccessToken) %>
@@ -66,6 +66,13 @@
</li> </li>
<% end %> <% end %>
<%= render 'shared/settings_nav_extra' %> <%= render 'shared/settings_nav_extra' %>
<% if !Docuseal.demo? && can?(:manage, EncryptedConfig) %>
<li>
<%= link_to Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CONSOLE_URL}/on_premise", class: 'text-base hover:bg-base-300', data: { prefetch: false } do %>
Plans
<% end %>
</li>
<% end %>
<% if !Docuseal.demo? && can?(:manage, EncryptedConfig) %> <% if !Docuseal.demo? && can?(:manage, EncryptedConfig) %>
<li> <li>
<%= link_to Docuseal.multitenant? ? console_redirect_index_path : Docuseal::CONSOLE_URL, class: 'text-base hover:bg-base-300', data: { prefetch: false } do %> <%= link_to Docuseal.multitenant? ? console_redirect_index_path : Docuseal::CONSOLE_URL, class: 'text-base hover:bg-base-300', data: { prefetch: false } do %>
@@ -0,0 +1,11 @@
<div class="alert">
<%= svg_icon('info_circle', class: 'w-6 h-6') %>
<div>
<p class="font-bold">Single Sign On with SAML 2.0</p>
<p class="text-gray-700">
Unlock with DocuSeal Enterprise
<br>
<a class="link font-medium" target="_blank" href="<%= "#{Docuseal::CONSOLE_URL}/#{Docuseal.multitenant? ? 'plans' : 'on_premise'}" %>">Learn More</a>
</p>
</div>
</div>
+8
View File
@@ -0,0 +1,8 @@
<div class="flex flex-wrap space-y-4 md:flex-nowrap md:space-y-0">
<%= render 'shared/settings_nav' %>
<div class="flex-grow max-w-xl mx-auto">
<h1 class="text-4xl font-bold mb-4">SMS</h1>
<%= render 'placeholder' %>
</div>
<div class="w-0 md:w-52"></div>
</div>
+3 -3
View File
@@ -52,9 +52,9 @@
</a> </a>
</span> </span>
<% end %> <% end %>
<% if !template.deleted_at? && can?(:destroy, template) %> <% if (!template.deleted_at? || !Docuseal.multitenant?) && can?(:destroy, template) %>
<span class="tooltip tooltip-left" data-tip="Archive"> <span class="tooltip tooltip-left" data-tip="<%= template.deleted_at? ? 'Remove' : 'Archive' %>">
<%= button_to template_path(template), data: { turbo_confirm: 'Are you sure?' }, method: :delete, class: 'btn btn-xs hover:btn-outline bg-base-200 btn-circle', aria_label: 'Restore' do %> <%= button_to template_path(template), data: { turbo_confirm: 'Are you sure?' }, params: { permanently: template.deleted_at? }.compact_blank, method: :delete, class: 'btn btn-xs hover:btn-outline bg-base-200 btn-circle', aria_label: 'Restore' do %>
<%= svg_icon('trash', class: 'w-4 h-4 enabled') %> <%= svg_icon('trash', class: 'w-4 h-4 enabled') %>
<%= svg_icon('loader', class: 'w-4 h-4 animate-spin disabled') %> <%= svg_icon('loader', class: 'w-4 h-4 animate-spin disabled') %>
<% end %> <% end %>
+18
View File
@@ -0,0 +1,18 @@
<div class="h-screen">
<div
class="text-center p-8 h-full flex items-center justify-center">
<div>
<%= render 'shared/logo', width: 50, height: 50, class: 'mx-auto animate-bounce' %>
<span>
Processing...
</span>
</div>
</div>
</div>
<submit-form>
<%= form_for '', url: templates_upload_path, method: :post, class: 'hidden' do %>
<button type="submit"></button>
<input name="url" value="<%= params[:url] %>">
<input name="filename" value="<%= params[:filename] %>">
<% end %>
</submit-form>
+1 -1
View File
@@ -59,7 +59,7 @@ Rails.application.configure do
# config.action_cable.allowed_request_origins = [ "http://example.com", /http:\/\/example.*/ ] # config.action_cable.allowed_request_origins = [ "http://example.com", /http:\/\/example.*/ ]
# Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
config.force_ssl = ENV['FORCE_SSL'] == 'true' config.force_ssl = ENV['FORCE_SSL'].present?
# Include generic and useful information about system operation, but avoid logging too much # Include generic and useful information about system operation, but avoid logging too much
# information to avoid inadvertent exposure of personally identifiable information (PII). # information to avoid inadvertent exposure of personally identifiable information (PII).
+12
View File
@@ -66,4 +66,16 @@ Rails.application.configure do
deterministic_key: 'test deterministic key', deterministic_key: 'test deterministic key',
key_derivation_salt: 'test key derivation salt' key_derivation_salt: 'test key derivation salt'
} }
config.middleware.use(Class.new do
def initialize(app)
@app = app
end
def call(env)
return [200, {}, []] if env['PATH_INFO'].starts_with?('/fonts')
@app.call(env)
end
end)
end end
+1 -1
View File
@@ -24,7 +24,7 @@ port ENV.fetch('PORT', 3000)
environment ENV.fetch('RAILS_ENV', 'development') environment ENV.fetch('RAILS_ENV', 'development')
# Specifies the `pidfile` that Puma will use. # Specifies the `pidfile` that Puma will use.
# pidfile ENV.fetch('PIDFILE', 'tmp/pids/server.pid') pidfile ENV.fetch('PIDFILE', "#{Dir.tmpdir}/server.pid")
# Specifies the number of `workers` to boot in clustered mode. # Specifies the number of `workers` to boot in clustered mode.
# Workers are forked web server processes. If using threads and workers together # Workers are forked web server processes. If using threads and workers together
+4
View File
@@ -58,6 +58,9 @@ Rails.application.routes.draw do
resources :submissions, only: %i[show destroy] resources :submissions, only: %i[show destroy]
resources :console_redirect, only: %i[index] resources :console_redirect, only: %i[index]
resource :templates_upload, only: %i[create] resource :templates_upload, only: %i[create]
authenticated do
resource :templates_upload, only: %i[show], path: 'new'
end
resources :templates_archived, only: %i[index], path: 'archived' resources :templates_archived, only: %i[index], path: 'archived'
resources :folders, only: %i[show edit update destroy], controller: 'template_folders' resources :folders, only: %i[show edit update destroy], controller: 'template_folders'
resources :templates, only: %i[new create edit show destroy] do resources :templates, only: %i[new create edit show destroy] do
@@ -92,6 +95,7 @@ Rails.application.routes.draw do
resources :storage, only: %i[index create], controller: 'storage_settings' resources :storage, only: %i[index create], controller: 'storage_settings'
resources :email, only: %i[index create], controller: 'email_smtp_settings' resources :email, only: %i[index create], controller: 'email_smtp_settings'
resources :sms, only: %i[index], controller: 'sms_settings' resources :sms, only: %i[index], controller: 'sms_settings'
resources :sso, only: %i[index], controller: 'sso_settings'
end end
resources :notifications, only: %i[index create], controller: 'notifications_settings' resources :notifications, only: %i[index create], controller: 'notifications_settings'
resource :esign, only: %i[show create new update destroy], controller: 'esign_settings' resource :esign, only: %i[show create new update destroy], controller: 'esign_settings'
@@ -20,7 +20,7 @@ class AddFolderIdToTemplates < ActiveRecord::Migration[7.0]
def up def up
add_reference :templates, :folder, foreign_key: { to_table: :template_folders }, index: true, null: true add_reference :templates, :folder, foreign_key: { to_table: :template_folders }, index: true, null: true
MigrationAccount.all.pluck(:id).each do |account_id| MigrationAccount.pluck(:id).each do |account_id|
author_id = MigrationUser.where(account_id:).minimum(:id) author_id = MigrationUser.where(account_id:).minimum(:id)
next if author_id.blank? next if author_id.blank?
+2 -2
View File
@@ -11,8 +11,8 @@ services:
volumes: volumes:
- .:/data - .:/data
environment: environment:
FORCE_SSL: 'true' - FORCE_SSL=${HOST}
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/docuseal - DATABASE_URL=postgresql://postgres:postgres@postgres:5432/docuseal
postgres: postgres:
image: postgres:15 image: postgres:15
+4 -5
View File
@@ -47,7 +47,7 @@ module Accounts
end end
def load_webhook_configs(account) def load_webhook_configs(account)
account = Account.order(:id).first unless Docuseal.multitenant? EncryptedConfig.find_by(key: EncryptedConfig::WEBHOOK_URL_KEY) unless Docuseal.multitenant?
account.encrypted_configs.find_by(key: EncryptedConfig::WEBHOOK_URL_KEY) account.encrypted_configs.find_by(key: EncryptedConfig::WEBHOOK_URL_KEY)
end end
@@ -57,8 +57,7 @@ module Accounts
if Docuseal.multitenant? if Docuseal.multitenant?
EncryptedConfig.find_by(account:, key: EncryptedConfig::ESIGN_CERTS_KEY)&.value || Docuseal::CERTS EncryptedConfig.find_by(account:, key: EncryptedConfig::ESIGN_CERTS_KEY)&.value || Docuseal::CERTS
else else
EncryptedConfig.find_by(account: Account.order(:id).first, EncryptedConfig.find_by(key: EncryptedConfig::ESIGN_CERTS_KEY).value
key: EncryptedConfig::ESIGN_CERTS_KEY).value
end end
if (default_cert = cert_data['custom']&.find { |e| e['status'] == 'default' }) if (default_cert = cert_data['custom']&.find { |e| e['status'] == 'default' })
@@ -68,11 +67,11 @@ module Accounts
end end
end end
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? return true if ENV['SMTP_ADDRESS'].present?
EncryptedConfig.exists?(account_id: account.id, key: EncryptedConfig::EMAIL_SMTP_KEY) EncryptedConfig.exists?(key: EncryptedConfig::EMAIL_SMTP_KEY)
end end
def normalize_timezone(timezone) def normalize_timezone(timezone)
+1 -1
View File
@@ -31,7 +31,7 @@ module Docuseal
DEFAULT_URL_OPTIONS = { DEFAULT_URL_OPTIONS = {
host: HOST, host: HOST,
protocol: ENV['FORCE_SSL'] == 'true' ? 'https' : 'http' protocol: ENV['FORCE_SSL'].present? ? 'https' : 'http'
}.freeze }.freeze
module_function module_function
+58 -46
View File
@@ -4,8 +4,8 @@ module Submissions
module GenerateAuditTrail module GenerateAuditTrail
FONT_SIZE = 9 FONT_SIZE = 9
TEXT_COLOR = '525252' TEXT_COLOR = '525252'
FONT_PATH = '/LiberationSans-Regular.ttf' FONT_PATH = '/fonts/LiberationSans-Regular.ttf'
FONT_BOLD_PATH = '/LiberationSans-Bold.ttf' FONT_BOLD_PATH = '/fonts/LiberationSans-Bold.ttf'
FONT_NAME = if File.exist?(FONT_PATH) FONT_NAME = if File.exist?(FONT_PATH)
FONT_PATH FONT_PATH
else else
@@ -105,7 +105,8 @@ module Submissions
document.metadata['sha256'] || document.checksum, document.metadata['sha256'] || document.checksum,
"\n", "\n",
{ text: 'Generated at: ', font: [FONT_BOLD_NAME, { variant: :bold }] }, { text: 'Generated at: ', font: [FONT_BOLD_NAME, { variant: :bold }] },
I18n.l(document.created_at, format: :long, locale: account.locale) "#{I18n.l(document.created_at.in_time_zone(account.timezone), format: :long, locale: account.locale)} " \
"#{timezone_abbr(account.timezone, document.created_at)}"
], line_spacing: 1.8 ], line_spacing: 1.8
) )
] ]
@@ -166,57 +167,59 @@ module Submissions
composer.table(info_rows, cell_style: { padding: [0, 0, 0, 0], border: { width: 0 } }) composer.table(info_rows, cell_style: { padding: [0, 0, 0, 0], border: { width: 0 } })
composer.column(columns: 1, gaps: 0, style: { padding: [0, 200, 0, 0] }) do |column| submission.template_fields.filter_map do |field|
submission.template_fields.filter_map do |field| next if field['submitter_uuid'] != submitter.uuid
next if field['submitter_uuid'] != submitter.uuid
submitter_field_counters[field['type']] += 1 submitter_field_counters[field['type']] += 1
value = submitter.values[field['uuid']] value = submitter.values[field['uuid']]
next if Array.wrap(value).compact_blank.blank? next if Array.wrap(value).compact_blank.blank?
[ [
column.formatted_text_box( composer.formatted_text_box(
[ [
{ {
text: field['name'].to_s.upcase.presence || text: field['name'].to_s.upcase.presence ||
"#{field['type']} Field #{submitter_field_counters[field['type']]}\n".upcase, "#{field['type']} Field #{submitter_field_counters[field['type']]}\n".upcase,
font_size: 6 font_size: 6
} }
].compact_blank, line_spacing: 1.8, padding: [0, 0, 5, 0] ].compact_blank, line_spacing: 1.8, padding: [0, 0, 5, 0]
), ),
if field['type'].in?(%w[image signature initials]) if field['type'].in?(%w[image signature initials])
attachment = submitter.attachments.find { |a| a.uuid == value } attachment = submitter.attachments.find { |a| a.uuid == value }
image = Vips::Image.new_from_buffer(attachment.download, '').autorot image = Vips::Image.new_from_buffer(attachment.download, '').autorot
scale = [300.0 / image.width, 300.0 / image.height].min scale = [600.0 / image.width, 600.0 / image.height].min
io = StringIO.new(image.resize([scale, 1].min).write_to_buffer('.png')) resized_image = image.resize([scale, 1].min)
io = StringIO.new(resized_image.write_to_buffer('.png'))
column.image(io, padding: [0, field['type'] == 'initials' ? 200 : 100, 10, 0]) width = field['type'] == 'initials' ? 100 : 200
column.formatted_text_box([{ text: '' }]) height = resized_image.height * (width.to_f / resized_image.width)
elsif field['type'] == 'file'
column.formatted_text_box(
Array.wrap(value).map do |uuid|
attachment = submitter.attachments.find { |a| a.uuid == uuid }
link =
Rails.application.routes.url_helpers.rails_blob_url(attachment, **Docuseal.default_url_options)
{ link:, text: "#{attachment.filename}\n", style: :link } composer.image(io, width:, height:, margin: [0, 0, 10, 0])
end, composer.formatted_text_box([{ text: '' }])
padding: [0, 0, 10, 0] elsif field['type'] == 'file'
) composer.formatted_text_box(
elsif field['type'] == 'checkbox' Array.wrap(value).map do |uuid|
column.formatted_text_box([{ text: value.to_s.titleize }], padding: [0, 0, 10, 0]) attachment = submitter.attachments.find { |a| a.uuid == uuid }
else link =
value = I18n.l(Date.parse(value), format: :long, locale: account.locale) if field['type'] == 'date' Rails.application.routes.url_helpers.rails_blob_url(attachment, **Docuseal.default_url_options)
value = value.join(', ') if value.is_a?(Array)
column.formatted_text_box([{ text: value.to_s.presence || 'n/a' }], padding: [0, 0, 10, 0]) { link:, text: "#{attachment.filename}\n", style: :link }
end end,
] padding: [0, 0, 10, 0]
end )
elsif field['type'] == 'checkbox'
composer.formatted_text_box([{ text: value.to_s.titleize }], padding: [0, 0, 10, 0])
else
value = I18n.l(Date.parse(value), format: :long, locale: account.locale) if field['type'] == 'date'
value = value.join(', ') if value.is_a?(Array)
composer.formatted_text_box([{ text: value.to_s.presence || 'n/a' }], padding: [0, 0, 10, 0])
end
]
end end
composer.draw_box(divider) composer.draw_box(divider)
@@ -227,7 +230,8 @@ module Submissions
events_data = submission.submission_events.sort_by(&:event_timestamp).map do |event| events_data = submission.submission_events.sort_by(&:event_timestamp).map do |event|
submitter = submission.submitters.find { |e| e.id == event.submitter_id } submitter = submission.submitters.find { |e| e.id == event.submitter_id }
[ [
I18n.l(event.event_timestamp, format: :long, locale: account.locale), "#{I18n.l(event.event_timestamp.in_time_zone(account.timezone), format: :long, locale: account.locale)} " \
"#{timezone_abbr(account.timezone, event.event_timestamp)}",
composer.document.layout.formatted_text_box( composer.document.layout.formatted_text_box(
[ [
{ text: SubmissionEvents::EVENT_NAMES[event.event_type.to_sym], { text: SubmissionEvents::EVENT_NAMES[event.event_type.to_sym],
@@ -263,6 +267,14 @@ module Submissions
) )
end end
def timezone_abbr(timezone, time = Time.current)
tz_info = TZInfo::Timezone.get(
ActiveSupport::TimeZone::MAPPING[timezone] || timezone || 'UTC'
)
tz_info.abbreviation(time)
end
def add_logo(column) def add_logo(column)
column.image(PdfIcons.logo_io, width: 40, height: 40, position: :float) column.image(PdfIcons.logo_io, width: 40, height: 40, position: :float)
@@ -3,7 +3,7 @@
module Submissions module Submissions
module GenerateResultAttachments module GenerateResultAttachments
FONT_SIZE = 11 FONT_SIZE = 11
FONT_PATH = '/LiberationSans-Regular.ttf' FONT_PATH = '/fonts/LiberationSans-Regular.ttf'
FONT_NAME = if File.exist?(FONT_PATH) FONT_NAME = if File.exist?(FONT_PATH)
FONT_PATH FONT_PATH
else else
+18 -9
View File
@@ -9,12 +9,7 @@ module Templates
PDF_CONTENT_TYPE = 'application/pdf' PDF_CONTENT_TYPE = 'application/pdf'
Q = 35 Q = 35
MAX_WIDTH = 1400 MAX_WIDTH = 1400
MAX_NUMBER_OF_PAGES_PROCESSED = MAX_NUMBER_OF_PAGES_PROCESSED = 15
if Docuseal.multitenant?
70
else
40
end
module_function module_function
@@ -74,10 +69,24 @@ module Templates
end end
def generate_pdf_preview_from_file(attachment, file_path, page_number) def generate_pdf_preview_from_file(attachment, file_path, page_number)
page = Vips::Image.new_from_file(file_path, dpi: DPI, page: page_number) io = StringIO.new
page = page.resize(MAX_WIDTH / page.width.to_f)
io = StringIO.new(page.write_to_buffer(FORMAT, Q: Q, interlace: true)) command = [
'pdftocairo', '-jpeg', '-jpegopt', "progressive=y,quality=#{Q},optimize=y",
'-scale-to-x', MAX_WIDTH, '-scale-to-y', '-1',
'-r', DPI, '-f', page_number + 1, '-l', page_number + 1,
'-singlefile', Shellwords.escape(file_path), '-'
].join(' ')
Open3.popen3(command) do |_, stdout, _, _|
io.write(stdout.read)
io.rewind
end
page = Vips::Image.new_from_buffer(io.read, '')
io.rewind
ApplicationRecord.no_touching do ApplicationRecord.no_touching do
ActiveStorage::Attachment.create!( ActiveStorage::Attachment.create!(
+1 -1
View File
@@ -2,7 +2,7 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe 'App Setup', js: true do RSpec.describe 'App Setup' do
let(:form_data) do let(:form_data) do
{ {
first_name: 'John', first_name: 'John',