mirror of
https://github.com/docusealco/docuseal
synced 2026-11-04 14:42:32 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66d1ae2f64 | ||
|
|
d130835902 | ||
|
|
52f66368a3 | ||
|
|
89d0810625 | ||
|
|
06d3d1202b | ||
|
|
0698aad8e1 | ||
|
|
3575a25935 | ||
|
|
cdd034e72d | ||
|
|
26f7d5cf0d | ||
|
|
3c69430b94 | ||
|
|
c0162bccc8 | ||
|
|
1adb800f94 | ||
|
|
333a5d6b7b | ||
|
|
9aa68fe955 | ||
|
|
46af87d89a | ||
|
|
1066131143 | ||
|
|
a25022532e | ||
|
|
5162f9a9dc | ||
|
|
cc5694de43 | ||
|
|
f734ba280d | ||
|
|
500d0da563 | ||
|
|
6626a2b602 | ||
|
|
07e66a6e53 | ||
|
|
deef9c7d3b | ||
|
|
2ec8954dc8 | ||
|
|
c1af803e5b | ||
|
|
10abdbeba4 | ||
|
|
1dbde7a6d8 | ||
|
|
a27eb18670 | ||
|
|
44a49a4533 | ||
|
|
fc11078cae | ||
|
|
a42ae0b39e | ||
|
|
3163cdc761 |
No files matched your search
@@ -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)'
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
module Api
|
module Api
|
||||||
class SubmissionsController < ApiBaseController
|
class SubmissionsController < ApiBaseController
|
||||||
load_and_authorize_resource :template, only: :create
|
load_and_authorize_resource :template, only: :create
|
||||||
load_and_authorize_resource :submission, only: %i[show index]
|
load_and_authorize_resource :submission, only: %i[show index destroy]
|
||||||
|
|
||||||
before_action only: :create do
|
before_action only: :create do
|
||||||
authorize!(:create, Submission)
|
authorize!(:create, Submission)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class SubmissionsPreviewController < ApplicationController
|
||||||
|
skip_before_action :authenticate_user!
|
||||||
|
skip_authorization_check
|
||||||
|
|
||||||
|
PRELOAD_ALL_PAGES_AMOUNT = 200
|
||||||
|
|
||||||
|
def show
|
||||||
|
@submission = Submission.find_by!(slug: params[:slug])
|
||||||
|
|
||||||
|
ActiveRecord::Associations::Preloader.new(
|
||||||
|
records: [@submission],
|
||||||
|
associations: [:template, { template_schema_documents: :blob }]
|
||||||
|
).call
|
||||||
|
|
||||||
|
total_pages =
|
||||||
|
@submission.template_schema_documents.sum { |e| e.metadata.dig('pdf', 'number_of_pages').to_i }
|
||||||
|
|
||||||
|
if total_pages < PRELOAD_ALL_PAGES_AMOUNT
|
||||||
|
ActiveRecord::Associations::Preloader.new(
|
||||||
|
records: @submission.template_schema_documents,
|
||||||
|
associations: [:blob, { preview_images_attachments: :blob }]
|
||||||
|
).call
|
||||||
|
end
|
||||||
|
|
||||||
|
render 'submissions/show', layout: 'plain'
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -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(
|
||||||
|
|||||||
@@ -1,7 +1,21 @@
|
|||||||
export default class extends HTMLElement {
|
export default class extends HTMLElement {
|
||||||
connectedCallback () {
|
connectedCallback () {
|
||||||
if (this.dataset.inputId) {
|
if (this.dataset.inputId) {
|
||||||
document.getElementById(this.dataset.inputId).value = Intl.DateTimeFormat().resolvedOptions().timeZone
|
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||||
|
|
||||||
|
if (this.dataset.params === 'true') {
|
||||||
|
const params = new URLSearchParams(this.input.value)
|
||||||
|
|
||||||
|
params.set('timezone', timezone)
|
||||||
|
|
||||||
|
this.input.value = params.toString()
|
||||||
|
} else {
|
||||||
|
this.input.value = timezone
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get input () {
|
||||||
|
return document.getElementById(this.dataset.inputId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
<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 && !area.option_uuid"
|
||||||
class="absolute top-0 bottom-0 right-0 left-0 items-center justify-center h-full w-full"
|
class="absolute top-0 bottom-0 right-0 left-0 items-center justify-center h-full w-full"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="isActive && withLabel"
|
v-if="isActive && withLabel && !area.option_uuid"
|
||||||
class="absolute -top-7 rounded bg-base-content text-base-100 px-2 text-sm whitespace-nowrap"
|
class="absolute -top-7 rounded bg-base-content text-base-100 px-2 text-sm whitespace-nowrap"
|
||||||
>
|
>
|
||||||
{{ field.name || fieldNames[field.type] }}
|
{{ field.name || fieldNames[field.type] }}
|
||||||
@@ -87,6 +87,44 @@
|
|||||||
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
|
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="field.type === 'radio' && area.option_uuid"
|
||||||
|
class="w-full p-[0.2vw] flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-if="submittable"
|
||||||
|
type="radio"
|
||||||
|
:value="false"
|
||||||
|
class="aspect-square base-radio"
|
||||||
|
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
|
||||||
|
:checked="!!modelValue && modelValue === field.options.find((o) => o.uuid === area.option_uuid)?.value"
|
||||||
|
@click="$emit('update:model-value', field.options.find((o) => o.uuid === area.option_uuid)?.value)"
|
||||||
|
>
|
||||||
|
<IconCheck
|
||||||
|
v-else-if="!!modelValue && modelValue === field.options.find((o) => o.uuid === area.option_uuid)?.value"
|
||||||
|
class="aspect-square"
|
||||||
|
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="field.type === 'multiple' && area.option_uuid"
|
||||||
|
class="w-full p-[0.2vw] flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-if="submittable"
|
||||||
|
type="checkbox"
|
||||||
|
:value="false"
|
||||||
|
class="aspect-square base-checkbox"
|
||||||
|
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
|
||||||
|
:checked="!!modelValue && modelValue.includes(field.options.find((o) => o.uuid === area.option_uuid)?.value)"
|
||||||
|
@change="updateMultipleSelectValue(field.options.find((o) => o.uuid === area.option_uuid)?.value)"
|
||||||
|
>
|
||||||
|
<IconCheck
|
||||||
|
v-else-if="!!modelValue && modelValue.includes(field.options.find((o) => o.uuid === area.option_uuid)?.value)"
|
||||||
|
class="aspect-square"
|
||||||
|
:class="{ '!w-auto !h-full': area.w > area.h, '!w-full !h-auto': area.w <= area.h }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else-if="field.type === 'cells'"
|
v-else-if="field.type === 'cells'"
|
||||||
class="w-full flex items-center"
|
class="w-full flex items-center"
|
||||||
@@ -102,6 +140,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 +212,11 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
emits: ['update:model-value'],
|
emits: ['update:model-value'],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
textOverflowChars: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
fieldNames () {
|
fieldNames () {
|
||||||
return {
|
return {
|
||||||
@@ -251,6 +295,37 @@ 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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
updateMultipleSelectValue (value) {
|
||||||
|
if (this.modelValue?.includes(value)) {
|
||||||
|
const newValue = [...this.modelValue]
|
||||||
|
|
||||||
|
newValue.splice(newValue.indexOf(value), 1)
|
||||||
|
|
||||||
|
this.$emit('update:model-value', newValue)
|
||||||
|
} else {
|
||||||
|
const newValue = this.modelValue ? [...this.modelValue] : []
|
||||||
|
|
||||||
|
newValue.push(value)
|
||||||
|
|
||||||
|
this.$emit('update:model-value', newValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</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)])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,11 @@ const en = {
|
|||||||
please_fill_all_required_fields: 'Please fill all required fields',
|
please_fill_all_required_fields: 'Please fill all required fields',
|
||||||
set_today: 'Set Today',
|
set_today: 'Set Today',
|
||||||
toggle_multiline_text: 'Toggle Multiline Text',
|
toggle_multiline_text: 'Toggle Multiline Text',
|
||||||
|
draw_signature: 'Draw signature',
|
||||||
|
type_initial: 'Type initials',
|
||||||
|
draw: 'Draw',
|
||||||
|
type: 'Type',
|
||||||
|
type_text: 'Type text',
|
||||||
date: 'Date',
|
date: 'Date',
|
||||||
email_has_been_sent: 'Email has been sent'
|
email_has_been_sent: 'Email has been sent'
|
||||||
}
|
}
|
||||||
@@ -71,6 +76,11 @@ const es = {
|
|||||||
set_today: 'Establecer Hoy',
|
set_today: 'Establecer Hoy',
|
||||||
date: 'Fecha',
|
date: 'Fecha',
|
||||||
toggle_multiline_text: 'Alternar Texto Multilínea',
|
toggle_multiline_text: 'Alternar Texto Multilínea',
|
||||||
|
draw_signature: 'Dibujar firma',
|
||||||
|
type_initial: 'Escribir iniciales',
|
||||||
|
draw: 'Dibujar',
|
||||||
|
type: 'Escribir',
|
||||||
|
type_text: 'Escribir texto',
|
||||||
email_has_been_sent: 'El correo electrónico ha sido enviado'
|
email_has_been_sent: 'El correo electrónico ha sido enviado'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,6 +118,11 @@ const it = {
|
|||||||
please_fill_all_required_fields: 'Si prega di compilare tutti i campi obbligatori',
|
please_fill_all_required_fields: 'Si prega di compilare tutti i campi obbligatori',
|
||||||
set_today: 'Imposta Oggi',
|
set_today: 'Imposta Oggi',
|
||||||
date: 'Data',
|
date: 'Data',
|
||||||
|
draw_signature: 'Disegna firma',
|
||||||
|
type_initial: 'Inserisci iniziali',
|
||||||
|
draw: 'Disegna',
|
||||||
|
type: 'Inserisci',
|
||||||
|
type_text: 'Inserisci testo',
|
||||||
toggle_multiline_text: 'Attiva Testo Multilinea',
|
toggle_multiline_text: 'Attiva Testo Multilinea',
|
||||||
email_has_been_sent: "L'email è stata inviata"
|
email_has_been_sent: "L'email è stata inviata"
|
||||||
}
|
}
|
||||||
@@ -146,6 +161,11 @@ const de = {
|
|||||||
please_fill_all_required_fields: 'Bitte füllen Sie alle erforderlichen Felder aus',
|
please_fill_all_required_fields: 'Bitte füllen Sie alle erforderlichen Felder aus',
|
||||||
set_today: 'Heute einstellen',
|
set_today: 'Heute einstellen',
|
||||||
date: 'Datum',
|
date: 'Datum',
|
||||||
|
draw_signature: 'Unterschrift zeichnen',
|
||||||
|
type_initial: 'Initialen eingeben',
|
||||||
|
draw: 'Zeichnen',
|
||||||
|
type: 'Eingeben',
|
||||||
|
type_text: 'Text eingeben',
|
||||||
toggle_multiline_text: 'Mehrzeiligen Text umschalten',
|
toggle_multiline_text: 'Mehrzeiligen Text umschalten',
|
||||||
email_has_been_sent: 'Die E-Mail wurde gesendet'
|
email_has_been_sent: 'Die E-Mail wurde gesendet'
|
||||||
}
|
}
|
||||||
@@ -184,6 +204,11 @@ const fr = {
|
|||||||
please_fill_all_required_fields: 'Veuillez remplir tous les champs obligatoires',
|
please_fill_all_required_fields: 'Veuillez remplir tous les champs obligatoires',
|
||||||
set_today: "Définir Aujourd'hui",
|
set_today: "Définir Aujourd'hui",
|
||||||
date: 'Date',
|
date: 'Date',
|
||||||
|
draw_signature: 'Dessiner une signature',
|
||||||
|
type_initial: 'Saisir les initiales',
|
||||||
|
draw: 'Dessiner',
|
||||||
|
type: 'Saisir',
|
||||||
|
type_text: 'Saisir du texte',
|
||||||
toggle_multiline_text: 'Basculer le Texte Multiligne',
|
toggle_multiline_text: 'Basculer le Texte Multiligne',
|
||||||
email_has_been_sent: "L'email a été envoyé"
|
email_has_been_sent: "L'email a été envoyé"
|
||||||
}
|
}
|
||||||
@@ -222,6 +247,11 @@ const pl = {
|
|||||||
please_fill_all_required_fields: 'Proszę wypełnić wszystkie wymagane pola',
|
please_fill_all_required_fields: 'Proszę wypełnić wszystkie wymagane pola',
|
||||||
set_today: 'Ustaw Dziś',
|
set_today: 'Ustaw Dziś',
|
||||||
date: 'Data',
|
date: 'Data',
|
||||||
|
draw_signature: 'Rysuj podpis',
|
||||||
|
type_initial: 'Wprowadź inicjały',
|
||||||
|
draw: 'Rysuj',
|
||||||
|
type: 'Wprowadź',
|
||||||
|
type_text: 'Wprowadź tekst',
|
||||||
toggle_multiline_text: 'Przełącz Tekst Wielolinijkowy',
|
toggle_multiline_text: 'Przełącz Tekst Wielolinijkowy',
|
||||||
email_has_been_sent: 'E-mail został wysłany'
|
email_has_been_sent: 'E-mail został wysłany'
|
||||||
}
|
}
|
||||||
@@ -260,6 +290,11 @@ const uk = {
|
|||||||
please_fill_all_required_fields: "Будь ласка, заповніть всі обов'язкові поля",
|
please_fill_all_required_fields: "Будь ласка, заповніть всі обов'язкові поля",
|
||||||
set_today: 'Задати Сьогодні',
|
set_today: 'Задати Сьогодні',
|
||||||
date: 'Дата',
|
date: 'Дата',
|
||||||
|
draw_signature: 'Намалюйте підпис',
|
||||||
|
type_initial: 'Введіть ініціали',
|
||||||
|
draw: 'Підпис',
|
||||||
|
type: 'Текст',
|
||||||
|
type_text: 'Введіть текст',
|
||||||
toggle_multiline_text: 'Перемкнути Багаторядковий Текст',
|
toggle_multiline_text: 'Перемкнути Багаторядковий Текст',
|
||||||
email_has_been_sent: 'Електронний лист був відправлений'
|
email_has_been_sent: 'Електронний лист був відправлений'
|
||||||
}
|
}
|
||||||
@@ -298,6 +333,11 @@ const cs = {
|
|||||||
please_fill_all_required_fields: 'Prosím vyplňte všechny povinné položky',
|
please_fill_all_required_fields: 'Prosím vyplňte všechny povinné položky',
|
||||||
set_today: 'Nastavit Dnes',
|
set_today: 'Nastavit Dnes',
|
||||||
date: 'Datum',
|
date: 'Datum',
|
||||||
|
draw_signature: 'Nakreslit podpis',
|
||||||
|
type_initial: 'Zadat iniciály',
|
||||||
|
draw: 'Kreslit',
|
||||||
|
type: 'Zadat',
|
||||||
|
type_text: 'Zadat text',
|
||||||
toggle_multiline_text: 'Přepnout Víceřádkový Text',
|
toggle_multiline_text: 'Přepnout Víceřádkový Text',
|
||||||
email_has_been_sent: 'E-mail byl odeslán'
|
email_has_been_sent: 'E-mail byl odeslán'
|
||||||
}
|
}
|
||||||
@@ -336,6 +376,11 @@ const pt = {
|
|||||||
please_fill_all_required_fields: 'Por favor, preencha todos os campos obrigatórios',
|
please_fill_all_required_fields: 'Por favor, preencha todos os campos obrigatórios',
|
||||||
set_today: 'Definir Hoje',
|
set_today: 'Definir Hoje',
|
||||||
date: 'Data',
|
date: 'Data',
|
||||||
|
draw_signature: 'Desenhar assinatura',
|
||||||
|
type_initial: 'Inserir iniciais',
|
||||||
|
draw: 'Desenhar',
|
||||||
|
type: 'Inserir',
|
||||||
|
type_text: 'Inserir texto',
|
||||||
toggle_multiline_text: 'Alternar Texto Multilinha',
|
toggle_multiline_text: 'Alternar Texto Multilinha',
|
||||||
email_has_been_sent: 'Email enviado'
|
email_has_been_sent: 'Email enviado'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,23 +6,43 @@
|
|||||||
>{{ field.name || t('initials') }}</label>
|
>{{ field.name || t('initials') }}</label>
|
||||||
<div class="space-x-2 flex">
|
<div class="space-x-2 flex">
|
||||||
<span
|
<span
|
||||||
|
v-if="isDrawInitials"
|
||||||
|
class="tooltip"
|
||||||
|
:data-tip="t('type_initials')"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
id="type_text_button"
|
||||||
|
href="#"
|
||||||
|
class="btn btn-outline font-medium btn-sm"
|
||||||
|
@click.prevent="toggleTextInput"
|
||||||
|
>
|
||||||
|
<IconTextSize :width="16" />
|
||||||
|
<span class="hidden sm:inline">
|
||||||
|
{{ t('type') }}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
class="tooltip"
|
class="tooltip"
|
||||||
:data-tip="t('draw_initials')"
|
:data-tip="t('draw_initials')"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
id="type_text_button"
|
id="type_text_button"
|
||||||
href="#"
|
href="#"
|
||||||
class="btn btn-sm btn-circle"
|
class="btn btn-outline font-medium btn-sm"
|
||||||
:class="{ 'btn-neutral': isDrawInitials, 'btn-outline': !isDrawInitials }"
|
|
||||||
@click.prevent="toggleTextInput"
|
@click.prevent="toggleTextInput"
|
||||||
>
|
>
|
||||||
<IconSignature :width="16" />
|
<IconSignature :width="16" />
|
||||||
|
<span class="hidden sm:inline">
|
||||||
|
{{ t('draw') }}
|
||||||
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
<a
|
<a
|
||||||
v-if="modelValue || computedPreviousValue"
|
v-if="modelValue || computedPreviousValue"
|
||||||
href="#"
|
href="#"
|
||||||
class="btn btn-outline btn-sm"
|
class="btn font-medium btn-outline btn-sm"
|
||||||
@click.prevent="remove"
|
@click.prevent="remove"
|
||||||
>
|
>
|
||||||
<IconReload :width="16" />
|
<IconReload :width="16" />
|
||||||
@@ -31,7 +51,7 @@
|
|||||||
<a
|
<a
|
||||||
v-else
|
v-else
|
||||||
href="#"
|
href="#"
|
||||||
class="btn btn-outline btn-sm"
|
class="btn font-medium btn-outline btn-sm"
|
||||||
@click.prevent="clear"
|
@click.prevent="clear"
|
||||||
>
|
>
|
||||||
<IconReload :width="16" />
|
<IconReload :width="16" />
|
||||||
@@ -63,7 +83,7 @@
|
|||||||
<canvas
|
<canvas
|
||||||
v-show="!modelValue && !computedPreviousValue"
|
v-show="!modelValue && !computedPreviousValue"
|
||||||
ref="canvas"
|
ref="canvas"
|
||||||
class="bg-white border border-base-300 rounded"
|
class="bg-white border border-base-300 rounded-2xl"
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
v-if="!isDrawInitials && !modelValue && !computedPreviousValue"
|
v-if="!isDrawInitials && !modelValue && !computedPreviousValue"
|
||||||
@@ -81,13 +101,14 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { cropCanvasAndExportToPNG } from './crop_canvas'
|
import { cropCanvasAndExportToPNG } from './crop_canvas'
|
||||||
import { IconReload, IconSignature, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
|
import { IconReload, IconTextSize, IconSignature, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
|
||||||
import SignaturePad from 'signature_pad'
|
import SignaturePad from 'signature_pad'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'InitialsStep',
|
name: 'InitialsStep',
|
||||||
components: {
|
components: {
|
||||||
IconReload,
|
IconReload,
|
||||||
|
IconTextSize,
|
||||||
IconSignature,
|
IconSignature,
|
||||||
IconArrowsDiagonalMinimize2
|
IconArrowsDiagonalMinimize2
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -6,17 +6,37 @@
|
|||||||
>{{ field.name || t('signature') }}</label>
|
>{{ field.name || t('signature') }}</label>
|
||||||
<div class="space-x-2 flex">
|
<div class="space-x-2 flex">
|
||||||
<span
|
<span
|
||||||
|
v-if="isTextSignature"
|
||||||
class="tooltip"
|
class="tooltip"
|
||||||
data-tip="Type text"
|
:data-tip="t('draw_signature')"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
id="type_text_button"
|
id="type_text_button"
|
||||||
href="#"
|
href="#"
|
||||||
class="btn btn-sm btn-circle"
|
class="btn btn-outline btn-sm font-medium"
|
||||||
:class="{ 'btn-neutral': isTextSignature, 'btn-outline': !isTextSignature }"
|
@click.prevent="toggleTextInput"
|
||||||
|
>
|
||||||
|
<IconSignature :width="16" />
|
||||||
|
<span class="hidden sm:inline">
|
||||||
|
{{ t('draw') }}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
class="tooltip"
|
||||||
|
:data-tip="t('type_text')"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
id="type_text_button"
|
||||||
|
href="#"
|
||||||
|
class="btn btn-outline btn-sm font-medium"
|
||||||
@click.prevent="toggleTextInput"
|
@click.prevent="toggleTextInput"
|
||||||
>
|
>
|
||||||
<IconTextSize :width="16" />
|
<IconTextSize :width="16" />
|
||||||
|
<span class="hidden sm:inline">
|
||||||
|
{{ t('type') }}
|
||||||
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
@@ -24,7 +44,7 @@
|
|||||||
data-tip="Take photo"
|
data-tip="Take photo"
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
class="btn btn-outline btn-sm btn-circle"
|
class="btn btn-outline btn-sm font-medium"
|
||||||
>
|
>
|
||||||
<IconCamera :width="16" />
|
<IconCamera :width="16" />
|
||||||
<input
|
<input
|
||||||
@@ -33,12 +53,15 @@
|
|||||||
accept="image/*"
|
accept="image/*"
|
||||||
@change="drawImage"
|
@change="drawImage"
|
||||||
>
|
>
|
||||||
|
<span class="hidden sm:inline">
|
||||||
|
{{ t('upload') }}
|
||||||
|
</span>
|
||||||
</label>
|
</label>
|
||||||
</span>
|
</span>
|
||||||
<a
|
<a
|
||||||
v-if="modelValue || computedPreviousValue"
|
v-if="modelValue || computedPreviousValue"
|
||||||
href="#"
|
href="#"
|
||||||
class="btn btn-outline btn-sm"
|
class="btn btn-outline btn-sm font-medium"
|
||||||
@click.prevent="remove"
|
@click.prevent="remove"
|
||||||
>
|
>
|
||||||
<IconReload :width="16" />
|
<IconReload :width="16" />
|
||||||
@@ -47,7 +70,7 @@
|
|||||||
<a
|
<a
|
||||||
v-else
|
v-else
|
||||||
href="#"
|
href="#"
|
||||||
class="btn btn-outline btn-sm"
|
class="btn btn-outline btn-sm font-medium"
|
||||||
@click.prevent="clear"
|
@click.prevent="clear"
|
||||||
>
|
>
|
||||||
<IconReload :width="16" />
|
<IconReload :width="16" />
|
||||||
@@ -79,7 +102,8 @@
|
|||||||
<canvas
|
<canvas
|
||||||
v-show="!modelValue && !computedPreviousValue"
|
v-show="!modelValue && !computedPreviousValue"
|
||||||
ref="canvas"
|
ref="canvas"
|
||||||
class="bg-white border border-base-300 rounded"
|
style="padding: 1px; 0"
|
||||||
|
class="bg-white border border-base-300 rounded-2xl"
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
v-if="isTextSignature"
|
v-if="isTextSignature"
|
||||||
@@ -95,7 +119,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { IconReload, IconCamera, IconTextSize, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
|
import { IconReload, IconCamera, IconSignature, IconTextSize, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
|
||||||
import { cropCanvasAndExportToPNG } from './crop_canvas'
|
import { cropCanvasAndExportToPNG } from './crop_canvas'
|
||||||
import SignaturePad from 'signature_pad'
|
import SignaturePad from 'signature_pad'
|
||||||
|
|
||||||
@@ -107,6 +131,7 @@ export default {
|
|||||||
IconReload,
|
IconReload,
|
||||||
IconCamera,
|
IconCamera,
|
||||||
IconTextSize,
|
IconTextSize,
|
||||||
|
IconSignature,
|
||||||
IconArrowsDiagonalMinimize2
|
IconArrowsDiagonalMinimize2
|
||||||
},
|
},
|
||||||
inject: ['baseUrl', 't'],
|
inject: ['baseUrl', 't'],
|
||||||
|
|||||||
@@ -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) + 1
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
},
|
||||||
text: {
|
text: {
|
||||||
set (value) {
|
set (value) {
|
||||||
this.$emit('update:model-value', value)
|
this.$emit('update:model-value', value)
|
||||||
|
|||||||
@@ -66,9 +66,9 @@
|
|||||||
@keydown.enter.prevent="onNameEnter"
|
@keydown.enter.prevent="onNameEnter"
|
||||||
@focus="onNameFocus"
|
@focus="onNameFocus"
|
||||||
@blur="onNameBlur"
|
@blur="onNameBlur"
|
||||||
>{{ field.name || defaultName }}</span>
|
>{{ optionIndexText }} {{ 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 class="flex items-center px-0.5">
|
<div
|
||||||
|
ref="textContainer"
|
||||||
|
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 }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -180,6 +185,13 @@ export default {
|
|||||||
defaultName: Field.computed.defaultName,
|
defaultName: Field.computed.defaultName,
|
||||||
fieldNames: FieldType.computed.fieldNames,
|
fieldNames: FieldType.computed.fieldNames,
|
||||||
fieldIcons: FieldType.computed.fieldIcons,
|
fieldIcons: FieldType.computed.fieldIcons,
|
||||||
|
optionIndexText () {
|
||||||
|
if (this.area.option_uuid && this.field.options) {
|
||||||
|
return `${this.field.options.findIndex((o) => o.uuid === this.area.option_uuid) + 1}.`
|
||||||
|
} else {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
},
|
||||||
cells () {
|
cells () {
|
||||||
const cells = []
|
const cells = []
|
||||||
|
|
||||||
@@ -241,6 +253,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 +311,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) => {
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -164,7 +168,7 @@
|
|||||||
:selected-submitter="selectedSubmitter"
|
:selected-submitter="selectedSubmitter"
|
||||||
class="md:hidden"
|
class="md:hidden"
|
||||||
:editable="editable"
|
:editable="editable"
|
||||||
@cancel="drawField = null"
|
@cancel="[drawField = null, drawOption = null]"
|
||||||
@change-submitter="[selectedSubmitter = $event, drawField.submitter_uuid = $event.uuid]"
|
@change-submitter="[selectedSubmitter = $event, drawField.submitter_uuid = $event.uuid]"
|
||||||
/>
|
/>
|
||||||
<FieldType
|
<FieldType
|
||||||
@@ -205,7 +209,7 @@
|
|||||||
<p>
|
<p>
|
||||||
<button
|
<button
|
||||||
class="base-button"
|
class="base-button"
|
||||||
@click="drawField = null"
|
@click="[drawField = null, drawOption = null]"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
@@ -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.field, drawOption = $event.option]"
|
||||||
@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,8 @@ export default {
|
|||||||
isSaving: false,
|
isSaving: false,
|
||||||
selectedSubmitter: null,
|
selectedSubmitter: null,
|
||||||
drawField: null,
|
drawField: null,
|
||||||
dragFieldType: null
|
drawOption: null,
|
||||||
|
dragField: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -366,6 +387,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,10 +430,11 @@ 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
|
||||||
|
this.drawOption = null
|
||||||
},
|
},
|
||||||
undo () {
|
undo () {
|
||||||
if (this.undoStack.length > 1) {
|
if (this.undoStack.length > 1) {
|
||||||
@@ -455,6 +484,7 @@ export default {
|
|||||||
onKeyUp (e) {
|
onKeyUp (e) {
|
||||||
if (e.code === 'Escape') {
|
if (e.code === 'Escape') {
|
||||||
this.drawField = null
|
this.drawField = null
|
||||||
|
this.drawOption = null
|
||||||
this.selectedAreaRef.value = null
|
this.selectedAreaRef.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -501,6 +531,16 @@ export default {
|
|||||||
},
|
},
|
||||||
onDraw (area) {
|
onDraw (area) {
|
||||||
if (this.drawField) {
|
if (this.drawField) {
|
||||||
|
if (this.drawOption) {
|
||||||
|
const areaWithoutOption = this.drawField.areas?.find((a) => !a.option_uuid)
|
||||||
|
|
||||||
|
if (areaWithoutOption && !this.drawField.areas.find((a) => a.option_uuid === this.drawField.options[0].uuid)) {
|
||||||
|
areaWithoutOption.option_uuid = this.drawField.options[0].uuid
|
||||||
|
}
|
||||||
|
|
||||||
|
area.option_uuid = this.drawOption.uuid
|
||||||
|
}
|
||||||
|
|
||||||
this.drawField.areas ||= []
|
this.drawField.areas ||= []
|
||||||
this.drawField.areas.push(area)
|
this.drawField.areas.push(area)
|
||||||
|
|
||||||
@@ -509,6 +549,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.drawField = null
|
this.drawField = null
|
||||||
|
this.drawOption = null
|
||||||
|
|
||||||
this.selectedAreaRef.value = area
|
this.selectedAreaRef.value = area
|
||||||
|
|
||||||
@@ -558,14 +599,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 +620,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 +657,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,
|
||||||
|
|||||||
@@ -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`
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -34,19 +34,21 @@
|
|||||||
v-if="isNameFocus"
|
v-if="isNameFocus"
|
||||||
class="flex items-center relative"
|
class="flex items-center relative"
|
||||||
>
|
>
|
||||||
<input
|
<template v-if="field.type != 'phone'">
|
||||||
:id="`required-checkbox-${field.uuid}`"
|
<input
|
||||||
v-model="field.required"
|
:id="`required-checkbox-${field.uuid}`"
|
||||||
type="checkbox"
|
v-model="field.required"
|
||||||
class="checkbox checkbox-xs no-animation rounded"
|
type="checkbox"
|
||||||
@mousedown.prevent
|
class="checkbox checkbox-xs no-animation rounded"
|
||||||
>
|
@mousedown.prevent
|
||||||
<label
|
>
|
||||||
:for="`required-checkbox-${field.uuid}`"
|
<label
|
||||||
class="label text-xs"
|
:for="`required-checkbox-${field.uuid}`"
|
||||||
@click.prevent="field.required = !field.required"
|
class="label text-xs"
|
||||||
@mousedown.prevent
|
@click.prevent="field.required = !field.required"
|
||||||
>Required</label>
|
@mousedown.prevent
|
||||||
|
>Required</label>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else-if="editable"
|
v-else-if="editable"
|
||||||
@@ -56,7 +58,7 @@
|
|||||||
v-if="field && !field.areas.length"
|
v-if="field && !field.areas.length"
|
||||||
title="Draw"
|
title="Draw"
|
||||||
class="relative cursor-pointer text-transparent group-hover:text-base-content"
|
class="relative cursor-pointer text-transparent group-hover:text-base-content"
|
||||||
@click="$emit('set-draw', field)"
|
@click="$emit('set-draw', { field })"
|
||||||
>
|
>
|
||||||
<IconNewSection
|
<IconNewSection
|
||||||
:width="18"
|
:width="18"
|
||||||
@@ -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"
|
||||||
@@ -146,11 +151,11 @@
|
|||||||
Page {{ area.page + 1 }}
|
Page {{ area.page + 1 }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li v-if="!field.areas?.length || !['radio', 'multiple'].includes(field.type)">
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
class="text-sm py-1 px-2"
|
class="text-sm py-1 px-2"
|
||||||
@click.prevent="$emit('set-draw', field)"
|
@click.prevent="$emit('set-draw', { field })"
|
||||||
>
|
>
|
||||||
<IconNewSection
|
<IconNewSection
|
||||||
:width="20"
|
:width="20"
|
||||||
@@ -188,26 +193,54 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="field.options"
|
v-if="field.options"
|
||||||
|
ref="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>
|
||||||
|
<div
|
||||||
|
v-if="['radio', 'multiple'].includes(field.type) && (index > 0 || field.areas.find((a) => a.option_uuid) || !field.areas.length) && !field.areas.find((a) => a.option_uuid === option.uuid)"
|
||||||
|
class="items-center flex w-full"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-model="option.value"
|
||||||
|
class="w-full input input-primary input-xs text-sm bg-transparent !pr-7 -mr-6"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
@blur="save"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
title="Draw"
|
||||||
|
tabindex="-1"
|
||||||
|
@click.prevent="$emit('set-draw', { field, option })"
|
||||||
|
>
|
||||||
|
<IconNewSection
|
||||||
|
:width="18"
|
||||||
|
:stroke-width="1.6"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<input
|
<input
|
||||||
v-model="field.options[index]"
|
v-else
|
||||||
|
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
|
||||||
|
@focus="maybeFocusOnOptionArea(option)"
|
||||||
@blur="save"
|
@blur="save"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="text-sm w-3.5"
|
class="text-sm w-3.5"
|
||||||
@click="[field.options.splice(index, 1), save()]"
|
tabindex="-1"
|
||||||
|
@click="removeOption(option)"
|
||||||
>
|
>
|
||||||
×
|
×
|
||||||
</button>
|
</button>
|
||||||
@@ -215,7 +248,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 +261,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',
|
||||||
@@ -240,7 +274,7 @@ export default {
|
|||||||
IconCopy,
|
IconCopy,
|
||||||
FieldType
|
FieldType
|
||||||
},
|
},
|
||||||
inject: ['template', 'save', 'backgroundColor'],
|
inject: ['template', 'save', 'backgroundColor', 'selectedAreaRef'],
|
||||||
props: {
|
props: {
|
||||||
field: {
|
field: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -298,12 +332,36 @@ export default {
|
|||||||
}, 1)
|
}, 1)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
maybeFocusOnOptionArea (option) {
|
||||||
|
const area = this.field.areas.find((a) => a.option_uuid === option.uuid)
|
||||||
|
|
||||||
|
if (area) {
|
||||||
|
this.selectedAreaRef.value = area
|
||||||
|
}
|
||||||
|
},
|
||||||
scrollToFirstArea () {
|
scrollToFirstArea () {
|
||||||
return this.field.areas?.[0] && this.$emit('scroll-to', this.field.areas[0])
|
return this.field.areas?.[0] && this.$emit('scroll-to', this.field.areas[0])
|
||||||
},
|
},
|
||||||
closeDropdown () {
|
closeDropdown () {
|
||||||
document.activeElement.blur()
|
document.activeElement.blur()
|
||||||
},
|
},
|
||||||
|
addOption () {
|
||||||
|
this.field.options.push({ value: '', uuid: v4() })
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
const inputs = this.$refs.options.querySelectorAll('input')
|
||||||
|
|
||||||
|
inputs[inputs.length - 1]?.focus()
|
||||||
|
})
|
||||||
|
|
||||||
|
this.save()
|
||||||
|
},
|
||||||
|
removeOption (option) {
|
||||||
|
this.field.options.splice(this.field.options.indexOf(option), 1)
|
||||||
|
this.field.areas.splice(this.field.areas.findIndex((a) => a.option_uuid === option.uuid), 1)
|
||||||
|
|
||||||
|
this.save()
|
||||||
|
},
|
||||||
maybeUpdateOptions () {
|
maybeUpdateOptions () {
|
||||||
delete this.field.default_value
|
delete this.field.default_value
|
||||||
|
|
||||||
@@ -312,7 +370,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) => {
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ class SubmitterMailer < ApplicationMailer
|
|||||||
@submitter = submitter
|
@submitter = submitter
|
||||||
@body = body.presence
|
@body = body.presence
|
||||||
|
|
||||||
@email_config = @current_account.account_configs.find_by(key: AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY)
|
@email_config = AccountConfigs.find_for_account(@current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY)
|
||||||
|
|
||||||
subject =
|
subject =
|
||||||
if @email_config || subject.present?
|
if @email_config || subject.present?
|
||||||
@@ -31,7 +31,7 @@ class SubmitterMailer < ApplicationMailer
|
|||||||
|
|
||||||
Submissions::EnsureResultGenerated.call(submitter)
|
Submissions::EnsureResultGenerated.call(submitter)
|
||||||
|
|
||||||
@email_config = @current_account.account_configs.find_by(key: AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY)
|
@email_config = AccountConfigs.find_for_account(@current_account, AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY)
|
||||||
|
|
||||||
add_completed_email_attachments!(submitter)
|
add_completed_email_attachments!(submitter)
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ class SubmitterMailer < ApplicationMailer
|
|||||||
|
|
||||||
@documents = add_completed_email_attachments!(submitter)
|
@documents = add_completed_email_attachments!(submitter)
|
||||||
|
|
||||||
@email_config = @current_account.account_configs.find_by(key: AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY)
|
@email_config = AccountConfigs.find_for_account(@current_account, AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY)
|
||||||
|
|
||||||
subject =
|
subject =
|
||||||
if @email_config
|
if @email_config
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ class AccountConfig < ApplicationRecord
|
|||||||
'body' => "Hi there,\n\n" \
|
'body' => "Hi there,\n\n" \
|
||||||
"Please check the copy of your \"{{template.name}}\" submission in the email attachments.\n" \
|
"Please check the copy of your \"{{template.name}}\" submission in the email attachments.\n" \
|
||||||
"Alternatively, you can download your copy using:\n\n" \
|
"Alternatively, you can download your copy using:\n\n" \
|
||||||
"{{documents.links}}\n\n" \
|
"{{documents.link}}\n\n" \
|
||||||
"Thanks,\n" \
|
"Thanks,\n" \
|
||||||
'{{account.name}}'
|
'{{account.name}}'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#
|
#
|
||||||
# id :bigint not null, primary key
|
# id :bigint not null, primary key
|
||||||
# deleted_at :datetime
|
# deleted_at :datetime
|
||||||
|
# slug :string not null
|
||||||
# source :text not null
|
# source :text not null
|
||||||
# submitters_order :string not null
|
# submitters_order :string not null
|
||||||
# template_fields :text
|
# template_fields :text
|
||||||
@@ -19,6 +20,7 @@
|
|||||||
# Indexes
|
# Indexes
|
||||||
#
|
#
|
||||||
# index_submissions_on_created_by_user_id (created_by_user_id)
|
# index_submissions_on_created_by_user_id (created_by_user_id)
|
||||||
|
# index_submissions_on_slug (slug) UNIQUE
|
||||||
# index_submissions_on_template_id (template_id)
|
# index_submissions_on_template_id (template_id)
|
||||||
#
|
#
|
||||||
# Foreign Keys
|
# Foreign Keys
|
||||||
@@ -41,6 +43,8 @@ class Submission < ApplicationRecord
|
|||||||
attribute :source, :string, default: 'link'
|
attribute :source, :string, default: 'link'
|
||||||
attribute :submitters_order, :string, default: 'random'
|
attribute :submitters_order, :string, default: 'random'
|
||||||
|
|
||||||
|
attribute :slug, :string, default: -> { SecureRandom.base58(14) }
|
||||||
|
|
||||||
has_one_attached :audit_trail
|
has_one_attached :audit_trail
|
||||||
|
|
||||||
has_many :template_schema_documents,
|
has_many :template_schema_documents,
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ class Submitter < ApplicationRecord
|
|||||||
|
|
||||||
def friendly_name
|
def friendly_name
|
||||||
if name.present? && email.present? && email.exclude?(',')
|
if name.present? && email.present? && email.exclude?(',')
|
||||||
"#{name} <#{email}>"
|
%("#{name.delete('"')}" <#{email}>)
|
||||||
else
|
else
|
||||||
email
|
email
|
||||||
end
|
end
|
||||||
|
|||||||
+2
-1
@@ -52,6 +52,7 @@ class User < ApplicationRecord
|
|||||||
belongs_to :account
|
belongs_to :account
|
||||||
has_one :access_token, dependent: :destroy
|
has_one :access_token, dependent: :destroy
|
||||||
has_many :templates, dependent: :destroy, foreign_key: :author_id, inverse_of: :author
|
has_many :templates, dependent: :destroy, foreign_key: :author_id, inverse_of: :author
|
||||||
|
has_many :template_folders, dependent: :destroy, foreign_key: :author_id, inverse_of: :author
|
||||||
has_many :user_configs, dependent: :destroy
|
has_many :user_configs, dependent: :destroy
|
||||||
has_many :encrypted_configs, dependent: :destroy, class_name: 'EncryptedUserConfig'
|
has_many :encrypted_configs, dependent: :destroy, class_name: 'EncryptedUserConfig'
|
||||||
|
|
||||||
@@ -94,7 +95,7 @@ class User < ApplicationRecord
|
|||||||
|
|
||||||
def friendly_name
|
def friendly_name
|
||||||
if full_name.present?
|
if full_name.present?
|
||||||
"#{full_name} <#{email}>"
|
%("#{full_name.delete('"')}" <#{email}>)
|
||||||
else
|
else
|
||||||
email
|
email
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -16,7 +16,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if devise_mapping.omniauthable? %>
|
<% if devise_mapping.omniauthable? %>
|
||||||
<%= button_to button_title(title: 'Sign up with Google', icon: svg_icon('brand_google', class: 'w-6 h-6')), omniauth_authorize_path(resource_name, :google_oauth2), class: 'white-button w-full mt-4', data: { turbo: false }, method: :post %>
|
<%= form_for '', url: omniauth_authorize_path(resource_name, :google_oauth2), data: { turbo: false }, method: :post do |f| %>
|
||||||
|
<set-timezone data-input-id="state" data-params="true"></set-timezone>
|
||||||
|
<%= hidden_field_tag :state, { redir: params[:redir].to_s }.compact_blank.to_query %>
|
||||||
|
<%= f.button button_title(title: 'Sign up with Google', icon: svg_icon('brand_google', class: 'w-6 h-6')), class: 'white-button w-full mt-4' %>
|
||||||
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= render 'devise/shared/links' %>
|
<%= render 'devise/shared/links' %>
|
||||||
</div>
|
</div>
|
||||||
@@ -20,7 +20,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if devise_mapping.omniauthable? %>
|
<% if devise_mapping.omniauthable? %>
|
||||||
<%= button_to button_title(title: 'Sign in with Google', icon: svg_icon('brand_google', class: 'w-6 h-6')), omniauth_authorize_path(resource_name, :google_oauth2), class: 'white-button w-full mt-4', data: { turbo: false }, method: :post %>
|
<%= form_for '', url: omniauth_authorize_path(resource_name, :google_oauth2), data: { turbo: false }, method: :post do |f| %>
|
||||||
|
<set-timezone data-input-id="state" data-params="true"></set-timezone>
|
||||||
|
<%= hidden_field_tag :state, { redir: params[:redir].to_s }.compact_blank.to_query %>
|
||||||
|
<%= f.button button_title(title: 'Sign in with Google', icon: svg_icon('brand_google', class: 'w-6 h-6')), class: 'white-button w-full mt-4' %>
|
||||||
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= render 'devise/shared/links' %>
|
<%= render 'devise/shared/links' %>
|
||||||
</div>
|
</div>
|
||||||
@@ -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>
|
|
||||||
@@ -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,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' %>
|
||||||
@@ -11,9 +11,16 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% elsif field['type'] == 'checkbox' %>
|
<% elsif field['type'] == 'checkbox' %>
|
||||||
<div class="w-full p-[0.2vw] flex items-center justify-center">
|
<div class="w-full flex items-center justify-center">
|
||||||
<%= svg_icon('check', class: "aspect-square #{area['w'] > area['h'] ? '!w-auto !h-full' : '!w-full !h-auto'}") %>
|
<%= svg_icon('check', class: "aspect-square #{area['w'] > area['h'] ? '!w-auto !h-full' : '!w-full !h-auto'}") %>
|
||||||
</div>
|
</div>
|
||||||
|
<% elsif field['type'].in?(%w[multiple radio]) && area['option_uuid'] %>
|
||||||
|
<% option = field['options']&.find { |o| o['uuid'] == area['option_uuid'] } %>
|
||||||
|
<% if option && Array.wrap(value).include?(option['value']) %>
|
||||||
|
<div class="w-full flex items-center justify-center">
|
||||||
|
<%= svg_icon('check', class: "aspect-square #{area['w'] > area['h'] ? '!w-auto !h-full' : '!w-full !h-auto'}") %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
<% elsif field['type'] == 'cells' %>
|
<% elsif field['type'] == 'cells' %>
|
||||||
<% cell_width = area['cell_w'] / area['w'] * 100 %>
|
<% cell_width = area['cell_w'] / area['w'] * 100 %>
|
||||||
<div class="w-full flex items-center">
|
<div class="w-full flex items-center">
|
||||||
|
|||||||
@@ -61,7 +61,7 @@
|
|||||||
<% fields_index.dig(document.uuid, index)&.each do |(area, field)| %>
|
<% fields_index.dig(document.uuid, index)&.each do |(area, field)| %>
|
||||||
<% value = values[field['uuid']] %>
|
<% value = values[field['uuid']] %>
|
||||||
<% next if value.blank? %>
|
<% next if value.blank? %>
|
||||||
<%= render 'submissions/value', area:, field:, attachments_index:, value:, locale: current_account.locale %>
|
<%= render 'submissions/value', area:, field:, attachments_index:, value:, locale: @submission.template.account.locale %>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -109,18 +109,18 @@
|
|||||||
<div class="flex items-center space-x-1 mt-1">
|
<div class="flex items-center space-x-1 mt-1">
|
||||||
<%= svg_icon('writing', class: 'w-5 h-5') %>
|
<%= svg_icon('writing', class: 'w-5 h-5') %>
|
||||||
<span>
|
<span>
|
||||||
<%= submitter&.completed_at? ? l(submitter.completed_at.in_time_zone(current_account.timezone), format: :long, locale: current_account.locale) : 'Not completed yet' %>
|
<%= submitter&.completed_at? ? l(submitter.completed_at.in_time_zone(@submission.template.account.timezone), format: :long, locale: @submission.template.account.locale) : 'Not completed yet' %>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<% if submitter && submitter.email && !submitter.completed_at && can?(:update, submitter) %>
|
<% if signed_in? && submitter && submitter.email && !submitter.completed_at && can?(:update, submitter) %>
|
||||||
<div class="mt-2 mb-1">
|
<div class="mt-2 mb-1">
|
||||||
<%= button_to button_title(title: submitter.sent_at? ? 'Re-send Email' : 'Send Email', disabled_with: 'Sending'), submitter_send_email_index_path(submitter_slug: submitter.slug), class: 'btn btn-sm btn-primary w-full' %>
|
<%= button_to button_title(title: submitter.sent_at? ? 'Re-send Email' : 'Send Email', disabled_with: 'Sending'), submitter_send_email_index_path(submitter_slug: submitter.slug), class: 'btn btn-sm btn-primary w-full' %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if submitter && submitter.phone && !submitter.completed_at && can?(:update, submitter) %>
|
<% if signed_in? && submitter && submitter.phone && !submitter.completed_at && can?(:update, submitter) %>
|
||||||
<%= render 'send_sms_button', submitter: %>
|
<%= render 'send_sms_button', submitter: %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if submitter && !submitter.completed_at? && can?(:create, submitter) %>
|
<% if signed_in? && submitter && !submitter.completed_at? && can?(:create, submitter) %>
|
||||||
<div class="mt-2 mb-1">
|
<div class="mt-2 mb-1">
|
||||||
<a class="btn btn-sm btn-primary w-full" target="_blank" href="<%= submit_form_path(slug: submitter.slug) %>">
|
<a class="btn btn-sm btn-primary w-full" target="_blank" href="<%= submit_form_path(slug: submitter.slug) %>">
|
||||||
Submit Form
|
Submit Form
|
||||||
@@ -160,7 +160,7 @@
|
|||||||
<% elsif field['type'] == 'checkbox' %>
|
<% elsif field['type'] == 'checkbox' %>
|
||||||
<%= svg_icon('check', class: 'w-6 h-6') %>
|
<%= svg_icon('check', class: 'w-6 h-6') %>
|
||||||
<% elsif field['type'] == 'date' %>
|
<% elsif field['type'] == 'date' %>
|
||||||
<%= l(Date.parse(value), locale: current_account.locale, format: :long) %>
|
<%= l(Date.parse(value), locale: @submission.template.account.locale, format: :long) %>
|
||||||
<% else %>
|
<% else %>
|
||||||
<%= Array.wrap(value).join(', ') %>
|
<%= Array.wrap(value).join(', ') %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -1 +1,3 @@
|
|||||||
<%= render 'docuseal_logo' %>
|
<div class="flex mt-4">
|
||||||
|
<%= render 'docuseal_logo' %>
|
||||||
|
</div>
|
||||||
@@ -5,9 +5,8 @@
|
|||||||
<div style="max-height: -webkit-fill-available;">
|
<div style="max-height: -webkit-fill-available;">
|
||||||
<div id="scrollbox">
|
<div id="scrollbox">
|
||||||
<div class="mx-auto block pb-72" style="max-width: 1000px">
|
<div class="mx-auto block pb-72" style="max-width: 1000px">
|
||||||
<div class="mt-4 flex">
|
<%# flex block w-full sticky top-0 z-50 space-x-2 items-center bg-yellow-100 p-2 border-y border-yellow-200 %>
|
||||||
<%= render 'banner' %>
|
<%= render 'banner' %>
|
||||||
</div>
|
|
||||||
<% (@submitter.submission.template_schema || @submitter.submission.template.schema).each do |item| %>
|
<% (@submitter.submission.template_schema || @submitter.submission.template.schema).each do |item| %>
|
||||||
<% document = @submitter.submission.template_schema_documents.find { |a| a.uuid == item['attachment_uuid'] } %>
|
<% document = @submitter.submission.template_schema_documents.find { |a| a.uuid == item['attachment_uuid'] } %>
|
||||||
<% document_annots_index = document.metadata.dig('pdf', 'annotations')&.group_by { |e| e['page'] } || {} %>
|
<% document_annots_index = document.metadata.dig('pdf', 'annotations')&.group_by { |e| e['page'] } || {} %>
|
||||||
|
|||||||
@@ -3,14 +3,10 @@
|
|||||||
<% else %>
|
<% else %>
|
||||||
<p>Hi there,</p>
|
<p>Hi there,</p>
|
||||||
<p>Please check the copy of your "<%= @submitter.submission.template.name %>" submission in the email attachments.</p>
|
<p>Please check the copy of your "<%= @submitter.submission.template.name %>" submission in the email attachments.</p>
|
||||||
<p>Alternatively, you can download your copy using:</p>
|
<p>Alternatively, you can review and download your copy using:</p>
|
||||||
<% @documents.each do |document| %>
|
<p>
|
||||||
<ul>
|
<%= link_to @submitter.template.name, submissions_preview_url(@submitter.submission.slug) %>
|
||||||
<li>
|
</p>
|
||||||
<%= link_to document.filename.to_s, rails_blob_url(document) %>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<% end %>
|
|
||||||
<p>
|
<p>
|
||||||
Thanks,<br><%= @current_account.name %>
|
Thanks,<br><%= @current_account.name %>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1,2 +1,9 @@
|
|||||||
en:
|
en-US:
|
||||||
hello: "Hello world"
|
date:
|
||||||
|
formats:
|
||||||
|
default: "%m/%d/%Y"
|
||||||
|
|
||||||
|
en-GB:
|
||||||
|
date:
|
||||||
|
formats:
|
||||||
|
default: "%d/%m/%Y"
|
||||||
@@ -80,6 +80,8 @@ Rails.application.routes.draw do
|
|||||||
get :completed
|
get :completed
|
||||||
end
|
end
|
||||||
|
|
||||||
|
resources :submissions_preview, only: %i[show], path: 'e', param: 'slug'
|
||||||
|
|
||||||
resources :send_submission_email, only: %i[create] do
|
resources :send_submission_email, only: %i[create] do
|
||||||
get :success, on: :collection
|
get :success, on: :collection
|
||||||
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
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddSlugToSubmissions < ActiveRecord::Migration[7.0]
|
||||||
|
class MigrationSubmission < ApplicationRecord
|
||||||
|
self.table_name = 'submissions'
|
||||||
|
end
|
||||||
|
|
||||||
|
def up
|
||||||
|
add_column :submissions, :slug, :string
|
||||||
|
|
||||||
|
MigrationSubmission.where(slug: nil).find_each do |submission|
|
||||||
|
submission.update_columns(slug: SecureRandom.base58(14))
|
||||||
|
end
|
||||||
|
|
||||||
|
change_column_null :submissions, :slug, false
|
||||||
|
|
||||||
|
add_index :submissions, :slug, unique: true
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
remove_column :submissions, :slug
|
||||||
|
end
|
||||||
|
end
|
||||||
+3
-1
@@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[7.0].define(version: 2023_11_02_171817) do
|
ActiveRecord::Schema[7.0].define(version: 2023_11_19_222105) 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"
|
||||||
|
|
||||||
@@ -124,7 +124,9 @@ ActiveRecord::Schema[7.0].define(version: 2023_11_02_171817) do
|
|||||||
t.text "template_submitters"
|
t.text "template_submitters"
|
||||||
t.text "source", null: false
|
t.text "source", null: false
|
||||||
t.string "submitters_order", null: false
|
t.string "submitters_order", null: false
|
||||||
|
t.string "slug", null: false
|
||||||
t.index ["created_by_user_id"], name: "index_submissions_on_created_by_user_id"
|
t.index ["created_by_user_id"], name: "index_submissions_on_created_by_user_id"
|
||||||
|
t.index ["slug"], name: "index_submissions_on_slug", unique: true
|
||||||
t.index ["template_id"], name: "index_submissions_on_template_id"
|
t.index ["template_id"], name: "index_submissions_on_template_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,15 @@ module AccountConfigs
|
|||||||
module_function
|
module_function
|
||||||
|
|
||||||
def find_or_initialize_for_key(account, key)
|
def find_or_initialize_for_key(account, key)
|
||||||
account.account_configs.find_by(key:) ||
|
find_for_account(account, key) ||
|
||||||
account.account_configs.new(key:, value: AccountConfig::DEFAULT_VALUES[key])
|
account.account_configs.new(key:, value: AccountConfig::DEFAULT_VALUES[key])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def find_for_account(account, key)
|
||||||
|
configs = account.account_configs.find_by(key:)
|
||||||
|
|
||||||
|
configs ||= Account.order(:id).first.account_configs.find_by(key:) unless Docuseal.multitenant?
|
||||||
|
|
||||||
|
configs
|
||||||
|
end
|
||||||
end
|
end
|
||||||
@@ -23,7 +23,7 @@ module ActionMailerConfigsInterceptor
|
|||||||
if email_configs
|
if email_configs
|
||||||
message.delivery_method(:smtp, build_smtp_configs_hash(email_configs))
|
message.delivery_method(:smtp, build_smtp_configs_hash(email_configs))
|
||||||
|
|
||||||
message.from = "#{email_configs.account.name} <#{email_configs.value['from_email']}>"
|
message.from = %("#{email_configs.account.name.to_s.delete('"')}" <#{email_configs.value['from_email']}>)
|
||||||
else
|
else
|
||||||
message.delivery_method(:test)
|
message.delivery_method(:test)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -9,6 +9,7 @@ module ReplaceEmailVariables
|
|||||||
SUBMISSION_LINK = '{{submission.link}}'
|
SUBMISSION_LINK = '{{submission.link}}'
|
||||||
SUBMISSION_SUBMITTERS = '{{submission.submitters}}'
|
SUBMISSION_SUBMITTERS = '{{submission.submitters}}'
|
||||||
DOCUMENTS_LINKS = '{{documents.links}}'
|
DOCUMENTS_LINKS = '{{documents.links}}'
|
||||||
|
DOCUMENTS_LINK = '{{documents.link}}'
|
||||||
|
|
||||||
module_function
|
module_function
|
||||||
|
|
||||||
@@ -26,6 +27,7 @@ module ReplaceEmailVariables
|
|||||||
text = text.gsub(SUBMISSION_SUBMITTERS, build_submission_submitters(submitter.submission))
|
text = text.gsub(SUBMISSION_SUBMITTERS, build_submission_submitters(submitter.submission))
|
||||||
end
|
end
|
||||||
text = text.gsub(DOCUMENTS_LINKS, build_documents_links_text(submitter))
|
text = text.gsub(DOCUMENTS_LINKS, build_documents_links_text(submitter))
|
||||||
|
text = text.gsub(DOCUMENTS_LINK, build_documents_links_text(submitter))
|
||||||
|
|
||||||
text = text.gsub(ACCOUNT_NAME, submitter.template.account.name) if submitter.template
|
text = text.gsub(ACCOUNT_NAME, submitter.template.account.name) if submitter.template
|
||||||
|
|
||||||
@@ -33,14 +35,9 @@ module ReplaceEmailVariables
|
|||||||
end
|
end
|
||||||
|
|
||||||
def build_documents_links_text(submitter)
|
def build_documents_links_text(submitter)
|
||||||
Submitters.select_attachments_for_download(submitter).map do |document|
|
Rails.application.routes.url_helpers.submissions_preview_url(
|
||||||
link =
|
submitter.submission.slug, **Docuseal.default_url_options
|
||||||
Rails.application.routes.url_helpers.rails_blob_url(
|
)
|
||||||
document, **Docuseal.default_url_options
|
|
||||||
)
|
|
||||||
|
|
||||||
"#{link}\n"
|
|
||||||
end.join
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_submitter_link(submitter, tracking_event_type)
|
def build_submitter_link(submitter, tracking_event_type)
|
||||||
|
|||||||
+7
-3
@@ -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(
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ module Submissions
|
|||||||
"\n",
|
"\n",
|
||||||
{ text: 'Generated at: ', font: [FONT_BOLD_NAME, { variant: :bold }] },
|
{ text: 'Generated at: ', font: [FONT_BOLD_NAME, { variant: :bold }] },
|
||||||
"#{I18n.l(document.created_at.in_time_zone(account.timezone), format: :long, locale: account.locale)} " \
|
"#{I18n.l(document.created_at.in_time_zone(account.timezone), format: :long, locale: account.locale)} " \
|
||||||
"#{timezone_abbr(account.timezone, document.created_at)}"
|
"#{TimeUtils.timezone_abbr(account.timezone, document.created_at)}"
|
||||||
], line_spacing: 1.8
|
], line_spacing: 1.8
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
@@ -232,7 +232,7 @@ module Submissions
|
|||||||
submitter = submission.submitters.find { |e| e.id == event.submitter_id }
|
submitter = submission.submitters.find { |e| e.id == event.submitter_id }
|
||||||
[
|
[
|
||||||
"#{I18n.l(event.event_timestamp.in_time_zone(account.timezone), format: :long, locale: account.locale)} " \
|
"#{I18n.l(event.event_timestamp.in_time_zone(account.timezone), format: :long, locale: account.locale)} " \
|
||||||
"#{timezone_abbr(account.timezone, event.event_timestamp)}",
|
"#{TimeUtils.timezone_abbr(account.timezone, event.event_timestamp)}",
|
||||||
composer.document.layout.formatted_text_box(
|
composer.document.layout.formatted_text_box(
|
||||||
[
|
[
|
||||||
{ text: SubmissionEvents::EVENT_NAMES[event.event_type.to_sym],
|
{ text: SubmissionEvents::EVENT_NAMES[event.event_type.to_sym],
|
||||||
@@ -268,14 +268,6 @@ module Submissions
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def timezone_abbr(timezone, time = Time.current)
|
|
||||||
tz_info = TZInfo::Timezone.get(
|
|
||||||
ActiveSupport::TimeZone::MAPPING[timezone] || timezone || 'UTC'
|
|
||||||
)
|
|
||||||
|
|
||||||
tz_info.abbreviation(time)
|
|
||||||
end
|
|
||||||
|
|
||||||
def add_logo(column)
|
def add_logo(column)
|
||||||
column.image(PdfIcons.logo_io, width: 40, height: 40, position: :float)
|
column.image(PdfIcons.logo_io, width: 40, height: 40, position: :float)
|
||||||
|
|
||||||
|
|||||||
@@ -124,7 +124,13 @@ module Submissions
|
|||||||
layouter.fit(items, area['w'] * width, height_diff.positive? ? box_height : area['h'] * height)
|
layouter.fit(items, area['w'] * width, height_diff.positive? ? box_height : area['h'] * height)
|
||||||
.draw(canvas, (area['x'] * width) + TEXT_LEFT_MARGIN,
|
.draw(canvas, (area['x'] * width) + TEXT_LEFT_MARGIN,
|
||||||
height - (area['y'] * height) + height_diff - TEXT_TOP_MARGIN)
|
height - (area['y'] * height) + height_diff - TEXT_TOP_MARGIN)
|
||||||
when 'checkbox'
|
when ->(type) { type == 'checkbox' || (type.in?(%w[multiple radio]) && area['option_uuid'].present?) }
|
||||||
|
if field['type'].in?(%w[multiple radio])
|
||||||
|
option = field['options']&.find { |o| o['uuid'] == area['option_uuid'] }
|
||||||
|
|
||||||
|
value = Array.wrap(value).include?(option['value'])
|
||||||
|
end
|
||||||
|
|
||||||
next unless value == true
|
next unless value == true
|
||||||
|
|
||||||
scale = [(area['w'] * width) / PdfIcons::WIDTH, (area['h'] * height) / PdfIcons::HEIGHT].min
|
scale = [(area['w'] * width) / PdfIcons::WIDTH, (area['h'] * height) / PdfIcons::HEIGHT].min
|
||||||
@@ -157,6 +163,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)
|
||||||
|
|||||||
@@ -19,7 +19,9 @@ module Submitters
|
|||||||
fields.each do |field|
|
fields.each do |field|
|
||||||
next if field['submitter_uuid'] != submitter.uuid
|
next if field['submitter_uuid'] != submitter.uuid
|
||||||
|
|
||||||
submitter.values[field['uuid']] ||= get_default_value_for_field(field, user, submitter)
|
default_value = get_default_value_for_field(field, user, submitter)
|
||||||
|
|
||||||
|
submitter.values[field['uuid']] ||= default_value if default_value.present?
|
||||||
end
|
end
|
||||||
|
|
||||||
submitter.save!
|
submitter.save!
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ module Submitters
|
|||||||
|
|
||||||
def template_default_value_for_submitter(value, submitter, with_time: false)
|
def template_default_value_for_submitter(value, submitter, with_time: false)
|
||||||
return if value.blank?
|
return if value.blank?
|
||||||
|
return if submitter.blank?
|
||||||
|
|
||||||
role = submitter.submission.template_submitters.find { |e| e['uuid'] == submitter.uuid }['name']
|
role = submitter.submission.template_submitters.find { |e| e['uuid'] == submitter.uuid }['name']
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,9 @@ module Templates
|
|||||||
build_external_link_hash(page, annot).merge('page' => index)
|
build_external_link_hash(page, annot).merge('page' => index)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
rescue PDF::Reader::MalformedPDFError, OpenSSL::Cipher::CipherError
|
rescue StandardError => e
|
||||||
|
Rollbar.error(e) if defined?(Rollbar)
|
||||||
|
|
||||||
[]
|
[]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module TimeUtils
|
||||||
|
module_function
|
||||||
|
|
||||||
|
def timezone_abbr(timezone, time = Time.current)
|
||||||
|
tz_info = TZInfo::Timezone.get(
|
||||||
|
ActiveSupport::TimeZone::MAPPING[timezone] || timezone || 'UTC'
|
||||||
|
)
|
||||||
|
|
||||||
|
tz_info.abbreviation(time)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'Submission Preview' do
|
||||||
|
let(:account) { create(:account) }
|
||||||
|
let(:user) { create(:user, account:) }
|
||||||
|
let(:template) { create(:template, account:, author: user) }
|
||||||
|
|
||||||
|
context 'when not submitted' do
|
||||||
|
let(:submission) { create(:submission, template:, created_by_user: user) }
|
||||||
|
let(:submitters) { template.submitters.map { |s| create(:submitter, submission:, uuid: s['uuid']) } }
|
||||||
|
|
||||||
|
before do
|
||||||
|
visit submissions_preview_path(slug: submission.slug)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'completes the form' do
|
||||||
|
expect(page).to have_content('Not completed')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Reference in new issue
Block a user