Compare commits

..
11 Commits
28 changed files with 309 additions and 99 deletions

No files matched your search

+3
View File
@@ -61,5 +61,8 @@ RSpec/ExampleLength:
Rails/I18nLocaleTexts: Rails/I18nLocaleTexts:
Enabled: false Enabled: false
Rails/SkipsModelValidations:
Enabled: false
Rails/ApplicationController: Rails/ApplicationController:
Enabled: false Enabled: false
+7 -7
View File
@@ -44,13 +44,6 @@ DocuSeal is an open source platform that provides secure and efficient digital d
- [x] Mobile-optimized - [x] Mobile-optimized
- [x] Easy to deploy in minutes - [x] Easy to deploy in minutes
## For Companies
### Integrate seamless document signing into your web or mobile apps with DocuSeal!
At DocuSeal we have expertise and technologies to make documents creation, filling, signing and processing seamlessly integrated with your product. We specialize in working with various industries, including **Banking, Healthcare, Transport, eCommerce, KYC, CRM, and other software products** that require bulk document signing. By leveraging DocuSeal, we can assist in reducing the overall cost of developing and processing electronic documents while ensuring security and compliance with local electronic document laws.
[![Book an Integration Demo](https://cal.com/book-with-cal-dark.svg)](https://cal.com/docuseal)
## Deploy ## Deploy
|Heroku|Railway| |Heroku|Railway|
@@ -80,6 +73,13 @@ Run the app under a custom domain over https using docker compose (make sure you
HOST=your-domain-name.com docker-compose up HOST=your-domain-name.com docker-compose up
``` ```
## For Companies
### Integrate seamless document signing into your web or mobile apps with DocuSeal!
At DocuSeal we have expertise and technologies to make documents creation, filling, signing and processing seamlessly integrated with your product. We specialize in working with various industries, including **Banking, Healthcare, Transport, eCommerce, KYC, CRM, and other software products** that require bulk document signing. By leveraging DocuSeal, we can assist in reducing the overall cost of developing and processing electronic documents while ensuring security and compliance with local electronic document laws.
[![Book an Integration Demo](https://cal.com/book-with-cal-dark.svg)](https://cal.com/docuseal)
## License ## License
DocuSeal is released under the GNU Affero General Public License v3.0. DocuSeal is released under the GNU Affero General Public License v3.0.
@@ -23,6 +23,10 @@ class SubmitFormController < ApplicationController
submitter.save! submitter.save!
Submissions.update_template_fields!(submitter.submission) if submitter.submission.template_fields.blank?
submitter.submission.save!
if submitter.completed_at? if submitter.completed_at?
GenerateSubmitterResultAttachmentsJob.perform_later(submitter) GenerateSubmitterResultAttachmentsJob.perform_later(submitter)
+1 -1
View File
@@ -13,7 +13,7 @@ export default actionable(class extends HTMLElement {
document.body.classList.remove('overflow-hidden') document.body.classList.remove('overflow-hidden')
document.removeEventListener('keyup', this.onEscKey) document.removeEventListener('keyup', this.onEscKey)
document.removeEventListener('turbo:submit-end', this.handleSubmit) document.removeEventListener('turbo:submit-end', this.onSubmit)
document.removeEventListener('turbo:before-cache', this.close) document.removeEventListener('turbo:before-cache', this.close)
} }
+6 -1
View File
@@ -21,7 +21,7 @@
</span> </span>
</div> </div>
<div <div
v-if="isActive" v-if="isActive && withLabel"
class="absolute -top-7 rounded bg-base-content text-base-100 px-2 text-sm whitespace-nowrap" class="absolute -top-7 rounded bg-base-content text-base-100 px-2 text-sm whitespace-nowrap"
> >
{{ field.name || fieldNames[field.type] }} {{ field.name || fieldNames[field.type] }}
@@ -146,6 +146,11 @@ export default {
required: false, required: false,
default: false default: false
}, },
withLabel: {
type: Boolean,
required: false,
default: true
},
fieldIndex: { fieldIndex: {
type: Number, type: Number,
required: false, required: false,
+9 -1
View File
@@ -11,7 +11,9 @@
v-for="(area, areaIndex) in field.areas" v-for="(area, areaIndex) in field.areas"
:key="areaIndex" :key="areaIndex"
> >
<Teleport :to="`#page-${area.attachment_uuid}-${area.page}`"> <Teleport
:to="`#page-${area.attachment_uuid}-${area.page}`"
>
<FieldArea <FieldArea
:ref="setAreaRef" :ref="setAreaRef"
v-model="values[field.uuid]" v-model="values[field.uuid]"
@@ -20,6 +22,7 @@
:submittable="true" :submittable="true"
:field-index="fieldIndex" :field-index="fieldIndex"
:is-active="currentStep === step" :is-active="currentStep === step"
:with-label="withLabel"
:is-value-set="step.some((f) => f.uuid in values)" :is-value-set="step.some((f) => f.uuid in values)"
:attachments-index="attachmentsIndex" :attachments-index="attachmentsIndex"
@click="$emit('focus-step', step)" @click="$emit('focus-step', step)"
@@ -54,6 +57,11 @@ export default {
required: false, required: false,
default: () => ({}) default: () => ({})
}, },
withLabel: {
type: Boolean,
required: false,
default: true
},
currentStep: { currentStep: {
type: Array, type: Array,
required: false, required: false,
+33 -24
View File
@@ -4,6 +4,7 @@
:steps="stepFields" :steps="stepFields"
:values="values" :values="values"
:attachments-index="attachmentsIndex" :attachments-index="attachmentsIndex"
:with-label="!isAnonymousChecboxes"
:current-step="currentStepFields" :current-step="currentStepFields"
@focus-step="[saveStep(), goToStep($event, false, true), currentField.type !== 'checkbox' ? isFormVisible = true : '']" @focus-step="[saveStep(), goToStep($event, false, true), currentField.type !== 'checkbox' ? isFormVisible = true : '']"
/> />
@@ -182,7 +183,7 @@
/> />
<div <div
v-else-if="currentField.type === 'checkbox'" v-else-if="currentField.type === 'checkbox'"
class="flex w-full" class="flex w-full max-h-44 overflow-y-auto"
> >
<input <input
type="hidden" type="hidden"
@@ -192,31 +193,36 @@
<div <div
class="space-y-3.5 mx-auto" class="space-y-3.5 mx-auto"
> >
<div <template v-if="isAnonymousChecboxes">
v-for="(field, index) in currentStepFields" Complete hightlighted checkboxes and click <span class="font-semibold">{{ stepFields.length === currentStep + 1 ? 'submit' : 'next' }}</span>.
:key="field.uuid" </template>
> <template v-else>
<label <div
:for="field.uuid" v-for="(field, index) in currentStepFields"
class="flex items-center space-x-3" :key="field.uuid"
> >
<input <label
type="hidden" :for="field.uuid"
:name="`values[${field.uuid}]`" class="flex items-center space-x-3"
:value="!!values[field.uuid]"
> >
<input <input
:id="field.uuid" type="hidden"
type="checkbox" :name="`values[${field.uuid}]`"
class="base-checkbox !h-7 !w-7" :value="!!values[field.uuid]"
:checked="!!values[field.uuid]" >
@click="[$refs.areas.scrollIntoField(field), values[field.uuid] = !values[field.uuid]]" <input
> :id="field.uuid"
<span class="text-xl"> type="checkbox"
{{ field.name || field.type + ' ' + (index + 1) }} class="base-checkbox !h-7 !w-7"
</span> :checked="!!values[field.uuid]"
</label> @click="[$refs.areas.scrollIntoField(field), values[field.uuid] = !values[field.uuid]]"
</div> >
<span class="text-xl">
{{ field.name || field.type + ' ' + (index + 1) }}
</span>
</label>
</div>
</template>
</div> </div>
</div> </div>
<ImageStep <ImageStep
@@ -374,6 +380,9 @@ export default {
currentStepFields () { currentStepFields () {
return this.stepFields[this.currentStep] return this.stepFields[this.currentStep]
}, },
isAnonymousChecboxes () {
return this.currentField.type === 'checkbox' && this.currentStepFields.every((e) => !e.name) && this.currentStepFields.length > 4
},
isButtonDisabled () { isButtonDisabled () {
return this.isSubmitting || return this.isSubmitting ||
(this.currentField.required && ['image', 'file'].includes(this.currentField.type) && !this.values[this.currentField.uuid]?.length) || (this.currentField.required && ['image', 'file'].includes(this.currentField.type) && !this.values[this.currentField.uuid]?.length) ||
+26 -2
View File
@@ -64,8 +64,11 @@
:with-arrows="template.schema.length > 1" :with-arrows="template.schema.length > 1"
:item="item" :item="item"
:document="sortedDocuments[index]" :document="sortedDocuments[index]"
:template="template"
:is-direct-upload="isDirectUpload"
@scroll-to="scrollIntoDocument(item)" @scroll-to="scrollIntoDocument(item)"
@remove="onDocumentRemove" @remove="onDocumentRemove"
@replace="onDocumentReplace"
@up="moveDocument(item, -1)" @up="moveDocument(item, -1)"
@down="moveDocument(item, 1)" @down="moveDocument(item, 1)"
@change="save" @change="save"
@@ -133,7 +136,7 @@
<p> <p>
<button <button
class="base-button" class="base-button"
@click="drawField = false" @click="drawField = null"
> >
Cancel Cancel
</button> </button>
@@ -353,7 +356,7 @@ export default {
if (this.selectedField?.type === this.dragFieldType) { if (this.selectedField?.type === this.dragFieldType) {
baseArea = this.selectedAreaRef.value baseArea = this.selectedAreaRef.value
} else if (previousField?.areas) { } else if (previousField?.areas?.length) {
baseArea = previousField.areas[previousField.areas.length - 1] baseArea = previousField.areas[previousField.areas.length - 1]
} else { } else {
if (['checkbox'].includes(this.dragFieldType)) { if (['checkbox'].includes(this.dragFieldType)) {
@@ -417,6 +420,27 @@ export default {
this.template.schema.splice(this.template.schema.indexOf(item), 1) this.template.schema.splice(this.template.schema.indexOf(item), 1)
} }
this.template.fields.forEach((field) => {
[...(field.areas || [])].forEach((area) => {
if (area.attachment_uuid === item.attachment_uuid) {
field.areas.splice(field.areas.indexOf(area), 1)
}
})
})
this.save()
},
onDocumentReplace ({ replaceSchemaItem, schema, documents }) {
this.template.schema.splice(this.template.schema.indexOf(replaceSchemaItem), 1, schema[0])
this.template.documents.push(...documents)
this.template.fields.forEach((field) => {
(field.areas || []).forEach((area) => {
if (area.attachment_uuid === replaceSchemaItem.attachment_uuid) {
area.attachment_uuid = schema[0].attachment_uuid
}
})
})
this.save() this.save()
}, },
moveDocument (item, direction) { moveDocument (item, direction) {
@@ -93,6 +93,11 @@ export default {
} }
} }
}, },
mounted () {
if (this.isDirectUpload) {
import('@rails/activestorage')
}
},
methods: { methods: {
upload: Upload.methods.upload, upload: Upload.methods.upload,
onDropFiles (e) { onDropFiles (e) {
@@ -172,6 +172,7 @@ export default {
name: '', name: '',
uuid: v4(), uuid: v4(),
required: type !== 'checkbox', required: type !== 'checkbox',
areas: [],
submitter_uuid: this.selectedSubmitter.uuid, submitter_uuid: this.selectedSubmitter.uuid,
type type
} }
+59 -29
View File
@@ -9,39 +9,51 @@
loading="lazy" loading="lazy"
> >
<div <div
class="group flex justify-end cursor-pointer top-0 bottom-0 left-0 right-0 absolute" class="group flex justify-end cursor-pointer top-0 bottom-0 left-0 right-0 absolute p-1"
@click="$emit('scroll-to', item)" @click="$emit('scroll-to', item)"
> >
<div <div class="flex justify-between w-full">
class="flex flex-col justify-between opacity-0 group-hover:opacity-100" <div style="width: 26px" />
> <div class="">
<div class="rounded-bl rounded-tr bg-white border"> <ReplaceButton
<button :is-direct-upload="isDirectUpload"
class="rounded-bl rounded-tr hover:text-base-100 hover:bg-base-content w-full transition-colors" :template-id="template.id"
style="width: 24px" class="opacity-0 group-hover:opacity-100"
@click.stop="$emit('remove', item)" @click.stop
> @success="$emit('replace', { replaceSchemaItem: item, ...$event })"
&times; />
</button>
</div> </div>
<div <div
v-if="withArrows" class="flex flex-col justify-between opacity-0 group-hover:opacity-100"
class="flex flex-col border rounded-br rounded-tl bg-white divide-y"
> >
<button <div>
class="rounded-tl hover:text-base-100 hover:bg-base-content w-full transition-colors" <button
style="width: 24px" class="btn border-base-200 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors"
@click.stop="$emit('up', item)" style="width: 24px; height: 24px"
@click.stop="$emit('remove', item)"
>
&times;
</button>
</div>
<div
v-if="withArrows"
class="flex flex-col space-y-1"
> >
&uarr; <button
</button> class="btn border-base-200 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors"
<button style="width: 24px; height: 24px"
class="rounded-br hover:text-base-100 hover:bg-base-content w-full transition-colors" @click.stop="$emit('up', item)"
style="width: 24px" >
@click.stop="$emit('down', item)" &uarr;
> </button>
&darr; <button
</button> class="btn border-base-200 bg-white text-base-content btn-xs rounded hover:text-base-100 hover:bg-base-content hover:border-base-content w-full transition-colors"
style="width: 24px; height: 24px"
@click.stop="$emit('down', item)"
>
&darr;
</button>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -60,34 +72,52 @@
<script> <script>
import Contenteditable from './contenteditable' import Contenteditable from './contenteditable'
import Upload from './upload'
import ReplaceButton from './replace'
export default { export default {
name: 'DocumentPreview', name: 'DocumentPreview',
components: { components: {
Contenteditable Contenteditable,
ReplaceButton
}, },
props: { props: {
item: { item: {
type: Object, type: Object,
required: true required: true
}, },
template: {
type: Object,
required: true
},
document: { document: {
type: Object, type: Object,
required: true required: true
}, },
isDirectUpload: {
type: Boolean,
required: true,
default: false
},
withArrows: { withArrows: {
type: Boolean, type: Boolean,
required: false, required: false,
default: true default: true
} }
}, },
emits: ['scroll-to', 'change', 'remove', 'up', 'down'], emits: ['scroll-to', 'change', 'remove', 'up', 'down', 'replace'],
computed: { computed: {
previewImage () { previewImage () {
return this.document.preview_images[0] return this.document.preview_images[0]
} }
}, },
mounted () {
if (this.isDirectUpload) {
import('@rails/activestorage')
}
},
methods: { methods: {
upload: Upload.methods.upload,
onUpdateName (value) { onUpdateName (value) {
this.item.name = value this.item.name = value
@@ -0,0 +1,70 @@
<template>
<label
:for="inputId"
class="btn btn-neutral btn-xs text-white transition-none"
:class="{ 'opacity-100': isLoading || isProcessing }"
>
{{ message }}
<form
ref="form"
class="hidden"
>
<input
:id="inputId"
ref="input"
name="files[]"
type="file"
accept="image/*, application/pdf"
@change="upload"
>
</form>
</label>
</template>
<script>
import Upload from './upload'
export default {
name: 'ReplaceDocument',
props: {
templateId: {
type: [Number, String],
required: true
},
isDirectUpload: {
type: Boolean,
required: true,
default: false
}
},
emits: ['success'],
data () {
return {
isLoading: false,
isProcessing: false
}
},
computed: {
inputId () {
return 'el' + Math.random().toString(32).split('.')[1]
},
message () {
if (this.isLoading) {
return 'Uploading...'
} else if (this.isProcessing) {
return 'Processing...'
} else {
return 'Replace'
}
}
},
mounted () {
if (this.isDirectUpload) {
import('@rails/activestorage')
}
},
methods: {
upload: Upload.methods.upload
}
}
</script>
+13 -6
View File
@@ -4,12 +4,15 @@
# #
# Table name: submissions # Table name: submissions
# #
# id :bigint not null, primary key # id :bigint not null, primary key
# deleted_at :datetime # deleted_at :datetime
# created_at :datetime not null # template_fields :text
# updated_at :datetime not null # template_schema :text
# created_by_user_id :bigint # template_submitters :text
# template_id :bigint not null # created_at :datetime not null
# updated_at :datetime not null
# created_by_user_id :bigint
# template_id :bigint not null
# #
# Indexes # Indexes
# #
@@ -27,5 +30,9 @@ class Submission < ApplicationRecord
has_many :submitters, dependent: :destroy has_many :submitters, dependent: :destroy
serialize :template_fields, JSON
serialize :template_schema, JSON
serialize :template_submitters, JSON
scope :active, -> { where(deleted_at: nil) } scope :active, -> { where(deleted_at: nil) }
end end
+3
View File
@@ -44,6 +44,9 @@ class Template < ApplicationRecord
has_many_attached :documents has_many_attached :documents
has_many :schema_documents, ->(e) { where(uuid: e.schema.pluck('attachment_uuid')) },
class_name: 'ActiveStorage::Attachment', dependent: :destroy, as: :record, inverse_of: :record
has_many :submissions, dependent: :destroy has_many :submissions, dependent: :destroy
scope :active, -> { where(deleted_at: nil) } scope :active, -> { where(deleted_at: nil) }
+4
View File
@@ -56,6 +56,10 @@ class User < ApplicationRecord
!deleted_at? !deleted_at?
end end
def remember_me
true
end
def initials def initials
[first_name.first, last_name.first].join.upcase [first_name.first, last_name.first].join.upcase
end end
-8
View File
@@ -10,14 +10,6 @@
<%= f.label :password, class: 'label' %> <%= f.label :password, class: 'label' %>
<%= f.password_field :password, autocomplete: 'current-password', class: 'base-input' %> <%= f.password_field :password, autocomplete: 'current-password', class: 'base-input' %>
</div> </div>
<% if devise_mapping.rememberable? %>
<div class="form-control">
<%= f.label :remember_me, class: 'flex items-center cursor-pointer' do %>
<%= f.check_box :remember_me, checked: true, class: 'base-checkbox' %>
<span class="label">Remember me</span>
<% end %>
</div>
<% end %>
</div> </div>
<div class="form-control"> <div class="form-control">
<%= f.button button_title(title: 'Log In', disabled_with: 'Logging In'), class: 'base-button' %> <%= f.button button_title(title: 'Log In', disabled_with: 'Logging In'), class: 'base-button' %>
+6 -6
View File
@@ -23,7 +23,7 @@
</div> </div>
<div class="flex md:max-h-[calc(100vh-60px)]"> <div class="flex md:max-h-[calc(100vh-60px)]">
<div class="overflow-y-auto overflow-x-hidden hidden lg:block w-52 flex-none pr-3 mt-0.5 pt-0.5"> <div class="overflow-y-auto overflow-x-hidden hidden lg:block w-52 flex-none pr-3 mt-0.5 pt-0.5">
<% @submission.template.schema.each do |item| %> <% (@submission.template_schema || @submission.template.schema).each do |item| %>
<% document = @submission.template.documents.find { |a| item['attachment_uuid'] == a.uuid } %> <% document = @submission.template.documents.find { |a| item['attachment_uuid'] == a.uuid } %>
<a href="#<%= "page-#{document.uuid}-0" %>" onclick="[event.preventDefault(), window[event.target.closest('a').href.split('#')[1]].scrollIntoView({ behavior: 'smooth', block: 'start' })]" class="block cursor-pointer"> <a href="#<%= "page-#{document.uuid}-0" %>" onclick="[event.preventDefault(), window[event.target.closest('a').href.split('#')[1]].scrollIntoView({ behavior: 'smooth', block: 'start' })]" class="block cursor-pointer">
<img src="<%= document.preview_images.first.url %>" width="<%= document.preview_images.first.metadata['width'] %>" height="<%= document.preview_images.first.metadata['height'] %>" class="rounded border" loading="lazy"> <img src="<%= document.preview_images.first.url %>" width="<%= document.preview_images.first.metadata['width'] %>" height="<%= document.preview_images.first.metadata['height'] %>" class="rounded border" loading="lazy">
@@ -35,10 +35,10 @@
</div> </div>
<div class="w-full overflow-y-auto overflow-x-hidden mt-0.5 pt-0.5"> <div class="w-full overflow-y-auto overflow-x-hidden mt-0.5 pt-0.5">
<div class="pr-3.5 pl-0.5"> <div class="pr-3.5 pl-0.5">
<% fields_index = Templates.build_field_areas_index(@submission.template) %> <% fields_index = Templates.build_field_areas_index(@submission.template_fields || @submission.template.fields) %>
<% values = @submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) } %> <% values = @submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) } %>
<% attachments_index = ActiveStorage::Attachment.where(record: @submission.submitters, name: :attachments).preload(:blob).index_by(&:uuid) %> <% attachments_index = ActiveStorage::Attachment.where(record: @submission.submitters, name: :attachments).preload(:blob).index_by(&:uuid) %>
<% @submission.template.schema.each do |item| %> <% (@submission.template_schema || @submission.template.schema).each do |item| %>
<% document = @submission.template.documents.find { |e| e.uuid == item['attachment_uuid'] } %> <% document = @submission.template.documents.find { |e| e.uuid == item['attachment_uuid'] } %>
<% document.preview_images.sort_by { |a| a.filename.base.to_i }.each_with_index do |page, index| %> <% document.preview_images.sort_by { |a| a.filename.base.to_i }.each_with_index do |page, index| %>
<div id="<%= "page-#{document.uuid}-#{index}" %>" class="relative"> <div id="<%= "page-#{document.uuid}-#{index}" %>" class="relative">
@@ -57,15 +57,15 @@
</div> </div>
<div class="hidden md:block relative w-80 flex-none pt-0.5 pr-4 pl-0.5 overflow-auto space"> <div class="hidden md:block relative w-80 flex-none pt-0.5 pr-4 pl-0.5 overflow-auto space">
<% colors = %w[bg-red-500 bg-sky-500 bg-emerald-500 bg-yellow-300 bg-purple-600] %> <% colors = %w[bg-red-500 bg-sky-500 bg-emerald-500 bg-yellow-300 bg-purple-600] %>
<% submitter_fields_index = @submission.template.fields.group_by { |f| f['submitter_uuid'] } %> <% submitter_fields_index = (@submission.template_fields || @submission.template.fields).group_by { |f| f['submitter_uuid'] } %>
<% @submission.template.submitters.each_with_index do |item, index| %> <% (@submission.template_submitters || @submission.template.submitters).each_with_index do |item, index| %>
<% submitter = @submission.submitters.find { |e| e.uuid == item['uuid'] } %> <% submitter = @submission.submitters.find { |e| e.uuid == item['uuid'] } %>
<div class="sticky -top-1 bg-base-100 pt-1 -mt-1 z-10"> <div class="sticky -top-1 bg-base-100 pt-1 -mt-1 z-10">
<div class="border border-base-300 rounded-md px-2 py-1 mb-1"> <div class="border border-base-300 rounded-md px-2 py-1 mb-1">
<div class="flex items-center space-x-1"> <div class="flex items-center space-x-1">
<span class="mx-1 w-3 h-3 rounded-full <%= colors[index] %>"></span> <span class="mx-1 w-3 h-3 rounded-full <%= colors[index] %>"></span>
<span class="text-lg"> <span class="text-lg">
<%= @submission.template.submitters.find { |e| e['uuid'] == submitter&.uuid }&.dig('name') || "#{(index + 1).ordinalize} Submitter" %> <%= (@submission.template_submitters || @submission.template.submitters).find { |e| e['uuid'] == submitter&.uuid }&.dig('name') || "#{(index + 1).ordinalize} Submitter" %>
</span> </span>
</div> </div>
<div class="flex items-center space-x-1 mt-1"> <div class="flex items-center space-x-1 mt-1">
+3 -3
View File
@@ -1,4 +1,4 @@
<% fields_index = Templates.build_field_areas_index(@submitter.submission.template) %> <% fields_index = Templates.build_field_areas_index(@submitter.submission.template_fields || @submitter.submission.template.fields) %>
<% values = @submitter.submission.submitters.where.not(id: @submitter.id).reduce({}) { |acc, sub| acc.merge(sub.values) } %> <% values = @submitter.submission.submitters.where.not(id: @submitter.id).reduce({}) { |acc, sub| acc.merge(sub.values) } %>
<% attachments_index = ActiveStorage::Attachment.where(record: @submitter.submission.submitters, name: :attachments).preload(:blob).index_by(&:uuid) %> <% attachments_index = ActiveStorage::Attachment.where(record: @submitter.submission.submitters, name: :attachments).preload(:blob).index_by(&:uuid) %>
<div style="max-height: -webkit-fill-available;"> <div style="max-height: -webkit-fill-available;">
@@ -10,7 +10,7 @@
<span>DocuSeal</span> <span>DocuSeal</span>
</a> </a>
</div> </div>
<% @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'] } %>
<% document.preview_images.sort_by { |a| a.filename.base.to_i }.each_with_index do |page, index| %> <% document.preview_images.sort_by { |a| a.filename.base.to_i }.each_with_index do |page, index| %>
<div class="relative my-4 shadow-md"> <div class="relative my-4 shadow-md">
@@ -32,7 +32,7 @@
<div class="fixed bottom-0 w-full h-0 z-20"> <div class="fixed bottom-0 w-full h-0 z-20">
<div class="mx-auto" style="max-width: 1000px"> <div class="mx-auto" style="max-width: 1000px">
<div class="relative md:mx-32"> <div class="relative md:mx-32">
<submission-form data-is-demo="<%= Docuseal.demo? %>" data-is-direct-upload="<%= Docuseal.active_storage_public? %>" data-submitter-uuid="<%= @submitter.uuid %>" data-submitter-slug="<%= @submitter.slug %>" data-can-send-email="<%= Accounts.can_send_emails?(Struct.new(:id).new(@submitter.submission.template.account_id)) %>" data-attachments="<%= attachments_index.values.select { |e| e.record_id == @submitter.id }.to_json(only: %i[uuid], methods: %i[url filename content_type]) %>" data-fields="<%= @submitter.submission.template.fields.select { |f| f['submitter_uuid'] == @submitter.uuid }.to_json %>" data-values="<%= @submitter.values.to_json %>" data-authenticity-token="<%= form_authenticity_token %>"></submission-form> <submission-form data-is-demo="<%= Docuseal.demo? %>" data-is-direct-upload="<%= Docuseal.active_storage_public? %>" data-submitter-uuid="<%= @submitter.uuid %>" data-submitter-slug="<%= @submitter.slug %>" data-can-send-email="<%= Accounts.can_send_emails?(Struct.new(:id).new(@submitter.submission.template.account_id)) %>" data-attachments="<%= attachments_index.values.select { |e| e.record_id == @submitter.id }.to_json(only: %i[uuid], methods: %i[url filename content_type]) %>" data-fields="<%= (@submitter.submission.template_fields || @submitter.submission.template.fields).select { |f| f['submitter_uuid'] == @submitter.uuid }.to_json %>" data-values="<%= @submitter.values.to_json %>" data-authenticity-token="<%= form_authenticity_token %>"></submission-form>
</div> </div>
</div> </div>
</div> </div>
+1 -1
View File
@@ -1 +1 @@
<template-builder data-is-direct-upload="<%= Docuseal.active_storage_public? %>" data-template="<%= @template.to_json(include: { documents: { include: { preview_images: { methods: %i[url metadata filename] } } } }) %>"></template-builder> <template-builder data-is-direct-upload="<%= Docuseal.active_storage_public? %>" data-template="<%= @template.as_json.merge(documents: @template.schema_documents.as_json(include: { preview_images: { methods: %i[url metadata filename] } })).to_json %>"></template-builder>
+1 -1
View File
@@ -37,7 +37,7 @@
<div class="space-y-4"> <div class="space-y-4">
<% @submissions.each do |submission| %> <% @submissions.each do |submission| %>
<a href="<%= submission_path(submission) %>" class="bg-base-200 w-full flex flex-col md:flex-row space-y-4 md:space-y-0 md:justify-between rounded-2xl px-6 py-5 md:items-center"> <a href="<%= submission_path(submission) %>" class="bg-base-200 w-full flex flex-col md:flex-row space-y-4 md:space-y-0 md:justify-between rounded-2xl px-6 py-5 md:items-center">
<% submitters = submission.template.submitters.filter_map { |item| submission.submitters.find { |e| e.uuid == item['uuid'] } } %> <% submitters = (submission.template_submitters || submission.template.submitters).filter_map { |item| submission.submitters.find { |e| e.uuid == item['uuid'] } } %>
<% is_submission_complated = submitters.all?(&:completed_at?) %> <% is_submission_complated = submitters.all?(&:completed_at?) %>
<% if submitters.size == 1 %> <% if submitters.size == 1 %>
<div> <div>
+2 -2
View File
@@ -76,7 +76,7 @@ Rails.application.configure do
# Set this to true and configure the email server for immediate delivery to raise delivery errors. # Set this to true and configure the email server for immediate delivery to raise delivery errors.
config.action_mailer.raise_delivery_errors = false config.action_mailer.raise_delivery_errors = false
if ENV['SMTP_USERNAME'] if ENV['SMTP_ADDRESS']
config.action_mailer.delivery_method = :smtp config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = { config.action_mailer.smtp_settings = {
address: ENV.fetch('SMTP_ADDRESS', nil), address: ENV.fetch('SMTP_ADDRESS', nil),
@@ -86,7 +86,7 @@ Rails.application.configure do
password: ENV.fetch('SMTP_PASSWORD', nil), password: ENV.fetch('SMTP_PASSWORD', nil),
authentication: ENV.fetch('SMTP_AUTHENTICATION', 'plain'), authentication: ENV.fetch('SMTP_AUTHENTICATION', 'plain'),
enable_starttls_auto: ENV['SMTP_ENABLE_STARTTLS_AUTO'] == 'true' enable_starttls_auto: ENV['SMTP_ENABLE_STARTTLS_AUTO'] == 'true'
} }.compact
end end
# Enable locale fallbacks for I18n (makes lookups for any locale fall back to # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
+1 -1
View File
@@ -159,7 +159,7 @@ Devise.setup do |config|
# ==> Configuration for :rememberable # ==> Configuration for :rememberable
# The time the user will be remembered without asking for credentials again. # The time the user will be remembered without asking for credentials again.
# config.remember_for = 2.weeks config.remember_for = 2.years
# Invalidates all the remember me tokens when the user signs out. # Invalidates all the remember me tokens when the user signs out.
config.expire_all_remember_me_on_sign_out = true config.expire_all_remember_me_on_sign_out = true
+3 -1
View File
@@ -1,3 +1,5 @@
# frozen_string_literal: true # frozen_string_literal: true
ActiveRecord::Tasks::DatabaseTasks.migrate if ENV['RAILS_ENV'] == 'production' Rails.configuration.to_prepare do
ActiveRecord::Tasks::DatabaseTasks.migrate if ENV['RAILS_ENV'] == 'production'
end
@@ -0,0 +1,31 @@
# frozen_string_literal: true
class AddTemplateFieldsToSubmission < ActiveRecord::Migration[7.0]
class MigrationTemplate < ApplicationRecord
self.table_name = 'templates'
end
class MigrationSubmission < ApplicationRecord
self.table_name = 'submissions'
end
def up
add_column :submissions, :template_fields, :text
add_column :submissions, :template_schema, :text
add_column :submissions, :template_submitters, :text
MigrationTemplate.all.each do |template|
MigrationSubmission.where(template_id: template.id).each do |submission|
submission.update_columns(template_fields: template.fields,
template_schema: template.schema,
template_submitters: template.submitters)
end
end
end
def down
remove_column :submissions, :template_fields
remove_column :submissions, :template_schema
remove_column :submissions, :template_submitters
end
end
+4 -1
View File
@@ -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_07_01_075115) do ActiveRecord::Schema[7.0].define(version: 2023_07_26_062428) 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"
@@ -77,6 +77,9 @@ ActiveRecord::Schema[7.0].define(version: 2023_07_01_075115) do
t.datetime "deleted_at" t.datetime "deleted_at"
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.text "template_fields"
t.text "template_schema"
t.text "template_submitters"
t.index ["created_by_user_id"], name: "index_submissions_on_created_by_user_id" t.index ["created_by_user_id"], name: "index_submissions_on_created_by_user_id"
t.index ["template_id"], name: "index_submissions_on_template_id" t.index ["template_id"], name: "index_submissions_on_template_id"
end end
+9
View File
@@ -1,4 +1,13 @@
# frozen_string_literal: true # frozen_string_literal: true
module Submissions module Submissions
module_function
def update_template_fields!(submission)
submission.template_fields = submission.template.fields
submission.template_schema = submission.template.schema
submission.template_submitters = submission.template.submitters
submission.save!
end
end end
@@ -27,7 +27,7 @@ module Submissions
pdfs_index = build_pdfs_index(submitter) pdfs_index = build_pdfs_index(submitter)
template.fields.each do |field| submitter.submission.template_fields.each do |field|
next if field['submitter_uuid'] != submitter.uuid next if field['submitter_uuid'] != submitter.uuid
field.fetch('areas', []).each do |area| field.fetch('areas', []).each do |area|
@@ -159,7 +159,7 @@ module Submissions
original_documents = template.documents.preload(:blob) original_documents = template.documents.preload(:blob)
results = results =
template.schema.map do |item| submitter.submission.template_schema.map do |item|
pdf = pdfs_index[item['attachment_uuid']] pdf = pdfs_index[item['attachment_uuid']]
attachment = save_signed_pdf(pdf:, submitter:, certs:, uuid: item['attachment_uuid'], name: item['name']) attachment = save_signed_pdf(pdf:, submitter:, certs:, uuid: item['attachment_uuid'], name: item['name'])
+2 -2
View File
@@ -3,10 +3,10 @@
module Templates module Templates
module_function module_function
def build_field_areas_index(template) def build_field_areas_index(fields)
hash = {} hash = {}
template.fields.each do |field| fields.each do |field|
(field['areas'] || []).each do |area| (field['areas'] || []).each do |area|
hash[area['attachment_uuid']] ||= {} hash[area['attachment_uuid']] ||= {}
acc = (hash[area['attachment_uuid']][area['page']] ||= []) acc = (hash[area['attachment_uuid']][area['page']] ||= [])