mirror of
https://github.com/docusealco/docuseal
synced 2026-11-04 14:42:32 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
182f593d86 | ||
|
|
7f4548e5c7 | ||
|
|
5e4f207844 | ||
|
|
1f29e624e7 | ||
|
|
658e8a7bd6 | ||
|
|
08c3b04511 | ||
|
|
7779eb880d | ||
|
|
21b9ca8ea5 | ||
|
|
db7e3794d9 | ||
|
|
df1d78897a | ||
|
|
e1eb2da6f9 |
No files matched your search
@@ -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
|
||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -207,7 +212,7 @@ export default {
|
|||||||
},
|
},
|
||||||
formattedDate () {
|
formattedDate () {
|
||||||
if (this.field.type === 'date' && this.modelValue) {
|
if (this.field.type === 'date' && this.modelValue) {
|
||||||
return new Intl.DateTimeFormat([], { year: 'numeric', month: 'long', day: 'numeric' }).format(new Date(this.modelValue))
|
return new Intl.DateTimeFormat([], { year: 'numeric', month: 'long', day: 'numeric', timeZone: 'UTC' }).format(new Date(this.modelValue))
|
||||||
} else {
|
} else {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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) ||
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 })"
|
||||||
×
|
/>
|
||||||
</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)"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="withArrows"
|
||||||
|
class="flex flex-col space-y-1"
|
||||||
>
|
>
|
||||||
↑
|
<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)"
|
↑
|
||||||
>
|
</button>
|
||||||
↓
|
<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)"
|
||||||
|
>
|
||||||
|
↓
|
||||||
|
</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>
|
||||||
@@ -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
|
||||||
@@ -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) }
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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' %>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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 @@
|
|||||||
<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>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ module ActionMailerConfigsInterceptor
|
|||||||
port: email_configs.value['port'],
|
port: email_configs.value['port'],
|
||||||
tls: email_configs.value['port'].to_s == '465')
|
tls: email_configs.value['port'].to_s == '465')
|
||||||
|
|
||||||
message.from = email_configs.value['from_email']
|
message.from = "#{email_configs.account.name} <#{email_configs.value['from_email']}>"
|
||||||
else
|
else
|
||||||
message.delivery_method(:test)
|
message.delivery_method(:test)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -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
@@ -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']] ||= [])
|
||||||
|
|||||||
Reference in new issue
Block a user