Compare commits

...

41 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
Pete Matsyburka 5439c63ce7 hide mobile field draw box 2023-11-06 21:23:46 +02:00
Pete Matsyburka b78287242b fix previous submitter values on the form 2023-11-06 20:52:59 +02:00
Alex Turchyn b869ae38b3 allow to add multiple submitters on mobile 2023-11-06 20:50:29 +02:00
DocuSeal 4a320e9ba6 pass name on resubmit 2023-11-06 13:45:15 +02:00
DocuSeal 0025928a35 fix replace default value variable 2023-11-06 13:40:40 +02:00
DocuSeal c227a1988a fix api pagination limit 2023-11-06 13:18:16 +02:00
Pete Matsyburka 3ade45f541 add field draw button if without areas 2023-11-05 16:31:32 +02:00
Pete Matsyburka 17da8bde80 add upgrade button 2023-11-05 16:31:13 +02:00
Pete Matsyburka 59d9a59769 add field default values 2023-11-05 01:41:54 +02:00
Pete Matsyburka 7210badde9 drag and drop fields to order 2023-11-04 14:06:59 +02:00
DocuSeal 21659a73f3 adjust new template form 2023-11-04 01:31:27 +02:00
DocuSeal c0e49ebeea add builder editable prop 2023-11-04 01:19:32 +02:00
DocuSeal 2d24eac4a3 fix completed submissions scope query 2023-11-02 20:32:51 +02:00
DocuSeal 14c3c8181b add template application key 2023-11-02 19:33:01 +02:00
DocuSeal 036f6049a2 Revert "add new signup page"
This reverts commit f7132445a1.
2023-11-02 13:37:57 +02:00
DocuSeal 236e4cb35f fix typo 2023-11-01 21:32:56 +02:00
DocuSeal ee77cb0982 use stronger api token 2023-11-01 15:20:39 +02:00
DocuSeal d474ad05e5 prefill shared link with email 2023-11-01 14:10:01 +02:00
DocuSeal dbf1fa37c5 adjust completed webhook sending 2023-11-01 13:11:42 +02:00
DocuSeal 7444bb8b68 add audit log url to api and webhooks 2023-11-01 12:17:35 +02:00
Alex Turchyn f7132445a1 add new signup page 2023-11-01 02:17:56 +02:00
Alex Turchyn 0127dd738e fix navbar mobile layout 2023-10-30 23:03:52 +02:00
52 changed files with 948 additions and 302 deletions
+1
View File
@@ -4,6 +4,7 @@ class AccountsController < ApplicationController
LOCALE_OPTIONS = {
'en-US' => 'English (United States)',
'en-GB' => 'English (United Kingdom)',
'fr-FR' => 'French (France)',
'es-ES' => 'Spanish (Spain)',
'pt-PT' => 'Portuguese (Portugal)',
'de-DE' => 'German (Germany)'
+1 -1
View File
@@ -25,7 +25,7 @@ module Api
def paginate(relation)
result = relation.order(id: :desc)
.limit([params[:limit] || DEFAULT_LIMIT, MAX_LIMIT].min)
.limit([params.fetch(:limit, DEFAULT_LIMIT).to_i, MAX_LIMIT].min)
result = result.where('id < ?', params[:after]) if params[:after].present?
result = result.where('id > ?', params[:before]) if params[:before].present?
@@ -13,7 +13,8 @@ module Api
submissions = Submissions.search(@submissions, params[:q])
submissions = submissions.where(template_id: params[:template_id]) if params[:template_id].present?
submissions = paginate(submissions.preload(:created_by_user, :template, :submitters))
submissions = paginate(submissions.preload(:created_by_user, :template, :submitters,
audit_trail_attachment: :blob))
render json: {
data: submissions.as_json(serialize_params),
@@ -93,6 +94,7 @@ module Api
def serialize_params
{
only: %i[id source submitters_order created_at updated_at],
methods: %i[audit_log_url],
include: {
submitters: { only: %i[id slug uuid name email phone
completed_at opened_at sent_at
+8 -5
View File
@@ -8,6 +8,7 @@ module Api
templates = Templates.search(@templates, params[:q])
templates = params[:archived] ? templates.archived : templates.active
templates = templates.where(application_key: params[:application_key]) if params[:application_key].present?
templates = paginate(templates.preload(:author, documents_attachments: :blob))
@@ -51,11 +52,13 @@ module Api
end
def template_params
params.require(:template).permit(:name,
schema: [%i[attachment_uuid name]],
submitters: [%i[name uuid]],
fields: [[:uuid, :submitter_uuid, :name, :type, :required,
{ options: [], areas: [%i[x y w h cell_w attachment_uuid page]] }]])
params.require(:template).permit(
:name,
schema: [%i[attachment_uuid name]],
submitters: [%i[name uuid]],
fields: [[:uuid, :submitter_uuid, :name, :type, :required, :readonly, :default_value,
{ options: [%i[value uuid]], areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]]
)
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)
render plain: io.tap(&:rewind).read
render plain: io.tap(&:rewind).read, content_type: 'image/jpeg'
end
def find_or_create_document_tempfile_path(attachment)
+1 -1
View File
@@ -47,7 +47,7 @@ class StartFormController < ApplicationController
private
def submitter_params
params.require(:submitter).permit(:email, :phone).tap do |attrs|
params.require(:submitter).permit(:email, :phone, :name).tap do |attrs|
attrs[:email] = Submissions.normalize_email(attrs[:email])
end
end
@@ -35,7 +35,7 @@ class TemplatesUploadsController < ApplicationController
def create_file_params_from_url
tempfile = Tempfile.new
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
file = ActionDispatch::Http::UploadedFile.new(
+22 -2
View File
@@ -1,8 +1,8 @@
<template>
<div
class="flex absolute text-[1.5vw] lg:text-base"
class="flex absolute lg:text-base"
: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
v-if="!isActive && !isValueSet && field.type !== 'checkbox' && submittable"
@@ -102,6 +102,7 @@
</div>
<div
v-else
ref="textContainer"
class="flex items-center px-0.5"
>
<span v-if="Array.isArray(modelValue)">
@@ -173,6 +174,11 @@ export default {
}
},
emits: ['update:model-value'],
data () {
return {
textOverflowChars: 0
}
},
computed: {
fieldNames () {
return {
@@ -251,6 +257,20 @@ export default {
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>
@@ -1,8 +1,8 @@
<template>
<div>
<div v-if="modelValue.length">
<div v-if="value.length">
<div
v-for="(val, index) in modelValue"
v-for="(val, index) in value"
:key="index"
class="flex mb-2"
>
@@ -26,10 +26,7 @@
{{ attachmentsIndex[val].filename }}
</span>
</a>
<button
v-if="modelValue"
@click.prevent="removeAttachment(val)"
>
<button @click.prevent="removeAttachment(val)">
<IconTrashX
:width="18"
:heigh="19"
@@ -91,18 +88,28 @@ export default {
}
},
emits: ['attached', 'update:model-value'],
computed: {
value: {
set (value) {
this.$emit('update:model-value', this.modelValue || [])
},
get () {
return this.modelValue || []
}
}
},
methods: {
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) {
attachments.forEach((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)])
}
}
}
+27 -15
View File
@@ -99,12 +99,12 @@
{{ t('select_your_option') }}
</option>
<option
v-for="(option, index) in currentField.options"
:key="index"
:selected="values[currentField.uuid] == option"
:value="option"
v-for="option in currentField.options"
:key="option.uuid"
:selected="values[currentField.uuid] == option.value"
:value="option.value"
>
{{ option }}
{{ option.value }}
</option>
</select>
</div>
@@ -119,24 +119,24 @@
<div class="flex w-full">
<div class="space-y-3.5 mx-auto">
<div
v-for="(option, index) in currentField.options"
:key="index"
v-for="option in currentField.options"
:key="option.uuid"
>
<label
:for="currentField.uuid + option"
:for="option.uuid"
class="flex items-center space-x-3"
>
<input
:id="currentField.uuid + option"
:id="option.uuid"
v-model="values[currentField.uuid]"
type="radio"
class="base-radio !h-7 !w-7"
:name="`values[${currentField.uuid}]`"
:value="option"
:value="option.value"
:required="currentField.required"
>
<span class="text-xl">
{{ option }}
{{ option.value }}
</span>
</label>
</div>
@@ -513,9 +513,15 @@ export default {
mounted () {
this.submittedValues = JSON.parse(JSON.stringify(this.values))
this.fields.forEach((field) => {
if (field.default_value && !field.readonly) {
this.values[field.uuid] = field.default_value
}
})
if (this.goToLast) {
const requiredEmptyStepIndex = this.stepFields.indexOf(this.stepFields.find((fields) => fields.some((f) => f.required && !this.values[f.uuid])))
const lastFilledStepIndex = this.stepFields.indexOf([...this.stepFields].reverse().find((fields) => fields.some((f) => !!this.values[f.uuid]))) + 1
const requiredEmptyStepIndex = this.stepFields.indexOf(this.stepFields.find((fields) => fields.some((f) => f.required && !this.submittedValues[f.uuid])))
const lastFilledStepIndex = this.stepFields.indexOf([...this.stepFields].reverse().find((fields) => fields.some((f) => !!this.submittedValues[f.uuid]))) + 1
const indexesList = [this.stepFields.length - 1]
@@ -635,12 +641,20 @@ export default {
})
},
saveStep (formData) {
const currentFieldUuid = this.currentField.uuid
if (this.isCompleted) {
return Promise.resolve({})
} else {
return fetch(this.baseUrl + this.submitPath, {
method: 'POST',
body: formData || new FormData(this.$refs.form)
}).then((response) => {
if (response.status === 200) {
this.submittedValues[currentFieldUuid] = this.values[currentFieldUuid]
}
return response
})
}
},
@@ -672,8 +686,6 @@ export default {
return Promise.reject(new Error(data.error))
}
this.submittedValues[this.currentField.uuid] = this.values[this.currentField.uuid]
if (isLastStep) {
this.isSecondWalkthrough = true
}
@@ -7,25 +7,25 @@
<div class="flex w-full">
<div class="space-y-3.5 mx-auto">
<div
v-for="(option, index) in field.options"
:key="index"
v-for="option in field.options"
:key="option.uuid"
>
<label
:for="field.uuid + option"
:for="option.uuid"
class="flex items-center space-x-3"
>
<input
:id="field.uuid + option"
:id="option.uuid"
:ref="setInputRef"
type="checkbox"
:name="`values[${field.uuid}][]`"
:value="option"
:value="option.value"
class="base-checkbox !h-7 !w-7"
:checked="(modelValue || []).includes(option)"
:checked="(modelValue || []).includes(option.value)"
@change="onChange"
>
<span class="text-xl">
{{ option }}
{{ option.value }}
</span>
</label>
</div>
+15 -1
View File
@@ -15,6 +15,7 @@
v-if="!isTextArea"
:id="field.uuid"
v-model="text"
:maxlength="cellsMaxLegth"
class="base-input !text-2xl w-full !pr-11 -mr-10"
:required="field.required"
:pattern="field.validation?.pattern"
@@ -38,7 +39,7 @@
@focus="$emit('focus')"
/>
<div
v-if="!isTextArea"
v-if="!isTextArea && field.type !== 'cells'"
class="tooltip"
:data-tip="t('toggle_multiline_text')"
>
@@ -80,6 +81,19 @@ export default {
}
},
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: {
set (value) {
this.$emit('update:model-value', value)
+70 -29
View File
@@ -23,7 +23,7 @@
:style="{ left: (cellW / area.w * 100) + '%' }"
>
<span
v-if="index === 0"
v-if="index === 0 && editable"
class="h-2.5 w-2.5 rounded-full -bottom-1 border-gray-400 bg-white shadow-md border absolute cursor-ew-resize z-10"
style="left: -4px"
@mousedown.stop="startResizeCell"
@@ -42,6 +42,7 @@
v-model="field.submitter_uuid"
class="border-r"
:compact="true"
:editable="editable"
:menu-classes="'dropdown-content bg-white menu menu-xs p-2 shadow rounded-box w-52 rounded-t-none -left-[1px]'"
:submitters="template.submitters"
@update:model-value="save"
@@ -50,6 +51,7 @@
<FieldType
v-model="field.type"
:button-width="27"
:editable="editable"
:button-classes="'px-1'"
:menu-classes="'bg-white rounded-t-none'"
@update:model-value="[maybeUpdateOptions(), save()]"
@@ -58,7 +60,7 @@
<span
v-if="field.type !== 'checkbox' || field.name"
ref="name"
contenteditable
:contenteditable="editable"
class="pr-1 cursor-text outline-none block"
style="min-width: 2px"
@keydown.enter.prevent="onNameEnter"
@@ -66,7 +68,7 @@
@blur="onNameBlur"
>{{ field.name || defaultName }}</span>
<div
v-if="isNameFocus && field.type !== 'checkbox'"
v-if="isNameFocus && !['checkbox', 'phone'].includes(field.type)"
class="flex items-center ml-1.5"
>
<input
@@ -84,7 +86,7 @@
>Required</label>
</div>
<button
v-else
v-else-if="editable"
class="pr-1"
title="Remove"
@click.prevent="$emit('remove')"
@@ -93,18 +95,30 @@
</button>
</div>
<div
class="opacity-50 flex items-center justify-center h-full w-full"
:class="bgColors[submitterIndex]"
class="flex items-center h-full w-full"
:class="[bgColors[submitterIndex], field?.default_value ? '' : 'justify-center']"
>
<span
v-if="field"
class="flex justify-center items-center space-x-1 h-full"
>
<div
v-if="field?.default_value"
: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"
>
<span class="whitespace-pre-wrap">{{ field.default_value }}</span>
</div>
</div>
<component
:is="fieldIcons[field.type]"
v-else
width="100%"
height="100%"
class="max-h-10"
class="max-h-10 opacity-50"
/>
</span>
</div>
@@ -113,7 +127,7 @@
class="absolute top-0 bottom-0 right-0 left-0 cursor-pointer"
/>
<span
v-if="field?.type"
v-if="field?.type && editable"
class="h-4 w-4 md:h-2.5 md:w-2.5 -right-1 rounded-full -bottom-1 border-gray-400 bg-white shadow-md border absolute cursor-nwse-resize"
@mousedown.stop="startResize"
@touchstart="startTouchResize"
@@ -126,6 +140,7 @@ import FieldSubmitter from './field_submitter'
import FieldType from './field_type'
import Field from './field'
import { IconX } from '@tabler/icons-vue'
import { v4 } from 'uuid'
export default {
name: 'FieldArea',
@@ -145,6 +160,11 @@ export default {
required: false,
default: false
},
editable: {
type: Boolean,
required: false,
default: true
},
field: {
type: Object,
required: false,
@@ -157,6 +177,7 @@ export default {
isResize: false,
isDragged: false,
isNameFocus: false,
textOverflowChars: 0,
dragFrom: { x: 0, y: 0 }
}
},
@@ -185,30 +206,30 @@ export default {
},
borderColors () {
return [
'border-red-500',
'border-sky-500',
'border-emerald-500',
'border-yellow-300',
'border-purple-600',
'border-pink-500',
'border-cyan-500',
'border-orange-500',
'border-lime-500',
'border-indigo-500'
'border-red-500/50',
'border-sky-500/50',
'border-emerald-500/50',
'border-yellow-300/50',
'border-purple-600/50',
'border-pink-500/50',
'border-cyan-500/50',
'border-orange-500/50',
'border-lime-500/50',
'border-indigo-500/50'
]
},
bgColors () {
return [
'bg-red-100',
'bg-sky-100',
'bg-emerald-100',
'bg-yellow-100',
'bg-purple-100',
'bg-pink-100',
'bg-cyan-100',
'bg-orange-100',
'bg-lime-100',
'bg-indigo-100'
'bg-red-100/50',
'bg-sky-100/50',
'bg-emerald-100/50',
'bg-yellow-100/50',
'bg-purple-100/50',
'bg-pink-100/50',
'bg-cyan-100/50',
'bg-orange-100/50',
'bg-lime-100/50',
'bg-indigo-100/50'
]
},
isSelected () {
@@ -225,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: {
onNameFocus (e) {
this.selectedAreaRef.value = this.area
@@ -262,12 +297,14 @@ export default {
}
},
maybeUpdateOptions () {
delete this.field.default_value
if (!['radio', 'multiple', 'select'].includes(this.field.type)) {
delete this.field.options
}
if (['select', 'multiple', 'radio'].includes(this.field.type)) {
this.field.options ||= ['']
this.field.options ||= [{ value: '', uuid: v4() }]
}
(this.field.areas || []).forEach((area) => {
@@ -313,6 +350,10 @@ export default {
startDrag (e) {
this.selectedAreaRef.value = this.area
if (!this.editable) {
return
}
const rect = e.target.getBoundingClientRect()
this.dragFrom = { x: e.clientX - rect.left, y: e.clientY - rect.top }
+69 -46
View File
@@ -16,6 +16,7 @@
</a>
<Contenteditable
:model-value="template.name"
:editable="editable"
class="text-3xl font-semibold focus:text-clip"
:icon-stroke-width="2.3"
@update:model-value="updateName"
@@ -76,6 +77,8 @@
:item="item"
:document="sortedDocuments[index]"
:accept-file-types="acceptFileTypes"
:with-replace-button="withUploadButton"
:editable="editable"
:template="template"
:is-direct-upload="isDirectUpload"
@scroll-to="scrollIntoDocument(item)"
@@ -90,7 +93,7 @@
:class="{ 'bg-base-100': withStickySubmitters }"
>
<Upload
v-if="sortedDocuments.length"
v-if="sortedDocuments.length && editable && withUploadButton"
:accept-file-types="acceptFileTypes"
:template-id="template.id"
:is-direct-upload="isDirectUpload"
@@ -104,7 +107,7 @@
class="pr-3.5 pl-0.5"
>
<Dropzone
v-if="!sortedDocuments.length"
v-if="!sortedDocuments.length && withUploadButton"
:template-id="template.id"
:accept-file-types="acceptFileTypes"
:is-direct-upload="isDirectUpload"
@@ -120,16 +123,19 @@
:areas-index="fieldAreasIndex[document.uuid]"
:selected-submitter="selectedSubmitter"
:document="document"
:is-drag="!!dragFieldType"
:is-drag="!!dragField"
:draw-field="drawField"
:editable="editable"
:base-url="baseUrl"
@draw="onDraw"
@drop-field="onDropfield"
@remove-area="removeArea"
/>
<DocumentControls
v-if="isBreakpointLg"
v-if="isBreakpointLg && editable"
:with-arrows="template.schema.length > 1"
:item="template.schema.find((item) => item.attachment_uuid === document.uuid)"
:with-replace-button="withUploadButton"
:document="document"
:template="template"
:is-direct-upload="isDirectUpload"
@@ -142,10 +148,11 @@
/>
</template>
<div
v-if="sortedDocuments.length && isBreakpointLg"
v-if="sortedDocuments.length && isBreakpointLg && editable"
class="pb-4"
>
<Upload
v-if="withUploadButton"
:template-id="template.id"
:is-direct-upload="isDirectUpload"
@success="updateFromUpload"
@@ -153,34 +160,19 @@
</div>
</template>
</div>
<span
v-if="drawField"
class="fixed text-center w-full left-1/2 bottom-0 transform -translate-x-1/2"
>
<span
class="rounded bg-base-200 px-4 py-2 rounded-full inline-flex space-x-2 mx-auto items-center mb-4 z-20 md:hidden"
>
<component
:is="fieldIcons[drawField.type]"
:width="20"
:height="20"
class="inline"
:stroke-width="1.6"
/>
<span>
Draw {{ fieldNames[drawField.type] }} Field
</span>
<a
href="#"
class="link block text-center"
@click.prevent="drawField = null"
>
Cancel
</a>
</span>
</span>
<MobileDrawField
v-if="drawField && isBreakpointLg"
:draw-field="drawField"
:fields="template.fields"
:submitters="template.submitters"
:selected-submitter="selectedSubmitter"
class="md:hidden"
:editable="editable"
@cancel="drawField = null"
@change-submitter="[selectedSubmitter = $event, drawField.submitter_uuid = $event.uuid]"
/>
<FieldType
v-if="sortedDocuments.length && !drawField"
v-if="sortedDocuments.length && !drawField && editable"
class="dropdown-top dropdown-end fixed bottom-4 right-4 z-10 md:hidden"
:model-value="''"
@update:model-value="startFieldDraw($event)"
@@ -230,11 +222,13 @@
:fields="template.fields"
:submitters="template.submitters"
:selected-submitter="selectedSubmitter"
:default-fields="defaultFields"
:with-sticky-submitters="withStickySubmitters"
:editable="editable"
@set-draw="drawField = $event"
@set-drag="dragFieldType = $event"
@set-drag="dragField = $event"
@change-submitter="selectedSubmitter = $event"
@drag-end="dragFieldType = null"
@drag-end="dragField = null"
@scroll-to-area="scrollToArea"
/>
</div>
@@ -247,6 +241,7 @@
import Upload from './upload'
import Dropzone from './dropzone'
import Fields from './fields'
import MobileDrawField from './mobile_draw_field'
import Document from './document'
import Logo from './logo'
import Contenteditable from './contenteditable'
@@ -263,6 +258,7 @@ export default {
Upload,
Document,
Fields,
MobileDrawField,
IconPlus,
FieldType,
IconX,
@@ -300,6 +296,21 @@ export default {
required: false,
default: ''
},
editable: {
type: Boolean,
required: false,
default: true
},
defaultFields: {
type: Array,
required: false,
default: () => []
},
defaultSubmitters: {
type: Array,
required: false,
default: () => []
},
acceptFileTypes: {
type: String,
required: false,
@@ -320,6 +331,11 @@ export default {
required: false,
default: true
},
withUploadButton: {
type: Boolean,
required: false,
default: true
},
withPhone: {
type: Boolean,
required: false,
@@ -338,7 +354,7 @@ export default {
isSaving: false,
selectedSubmitter: null,
drawField: null,
dragFieldType: null
dragField: null
}
},
computed: {
@@ -370,6 +386,13 @@ export default {
}
},
created () {
this.defaultSubmitters.forEach((name, index) => {
const submitter = (this.template.submitters[index] ||= {})
submitter.name = name
submitter.uuid ||= v4()
})
this.selectedSubmitter = this.template.submitters[0]
},
mounted () {
@@ -406,7 +429,7 @@ export default {
}
if (['select', 'multiple', 'radio'].includes(type)) {
field.options = ['']
field.options = [{ value: '', uuid: v4() }]
}
this.drawField = field
@@ -562,14 +585,14 @@ export default {
onDropfield (area) {
const field = {
name: '',
type: this.dragFieldType,
uuid: v4(),
submitter_uuid: this.selectedSubmitter.uuid,
required: this.dragFieldType !== 'checkbox'
required: this.dragField.type !== 'checkbox',
...this.dragField
}
if (['select', 'multiple', 'radio'].includes(this.dragFieldType)) {
field.options = ['']
if (['select', 'multiple', 'radio'].includes(field.type)) {
field.options = [{ value: '', uuid: v4() }]
}
const fieldArea = {
@@ -583,27 +606,27 @@ export default {
let baseArea
if (this.selectedField?.type === this.dragFieldType) {
if (this.selectedField?.type === field.type) {
baseArea = this.selectedAreaRef.value
} else if (previousField?.areas?.length) {
baseArea = previousField.areas[previousField.areas.length - 1]
} else {
if (['checkbox'].includes(this.dragFieldType)) {
if (['checkbox'].includes(field.type)) {
baseArea = {
w: area.maskW / 30 / area.maskW,
h: area.maskW / 30 / area.maskW * (area.maskW / area.maskH)
}
} else if (this.dragFieldType === 'image') {
} else if (field.type === 'image') {
baseArea = {
w: area.maskW / 5 / area.maskW,
h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH)
}
} else if (this.dragFieldType === 'signature') {
} else if (field.type === 'signature') {
baseArea = {
w: area.maskW / 5 / area.maskW,
h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH) / 2
}
} else if (this.dragFieldType === 'initials') {
} else if (field.type === 'initials') {
baseArea = {
w: area.maskW / 10 / area.maskW,
h: area.maskW / 35 / area.maskW
@@ -620,7 +643,7 @@ export default {
fieldArea.h = baseArea.h
fieldArea.y = fieldArea.y - baseArea.h / 2
if (this.dragFieldType === 'cells') {
if (field.type === 'cells') {
fieldArea.cell_w = baseArea.cell_w || (baseArea.w / 5)
}
@@ -5,7 +5,7 @@
>
<span
ref="contenteditable"
contenteditable
:contenteditable="editable"
style="min-width: 2px"
:class="iconInline ? 'inline' : 'block'"
class="peer outline-none focus:block"
@@ -24,6 +24,7 @@
*
</span>
<IconPencil
v-if="editable"
class="cursor-pointer flex-none opacity-0 group-hover/contenteditable-container:opacity-100 group-hover/contenteditable:opacity-100 align-middle peer-focus:hidden"
:style="iconInline ? {} : { right: -(1.1 * iconWidth) + 'px' }"
title="Edit"
@@ -69,6 +70,11 @@ export default {
required: false,
default: false
},
editable: {
type: Boolean,
required: false,
default: true
},
iconStrokeWidth: {
type: Number,
required: false,
@@ -7,6 +7,7 @@
@update:model-value="onUpdateName"
/>
<ReplaceButton
v-if="withReplaceButton"
:is-direct-upload="isDirectUpload"
:template-id="template.id"
@click.stop
@@ -67,6 +68,11 @@ export default {
required: true,
default: false
},
withReplaceButton: {
type: Boolean,
required: true,
default: true
},
withArrows: {
type: Boolean,
required: false,
+19 -1
View File
@@ -5,6 +5,7 @@
:key="image.id"
:ref="setPageRefs"
:number="index"
:editable="editable"
:areas="areasIndex[index]"
:is-drag="isDrag"
:draw-field="drawField"
@@ -38,11 +39,21 @@ export default {
type: Object,
required: true
},
editable: {
type: Boolean,
required: false,
default: true
},
drawField: {
type: Object,
required: false,
default: null
},
baseUrl: {
type: String,
required: false,
default: ''
},
isDrag: {
type: Boolean,
required: false,
@@ -56,6 +67,13 @@ export default {
}
},
computed: {
basePreviewUrl () {
if (this.baseUrl) {
return new URL(this.baseUrl).origin
} else {
return ''
}
},
numberOfPages () {
return this.document.metadata?.pdf?.number_of_pages || this.document.preview_images.length
},
@@ -66,7 +84,7 @@ export default {
return this.previewImagesIndex[i] || {
metadata: lazyloadMetadata,
id: Math.random().toString(),
url: `/preview/${this.document.uuid}/${i}.jpg`
url: this.basePreviewUrl + `/preview/${this.document.uuid}/${i}.jpg`
}
})
},
+108 -54
View File
@@ -4,6 +4,7 @@
>
<div
class="border border-base-300 rounded rounded-tr-none relative group"
:style="{ backgroundColor: backgroundColor }"
>
<div class="flex items-center justify-between relative group/contenteditable-container">
<div
@@ -13,6 +14,7 @@
<div class="flex items-center p-1 space-x-1">
<FieldType
v-model="field.type"
:editable="editable"
:button-width="20"
@update:model-value="[maybeUpdateOptions(), save()]"
@click="scrollToFirstArea"
@@ -20,6 +22,7 @@
<Contenteditable
ref="name"
:model-value="field.name || defaultName"
:editable="editable"
:icon-inline="true"
:icon-width="18"
:icon-stroke-width="1.6"
@@ -31,34 +34,46 @@
v-if="isNameFocus"
class="flex items-center relative"
>
<input
:id="`required-checkbox-${field.uuid}`"
v-model="field.required"
type="checkbox"
class="checkbox checkbox-xs no-animation rounded"
@mousedown.prevent
>
<label
:for="`required-checkbox-${field.uuid}`"
class="label text-xs"
@click.prevent="field.required = !field.required"
@mousedown.prevent
>Required</label>
<template v-if="field.type != 'phone'">
<input
:id="`required-checkbox-${field.uuid}`"
v-model="field.required"
type="checkbox"
class="checkbox checkbox-xs no-animation rounded"
@mousedown.prevent
>
<label
:for="`required-checkbox-${field.uuid}`"
class="label text-xs"
@click.prevent="field.required = !field.required"
@mousedown.prevent
>Required</label>
</template>
</div>
<div
v-else
v-else-if="editable"
class="flex items-center space-x-1"
>
<button
v-if="field && !field.areas.length"
title="Draw"
class="relative cursor-pointer text-transparent group-hover:text-base-content"
@click="$emit('set-draw', field)"
>
<IconNewSection
:width="18"
:stroke-width="1.6"
/>
</button>
<span
v-if="field.areas?.length"
class="dropdown dropdown-end"
>
<label
tabindex="0"
title="Areas"
title="Settings"
class="cursor-pointer text-transparent group-hover:text-base-content"
>
<IconShape
<IconSettings
:width="18"
:stroke-width="1.6"
/>
@@ -66,8 +81,60 @@
<ul
tabindex="0"
class="mt-1.5 dropdown-content menu menu-xs p-2 shadow bg-base-100 rounded-box w-52 z-10"
draggable="true"
@dragstart.prevent.stop
@click="closeDropdown"
>
<div
v-if="field.type === 'text'"
class="py-1.5 px-1 relative"
@click.stop
>
<input
v-model="field.default_value"
type="text"
placeholder="Default value"
class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0"
@blur="save"
>
<label
v-if="field.default_value"
:style="{ backgroundColor: backgroundColor }"
class="absolute -top-1 left-2.5 px-1 h-4"
style="font-size: 8px"
>
Default value
</label>
</div>
<li
v-if="field.type != 'phone'"
@click.stop
>
<label class="cursor-pointer py-1.5">
<input
v-model="field.required"
type="checkbox"
class="toggle toggle-xs"
@update:model-value="save"
>
<span class="label-text">Required</span>
</label>
</li>
<li
v-if="field.type === 'text'"
@click.stop
>
<label class="cursor-pointer py-1.5">
<input
v-model="field.readonly"
type="checkbox"
class="toggle toggle-xs"
@update:model-value="save"
>
<span class="label-text">Read-only</span>
</label>
</li>
<hr class="pb-0.5 mt-0.5">
<li
v-for="(area, index) in field.areas || []"
:key="index"
@@ -113,18 +180,7 @@
</ul>
</span>
<button
v-else
title="Areas"
class="relative cursor-pointer text-transparent group-hover:text-base-content"
@click="$emit('set-draw', field)"
>
<IconShape
:width="18"
:stroke-width="1.6"
/>
</button>
<button
class="relative text-transparent group-hover:text-base-content"
class="relative text-transparent group-hover:text-base-content pr-1"
title="Remove"
@click="$emit('remove', field)"
>
@@ -133,40 +189,24 @@
:stroke-width="1.6"
/>
</button>
<div class="flex flex-col pr-1 text-transparent group-hover:text-base-content">
<button
title="Up"
class="relative"
style="font-size: 10px; margin-bottom: -2px"
@click="$emit('move-up')"
>
</button>
<button
title="Down"
class="relative"
style="font-size: 10px; margin-top: -2px"
@click="$emit('move-down')"
>
</button>
</div>
</div>
</div>
<div
v-if="field.options"
class="border-t border-base-300 mx-2 pt-2 space-y-1.5"
draggable="true"
@dragstart.prevent.stop
>
<div
v-for="(option, index) in field.options"
:key="index"
:key="option.uuid"
class="flex space-x-1.5 items-center"
>
<span class="text-sm w-3.5">
{{ index + 1 }}.
</span>
<input
v-model="field.options[index]"
v-model="option.value"
class="w-full input input-primary input-xs text-sm bg-transparent"
type="text"
required
@@ -182,7 +222,7 @@
<button
v-if="field.options"
class="text-center text-sm w-full pb-1"
@click="[field.options.push(''), save()]"
@click="addOption"
>
+ Add option
</button>
@@ -194,26 +234,33 @@
<script>
import Contenteditable from './contenteditable'
import FieldType from './field_type'
import { IconShape, IconNewSection, IconTrashX, IconCopy } from '@tabler/icons-vue'
import { IconShape, IconNewSection, IconTrashX, IconCopy, IconSettings } from '@tabler/icons-vue'
import { v4 } from 'uuid'
export default {
name: 'TemplateField',
components: {
Contenteditable,
IconSettings,
IconShape,
IconNewSection,
IconTrashX,
IconCopy,
FieldType
},
inject: ['template', 'save'],
inject: ['template', 'save', 'backgroundColor'],
props: {
field: {
type: Object,
required: true
},
editable: {
type: Boolean,
required: false,
default: true
}
},
emits: ['set-draw', 'remove', 'move-up', 'move-down', 'scroll-to'],
emits: ['set-draw', 'remove', 'scroll-to'],
data () {
return {
isNameFocus: false
@@ -265,13 +312,20 @@ export default {
closeDropdown () {
document.activeElement.blur()
},
addOption () {
this.field.options.push({ value: '', uuid: v4() })
this.save()
},
maybeUpdateOptions () {
delete this.field.default_value
if (!['radio', 'multiple', 'select'].includes(this.field.type)) {
delete this.field.options
}
if (['radio', 'multiple', 'select'].includes(this.field.type)) {
this.field.options ||= ['']
this.field.options ||= [{ value: '', uuid: v4() }]
}
(this.field.areas || []).forEach((area) => {
@@ -1,5 +1,91 @@
<template>
<div class="dropdown">
<div v-if="mobileView">
<div class="flex space-x-2 items-end">
<div class="group/contenteditable-container bg-base-100 rounded-md p-2 border border-base-300 w-full flex justify-between items-end">
<div class="flex items-center space-x-2">
<span
class="w-3 h-3 flex-shrink-0 rounded-full"
:class="colors[submitters.indexOf(selectedSubmitter)]"
/>
<Contenteditable
v-model="selectedSubmitter.name"
class="cursor-text"
:icon-inline="true"
:editable="editable"
:select-on-edit-click="true"
:icon-width="18"
@update:model-value="$emit('name-change', selectedSubmitter)"
/>
</div>
</div>
<div class="dropdown dropdown-top dropdown-end">
<label
tabindex="0"
class="bg-base-100 cursor-pointer rounded-md p-2 border border-base-300 w-full flex justify-center"
>
<IconChevronUp
width="24"
height="24"
/>
</label>
<ul
v-if="editable"
tabindex="0"
class="rounded-md min-w-max mb-2"
:class="menuClasses"
@click="closeDropdown"
>
<li
v-for="(submitter, index) in submitters"
:key="submitter.uuid"
>
<a
href="#"
class="flex px-2 group justify-between items-center"
:class="{ 'active': submitter === selectedSubmitter }"
@click.prevent="selectSubmitter(submitter)"
>
<span class="py-1 flex items-center">
<span
class="rounded-full w-3 h-3 ml-1 mr-3"
:class="colors[index]"
/>
<span>
{{ submitter.name }}
</span>
</span>
<button
v-if="submitters.length > 1 && editable"
class="px-2"
@click.stop="remove(submitter)"
>
<IconTrashX :width="18" />
</button>
</a>
</li>
<li v-if="submitters.length < 10 && editable">
<a
href="#"
class="flex px-2"
@click.prevent="addSubmitter"
>
<IconUserPlus
:width="20"
:stroke-width="1.6"
/>
<span class="py-1">
Add Submitter
</span>
</a>
</li>
</ul>
</div>
</div>
</div>
<div
v-else
class="dropdown"
>
<label
v-if="compact"
tabindex="0"
@@ -25,6 +111,7 @@
v-model="selectedSubmitter.name"
class="cursor-text"
:icon-inline="true"
:editable="editable"
:select-on-edit-click="true"
:icon-width="18"
@update:model-value="$emit('name-change', selectedSubmitter)"
@@ -38,6 +125,7 @@
</span>
</label>
<ul
v-if="editable || !compact"
tabindex="0"
:class="menuClasses"
@click="closeDropdown"
@@ -62,7 +150,7 @@
</span>
</span>
<button
v-if="!compact && submitters.length > 1"
v-if="!compact && submitters.length > 1 && editable"
class="hidden group-hover:block px-2"
@click.stop="remove(submitter)"
>
@@ -70,7 +158,7 @@
</button>
</a>
</li>
<li v-if="submitters.length < 10">
<li v-if="submitters.length < 10 && editable">
<a
href="#"
class="flex px-2"
@@ -90,7 +178,7 @@
</template>
<script>
import { IconUserPlus, IconTrashX, IconPlus } from '@tabler/icons-vue'
import { IconUserPlus, IconTrashX, IconPlus, IconChevronUp } from '@tabler/icons-vue'
import Contenteditable from './contenteditable'
import { v4 } from 'uuid'
@@ -100,18 +188,29 @@ export default {
IconUserPlus,
Contenteditable,
IconPlus,
IconTrashX
IconTrashX,
IconChevronUp
},
props: {
submitters: {
type: Array,
required: true
},
editable: {
type: Boolean,
required: false,
default: true
},
compact: {
type: Boolean,
required: false,
default: false
},
mobileView: {
type: Boolean,
required: false,
default: false
},
modelValue: {
type: String,
required: true
@@ -15,6 +15,7 @@
</label>
</slot>
<ul
v-if="editable"
tabindex="0"
class="dropdown-content menu menu-xs p-2 shadow rounded-box w-52 z-10 mb-3"
:class="menuClasses"
@@ -65,6 +66,11 @@ export default {
required: false,
default: ''
},
editable: {
type: Boolean,
required: false,
default: true
},
buttonWidth: {
type: Number,
required: false,
+91 -53
View File
@@ -5,26 +5,66 @@
class="w-full rounded-lg"
:class="{ 'bg-base-100': withStickySubmitters }"
:submitters="submitters"
:editable="editable"
@new-submitter="save"
@remove="removeSubmitter"
@name-change="save"
@update:model-value="$emit('change-submitter', submitters.find((s) => s.uuid === $event))"
/>
</div>
<div class="mb-1 mt-2">
<div
class="mb-1 mt-2"
@dragover.prevent="onFieldDragover"
@drop="save"
>
<Field
v-for="field in submitterFields"
:key="field.uuid"
:data-uuid="field.uuid"
:field="field"
:type-index="fields.filter((f) => f.type === field.type).indexOf(field)"
:editable="editable && !dragField"
:draggable="editable"
@dragstart="dragField = field"
@dragend="dragField = null"
@remove="removeField"
@move-up="move(field, -1)"
@move-down="move(field, 1)"
@scroll-to="$emit('scroll-to-area', $event)"
@set-draw="$emit('set-draw', $event)"
/>
</div>
<div class="grid grid-cols-3 gap-1 pb-2">
<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
v-if="editable"
class="grid grid-cols-3 gap-1 pb-2"
>
<template
v-for="(icon, type) in fieldIcons"
:key="type"
@@ -34,35 +74,12 @@
draggable="true"
class="flex items-center justify-center border border-dashed border-base-300 w-full rounded relative"
:style="{ backgroundColor: backgroundColor }"
@dragstart="onDragstart(type)"
@dragstart="onDragstart({ type: type })"
@dragend="$emit('drag-end')"
@click="addField(type)"
>
<div class="w-0 absolute left-0">
<svg
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>
<IconDrag class="cursor-grab" />
</div>
<div class="flex items-center flex-col px-2 py-2">
<component :is="icon" />
@@ -100,7 +117,7 @@
</template>
</div>
<div
v-if="fields.length < 4"
v-if="fields.length < 4 && editable"
class="text-xs p-2 border border-base-200 rounded"
>
<ul class="list-disc list-outside ml-3">
@@ -123,12 +140,15 @@ import { v4 } from 'uuid'
import FieldType from './field_type'
import FieldSubmitter from './field_submitter'
import { IconLock } from '@tabler/icons-vue'
import IconDrag from './icon_drag'
export default {
name: 'TemplateFields',
components: {
Field,
FieldType,
FieldSubmitter,
IconDrag,
IconLock
},
inject: ['save', 'backgroundColor', 'withPhone'],
@@ -137,6 +157,16 @@ export default {
type: Array,
required: true
},
editable: {
type: Boolean,
required: false,
default: true
},
defaultFields: {
type: Array,
required: false,
default: () => []
},
withStickySubmitters: {
type: Boolean,
required: false,
@@ -152,16 +182,44 @@ export default {
}
},
emits: ['set-draw', 'set-drag', 'drag-end', 'scroll-to-area', 'change-submitter'],
data () {
return {
dragField: null
}
},
computed: {
fieldNames: FieldType.computed.fieldNames,
fieldIcons: FieldType.computed.fieldIcons,
submitterFields () {
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: {
onDragstart (fieldType) {
this.$emit('set-drag', fieldType)
onDragstart (field) {
this.$emit('set-drag', field)
},
onFieldDragover (e) {
const targetFieldUuid = e.target.closest('[data-uuid]')?.dataset?.uuid
if (this.dragField && targetFieldUuid && this.dragField.uuid !== targetFieldUuid) {
const field = this.fields.find((f) => f.uuid === targetFieldUuid)
const currentIndex = this.fields.indexOf(this.dragField)
const targetIndex = this.fields.indexOf(field)
if (currentIndex < targetIndex) {
this.fields.splice(targetIndex + 1, 0, this.dragField)
this.fields.splice(currentIndex, 1)
} else {
this.fields.splice(targetIndex, 0, this.dragField)
this.fields.splice(currentIndex + 1, 1)
}
}
},
removeSubmitter (submitter) {
[...this.fields].forEach((field) => {
@@ -178,26 +236,6 @@ export default {
this.save()
},
move (field, direction) {
const currentIndex = this.submitterFields.indexOf(field)
const fieldsIndex = this.fields.indexOf(field)
this.fields.splice(fieldsIndex, 1)
if (currentIndex + direction > this.submitterFields.length) {
const firstIndex = this.fields.indexOf(this.submitterFields[0])
this.fields.splice(firstIndex, 0, field)
} else if (currentIndex + direction < 0) {
const lastIndex = this.fields.indexOf(this.submitterFields[this.submitterFields.length - 1])
this.fields.splice(lastIndex + 1, 0, field)
} else {
this.fields.splice(fieldsIndex + direction, 0, field)
}
this.save()
},
removeField (field) {
this.fields.splice(this.fields.indexOf(field), 1)
@@ -214,7 +252,7 @@ export default {
}
if (['select', 'multiple', 'radio'].includes(type)) {
field.options = ['']
field.options = [{ value: '', uuid: v4() }]
}
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>
@@ -0,0 +1,98 @@
<template>
<div class="fixed text-center w-full bottom-0 pr-6 mb-4">
<span class="w-full bg-base-200 px-4 py-2 rounded-md inline-flex space-x-2 mx-auto items-center justify-between mb-2 z-20 md:hidden">
<div class="flex items-center space-x-2">
<component
:is="fieldIcons[drawField.type]"
:width="20"
:height="20"
class="inline"
:stroke-width="1.6"
/>
<span> Draw {{ fieldNames[drawField.type] }} Field </span>
</div>
<a
href="#"
class="link block text-center"
@click.prevent="$emit('cancel')"
>
Cancel
</a>
</span>
<FieldSubmitter
:model-value="selectedSubmitter.uuid"
:submitters="submitters"
:editable="editable"
:mobile-view="true"
@new-submitter="save"
@remove="removeSubmitter"
@name-change="save"
@update:model-value="$emit('change-submitter', submitters.find((s) => s.uuid === $event))"
/>
</div>
</template>
<script>
import Field from './field'
import FieldType from './field_type'
import FieldSubmitter from './field_submitter'
export default {
name: 'MobileDrawField',
components: {
Field,
FieldSubmitter
},
inject: ['save'],
props: {
drawField: {
type: Object,
required: true
},
editable: {
type: Boolean,
required: false,
default: true
},
submitters: {
type: Array,
required: true
},
fields: {
type: Array,
required: true
},
selectedSubmitter: {
type: Object,
required: true
}
},
emits: ['change-submitter', 'cancel'],
computed: {
fieldNames: FieldType.computed.fieldNames,
fieldIcons: FieldType.computed.fieldIcons
},
methods: {
removeSubmitter (submitter) {
[...this.fields].forEach((field) => {
if (field.submitter_uuid === submitter.uuid) {
this.removeField(field)
}
})
this.submitters.splice(this.submitters.indexOf(submitter), 1)
if (this.selectedSubmitter === submitter) {
this.$emit('change-submitter', this.submitters[0])
}
this.save()
},
removeField (field) {
this.fields.splice(this.fields.indexOf(field), 1)
this.save()
}
}
}
</script>
+12 -2
View File
@@ -22,6 +22,7 @@
:ref="setAreaRefs"
:area="item.area"
:field="item.field"
:editable="editable"
@start-resize="resizeDirection = $event"
@stop-resize="resizeDirection = null"
@start-drag="isMove = true"
@@ -39,8 +40,8 @@
v-show="resizeDirection || isMove || isDrag || showMask || (drawField && isMobile)"
id="mask"
ref="mask"
class="top-0 bottom-0 left-0 right-0 absolute z-10"
:class="{ 'cursor-grab': isDrag || isMove, 'cursor-nwse-resize': drawField, [resizeDirectionClasses[resizeDirection]]: !!resizeDirectionClasses }"
class="top-0 bottom-0 left-0 right-0 absolute"
:class="{ 'z-10': !isMobile, 'cursor-grab': isDrag || isMove, 'cursor-nwse-resize': drawField, [resizeDirectionClasses[resizeDirection]]: !!resizeDirectionClasses }"
@pointermove="onPointermove"
@pointerdown="onStartDraw"
@dragover.prevent
@@ -77,6 +78,11 @@ export default {
required: false,
default: null
},
editable: {
type: Boolean,
required: false,
default: true
},
isDrag: {
type: Boolean,
required: false,
@@ -141,6 +147,10 @@ export default {
return
}
if (!this.editable) {
return
}
this.showMask = true
this.$nextTick(() => {
+15 -1
View File
@@ -12,10 +12,14 @@
class="group flex justify-end cursor-pointer top-0 bottom-0 left-0 right-0 absolute p-1"
@click="$emit('scroll-to', item)"
>
<div class="flex justify-between w-full">
<div
v-if="editable"
class="flex justify-between w-full"
>
<div style="width: 26px" />
<div class="">
<ReplaceButton
v-if="withReplaceButton"
:is-direct-upload="isDirectUpload"
:template-id="template.id"
:accept-file-types="acceptFileTypes"
@@ -95,11 +99,21 @@ export default {
type: Object,
required: true
},
editable: {
type: Boolean,
required: false,
default: true
},
acceptFileTypes: {
type: String,
required: false,
default: 'image/*, application/pdf'
},
withReplaceButton: {
type: Boolean,
required: true,
default: true
},
isDirectUpload: {
type: Boolean,
required: true,
+7 -8
View File
@@ -10,19 +10,18 @@ class ProcessSubmitterCompletionJob < ApplicationJob
Submissions::EnsureResultGenerated.call(submitter)
if submitter.account.encrypted_configs.exists?(key: EncryptedConfig::WEBHOOK_URL_KEY)
SendFormCompletedWebhookRequestJob.perform_later(submitter)
if is_all_completed && submitter.completed_at == submitter.submission.submitters.maximum(:completed_at)
Submissions::GenerateAuditTrail.call(submitter.submission)
enqueue_completed_emails(submitter)
end
return unless is_all_completed
return if submitter.completed_at != submitter.submission.submitters.maximum(:completed_at)
return if Accounts.load_webhook_configs(submitter.account).blank?
Submissions::GenerateAuditTrail.call(submitter.submission)
enqueue_emails(submitter)
SendFormCompletedWebhookRequestJob.perform_later(submitter)
end
def enqueue_emails(submitter)
def enqueue_completed_emails(submitter)
user = submitter.submission.created_by_user || submitter.template.author
if submitter.template.account.users.exists?(id: user.id)
+1 -1
View File
@@ -21,7 +21,7 @@
# fk_rails_... (user_id => users.id)
#
class AccessToken < ApplicationRecord
TOKEN_LENGTH = 22
TOKEN_LENGTH = 43
belongs_to :user
+6 -1
View File
@@ -49,7 +49,7 @@ class Submission < ApplicationRecord
scope :active, -> { where(deleted_at: nil) }
scope :pending, -> { joins(:submitters).where(submitters: { completed_at: nil }).distinct }
scope :completed, -> { left_joins(:submitters).where.not(submitters: { completed_at: nil }).distinct }
scope :completed, -> { where.not(id: pending.select(:submission_id)) }
enum :source, {
invite: 'invite',
@@ -62,4 +62,9 @@ class Submission < ApplicationRecord
random: 'random',
preserved: 'preserved'
}, scope: false, prefix: true
def audit_trail_url
audit_trail&.url
end
alias audit_log_url audit_trail_url
end
+14 -13
View File
@@ -4,19 +4,20 @@
#
# Table name: templates
#
# id :bigint not null, primary key
# deleted_at :datetime
# fields :text not null
# name :string not null
# schema :text not null
# slug :string not null
# source :text not null
# submitters :text not null
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint not null
# author_id :bigint not null
# folder_id :bigint not null
# id :bigint not null, primary key
# application_key :string
# deleted_at :datetime
# fields :text not null
# name :string not null
# schema :text not null
# slug :string not null
# source :text not null
# submitters :text not null
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint not null
# author_id :bigint not null
# folder_id :bigint not null
#
# Indexes
#
+6 -9
View File
@@ -3,7 +3,9 @@
<a href="<%= root_path %>" class="text-2xl font-bold items-center flex space-x-2">
<%= render 'shared/title' %>
</a>
<%= render 'shared/github' %>
<span class="hidden sm:inline">
<%= render 'shared/github' %>
</span>
</div>
<% if signed_in? %>
<div class="space-x-4 flex items-center">
@@ -16,13 +18,7 @@
</span>
<% else %>
<div class="flex items-center justify-center space-x-4 mr-1">
<% if can?(:manage, EncryptedConfig) && !can?(:manage, :tenants) %>
<%= link_to Docuseal.multitenant? ? console_redirect_index_path : Docuseal::CONSOLE_URL, class: 'hidden md:inline-flex items-center font-medium text-lg', data: { prefetch: false } do %>
Console
<span class="badge badge-warning ml-1">New</span>
<% end %>
<span class="hidden md:inline-flex h-3 border-r border-base-content"></span>
<% end %>
<%= render 'shared/navbar_buttons' %>
<%= link_to 'Settings', settings_profile_index_path, class: 'hidden md:inline-flex font-medium text-lg' %>
</div>
<% end %>
@@ -75,7 +71,8 @@
<% if Docuseal.multitenant? && !request.path.in?([registration_path, new_registration_path]) %>
<%= link_to registration_path, class: 'btn btn-neutral btn-sm btn-outline' do %>
<span class="flex items-center justify-center space-x-1">
<span>Create free Account</span>
<span class="hidden sm:block">Create free Account</span>
<span class="md:hidden">Sign Up</span>
</span>
<% end %>
<% end %>
@@ -0,0 +1,4 @@
<%= link_to Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CONSOLE_URL}/on_premise", class: 'hidden md:inline-flex btn btn-warning btn-sm', data: { prefetch: false } do %>
Upgrade
<% end %>
<span class="hidden md:inline-flex h-3 border-r border-base-content"></span>
-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>
+12 -4
View File
@@ -1,7 +1,8 @@
<div class="block w-full md:w-52 flex-none">
<menu-active>
<ul class="menu px-0">
<li class="menu-title py-0"><span>Settings</span></li>
<li class="menu-title py-0 !bg-transparent mb-3 -mt-5"><a href="<%= '/' %>" class="!bg-transparent !text-neutral font-medium">&larr; Back</a></li>
<li class="menu-title py-0 !bg-transparent"><span class="!bg-transparent">Settings</span></li>
<li></li>
<li>
<%= link_to 'Profile', settings_profile_index_path, class: 'text-base hover:bg-base-300' %>
@@ -70,16 +71,23 @@
<li>
<%= link_to Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CONSOLE_URL}/on_premise", class: 'text-base hover:bg-base-300', data: { prefetch: false } do %>
Plans
<span class="badge badge-warning">New</span>
<% end %>
</li>
<% end %>
<% if !Docuseal.demo? && can?(:manage, EncryptedConfig) %>
<li>
<%= link_to Docuseal.multitenant? ? console_redirect_index_path : Docuseal::CONSOLE_URL, class: 'text-base hover:bg-base-300', data: { prefetch: false } do %>
Console
<span class="badge badge-warning">New</span>
<%= 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 %>
<% if Docuseal.multitenant? %> API <% else %> Console <% end %>
<% end %>
</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 %>
</ul>
</menu-active>
+1 -1
View File
@@ -27,7 +27,7 @@
<%= form_for @submitter, url: start_form_path(@template.slug), data: { turbo_frame: :_top }, method: :put, html: { class: 'space-y-4', onsubmit: 'event.submitter.disabled = true' } do |f| %>
<div class="form-control !mt-0">
<%= f.label :email, class: 'label' %>
<%= f.email_field :email, value: current_user&.email, required: true, class: 'base-input', placeholder: 'Provide your email to start' %>
<%= f.email_field :email, value: current_user&.email || params[:email], required: true, class: 'base-input', placeholder: 'Provide your email to start' %>
</div>
<div class="form-control">
<%= f.button button_title(title: 'Start', disabled_with: 'Starting'), class: 'base-button' %>
+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']) %>
<img class="object-contain mx-auto" src="<%= attachments_index[value].url %>" loading="lazy">
<% elsif field['type'] == 'file' %>
+1 -1
View File
@@ -39,7 +39,7 @@
<% if @submitter.submission.template.submitters.size == 1 %>
<div class="divider">OR</div>
<div>
<%= button_to button_title(title: 'Resubmit', disabled_with: 'Resubmit', icon: svg_icon('reload', class: 'w-6 h-6')), start_form_path(@submitter.submission.template.slug), params: { submitter: { email: @submitter.email, phone: @submitter.phone }, resubmit: true }, method: :put, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'white-button w-full' %>
<%= button_to button_title(title: 'Resubmit', disabled_with: 'Resubmit', icon: svg_icon('reload', class: 'w-6 h-6')), start_form_path(@submitter.submission.template.slug), params: { submitter: { email: @submitter.email, phone: @submitter.phone, name: @submitter.name }, resubmit: true }, method: :put, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'white-button w-full' %>
</div>
<% end %>
</div>
+1 -1
View File
@@ -22,7 +22,7 @@
<%= render 'submissions/annotation', annot: %>
<% end %>
<% fields_index.dig(document.uuid, index)&.each do |(area, field)| %>
<% value = values[field['uuid']] %>
<% value = values[field['uuid']].presence || (field['readonly'] ? Submitters::SubmitValues.template_default_value_for_submitter(field['default_value'], @submitter.submission.submitters.find { |e| e.uuid == field['submitter_uuid'] }, with_time: false) : nil) %>
<% next if value.blank? %>
<% next if !field['readonly'] && field['submitter_uuid'] == @submitter.uuid %>
<%= render 'submissions/value', area:, field:, attachments_index:, value:, locale: @submitter.submission.template.account.locale %>
+2 -2
View File
@@ -4,14 +4,14 @@
<%= hidden_field_tag :base_template_id, @base_template.id %>
<% end %>
<div class="form-control mt-6">
<%= f.text_field :name, required: true, placeholder: 'Template Name', class: 'base-input' %>
<%= f.text_field :name, required: true, placeholder: 'Document Template Name', class: 'base-input' %>
</div>
<div class="mt-3 mb-4 flex items-center justify-between">
<a href="#" onclick="[event.preventDefault(), window.folder_name.focus()]">
<%= svg_icon('folder', class: 'w-6 h-6') %>
</a>
<folder-autocomplete class="flex justify-between w-full">
<input id="folder_name" type="text" class="w-64 outline-none border-transparent focus:border-transparent focus:ring-0 bg-base-100 px-1 peer" name="folder_name" value="<%= params[:folder_name].presence || @base_template&.folder&.name || TemplateFolder::DEFAULT_NAME %>" onblur="window.folder_name.value = window.folder_name.value || 'Default'" autocomplete="off">
<input id="folder_name" placeholder="Folder Name" type="text" class="w-64 outline-none border-transparent focus:border-transparent focus:ring-0 bg-base-100 px-1 peer" name="folder_name" value="<%= params[:folder_name].presence || @base_template&.folder&.name || TemplateFolder::DEFAULT_NAME %>" onblur="window.folder_name.value = window.folder_name.value || 'Default'" autocomplete="off">
<a href="#" onclick="[event.preventDefault(), window.folder_name.value = '', window.folder_name.focus()]" class="link peer-focus:hidden mr-1.5">
Change Folder
</a>
+1 -1
View File
@@ -22,7 +22,7 @@ module DocuSeal
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.action_view.frozen_string_literal = true
+9 -2
View File
@@ -1,2 +1,9 @@
en:
hello: "Hello world"
en-US:
date:
formats:
default: "%m/%d/%Y"
en-GB:
date:
formats:
default: "%d/%m/%Y"
@@ -1,6 +1,6 @@
# frozen_string_literal: true
class AddApplicationIdToSubmitters < ActiveRecord::Migration[7.0]
class AddApplicationKeyToSubmitters < ActiveRecord::Migration[7.0]
def change
add_column :submitters, :application_key, :string
end
@@ -0,0 +1,7 @@
# frozen_string_literal: true
class AddApplicationKeyToTemplates < ActiveRecord::Migration[7.0]
def change
add_column :templates, :application_key, :string
end
end
@@ -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
+2 -1
View File
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.0].define(version: 2023_10_17_213639) 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
enable_extension "plpgsql"
@@ -173,6 +173,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_10_17_213639) do
t.datetime "updated_at", null: false
t.text "source", null: false
t.bigint "folder_id", null: false
t.string "application_key"
t.index ["account_id"], name: "index_templates_on_account_id"
t.index ["author_id"], name: "index_templates_on_author_id"
t.index ["folder_id"], name: "index_templates_on_folder_id"
+6 -2
View File
@@ -47,9 +47,13 @@ module Accounts
end
def load_webhook_configs(account)
EncryptedConfig.find_by(key: EncryptedConfig::WEBHOOK_URL_KEY) unless Docuseal.multitenant?
configs = account.encrypted_configs.find_by(key: EncryptedConfig::WEBHOOK_URL_KEY)
account.encrypted_configs.find_by(key: EncryptedConfig::WEBHOOK_URL_KEY)
unless Docuseal.multitenant?
configs ||= Account.order(:id).first.encrypted_configs.find_by(key: EncryptedConfig::WEBHOOK_URL_KEY)
end
configs
end
def load_signing_pkcs(account)
-2
View File
@@ -15,7 +15,5 @@ class AuthWithTokenStrategy < Devise::Strategies::Base
else
fail!('Invalid token')
end
rescue JWT::VerificationError
fail!('Invalid token')
end
end
+7 -3
View File
@@ -28,9 +28,7 @@ module Submissions
end
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)
emails.uniq.map do |email|
parse_emails(emails).uniq.map do |email|
submission = template.submissions.new(created_by_user: user, source:, template_submitters: template.submitters)
submission.submitters.new(email: normalize_email(email),
uuid: template.submitters.first['uuid'],
@@ -40,6 +38,12 @@ module Submissions
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,
submitters_order: DEFAULT_SUBMITTERS_ORDER)
Submissions::CreateFromSubmitters.call(
+3 -2
View File
@@ -24,6 +24,7 @@ module Submissions
else
'Verified'
end
UNVERIFIED_TEXT = 'Unverified'
module_function
@@ -151,10 +152,10 @@ module Submissions
composer.document.layout.formatted_text_box(
[
submitter.email && {
text: "Email verification: #{click_email_event ? VERIFIED_TEXT : 'Unverififed'}\n"
text: "Email verification: #{click_email_event ? VERIFIED_TEXT : UNVERIFIED_TEXT}\n"
},
submitter.phone && {
text: "Phone verification: #{is_phone_verified ? VERIFIED_TEXT : 'Unverififed'}\n"
text: "Phone verification: #{is_phone_verified ? VERIFIED_TEXT : UNVERIFIED_TEXT}\n"
},
completed_event.data['ip'] && { text: "IP: #{completed_event.data['ip']}\n" },
completed_event.data['sid'] && { text: "Session ID: #{completed_event.data['sid']}\n" },
@@ -157,6 +157,17 @@ module Submissions
lines = layouter.fit([text], area['w'] * width, height).lines
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
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
data = conn.get(url).body
data = conn.get(Addressable::URI.parse(url).display_uri.to_s).body
checksum = Digest::MD5.base64digest(data)
+1
View File
@@ -20,6 +20,7 @@ module Submitters
.except('uuid', 'values', 'slug')
.merge('values' => values,
'documents' => documents,
'audit_log_url' => submitter.submission.audit_log_url,
'role' => submitter_name)
end
+53
View File
@@ -4,6 +4,8 @@ module Submitters
module SubmitValues
ValidationError = Class.new(StandardError)
VARIABLE_REGEXP = /\{\{?(\w+)\}\}?/
module_function
def call(submitter, params, request)
@@ -36,6 +38,7 @@ module Submitters
submitter.completed_at = Time.current
submitter.ip = request.remote_ip
submitter.ua = request.user_agent
submitter.values = merge_default_values(submitter)
end
ApplicationRecord.transaction do
@@ -69,6 +72,56 @@ module Submitters
end
end
def merge_default_values(submitter)
default_values = submitter.submission.template_fields.each_with_object({}) do |field, acc|
next if field['submitter_uuid'] != submitter.uuid
value = field['default_value']
next if value.blank?
acc[field['uuid']] = template_default_value_for_submitter(value, submitter, with_time: true)
end
default_values.compact_blank.merge(submitter.values)
end
def template_default_value_for_submitter(value, submitter, with_time: false)
return if value.blank?
role = submitter.submission.template_submitters.find { |e| e['uuid'] == submitter.uuid }['name']
replace_default_variables(value,
submitter.attributes.merge('role' => role),
submitter.submission.template,
with_time:)
end
def replace_default_variables(value, attrs, template, with_time: false)
return if value.blank?
value.gsub(VARIABLE_REGEXP) do |e|
case key = ::Regexp.last_match(1)
when 'time'
if with_time
I18n.l(Time.current.in_time_zone(template.account.timezone), format: :long, locale: template.account.locale)
else
e
end
when 'date'
if with_time
I18n.l(Time.current.in_time_zone(template.account.timezone).to_date)
else
e
end
when 'role', 'email', 'phone', 'name'
attrs[key] || e
else
e
end
end
end
def validate_value!(_value, _field, _params, _submitter, _request)
true
end