Compare commits

..
19 Commits
Author SHA1 Message Date
Pete Matsyburka 46af87d89a mark field as submitter when navigating with areas 2023-11-13 23:13:49 +02:00
Pete Matsyburka 1066131143 use hash for field options 2023-11-13 01:56:50 +02:00
Pete Matsyburka a25022532e update default date format 2023-11-12 22:28:47 +02:00
Pete Matsyburka 5162f9a9dc remove redundant jwt rescue 2023-11-12 18:54:24 +02:00
Pete Matsyburka cc5694de43 set max length for cells form input 2023-11-12 00:24:58 +02:00
Pete Matsyburka f734ba280d fix cell resize 2023-11-12 00:24:58 +02:00
Pete Matsyburka 500d0da563 make phone field always required 2023-11-12 00:24:57 +02:00
Pete Matsyburka 6626a2b602 add default builder fields 2023-11-11 23:36:03 +02:00
Pete Matsyburka 07e66a6e53 adjust images preview loading in builder 2023-11-11 21:16:29 +02:00
Pete Matsyburka deef9c7d3b add FR time format locale 2023-11-11 15:28:43 +02:00
Pete Matsyburka 2ec8954dc8 add embedding to settings 2023-11-11 11:56:54 +02:00
Pete Matsyburka c1af803e5b adjust parse email 2023-11-11 02:43:39 +02:00
Pete Matsyburka 10abdbeba4 remove plausible from self-host 2023-11-11 02:43:39 +02:00
DocuSeal 1dbde7a6d8 add withUploadButton builder prop 2023-11-11 02:43:29 +02:00
Pete Matsyburka a27eb18670 add api link to settings 2023-11-09 01:20:28 +02:00
DocuSeal 44a49a4533 normalize file urls 2023-11-08 15:25:19 +02:00
DocuSeal fc11078cae adjust font size if long text 2023-11-07 11:47:26 +02:00
Pete Matsyburka a42ae0b39e fix file upload field 2023-11-07 02:28:06 +02:00
Pete Matsyburka 3163cdc761 fix builder tap on mobile 2023-11-06 22:08:49 +02:00
29 changed files with 364 additions and 114 deletions

No files matched your search

+1
View File
@@ -4,6 +4,7 @@ class AccountsController < ApplicationController
LOCALE_OPTIONS = { LOCALE_OPTIONS = {
'en-US' => 'English (United States)', 'en-US' => 'English (United States)',
'en-GB' => 'English (United Kingdom)', 'en-GB' => 'English (United Kingdom)',
'fr-FR' => 'French (France)',
'es-ES' => 'Spanish (Spain)', 'es-ES' => 'Spanish (Spain)',
'pt-PT' => 'Portuguese (Portugal)', 'pt-PT' => 'Portuguese (Portugal)',
'de-DE' => 'German (Germany)' 'de-DE' => 'German (Germany)'
+1 -1
View File
@@ -57,7 +57,7 @@ module Api
schema: [%i[attachment_uuid name]], schema: [%i[attachment_uuid name]],
submitters: [%i[name uuid]], submitters: [%i[name uuid]],
fields: [[:uuid, :submitter_uuid, :name, :type, :required, :readonly, :default_value, fields: [[:uuid, :submitter_uuid, :name, :type, :required, :readonly, :default_value,
{ options: [], areas: [%i[x y w h cell_w attachment_uuid page]] }]] { options: [%i[value uuid]], areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]]
) )
end end
end end
@@ -23,7 +23,7 @@ class PreviewDocumentPageController < ActionController::API
io = Templates::ProcessDocument.generate_pdf_preview_from_file(attachment, file_path, params[:id].to_i) io = Templates::ProcessDocument.generate_pdf_preview_from_file(attachment, file_path, params[:id].to_i)
render plain: io.tap(&:rewind).read render plain: io.tap(&:rewind).read, content_type: 'image/jpeg'
end end
def find_or_create_document_tempfile_path(attachment) def find_or_create_document_tempfile_path(attachment)
@@ -35,7 +35,7 @@ class TemplatesUploadsController < ApplicationController
def create_file_params_from_url def create_file_params_from_url
tempfile = Tempfile.new tempfile = Tempfile.new
tempfile.binmode tempfile.binmode
tempfile.write(conn.get(params[:url]).body) tempfile.write(conn.get(Addressable::URI.parse(params[:url]).display_uri.to_s).body)
tempfile.rewind tempfile.rewind
file = ActionDispatch::Http::UploadedFile.new( file = ActionDispatch::Http::UploadedFile.new(
+22 -2
View File
@@ -1,8 +1,8 @@
<template> <template>
<div <div
class="flex absolute text-[1.5vw] lg:text-base" class="flex absolute lg:text-base"
:style="computedStyle" :style="computedStyle"
:class="{ 'cursor-default': !submittable, 'bg-red-100 border cursor-pointer ': submittable, 'border-red-100': !isActive && submittable, 'bg-opacity-70': !isActive && !isValueSet && submittable, 'border-red-500 border-dashed z-10': isActive && submittable, 'bg-opacity-30': (isActive || isValueSet) && submittable }" :class="{ 'text-[1.5vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars, 'cursor-default': !submittable, 'bg-red-100 border cursor-pointer ': submittable, 'border-red-100': !isActive && submittable, 'bg-opacity-70': !isActive && !isValueSet && submittable, 'border-red-500 border-dashed z-10': isActive && submittable, 'bg-opacity-30': (isActive || isValueSet) && submittable }"
> >
<div <div
v-if="!isActive && !isValueSet && field.type !== 'checkbox' && submittable" v-if="!isActive && !isValueSet && field.type !== 'checkbox' && submittable"
@@ -102,6 +102,7 @@
</div> </div>
<div <div
v-else v-else
ref="textContainer"
class="flex items-center px-0.5" class="flex items-center px-0.5"
> >
<span v-if="Array.isArray(modelValue)"> <span v-if="Array.isArray(modelValue)">
@@ -173,6 +174,11 @@ export default {
} }
}, },
emits: ['update:model-value'], emits: ['update:model-value'],
data () {
return {
textOverflowChars: 0
}
},
computed: { computed: {
fieldNames () { fieldNames () {
return { return {
@@ -251,6 +257,20 @@ export default {
height: h * 100 + '%' height: h * 100 + '%'
} }
} }
},
watch: {
modelValue () {
if (this.field.type === 'text' && this.$refs.textContainer && (this.textOverflowChars === 0 || (this.textOverflowChars - 4) > this.modelValue.length)) {
this.textOverflowChars = this.$refs.textContainer.scrollHeight > this.$refs.textContainer.clientHeight ? this.modelValue.length : 0
}
}
},
mounted () {
if (this.field.type === 'text' && this.$refs.textContainer) {
this.$nextTick(() => {
this.textOverflowChars = this.$refs.textContainer.scrollHeight > this.$refs.textContainer.clientHeight ? this.modelValue.length : 0
})
}
} }
} }
</script> </script>
@@ -1,8 +1,8 @@
<template> <template>
<div> <div>
<div v-if="modelValue.length"> <div v-if="value.length">
<div <div
v-for="(val, index) in modelValue" v-for="(val, index) in value"
:key="index" :key="index"
class="flex mb-2" class="flex mb-2"
> >
@@ -26,10 +26,7 @@
{{ attachmentsIndex[val].filename }} {{ attachmentsIndex[val].filename }}
</span> </span>
</a> </a>
<button <button @click.prevent="removeAttachment(val)">
v-if="modelValue"
@click.prevent="removeAttachment(val)"
>
<IconTrashX <IconTrashX
:width="18" :width="18"
:heigh="19" :heigh="19"
@@ -91,18 +88,28 @@ export default {
} }
}, },
emits: ['attached', 'update:model-value'], emits: ['attached', 'update:model-value'],
computed: {
value: {
set (value) {
this.$emit('update:model-value', this.modelValue || [])
},
get () {
return this.modelValue || []
}
}
},
methods: { methods: {
removeAttachment (uuid) { removeAttachment (uuid) {
this.modelValue.splice(this.modelValue.indexOf(uuid), 1) this.value.splice(this.value.indexOf(uuid), 1)
this.$emit('update:model-value', this.modelValue) this.$emit('update:model-value', this.value)
}, },
onUpload (attachments) { onUpload (attachments) {
attachments.forEach((attachment) => { attachments.forEach((attachment) => {
this.$emit('attached', attachment) this.$emit('attached', attachment)
}) })
this.$emit('update:model-value', [...this.modelValue, ...attachments.map(a => a.uuid)]) this.$emit('update:model-value', [...this.value, ...attachments.map(a => a.uuid)])
} }
} }
} }
+19 -13
View File
@@ -99,12 +99,12 @@
{{ t('select_your_option') }} {{ t('select_your_option') }}
</option> </option>
<option <option
v-for="(option, index) in currentField.options" v-for="option in currentField.options"
:key="index" :key="option.uuid"
:selected="values[currentField.uuid] == option" :selected="values[currentField.uuid] == option.value"
:value="option" :value="option.value"
> >
{{ option }} {{ option.value }}
</option> </option>
</select> </select>
</div> </div>
@@ -119,24 +119,24 @@
<div class="flex w-full"> <div class="flex w-full">
<div class="space-y-3.5 mx-auto"> <div class="space-y-3.5 mx-auto">
<div <div
v-for="(option, index) in currentField.options" v-for="option in currentField.options"
:key="index" :key="option.uuid"
> >
<label <label
:for="currentField.uuid + option" :for="option.uuid"
class="flex items-center space-x-3" class="flex items-center space-x-3"
> >
<input <input
:id="currentField.uuid + option" :id="option.uuid"
v-model="values[currentField.uuid]" v-model="values[currentField.uuid]"
type="radio" type="radio"
class="base-radio !h-7 !w-7" class="base-radio !h-7 !w-7"
:name="`values[${currentField.uuid}]`" :name="`values[${currentField.uuid}]`"
:value="option" :value="option.value"
:required="currentField.required" :required="currentField.required"
> >
<span class="text-xl"> <span class="text-xl">
{{ option }} {{ option.value }}
</span> </span>
</label> </label>
</div> </div>
@@ -641,12 +641,20 @@ export default {
}) })
}, },
saveStep (formData) { saveStep (formData) {
const currentFieldUuid = this.currentField.uuid
if (this.isCompleted) { if (this.isCompleted) {
return Promise.resolve({}) return Promise.resolve({})
} else { } else {
return fetch(this.baseUrl + this.submitPath, { return fetch(this.baseUrl + this.submitPath, {
method: 'POST', method: 'POST',
body: formData || new FormData(this.$refs.form) body: formData || new FormData(this.$refs.form)
}).then((response) => {
if (response.status === 200) {
this.submittedValues[currentFieldUuid] = this.values[currentFieldUuid]
}
return response
}) })
} }
}, },
@@ -678,8 +686,6 @@ export default {
return Promise.reject(new Error(data.error)) return Promise.reject(new Error(data.error))
} }
this.submittedValues[this.currentField.uuid] = this.values[this.currentField.uuid]
if (isLastStep) { if (isLastStep) {
this.isSecondWalkthrough = true this.isSecondWalkthrough = true
} }
@@ -7,25 +7,25 @@
<div class="flex w-full"> <div class="flex w-full">
<div class="space-y-3.5 mx-auto"> <div class="space-y-3.5 mx-auto">
<div <div
v-for="(option, index) in field.options" v-for="option in field.options"
:key="index" :key="option.uuid"
> >
<label <label
:for="field.uuid + option" :for="option.uuid"
class="flex items-center space-x-3" class="flex items-center space-x-3"
> >
<input <input
:id="field.uuid + option" :id="option.uuid"
:ref="setInputRef" :ref="setInputRef"
type="checkbox" type="checkbox"
:name="`values[${field.uuid}][]`" :name="`values[${field.uuid}][]`"
:value="option" :value="option.value"
class="base-checkbox !h-7 !w-7" class="base-checkbox !h-7 !w-7"
:checked="(modelValue || []).includes(option)" :checked="(modelValue || []).includes(option.value)"
@change="onChange" @change="onChange"
> >
<span class="text-xl"> <span class="text-xl">
{{ option }} {{ option.value }}
</span> </span>
</label> </label>
</div> </div>
+15 -1
View File
@@ -15,6 +15,7 @@
v-if="!isTextArea" v-if="!isTextArea"
:id="field.uuid" :id="field.uuid"
v-model="text" v-model="text"
:maxlength="cellsMaxLegth"
class="base-input !text-2xl w-full !pr-11 -mr-10" class="base-input !text-2xl w-full !pr-11 -mr-10"
:required="field.required" :required="field.required"
:pattern="field.validation?.pattern" :pattern="field.validation?.pattern"
@@ -38,7 +39,7 @@
@focus="$emit('focus')" @focus="$emit('focus')"
/> />
<div <div
v-if="!isTextArea" v-if="!isTextArea && field.type !== 'cells'"
class="tooltip" class="tooltip"
:data-tip="t('toggle_multiline_text')" :data-tip="t('toggle_multiline_text')"
> >
@@ -80,6 +81,19 @@ export default {
} }
}, },
computed: { computed: {
cellsMaxLegth () {
if (this.field.type === 'cells') {
const area = this.field.areas?.[0]
if (area) {
return parseInt(area.w / area.cell_w)
} else {
return null
}
} else {
return null
}
},
text: { text: {
set (value) { set (value) {
this.$emit('update:model-value', value) this.$emit('update:model-value', value)
+23 -4
View File
@@ -68,7 +68,7 @@
@blur="onNameBlur" @blur="onNameBlur"
>{{ field.name || defaultName }}</span> >{{ field.name || defaultName }}</span>
<div <div
v-if="isNameFocus && field.type !== 'checkbox'" v-if="isNameFocus && !['checkbox', 'phone'].includes(field.type)"
class="flex items-center ml-1.5" class="flex items-center ml-1.5"
> >
<input <input
@@ -104,9 +104,12 @@
> >
<div <div
v-if="field?.default_value" v-if="field?.default_value"
class="text-[1.5vw] lg:text-base" :class="{ 'text-[1.5vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars }"
>
<div
ref="textContainer"
class="flex items-center px-0.5"
> >
<div class="flex items-center px-0.5">
<span class="whitespace-pre-wrap">{{ field.default_value }}</span> <span class="whitespace-pre-wrap">{{ field.default_value }}</span>
</div> </div>
</div> </div>
@@ -137,6 +140,7 @@ import FieldSubmitter from './field_submitter'
import FieldType from './field_type' import FieldType from './field_type'
import Field from './field' import Field from './field'
import { IconX } from '@tabler/icons-vue' import { IconX } from '@tabler/icons-vue'
import { v4 } from 'uuid'
export default { export default {
name: 'FieldArea', name: 'FieldArea',
@@ -173,6 +177,7 @@ export default {
isResize: false, isResize: false,
isDragged: false, isDragged: false,
isNameFocus: false, isNameFocus: false,
textOverflowChars: 0,
dragFrom: { x: 0, y: 0 } dragFrom: { x: 0, y: 0 }
} }
}, },
@@ -241,6 +246,20 @@ export default {
} }
} }
}, },
watch: {
'field.default_value' () {
if (this.field.type === 'text' && this.field.default_value && this.$refs.textContainer && (this.textOverflowChars === 0 || (this.textOverflowChars - 4) > this.field.default_value.length)) {
this.textOverflowChars = this.$el.clientHeight < this.$refs.textContainer.clientHeight ? this.field.default_value.length : 0
}
}
},
mounted () {
if (this.field.type === 'text' && this.field.default_value && this.$refs.textContainer && (this.textOverflowChars === 0 || (this.textOverflowChars - 4) > this.field.default_value)) {
this.$nextTick(() => {
this.textOverflowChars = this.$el.clientHeight < this.$refs.textContainer.clientHeight ? this.field.default_value.length : 0
})
}
},
methods: { methods: {
onNameFocus (e) { onNameFocus (e) {
this.selectedAreaRef.value = this.area this.selectedAreaRef.value = this.area
@@ -285,7 +304,7 @@ export default {
} }
if (['select', 'multiple', 'radio'].includes(this.field.type)) { if (['select', 'multiple', 'radio'].includes(this.field.type)) {
this.field.options ||= [''] this.field.options ||= [{ value: '', uuid: v4() }]
} }
(this.field.areas || []).forEach((area) => { (this.field.areas || []).forEach((area) => {
+44 -17
View File
@@ -77,6 +77,7 @@
:item="item" :item="item"
:document="sortedDocuments[index]" :document="sortedDocuments[index]"
:accept-file-types="acceptFileTypes" :accept-file-types="acceptFileTypes"
:with-replace-button="withUploadButton"
:editable="editable" :editable="editable"
:template="template" :template="template"
:is-direct-upload="isDirectUpload" :is-direct-upload="isDirectUpload"
@@ -92,7 +93,7 @@
:class="{ 'bg-base-100': withStickySubmitters }" :class="{ 'bg-base-100': withStickySubmitters }"
> >
<Upload <Upload
v-if="sortedDocuments.length && editable" v-if="sortedDocuments.length && editable && withUploadButton"
:accept-file-types="acceptFileTypes" :accept-file-types="acceptFileTypes"
:template-id="template.id" :template-id="template.id"
:is-direct-upload="isDirectUpload" :is-direct-upload="isDirectUpload"
@@ -106,7 +107,7 @@
class="pr-3.5 pl-0.5" class="pr-3.5 pl-0.5"
> >
<Dropzone <Dropzone
v-if="!sortedDocuments.length" v-if="!sortedDocuments.length && withUploadButton"
:template-id="template.id" :template-id="template.id"
:accept-file-types="acceptFileTypes" :accept-file-types="acceptFileTypes"
:is-direct-upload="isDirectUpload" :is-direct-upload="isDirectUpload"
@@ -122,9 +123,10 @@
:areas-index="fieldAreasIndex[document.uuid]" :areas-index="fieldAreasIndex[document.uuid]"
:selected-submitter="selectedSubmitter" :selected-submitter="selectedSubmitter"
:document="document" :document="document"
:is-drag="!!dragFieldType" :is-drag="!!dragField"
:draw-field="drawField" :draw-field="drawField"
:editable="editable" :editable="editable"
:base-url="baseUrl"
@draw="onDraw" @draw="onDraw"
@drop-field="onDropfield" @drop-field="onDropfield"
@remove-area="removeArea" @remove-area="removeArea"
@@ -133,6 +135,7 @@
v-if="isBreakpointLg && editable" v-if="isBreakpointLg && editable"
:with-arrows="template.schema.length > 1" :with-arrows="template.schema.length > 1"
:item="template.schema.find((item) => item.attachment_uuid === document.uuid)" :item="template.schema.find((item) => item.attachment_uuid === document.uuid)"
:with-replace-button="withUploadButton"
:document="document" :document="document"
:template="template" :template="template"
:is-direct-upload="isDirectUpload" :is-direct-upload="isDirectUpload"
@@ -149,6 +152,7 @@
class="pb-4" class="pb-4"
> >
<Upload <Upload
v-if="withUploadButton"
:template-id="template.id" :template-id="template.id"
:is-direct-upload="isDirectUpload" :is-direct-upload="isDirectUpload"
@success="updateFromUpload" @success="updateFromUpload"
@@ -218,12 +222,13 @@
:fields="template.fields" :fields="template.fields"
:submitters="template.submitters" :submitters="template.submitters"
:selected-submitter="selectedSubmitter" :selected-submitter="selectedSubmitter"
:default-fields="defaultFields"
:with-sticky-submitters="withStickySubmitters" :with-sticky-submitters="withStickySubmitters"
:editable="editable" :editable="editable"
@set-draw="drawField = $event" @set-draw="drawField = $event"
@set-drag="dragFieldType = $event" @set-drag="dragField = $event"
@change-submitter="selectedSubmitter = $event" @change-submitter="selectedSubmitter = $event"
@drag-end="dragFieldType = null" @drag-end="dragField = null"
@scroll-to-area="scrollToArea" @scroll-to-area="scrollToArea"
/> />
</div> </div>
@@ -296,6 +301,16 @@ export default {
required: false, required: false,
default: true default: true
}, },
defaultFields: {
type: Array,
required: false,
default: () => []
},
defaultSubmitters: {
type: Array,
required: false,
default: () => []
},
acceptFileTypes: { acceptFileTypes: {
type: String, type: String,
required: false, required: false,
@@ -316,6 +331,11 @@ export default {
required: false, required: false,
default: true default: true
}, },
withUploadButton: {
type: Boolean,
required: false,
default: true
},
withPhone: { withPhone: {
type: Boolean, type: Boolean,
required: false, required: false,
@@ -334,7 +354,7 @@ export default {
isSaving: false, isSaving: false,
selectedSubmitter: null, selectedSubmitter: null,
drawField: null, drawField: null,
dragFieldType: null dragField: null
} }
}, },
computed: { computed: {
@@ -366,6 +386,13 @@ export default {
} }
}, },
created () { created () {
this.defaultSubmitters.forEach((name, index) => {
const submitter = (this.template.submitters[index] ||= {})
submitter.name = name
submitter.uuid ||= v4()
})
this.selectedSubmitter = this.template.submitters[0] this.selectedSubmitter = this.template.submitters[0]
}, },
mounted () { mounted () {
@@ -402,7 +429,7 @@ export default {
} }
if (['select', 'multiple', 'radio'].includes(type)) { if (['select', 'multiple', 'radio'].includes(type)) {
field.options = [''] field.options = [{ value: '', uuid: v4() }]
} }
this.drawField = field this.drawField = field
@@ -558,14 +585,14 @@ export default {
onDropfield (area) { onDropfield (area) {
const field = { const field = {
name: '', name: '',
type: this.dragFieldType,
uuid: v4(), uuid: v4(),
submitter_uuid: this.selectedSubmitter.uuid, submitter_uuid: this.selectedSubmitter.uuid,
required: this.dragFieldType !== 'checkbox' required: this.dragField.type !== 'checkbox',
...this.dragField
} }
if (['select', 'multiple', 'radio'].includes(this.dragFieldType)) { if (['select', 'multiple', 'radio'].includes(field.type)) {
field.options = [''] field.options = [{ value: '', uuid: v4() }]
} }
const fieldArea = { const fieldArea = {
@@ -579,27 +606,27 @@ export default {
let baseArea let baseArea
if (this.selectedField?.type === this.dragFieldType) { if (this.selectedField?.type === field.type) {
baseArea = this.selectedAreaRef.value baseArea = this.selectedAreaRef.value
} else if (previousField?.areas?.length) { } 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(field.type)) {
baseArea = { baseArea = {
w: area.maskW / 30 / area.maskW, w: area.maskW / 30 / area.maskW,
h: area.maskW / 30 / area.maskW * (area.maskW / area.maskH) h: area.maskW / 30 / area.maskW * (area.maskW / area.maskH)
} }
} else if (this.dragFieldType === 'image') { } else if (field.type === 'image') {
baseArea = { baseArea = {
w: area.maskW / 5 / area.maskW, w: area.maskW / 5 / area.maskW,
h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH) h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH)
} }
} else if (this.dragFieldType === 'signature') { } else if (field.type === 'signature') {
baseArea = { baseArea = {
w: area.maskW / 5 / area.maskW, w: area.maskW / 5 / area.maskW,
h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH) / 2 h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH) / 2
} }
} else if (this.dragFieldType === 'initials') { } else if (field.type === 'initials') {
baseArea = { baseArea = {
w: area.maskW / 10 / area.maskW, w: area.maskW / 10 / area.maskW,
h: area.maskW / 35 / area.maskW h: area.maskW / 35 / area.maskW
@@ -616,7 +643,7 @@ export default {
fieldArea.h = baseArea.h fieldArea.h = baseArea.h
fieldArea.y = fieldArea.y - baseArea.h / 2 fieldArea.y = fieldArea.y - baseArea.h / 2
if (this.dragFieldType === 'cells') { if (field.type === 'cells') {
fieldArea.cell_w = baseArea.cell_w || (baseArea.w / 5) fieldArea.cell_w = baseArea.cell_w || (baseArea.w / 5)
} }
@@ -7,6 +7,7 @@
@update:model-value="onUpdateName" @update:model-value="onUpdateName"
/> />
<ReplaceButton <ReplaceButton
v-if="withReplaceButton"
:is-direct-upload="isDirectUpload" :is-direct-upload="isDirectUpload"
:template-id="template.id" :template-id="template.id"
@click.stop @click.stop
@@ -67,6 +68,11 @@ export default {
required: true, required: true,
default: false default: false
}, },
withReplaceButton: {
type: Boolean,
required: true,
default: true
},
withArrows: { withArrows: {
type: Boolean, type: Boolean,
required: false, required: false,
+13 -1
View File
@@ -49,6 +49,11 @@ export default {
required: false, required: false,
default: null default: null
}, },
baseUrl: {
type: String,
required: false,
default: ''
},
isDrag: { isDrag: {
type: Boolean, type: Boolean,
required: false, required: false,
@@ -62,6 +67,13 @@ export default {
} }
}, },
computed: { computed: {
basePreviewUrl () {
if (this.baseUrl) {
return new URL(this.baseUrl).origin
} else {
return ''
}
},
numberOfPages () { numberOfPages () {
return this.document.metadata?.pdf?.number_of_pages || this.document.preview_images.length return this.document.metadata?.pdf?.number_of_pages || this.document.preview_images.length
}, },
@@ -72,7 +84,7 @@ export default {
return this.previewImagesIndex[i] || { return this.previewImagesIndex[i] || {
metadata: lazyloadMetadata, metadata: lazyloadMetadata,
id: Math.random().toString(), id: Math.random().toString(),
url: `/preview/${this.document.uuid}/${i}.jpg` url: this.basePreviewUrl + `/preview/${this.document.uuid}/${i}.jpg`
} }
}) })
}, },
+18 -5
View File
@@ -34,6 +34,7 @@
v-if="isNameFocus" v-if="isNameFocus"
class="flex items-center relative" class="flex items-center relative"
> >
<template v-if="field.type != 'phone'">
<input <input
:id="`required-checkbox-${field.uuid}`" :id="`required-checkbox-${field.uuid}`"
v-model="field.required" v-model="field.required"
@@ -47,6 +48,7 @@
@click.prevent="field.required = !field.required" @click.prevent="field.required = !field.required"
@mousedown.prevent @mousedown.prevent
>Required</label> >Required</label>
</template>
</div> </div>
<div <div
v-else-if="editable" v-else-if="editable"
@@ -104,7 +106,10 @@
Default value Default value
</label> </label>
</div> </div>
<li @click.stop> <li
v-if="field.type != 'phone'"
@click.stop
>
<label class="cursor-pointer py-1.5"> <label class="cursor-pointer py-1.5">
<input <input
v-model="field.required" v-model="field.required"
@@ -189,17 +194,19 @@
<div <div
v-if="field.options" v-if="field.options"
class="border-t border-base-300 mx-2 pt-2 space-y-1.5" class="border-t border-base-300 mx-2 pt-2 space-y-1.5"
draggable="true"
@dragstart.prevent.stop
> >
<div <div
v-for="(option, index) in field.options" v-for="(option, index) in field.options"
:key="index" :key="option.uuid"
class="flex space-x-1.5 items-center" class="flex space-x-1.5 items-center"
> >
<span class="text-sm w-3.5"> <span class="text-sm w-3.5">
{{ index + 1 }}. {{ index + 1 }}.
</span> </span>
<input <input
v-model="field.options[index]" v-model="option.value"
class="w-full input input-primary input-xs text-sm bg-transparent" class="w-full input input-primary input-xs text-sm bg-transparent"
type="text" type="text"
required required
@@ -215,7 +222,7 @@
<button <button
v-if="field.options" v-if="field.options"
class="text-center text-sm w-full pb-1" class="text-center text-sm w-full pb-1"
@click="[field.options.push(''), save()]" @click="addOption"
> >
+ Add option + Add option
</button> </button>
@@ -228,6 +235,7 @@
import Contenteditable from './contenteditable' import Contenteditable from './contenteditable'
import FieldType from './field_type' import FieldType from './field_type'
import { IconShape, IconNewSection, IconTrashX, IconCopy, IconSettings } from '@tabler/icons-vue' import { IconShape, IconNewSection, IconTrashX, IconCopy, IconSettings } from '@tabler/icons-vue'
import { v4 } from 'uuid'
export default { export default {
name: 'TemplateField', name: 'TemplateField',
@@ -304,6 +312,11 @@ export default {
closeDropdown () { closeDropdown () {
document.activeElement.blur() document.activeElement.blur()
}, },
addOption () {
this.field.options.push({ value: '', uuid: v4() })
this.save()
},
maybeUpdateOptions () { maybeUpdateOptions () {
delete this.field.default_value delete this.field.default_value
@@ -312,7 +325,7 @@ export default {
} }
if (['radio', 'multiple', 'select'].includes(this.field.type)) { if (['radio', 'multiple', 'select'].includes(this.field.type)) {
this.field.options ||= [''] this.field.options ||= [{ value: '', uuid: v4() }]
} }
(this.field.areas || []).forEach((area) => { (this.field.areas || []).forEach((area) => {
+47 -28
View File
@@ -32,6 +32,35 @@
@set-draw="$emit('set-draw', $event)" @set-draw="$emit('set-draw', $event)"
/> />
</div> </div>
<div v-if="submitterDefaultFields.length">
<hr class="mb-2">
<template
v-for="field in submitterDefaultFields"
:key="field.name"
>
<div
:style="{ backgroundColor: backgroundColor }"
draggable="true"
class="border border-base-300 rounded rounded-tr-none relative group mb-2"
@dragstart="onDragstart({ type: 'text', ...field })"
@dragend="$emit('drag-end')"
>
<div class="flex items-center justify-between relative cursor-grab">
<div class="flex items-center p-1 space-x-1">
<IconDrag />
<FieldType
:model-value="field.type || 'text'"
:editable="false"
:button-width="20"
/>
<span class="block pl-0.5">
{{ field.name }}
</span>
</div>
</div>
</div>
</template>
</div>
<div <div
v-if="editable" v-if="editable"
class="grid grid-cols-3 gap-1 pb-2" class="grid grid-cols-3 gap-1 pb-2"
@@ -45,35 +74,12 @@
draggable="true" draggable="true"
class="flex items-center justify-center border border-dashed border-base-300 w-full rounded relative" class="flex items-center justify-center border border-dashed border-base-300 w-full rounded relative"
:style="{ backgroundColor: backgroundColor }" :style="{ backgroundColor: backgroundColor }"
@dragstart="onDragstart(type)" @dragstart="onDragstart({ type: type })"
@dragend="$emit('drag-end')" @dragend="$emit('drag-end')"
@click="addField(type)" @click="addField(type)"
> >
<div class="w-0 absolute left-0"> <div class="w-0 absolute left-0">
<svg <IconDrag class="cursor-grab" />
xmlns="http://www.w3.org/2000/svg"
class="cursor-grab"
width="18"
height="18"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path
stroke="none"
d="M0 0h24v24H0z"
fill="none"
/>
<path d="M9 5m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
<path d="M9 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
<path d="M9 19m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
<path d="M15 5m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
<path d="M15 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
<path d="M15 19m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
</svg>
</div> </div>
<div class="flex items-center flex-col px-2 py-2"> <div class="flex items-center flex-col px-2 py-2">
<component :is="icon" /> <component :is="icon" />
@@ -134,12 +140,15 @@ import { v4 } from 'uuid'
import FieldType from './field_type' import FieldType from './field_type'
import FieldSubmitter from './field_submitter' import FieldSubmitter from './field_submitter'
import { IconLock } from '@tabler/icons-vue' import { IconLock } from '@tabler/icons-vue'
import IconDrag from './icon_drag'
export default { export default {
name: 'TemplateFields', name: 'TemplateFields',
components: { components: {
Field, Field,
FieldType,
FieldSubmitter, FieldSubmitter,
IconDrag,
IconLock IconLock
}, },
inject: ['save', 'backgroundColor', 'withPhone'], inject: ['save', 'backgroundColor', 'withPhone'],
@@ -153,6 +162,11 @@ export default {
required: false, required: false,
default: true default: true
}, },
defaultFields: {
type: Array,
required: false,
default: () => []
},
withStickySubmitters: { withStickySubmitters: {
type: Boolean, type: Boolean,
required: false, required: false,
@@ -178,11 +192,16 @@ export default {
fieldIcons: FieldType.computed.fieldIcons, fieldIcons: FieldType.computed.fieldIcons,
submitterFields () { submitterFields () {
return this.fields.filter((f) => f.submitter_uuid === this.selectedSubmitter.uuid) return this.fields.filter((f) => f.submitter_uuid === this.selectedSubmitter.uuid)
},
submitterDefaultFields () {
return this.defaultFields.filter((f) => {
return !this.fields.find((field) => field.name === f.name) && (!f.role || f.role === this.selectedSubmitter.name)
})
} }
}, },
methods: { methods: {
onDragstart (fieldType) { onDragstart (field) {
this.$emit('set-drag', fieldType) this.$emit('set-drag', field)
}, },
onFieldDragover (e) { onFieldDragover (e) {
const targetFieldUuid = e.target.closest('[data-uuid]')?.dataset?.uuid const targetFieldUuid = e.target.closest('[data-uuid]')?.dataset?.uuid
@@ -233,7 +252,7 @@ export default {
} }
if (['select', 'multiple', 'radio'].includes(type)) { if (['select', 'multiple', 'radio'].includes(type)) {
field.options = [''] field.options = [{ value: '', uuid: v4() }]
} }
this.fields.push(field) this.fields.push(field)
@@ -0,0 +1,31 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path
stroke="none"
d="M0 0h24v24H0z"
fill="none"
/>
<path d="M9 5m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
<path d="M9 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
<path d="M9 19m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
<path d="M15 5m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
<path d="M15 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
<path d="M15 19m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
</svg>
</template>
<script>
export default {
name: 'DragIcon'
}
</script>
+2 -2
View File
@@ -40,8 +40,8 @@
v-show="resizeDirection || isMove || isDrag || showMask || (drawField && isMobile)" v-show="resizeDirection || isMove || isDrag || showMask || (drawField && isMobile)"
id="mask" id="mask"
ref="mask" ref="mask"
class="top-0 bottom-0 left-0 right-0 absolute z-10" class="top-0 bottom-0 left-0 right-0 absolute"
:class="{ 'cursor-grab': isDrag || isMove, 'cursor-nwse-resize': drawField, [resizeDirectionClasses[resizeDirection]]: !!resizeDirectionClasses }" :class="{ 'z-10': !isMobile, 'cursor-grab': isDrag || isMove, 'cursor-nwse-resize': drawField, [resizeDirectionClasses[resizeDirection]]: !!resizeDirectionClasses }"
@pointermove="onPointermove" @pointermove="onPointermove"
@pointerdown="onStartDraw" @pointerdown="onStartDraw"
@dragover.prevent @dragover.prevent
@@ -19,6 +19,7 @@
<div style="width: 26px" /> <div style="width: 26px" />
<div class=""> <div class="">
<ReplaceButton <ReplaceButton
v-if="withReplaceButton"
:is-direct-upload="isDirectUpload" :is-direct-upload="isDirectUpload"
:template-id="template.id" :template-id="template.id"
:accept-file-types="acceptFileTypes" :accept-file-types="acceptFileTypes"
@@ -108,6 +109,11 @@ export default {
required: false, required: false,
default: 'image/*, application/pdf' default: 'image/*, application/pdf'
}, },
withReplaceButton: {
type: Boolean,
required: true,
default: true
},
isDirectUpload: { isDirectUpload: {
type: Boolean, type: Boolean,
required: true, required: true,
-9
View File
@@ -1,9 +0,0 @@
<script defer data-domain="<%= ENV.fetch('PLAUSIBLE_DOMAIN', nil) %>" src="https://plausible.io/js/script.manual.js"></script>
<script>window.plausible = window.plausible || function() { (window.plausible.q = window.plausible.q || []).push(arguments) }</script>
<script>
document.addEventListener("turbo:load", function (e) {
if (e.detail.url.match(/sign_in|sign_up|password/)) {
plausible('pageview')
}
})
</script>
+9 -2
View File
@@ -77,10 +77,17 @@
<% end %> <% 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}/on_premise", class: 'text-base hover:bg-base-300', data: { prefetch: false } do %> <%= link_to Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/api") : "#{Docuseal::CONSOLE_URL}/on_premise", class: 'text-base hover:bg-base-300', data: { prefetch: false } do %>
Console <% if Docuseal.multitenant? %> API <% else %> Console <% end %>
<% end %> <% end %>
</li> </li>
<% if Docuseal.multitenant? %>
<li>
<%= link_to console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/embedding/form"), class: 'text-base hover:bg-base-300', data: { prefetch: false } do %>
Embedding
<% end %>
</li>
<% end %>
<% end %> <% end %>
</ul> </ul>
</menu-active> </menu-active>
+1 -1
View File
@@ -1,4 +1,4 @@
<div class="flex absolute text-[1.5vw] lg:text-base" style="width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%"> <div class="flex absolute <%= field['readonly'] ? 'text-[1.5vw] lg:text-xs' : 'text-[1.5vw] lg:text-base' %>" style="width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%">
<% if field['type'].in?(['signature', 'image', 'initials']) %> <% if field['type'].in?(['signature', 'image', 'initials']) %>
<img class="object-contain mx-auto" src="<%= attachments_index[value].url %>" loading="lazy"> <img class="object-contain mx-auto" src="<%= attachments_index[value].url %>" loading="lazy">
<% elsif field['type'] == 'file' %> <% elsif field['type'] == 'file' %>
+1 -1
View File
@@ -22,7 +22,7 @@ module DocuSeal
config.active_storage.routes_prefix = '' config.active_storage.routes_prefix = ''
config.i18n.available_locales = %i[en en-US en-GB es-ES pt-PT de-DE] config.i18n.available_locales = %i[en en-US en-GB es-ES fr-FR pt-PT de-DE]
config.i18n.fallbacks = [:en] config.i18n.fallbacks = [:en]
config.action_view.frozen_string_literal = true config.action_view.frozen_string_literal = true
+9 -2
View File
@@ -1,2 +1,9 @@
en: en-US:
hello: "Hello world" date:
formats:
default: "%m/%d/%Y"
en-GB:
date:
formats:
default: "%d/%m/%Y"
@@ -0,0 +1,51 @@
# frozen_string_literal: true
class UpdateFieldOptions < ActiveRecord::Migration[7.0]
class MigrationTemplate < ApplicationRecord
self.table_name = 'templates'
end
class MigrationSubmission < ApplicationRecord
self.table_name = 'submissions'
end
# rubocop:disable Metrics
def up
MigrationTemplate.find_each do |template|
next if template.fields.blank?
template_fields = JSON.parse(template.fields)
new_fields = template_fields.deep_dup
new_fields.each do |field|
if field['options'].present? && !field['options'].first.is_a?(Hash)
field['options'] = field['options'].map { |o| { value: o || '', uuid: SecureRandom.uuid } }
end
end
template.update_columns(fields: new_fields.to_json) if template_fields != new_fields
end
MigrationSubmission.find_each do |submission|
next if submission.template_fields.blank?
template_fields = JSON.parse(submission.template_fields)
new_fields = template_fields.deep_dup
new_fields.each do |field|
if field['options'].present? && !field['options'].first.is_a?(Hash)
field['options'] = field['options'].map { |o| { value: o || '', uuid: SecureRandom.uuid } }
end
end
submission.update_columns(template_fields: new_fields.to_json) if template_fields != new_fields
end
end
# rubocop:enable Metrics
def down
nil
end
end
+1 -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_11_02_171817) do ActiveRecord::Schema[7.0].define(version: 2023_11_12_224432) 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"
-2
View File
@@ -15,7 +15,5 @@ class AuthWithTokenStrategy < Devise::Strategies::Base
else else
fail!('Invalid token') fail!('Invalid token')
end end
rescue JWT::VerificationError
fail!('Invalid token')
end end
end end
+7 -3
View File
@@ -28,9 +28,7 @@ module Submissions
end end
def create_from_emails(template:, user:, emails:, source:, mark_as_sent: false) def create_from_emails(template:, user:, emails:, source:, mark_as_sent: false)
emails = emails.to_s.scan(User::EMAIL_REGEXP) unless emails.is_a?(Array) parse_emails(emails).uniq.map do |email|
emails.uniq.map do |email|
submission = template.submissions.new(created_by_user: user, source:, template_submitters: template.submitters) submission = template.submissions.new(created_by_user: user, source:, template_submitters: template.submitters)
submission.submitters.new(email: normalize_email(email), submission.submitters.new(email: normalize_email(email),
uuid: template.submitters.first['uuid'], uuid: template.submitters.first['uuid'],
@@ -40,6 +38,12 @@ module Submissions
end end
end end
def parse_emails(emails)
emails = emails.to_s.scan(User::EMAIL_REGEXP) unless emails.is_a?(Array)
emails
end
def create_from_submitters(template:, user:, submissions_attrs:, source:, mark_as_sent: false, def create_from_submitters(template:, user:, submissions_attrs:, source:, mark_as_sent: false,
submitters_order: DEFAULT_SUBMITTERS_ORDER) submitters_order: DEFAULT_SUBMITTERS_ORDER)
Submissions::CreateFromSubmitters.call( Submissions::CreateFromSubmitters.call(
@@ -157,6 +157,17 @@ module Submissions
lines = layouter.fit([text], area['w'] * width, height).lines lines = layouter.fit([text], area['w'] * width, height).lines
box_height = lines.sum(&:height) box_height = lines.sum(&:height)
if box_height > (area['h'] * height) + 1
text = HexaPDF::Layout::TextFragment.create(Array.wrap(value).join(', '),
font: pdf.fonts.add(FONT_NAME),
font_size: (font_size / 1.4).to_i)
lines = layouter.fit([text], area['w'] * width, height).lines
box_height = lines.sum(&:height)
end
height_diff = [0, box_height - (area['h'] * height)].max height_diff = [0, box_height - (area['h'] * height)].max
layouter.fit([text], area['w'] * width, height_diff.positive? ? box_height : area['h'] * height) layouter.fit([text], area['w'] * width, height_diff.positive? ? box_height : area['h'] * height)
+1 -1
View File
@@ -71,7 +71,7 @@ module Submitters
return blob if blob return blob if blob
data = conn.get(url).body data = conn.get(Addressable::URI.parse(url).display_uri.to_s).body
checksum = Digest::MD5.base64digest(data) checksum = Digest::MD5.base64digest(data)