mirror of
https://github.com/docusealco/docuseal.git
synced 2026-08-07 15:25:16 +00:00
Compare commits
39 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 | |||
| 5439c63ce7 | |||
| b78287242b | |||
| b869ae38b3 | |||
| 4a320e9ba6 | |||
| 0025928a35 | |||
| c227a1988a |
@@ -4,6 +4,7 @@ class AccountsController < ApplicationController
|
||||
LOCALE_OPTIONS = {
|
||||
'en-US' => 'English (United States)',
|
||||
'en-GB' => 'English (United Kingdom)',
|
||||
'fr-FR' => 'French (France)',
|
||||
'es-ES' => 'Spanish (Spain)',
|
||||
'pt-PT' => 'Portuguese (Portugal)',
|
||||
'de-DE' => 'German (Germany)'
|
||||
|
||||
@@ -25,7 +25,7 @@ module Api
|
||||
|
||||
def paginate(relation)
|
||||
result = relation.order(id: :desc)
|
||||
.limit([params[:limit] || DEFAULT_LIMIT, MAX_LIMIT].min)
|
||||
.limit([params.fetch(:limit, DEFAULT_LIMIT).to_i, MAX_LIMIT].min)
|
||||
|
||||
result = result.where('id < ?', params[:after]) if params[:after].present?
|
||||
result = result.where('id > ?', params[:before]) if params[:before].present?
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
module Api
|
||||
class SubmissionsController < ApiBaseController
|
||||
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
|
||||
authorize!(:create, Submission)
|
||||
|
||||
@@ -57,7 +57,7 @@ module Api
|
||||
schema: [%i[attachment_uuid name]],
|
||||
submitters: [%i[name uuid]],
|
||||
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
|
||||
|
||||
@@ -23,7 +23,7 @@ class PreviewDocumentPageController < ActionController::API
|
||||
|
||||
io = Templates::ProcessDocument.generate_pdf_preview_from_file(attachment, file_path, params[:id].to_i)
|
||||
|
||||
render plain: io.tap(&:rewind).read
|
||||
render plain: io.tap(&:rewind).read, content_type: 'image/jpeg'
|
||||
end
|
||||
|
||||
def find_or_create_document_tempfile_path(attachment)
|
||||
|
||||
@@ -47,7 +47,7 @@ class StartFormController < ApplicationController
|
||||
private
|
||||
|
||||
def submitter_params
|
||||
params.require(:submitter).permit(:email, :phone).tap do |attrs|
|
||||
params.require(:submitter).permit(:email, :phone, :name).tap do |attrs|
|
||||
attrs[:email] = Submissions.normalize_email(attrs[:email])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -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
|
||||
tempfile = Tempfile.new
|
||||
tempfile.binmode
|
||||
tempfile.write(conn.get(params[:url]).body)
|
||||
tempfile.write(conn.get(Addressable::URI.parse(params[:url]).display_uri.to_s).body)
|
||||
tempfile.rewind
|
||||
|
||||
file = ActionDispatch::Http::UploadedFile.new(
|
||||
|
||||
@@ -1,7 +1,21 @@
|
||||
export default class extends HTMLElement {
|
||||
connectedCallback () {
|
||||
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>
|
||||
<div
|
||||
class="flex absolute text-[1.5vw] lg:text-base"
|
||||
class="flex absolute lg:text-base"
|
||||
:style="computedStyle"
|
||||
:class="{ 'cursor-default': !submittable, 'bg-red-100 border cursor-pointer ': submittable, 'border-red-100': !isActive && submittable, 'bg-opacity-70': !isActive && !isValueSet && submittable, 'border-red-500 border-dashed z-10': isActive && submittable, 'bg-opacity-30': (isActive || isValueSet) && submittable }"
|
||||
:class="{ 'text-[1.5vw] lg:text-base': !textOverflowChars, 'text-[1.0vw] lg:text-xs': textOverflowChars, 'cursor-default': !submittable, 'bg-red-100 border cursor-pointer ': submittable, 'border-red-100': !isActive && submittable, 'bg-opacity-70': !isActive && !isValueSet && submittable, 'border-red-500 border-dashed z-10': isActive && submittable, 'bg-opacity-30': (isActive || isValueSet) && submittable }"
|
||||
>
|
||||
<div
|
||||
v-if="!isActive && !isValueSet && field.type !== 'checkbox' && submittable"
|
||||
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"
|
||||
>
|
||||
<span
|
||||
@@ -21,7 +21,7 @@
|
||||
</span>
|
||||
</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"
|
||||
>
|
||||
{{ 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 }"
|
||||
/>
|
||||
</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
|
||||
v-else-if="field.type === 'cells'"
|
||||
class="w-full flex items-center"
|
||||
@@ -102,6 +140,7 @@
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
ref="textContainer"
|
||||
class="flex items-center px-0.5"
|
||||
>
|
||||
<span v-if="Array.isArray(modelValue)">
|
||||
@@ -173,6 +212,11 @@ export default {
|
||||
}
|
||||
},
|
||||
emits: ['update:model-value'],
|
||||
data () {
|
||||
return {
|
||||
textOverflowChars: 0
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
fieldNames () {
|
||||
return {
|
||||
@@ -251,6 +295,37 @@ export default {
|
||||
height: h * 100 + '%'
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
modelValue () {
|
||||
if (this.field.type === 'text' && this.$refs.textContainer && (this.textOverflowChars === 0 || (this.textOverflowChars - 4) > this.modelValue.length)) {
|
||||
this.textOverflowChars = this.$refs.textContainer.scrollHeight > this.$refs.textContainer.clientHeight ? this.modelValue.length : 0
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
if (this.field.type === 'text' && this.$refs.textContainer) {
|
||||
this.$nextTick(() => {
|
||||
this.textOverflowChars = this.$refs.textContainer.scrollHeight > this.$refs.textContainer.clientHeight ? this.modelValue.length : 0
|
||||
})
|
||||
}
|
||||
},
|
||||
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>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="modelValue.length">
|
||||
<div v-if="value.length">
|
||||
<div
|
||||
v-for="(val, index) in modelValue"
|
||||
v-for="(val, index) in value"
|
||||
:key="index"
|
||||
class="flex mb-2"
|
||||
>
|
||||
@@ -26,10 +26,7 @@
|
||||
{{ attachmentsIndex[val].filename }}
|
||||
</span>
|
||||
</a>
|
||||
<button
|
||||
v-if="modelValue"
|
||||
@click.prevent="removeAttachment(val)"
|
||||
>
|
||||
<button @click.prevent="removeAttachment(val)">
|
||||
<IconTrashX
|
||||
:width="18"
|
||||
:heigh="19"
|
||||
@@ -91,18 +88,28 @@ export default {
|
||||
}
|
||||
},
|
||||
emits: ['attached', 'update:model-value'],
|
||||
computed: {
|
||||
value: {
|
||||
set (value) {
|
||||
this.$emit('update:model-value', this.modelValue || [])
|
||||
},
|
||||
get () {
|
||||
return this.modelValue || []
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
removeAttachment (uuid) {
|
||||
this.modelValue.splice(this.modelValue.indexOf(uuid), 1)
|
||||
this.value.splice(this.value.indexOf(uuid), 1)
|
||||
|
||||
this.$emit('update:model-value', this.modelValue)
|
||||
this.$emit('update:model-value', this.value)
|
||||
},
|
||||
onUpload (attachments) {
|
||||
attachments.forEach((attachment) => {
|
||||
this.$emit('attached', attachment)
|
||||
})
|
||||
|
||||
this.$emit('update:model-value', [...this.modelValue, ...attachments.map(a => a.uuid)])
|
||||
this.$emit('update:model-value', [...this.value, ...attachments.map(a => a.uuid)])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,12 +99,12 @@
|
||||
{{ t('select_your_option') }}
|
||||
</option>
|
||||
<option
|
||||
v-for="(option, index) in currentField.options"
|
||||
:key="index"
|
||||
:selected="values[currentField.uuid] == option"
|
||||
:value="option"
|
||||
v-for="option in currentField.options"
|
||||
:key="option.uuid"
|
||||
:selected="values[currentField.uuid] == option.value"
|
||||
:value="option.value"
|
||||
>
|
||||
{{ option }}
|
||||
{{ option.value }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
@@ -119,24 +119,24 @@
|
||||
<div class="flex w-full">
|
||||
<div class="space-y-3.5 mx-auto">
|
||||
<div
|
||||
v-for="(option, index) in currentField.options"
|
||||
:key="index"
|
||||
v-for="option in currentField.options"
|
||||
:key="option.uuid"
|
||||
>
|
||||
<label
|
||||
:for="currentField.uuid + option"
|
||||
:for="option.uuid"
|
||||
class="flex items-center space-x-3"
|
||||
>
|
||||
<input
|
||||
:id="currentField.uuid + option"
|
||||
:id="option.uuid"
|
||||
v-model="values[currentField.uuid]"
|
||||
type="radio"
|
||||
class="base-radio !h-7 !w-7"
|
||||
:name="`values[${currentField.uuid}]`"
|
||||
:value="option"
|
||||
:value="option.value"
|
||||
:required="currentField.required"
|
||||
>
|
||||
<span class="text-xl">
|
||||
{{ option }}
|
||||
{{ option.value }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
@@ -641,12 +641,20 @@ export default {
|
||||
})
|
||||
},
|
||||
saveStep (formData) {
|
||||
const currentFieldUuid = this.currentField.uuid
|
||||
|
||||
if (this.isCompleted) {
|
||||
return Promise.resolve({})
|
||||
} else {
|
||||
return fetch(this.baseUrl + this.submitPath, {
|
||||
method: 'POST',
|
||||
body: formData || new FormData(this.$refs.form)
|
||||
}).then((response) => {
|
||||
if (response.status === 200) {
|
||||
this.submittedValues[currentFieldUuid] = this.values[currentFieldUuid]
|
||||
}
|
||||
|
||||
return response
|
||||
})
|
||||
}
|
||||
},
|
||||
@@ -678,8 +686,6 @@ export default {
|
||||
return Promise.reject(new Error(data.error))
|
||||
}
|
||||
|
||||
this.submittedValues[this.currentField.uuid] = this.values[this.currentField.uuid]
|
||||
|
||||
if (isLastStep) {
|
||||
this.isSecondWalkthrough = true
|
||||
}
|
||||
|
||||
@@ -32,6 +32,11 @@ const en = {
|
||||
please_fill_all_required_fields: 'Please fill all required fields',
|
||||
set_today: 'Set Today',
|
||||
toggle_multiline_text: 'Toggle Multiline Text',
|
||||
draw_signature: 'Draw signature',
|
||||
type_initial: 'Type initials',
|
||||
draw: 'Draw',
|
||||
type: 'Type',
|
||||
type_text: 'Type text',
|
||||
date: 'Date',
|
||||
email_has_been_sent: 'Email has been sent'
|
||||
}
|
||||
@@ -71,6 +76,11 @@ const es = {
|
||||
set_today: 'Establecer Hoy',
|
||||
date: 'Fecha',
|
||||
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'
|
||||
}
|
||||
|
||||
@@ -108,6 +118,11 @@ const it = {
|
||||
please_fill_all_required_fields: 'Si prega di compilare tutti i campi obbligatori',
|
||||
set_today: 'Imposta Oggi',
|
||||
date: 'Data',
|
||||
draw_signature: 'Disegna firma',
|
||||
type_initial: 'Inserisci iniziali',
|
||||
draw: 'Disegna',
|
||||
type: 'Inserisci',
|
||||
type_text: 'Inserisci testo',
|
||||
toggle_multiline_text: 'Attiva Testo Multilinea',
|
||||
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',
|
||||
set_today: 'Heute einstellen',
|
||||
date: 'Datum',
|
||||
draw_signature: 'Unterschrift zeichnen',
|
||||
type_initial: 'Initialen eingeben',
|
||||
draw: 'Zeichnen',
|
||||
type: 'Eingeben',
|
||||
type_text: 'Text eingeben',
|
||||
toggle_multiline_text: 'Mehrzeiligen Text umschalten',
|
||||
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',
|
||||
set_today: "Définir Aujourd'hui",
|
||||
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',
|
||||
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',
|
||||
set_today: 'Ustaw Dziś',
|
||||
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',
|
||||
email_has_been_sent: 'E-mail został wysłany'
|
||||
}
|
||||
@@ -260,6 +290,11 @@ const uk = {
|
||||
please_fill_all_required_fields: "Будь ласка, заповніть всі обов'язкові поля",
|
||||
set_today: 'Задати Сьогодні',
|
||||
date: 'Дата',
|
||||
draw_signature: 'Намалюйте підпис',
|
||||
type_initial: 'Введіть ініціали',
|
||||
draw: 'Підпис',
|
||||
type: 'Текст',
|
||||
type_text: 'Введіть текст',
|
||||
toggle_multiline_text: 'Перемкнути Багаторядковий Текст',
|
||||
email_has_been_sent: 'Електронний лист був відправлений'
|
||||
}
|
||||
@@ -298,6 +333,11 @@ const cs = {
|
||||
please_fill_all_required_fields: 'Prosím vyplňte všechny povinné položky',
|
||||
set_today: 'Nastavit Dnes',
|
||||
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',
|
||||
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',
|
||||
set_today: 'Definir Hoje',
|
||||
date: 'Data',
|
||||
draw_signature: 'Desenhar assinatura',
|
||||
type_initial: 'Inserir iniciais',
|
||||
draw: 'Desenhar',
|
||||
type: 'Inserir',
|
||||
type_text: 'Inserir texto',
|
||||
toggle_multiline_text: 'Alternar Texto Multilinha',
|
||||
email_has_been_sent: 'Email enviado'
|
||||
}
|
||||
|
||||
@@ -6,23 +6,43 @@
|
||||
>{{ field.name || t('initials') }}</label>
|
||||
<div class="space-x-2 flex">
|
||||
<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"
|
||||
:data-tip="t('draw_initials')"
|
||||
>
|
||||
<a
|
||||
id="type_text_button"
|
||||
href="#"
|
||||
class="btn btn-sm btn-circle"
|
||||
:class="{ 'btn-neutral': isDrawInitials, 'btn-outline': !isDrawInitials }"
|
||||
class="btn btn-outline font-medium btn-sm"
|
||||
@click.prevent="toggleTextInput"
|
||||
>
|
||||
<IconSignature :width="16" />
|
||||
<span class="hidden sm:inline">
|
||||
{{ t('draw') }}
|
||||
</span>
|
||||
</a>
|
||||
</span>
|
||||
<a
|
||||
v-if="modelValue || computedPreviousValue"
|
||||
href="#"
|
||||
class="btn btn-outline btn-sm"
|
||||
class="btn font-medium btn-outline btn-sm"
|
||||
@click.prevent="remove"
|
||||
>
|
||||
<IconReload :width="16" />
|
||||
@@ -31,7 +51,7 @@
|
||||
<a
|
||||
v-else
|
||||
href="#"
|
||||
class="btn btn-outline btn-sm"
|
||||
class="btn font-medium btn-outline btn-sm"
|
||||
@click.prevent="clear"
|
||||
>
|
||||
<IconReload :width="16" />
|
||||
@@ -63,7 +83,7 @@
|
||||
<canvas
|
||||
v-show="!modelValue && !computedPreviousValue"
|
||||
ref="canvas"
|
||||
class="bg-white border border-base-300 rounded"
|
||||
class="bg-white border border-base-300 rounded-2xl"
|
||||
/>
|
||||
<input
|
||||
v-if="!isDrawInitials && !modelValue && !computedPreviousValue"
|
||||
@@ -81,13 +101,14 @@
|
||||
|
||||
<script>
|
||||
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'
|
||||
|
||||
export default {
|
||||
name: 'InitialsStep',
|
||||
components: {
|
||||
IconReload,
|
||||
IconTextSize,
|
||||
IconSignature,
|
||||
IconArrowsDiagonalMinimize2
|
||||
},
|
||||
|
||||
@@ -7,25 +7,25 @@
|
||||
<div class="flex w-full">
|
||||
<div class="space-y-3.5 mx-auto">
|
||||
<div
|
||||
v-for="(option, index) in field.options"
|
||||
:key="index"
|
||||
v-for="option in field.options"
|
||||
:key="option.uuid"
|
||||
>
|
||||
<label
|
||||
:for="field.uuid + option"
|
||||
:for="option.uuid"
|
||||
class="flex items-center space-x-3"
|
||||
>
|
||||
<input
|
||||
:id="field.uuid + option"
|
||||
:id="option.uuid"
|
||||
:ref="setInputRef"
|
||||
type="checkbox"
|
||||
:name="`values[${field.uuid}][]`"
|
||||
:value="option"
|
||||
:value="option.value"
|
||||
class="base-checkbox !h-7 !w-7"
|
||||
:checked="(modelValue || []).includes(option)"
|
||||
:checked="(modelValue || []).includes(option.value)"
|
||||
@change="onChange"
|
||||
>
|
||||
<span class="text-xl">
|
||||
{{ option }}
|
||||
{{ option.value }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -6,17 +6,37 @@
|
||||
>{{ field.name || t('signature') }}</label>
|
||||
<div class="space-x-2 flex">
|
||||
<span
|
||||
v-if="isTextSignature"
|
||||
class="tooltip"
|
||||
data-tip="Type text"
|
||||
:data-tip="t('draw_signature')"
|
||||
>
|
||||
<a
|
||||
id="type_text_button"
|
||||
href="#"
|
||||
class="btn btn-sm btn-circle"
|
||||
:class="{ 'btn-neutral': isTextSignature, 'btn-outline': !isTextSignature }"
|
||||
class="btn btn-outline btn-sm font-medium"
|
||||
@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"
|
||||
>
|
||||
<IconTextSize :width="16" />
|
||||
<span class="hidden sm:inline">
|
||||
{{ t('type') }}
|
||||
</span>
|
||||
</a>
|
||||
</span>
|
||||
<span
|
||||
@@ -24,7 +44,7 @@
|
||||
data-tip="Take photo"
|
||||
>
|
||||
<label
|
||||
class="btn btn-outline btn-sm btn-circle"
|
||||
class="btn btn-outline btn-sm font-medium"
|
||||
>
|
||||
<IconCamera :width="16" />
|
||||
<input
|
||||
@@ -33,12 +53,15 @@
|
||||
accept="image/*"
|
||||
@change="drawImage"
|
||||
>
|
||||
<span class="hidden sm:inline">
|
||||
{{ t('upload') }}
|
||||
</span>
|
||||
</label>
|
||||
</span>
|
||||
<a
|
||||
v-if="modelValue || computedPreviousValue"
|
||||
href="#"
|
||||
class="btn btn-outline btn-sm"
|
||||
class="btn btn-outline btn-sm font-medium"
|
||||
@click.prevent="remove"
|
||||
>
|
||||
<IconReload :width="16" />
|
||||
@@ -47,7 +70,7 @@
|
||||
<a
|
||||
v-else
|
||||
href="#"
|
||||
class="btn btn-outline btn-sm"
|
||||
class="btn btn-outline btn-sm font-medium"
|
||||
@click.prevent="clear"
|
||||
>
|
||||
<IconReload :width="16" />
|
||||
@@ -79,7 +102,8 @@
|
||||
<canvas
|
||||
v-show="!modelValue && !computedPreviousValue"
|
||||
ref="canvas"
|
||||
class="bg-white border border-base-300 rounded"
|
||||
style="padding: 1px; 0"
|
||||
class="bg-white border border-base-300 rounded-2xl"
|
||||
/>
|
||||
<input
|
||||
v-if="isTextSignature"
|
||||
@@ -95,7 +119,7 @@
|
||||
</template>
|
||||
|
||||
<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 SignaturePad from 'signature_pad'
|
||||
|
||||
@@ -107,6 +131,7 @@ export default {
|
||||
IconReload,
|
||||
IconCamera,
|
||||
IconTextSize,
|
||||
IconSignature,
|
||||
IconArrowsDiagonalMinimize2
|
||||
},
|
||||
inject: ['baseUrl', 't'],
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
v-if="!isTextArea"
|
||||
:id="field.uuid"
|
||||
v-model="text"
|
||||
:maxlength="cellsMaxLegth"
|
||||
class="base-input !text-2xl w-full !pr-11 -mr-10"
|
||||
:required="field.required"
|
||||
:pattern="field.validation?.pattern"
|
||||
@@ -38,7 +39,7 @@
|
||||
@focus="$emit('focus')"
|
||||
/>
|
||||
<div
|
||||
v-if="!isTextArea"
|
||||
v-if="!isTextArea && field.type !== 'cells'"
|
||||
class="tooltip"
|
||||
:data-tip="t('toggle_multiline_text')"
|
||||
>
|
||||
@@ -80,6 +81,19 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
cellsMaxLegth () {
|
||||
if (this.field.type === 'cells') {
|
||||
const area = this.field.areas?.[0]
|
||||
|
||||
if (area) {
|
||||
return parseInt(area.w / area.cell_w) + 1
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
},
|
||||
text: {
|
||||
set (value) {
|
||||
this.$emit('update:model-value', value)
|
||||
|
||||
@@ -66,9 +66,9 @@
|
||||
@keydown.enter.prevent="onNameEnter"
|
||||
@focus="onNameFocus"
|
||||
@blur="onNameBlur"
|
||||
>{{ field.name || defaultName }}</span>
|
||||
>{{ optionIndexText }} {{ field.name || defaultName }}</span>
|
||||
<div
|
||||
v-if="isNameFocus && field.type !== 'checkbox'"
|
||||
v-if="isNameFocus && !['checkbox', 'phone'].includes(field.type)"
|
||||
class="flex items-center ml-1.5"
|
||||
>
|
||||
<input
|
||||
@@ -104,9 +104,12 @@
|
||||
>
|
||||
<div
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -137,6 +140,7 @@ import FieldSubmitter from './field_submitter'
|
||||
import FieldType from './field_type'
|
||||
import Field from './field'
|
||||
import { IconX } from '@tabler/icons-vue'
|
||||
import { v4 } from 'uuid'
|
||||
|
||||
export default {
|
||||
name: 'FieldArea',
|
||||
@@ -173,6 +177,7 @@ export default {
|
||||
isResize: false,
|
||||
isDragged: false,
|
||||
isNameFocus: false,
|
||||
textOverflowChars: 0,
|
||||
dragFrom: { x: 0, y: 0 }
|
||||
}
|
||||
},
|
||||
@@ -180,6 +185,13 @@ export default {
|
||||
defaultName: Field.computed.defaultName,
|
||||
fieldNames: FieldType.computed.fieldNames,
|
||||
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 () {
|
||||
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: {
|
||||
onNameFocus (e) {
|
||||
this.selectedAreaRef.value = this.area
|
||||
@@ -285,7 +311,7 @@ export default {
|
||||
}
|
||||
|
||||
if (['select', 'multiple', 'radio'].includes(this.field.type)) {
|
||||
this.field.options ||= ['']
|
||||
this.field.options ||= [{ value: '', uuid: v4() }]
|
||||
}
|
||||
|
||||
(this.field.areas || []).forEach((area) => {
|
||||
|
||||
@@ -77,6 +77,7 @@
|
||||
:item="item"
|
||||
:document="sortedDocuments[index]"
|
||||
:accept-file-types="acceptFileTypes"
|
||||
:with-replace-button="withUploadButton"
|
||||
:editable="editable"
|
||||
:template="template"
|
||||
:is-direct-upload="isDirectUpload"
|
||||
@@ -92,7 +93,7 @@
|
||||
:class="{ 'bg-base-100': withStickySubmitters }"
|
||||
>
|
||||
<Upload
|
||||
v-if="sortedDocuments.length && editable"
|
||||
v-if="sortedDocuments.length && editable && withUploadButton"
|
||||
:accept-file-types="acceptFileTypes"
|
||||
:template-id="template.id"
|
||||
:is-direct-upload="isDirectUpload"
|
||||
@@ -106,7 +107,7 @@
|
||||
class="pr-3.5 pl-0.5"
|
||||
>
|
||||
<Dropzone
|
||||
v-if="!sortedDocuments.length"
|
||||
v-if="!sortedDocuments.length && withUploadButton"
|
||||
:template-id="template.id"
|
||||
:accept-file-types="acceptFileTypes"
|
||||
:is-direct-upload="isDirectUpload"
|
||||
@@ -122,9 +123,10 @@
|
||||
:areas-index="fieldAreasIndex[document.uuid]"
|
||||
:selected-submitter="selectedSubmitter"
|
||||
:document="document"
|
||||
:is-drag="!!dragFieldType"
|
||||
:is-drag="!!dragField"
|
||||
:draw-field="drawField"
|
||||
:editable="editable"
|
||||
:base-url="baseUrl"
|
||||
@draw="onDraw"
|
||||
@drop-field="onDropfield"
|
||||
@remove-area="removeArea"
|
||||
@@ -133,6 +135,7 @@
|
||||
v-if="isBreakpointLg && editable"
|
||||
:with-arrows="template.schema.length > 1"
|
||||
:item="template.schema.find((item) => item.attachment_uuid === document.uuid)"
|
||||
:with-replace-button="withUploadButton"
|
||||
:document="document"
|
||||
:template="template"
|
||||
:is-direct-upload="isDirectUpload"
|
||||
@@ -149,6 +152,7 @@
|
||||
class="pb-4"
|
||||
>
|
||||
<Upload
|
||||
v-if="withUploadButton"
|
||||
:template-id="template.id"
|
||||
:is-direct-upload="isDirectUpload"
|
||||
@success="updateFromUpload"
|
||||
@@ -156,32 +160,17 @@
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<span
|
||||
v-if="drawField"
|
||||
class="fixed text-center w-full left-1/2 bottom-0 transform -translate-x-1/2"
|
||||
>
|
||||
<span
|
||||
class="rounded bg-base-200 px-4 py-2 rounded-full inline-flex space-x-2 mx-auto items-center mb-4 z-20 md:hidden"
|
||||
>
|
||||
<component
|
||||
:is="fieldIcons[drawField.type]"
|
||||
:width="20"
|
||||
:height="20"
|
||||
class="inline"
|
||||
:stroke-width="1.6"
|
||||
/>
|
||||
<span>
|
||||
Draw {{ fieldNames[drawField.type] }} Field
|
||||
</span>
|
||||
<a
|
||||
href="#"
|
||||
class="link block text-center"
|
||||
@click.prevent="drawField = null"
|
||||
>
|
||||
Cancel
|
||||
</a>
|
||||
</span>
|
||||
</span>
|
||||
<MobileDrawField
|
||||
v-if="drawField && isBreakpointLg"
|
||||
:draw-field="drawField"
|
||||
:fields="template.fields"
|
||||
:submitters="template.submitters"
|
||||
:selected-submitter="selectedSubmitter"
|
||||
class="md:hidden"
|
||||
:editable="editable"
|
||||
@cancel="[drawField = null, drawOption = null]"
|
||||
@change-submitter="[selectedSubmitter = $event, drawField.submitter_uuid = $event.uuid]"
|
||||
/>
|
||||
<FieldType
|
||||
v-if="sortedDocuments.length && !drawField && editable"
|
||||
class="dropdown-top dropdown-end fixed bottom-4 right-4 z-10 md:hidden"
|
||||
@@ -220,7 +209,7 @@
|
||||
<p>
|
||||
<button
|
||||
class="base-button"
|
||||
@click="drawField = null"
|
||||
@click="[drawField = null, drawOption = null]"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
@@ -233,12 +222,13 @@
|
||||
:fields="template.fields"
|
||||
:submitters="template.submitters"
|
||||
:selected-submitter="selectedSubmitter"
|
||||
:default-fields="defaultFields"
|
||||
:with-sticky-submitters="withStickySubmitters"
|
||||
:editable="editable"
|
||||
@set-draw="drawField = $event"
|
||||
@set-drag="dragFieldType = $event"
|
||||
@set-draw="[drawField = $event.field, drawOption = $event.option]"
|
||||
@set-drag="dragField = $event"
|
||||
@change-submitter="selectedSubmitter = $event"
|
||||
@drag-end="dragFieldType = null"
|
||||
@drag-end="dragField = null"
|
||||
@scroll-to-area="scrollToArea"
|
||||
/>
|
||||
</div>
|
||||
@@ -251,6 +241,7 @@
|
||||
import Upload from './upload'
|
||||
import Dropzone from './dropzone'
|
||||
import Fields from './fields'
|
||||
import MobileDrawField from './mobile_draw_field'
|
||||
import Document from './document'
|
||||
import Logo from './logo'
|
||||
import Contenteditable from './contenteditable'
|
||||
@@ -267,6 +258,7 @@ export default {
|
||||
Upload,
|
||||
Document,
|
||||
Fields,
|
||||
MobileDrawField,
|
||||
IconPlus,
|
||||
FieldType,
|
||||
IconX,
|
||||
@@ -309,6 +301,16 @@ export default {
|
||||
required: false,
|
||||
default: true
|
||||
},
|
||||
defaultFields: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => []
|
||||
},
|
||||
defaultSubmitters: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => []
|
||||
},
|
||||
acceptFileTypes: {
|
||||
type: String,
|
||||
required: false,
|
||||
@@ -329,6 +331,11 @@ export default {
|
||||
required: false,
|
||||
default: true
|
||||
},
|
||||
withUploadButton: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
},
|
||||
withPhone: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
@@ -347,7 +354,8 @@ export default {
|
||||
isSaving: false,
|
||||
selectedSubmitter: null,
|
||||
drawField: null,
|
||||
dragFieldType: null
|
||||
drawOption: null,
|
||||
dragField: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -379,6 +387,13 @@ export default {
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.defaultSubmitters.forEach((name, index) => {
|
||||
const submitter = (this.template.submitters[index] ||= {})
|
||||
|
||||
submitter.name = name
|
||||
submitter.uuid ||= v4()
|
||||
})
|
||||
|
||||
this.selectedSubmitter = this.template.submitters[0]
|
||||
},
|
||||
mounted () {
|
||||
@@ -415,10 +430,11 @@ export default {
|
||||
}
|
||||
|
||||
if (['select', 'multiple', 'radio'].includes(type)) {
|
||||
field.options = ['']
|
||||
field.options = [{ value: '', uuid: v4() }]
|
||||
}
|
||||
|
||||
this.drawField = field
|
||||
this.drawOption = null
|
||||
},
|
||||
undo () {
|
||||
if (this.undoStack.length > 1) {
|
||||
@@ -468,6 +484,7 @@ export default {
|
||||
onKeyUp (e) {
|
||||
if (e.code === 'Escape') {
|
||||
this.drawField = null
|
||||
this.drawOption = null
|
||||
this.selectedAreaRef.value = null
|
||||
}
|
||||
|
||||
@@ -514,6 +531,16 @@ export default {
|
||||
},
|
||||
onDraw (area) {
|
||||
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.push(area)
|
||||
|
||||
@@ -522,6 +549,7 @@ export default {
|
||||
}
|
||||
|
||||
this.drawField = null
|
||||
this.drawOption = null
|
||||
|
||||
this.selectedAreaRef.value = area
|
||||
|
||||
@@ -571,14 +599,14 @@ export default {
|
||||
onDropfield (area) {
|
||||
const field = {
|
||||
name: '',
|
||||
type: this.dragFieldType,
|
||||
uuid: v4(),
|
||||
submitter_uuid: this.selectedSubmitter.uuid,
|
||||
required: this.dragFieldType !== 'checkbox'
|
||||
required: this.dragField.type !== 'checkbox',
|
||||
...this.dragField
|
||||
}
|
||||
|
||||
if (['select', 'multiple', 'radio'].includes(this.dragFieldType)) {
|
||||
field.options = ['']
|
||||
if (['select', 'multiple', 'radio'].includes(field.type)) {
|
||||
field.options = [{ value: '', uuid: v4() }]
|
||||
}
|
||||
|
||||
const fieldArea = {
|
||||
@@ -592,27 +620,27 @@ export default {
|
||||
|
||||
let baseArea
|
||||
|
||||
if (this.selectedField?.type === this.dragFieldType) {
|
||||
if (this.selectedField?.type === field.type) {
|
||||
baseArea = this.selectedAreaRef.value
|
||||
} else if (previousField?.areas?.length) {
|
||||
baseArea = previousField.areas[previousField.areas.length - 1]
|
||||
} else {
|
||||
if (['checkbox'].includes(this.dragFieldType)) {
|
||||
if (['checkbox'].includes(field.type)) {
|
||||
baseArea = {
|
||||
w: area.maskW / 30 / area.maskW,
|
||||
h: area.maskW / 30 / area.maskW * (area.maskW / area.maskH)
|
||||
}
|
||||
} else if (this.dragFieldType === 'image') {
|
||||
} else if (field.type === 'image') {
|
||||
baseArea = {
|
||||
w: area.maskW / 5 / area.maskW,
|
||||
h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH)
|
||||
}
|
||||
} else if (this.dragFieldType === 'signature') {
|
||||
} else if (field.type === 'signature') {
|
||||
baseArea = {
|
||||
w: area.maskW / 5 / area.maskW,
|
||||
h: (area.maskW / 5 / area.maskW) * (area.maskW / area.maskH) / 2
|
||||
}
|
||||
} else if (this.dragFieldType === 'initials') {
|
||||
} else if (field.type === 'initials') {
|
||||
baseArea = {
|
||||
w: area.maskW / 10 / area.maskW,
|
||||
h: area.maskW / 35 / area.maskW
|
||||
@@ -629,7 +657,7 @@ export default {
|
||||
fieldArea.h = baseArea.h
|
||||
fieldArea.y = fieldArea.y - baseArea.h / 2
|
||||
|
||||
if (this.dragFieldType === 'cells') {
|
||||
if (field.type === 'cells') {
|
||||
fieldArea.cell_w = baseArea.cell_w || (baseArea.w / 5)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
@update:model-value="onUpdateName"
|
||||
/>
|
||||
<ReplaceButton
|
||||
v-if="withReplaceButton"
|
||||
:is-direct-upload="isDirectUpload"
|
||||
:template-id="template.id"
|
||||
@click.stop
|
||||
@@ -67,6 +68,11 @@ export default {
|
||||
required: true,
|
||||
default: false
|
||||
},
|
||||
withReplaceButton: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
default: true
|
||||
},
|
||||
withArrows: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
|
||||
@@ -49,6 +49,11 @@ export default {
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
baseUrl: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: ''
|
||||
},
|
||||
isDrag: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
@@ -62,6 +67,13 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
basePreviewUrl () {
|
||||
if (this.baseUrl) {
|
||||
return new URL(this.baseUrl).origin
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
},
|
||||
numberOfPages () {
|
||||
return this.document.metadata?.pdf?.number_of_pages || this.document.preview_images.length
|
||||
},
|
||||
@@ -72,7 +84,7 @@ export default {
|
||||
return this.previewImagesIndex[i] || {
|
||||
metadata: lazyloadMetadata,
|
||||
id: Math.random().toString(),
|
||||
url: `/preview/${this.document.uuid}/${i}.jpg`
|
||||
url: this.basePreviewUrl + `/preview/${this.document.uuid}/${i}.jpg`
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
@@ -34,19 +34,21 @@
|
||||
v-if="isNameFocus"
|
||||
class="flex items-center relative"
|
||||
>
|
||||
<input
|
||||
:id="`required-checkbox-${field.uuid}`"
|
||||
v-model="field.required"
|
||||
type="checkbox"
|
||||
class="checkbox checkbox-xs no-animation rounded"
|
||||
@mousedown.prevent
|
||||
>
|
||||
<label
|
||||
:for="`required-checkbox-${field.uuid}`"
|
||||
class="label text-xs"
|
||||
@click.prevent="field.required = !field.required"
|
||||
@mousedown.prevent
|
||||
>Required</label>
|
||||
<template v-if="field.type != 'phone'">
|
||||
<input
|
||||
:id="`required-checkbox-${field.uuid}`"
|
||||
v-model="field.required"
|
||||
type="checkbox"
|
||||
class="checkbox checkbox-xs no-animation rounded"
|
||||
@mousedown.prevent
|
||||
>
|
||||
<label
|
||||
:for="`required-checkbox-${field.uuid}`"
|
||||
class="label text-xs"
|
||||
@click.prevent="field.required = !field.required"
|
||||
@mousedown.prevent
|
||||
>Required</label>
|
||||
</template>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="editable"
|
||||
@@ -56,7 +58,7 @@
|
||||
v-if="field && !field.areas.length"
|
||||
title="Draw"
|
||||
class="relative cursor-pointer text-transparent group-hover:text-base-content"
|
||||
@click="$emit('set-draw', field)"
|
||||
@click="$emit('set-draw', { field })"
|
||||
>
|
||||
<IconNewSection
|
||||
:width="18"
|
||||
@@ -104,7 +106,10 @@
|
||||
Default value
|
||||
</label>
|
||||
</div>
|
||||
<li @click.stop>
|
||||
<li
|
||||
v-if="field.type != 'phone'"
|
||||
@click.stop
|
||||
>
|
||||
<label class="cursor-pointer py-1.5">
|
||||
<input
|
||||
v-model="field.required"
|
||||
@@ -146,11 +151,11 @@
|
||||
Page {{ area.page + 1 }}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<li v-if="!field.areas?.length || !['radio', 'multiple'].includes(field.type)">
|
||||
<a
|
||||
href="#"
|
||||
class="text-sm py-1 px-2"
|
||||
@click.prevent="$emit('set-draw', field)"
|
||||
@click.prevent="$emit('set-draw', { field })"
|
||||
>
|
||||
<IconNewSection
|
||||
:width="20"
|
||||
@@ -188,26 +193,54 @@
|
||||
</div>
|
||||
<div
|
||||
v-if="field.options"
|
||||
ref="options"
|
||||
class="border-t border-base-300 mx-2 pt-2 space-y-1.5"
|
||||
draggable="true"
|
||||
@dragstart.prevent.stop
|
||||
>
|
||||
<div
|
||||
v-for="(option, index) in field.options"
|
||||
:key="index"
|
||||
:key="option.uuid"
|
||||
class="flex space-x-1.5 items-center"
|
||||
>
|
||||
<span class="text-sm w-3.5">
|
||||
{{ index + 1 }}.
|
||||
</span>
|
||||
<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
|
||||
v-model="field.options[index]"
|
||||
v-else
|
||||
v-model="option.value"
|
||||
class="w-full input input-primary input-xs text-sm bg-transparent"
|
||||
type="text"
|
||||
required
|
||||
@focus="maybeFocusOnOptionArea(option)"
|
||||
@blur="save"
|
||||
>
|
||||
<button
|
||||
class="text-sm w-3.5"
|
||||
@click="[field.options.splice(index, 1), save()]"
|
||||
tabindex="-1"
|
||||
@click="removeOption(option)"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
@@ -215,7 +248,7 @@
|
||||
<button
|
||||
v-if="field.options"
|
||||
class="text-center text-sm w-full pb-1"
|
||||
@click="[field.options.push(''), save()]"
|
||||
@click="addOption"
|
||||
>
|
||||
+ Add option
|
||||
</button>
|
||||
@@ -228,6 +261,7 @@
|
||||
import Contenteditable from './contenteditable'
|
||||
import FieldType from './field_type'
|
||||
import { IconShape, IconNewSection, IconTrashX, IconCopy, IconSettings } from '@tabler/icons-vue'
|
||||
import { v4 } from 'uuid'
|
||||
|
||||
export default {
|
||||
name: 'TemplateField',
|
||||
@@ -240,7 +274,7 @@ export default {
|
||||
IconCopy,
|
||||
FieldType
|
||||
},
|
||||
inject: ['template', 'save', 'backgroundColor'],
|
||||
inject: ['template', 'save', 'backgroundColor', 'selectedAreaRef'],
|
||||
props: {
|
||||
field: {
|
||||
type: Object,
|
||||
@@ -298,12 +332,36 @@ export default {
|
||||
}, 1)
|
||||
}
|
||||
},
|
||||
maybeFocusOnOptionArea (option) {
|
||||
const area = this.field.areas.find((a) => a.option_uuid === option.uuid)
|
||||
|
||||
if (area) {
|
||||
this.selectedAreaRef.value = area
|
||||
}
|
||||
},
|
||||
scrollToFirstArea () {
|
||||
return this.field.areas?.[0] && this.$emit('scroll-to', this.field.areas[0])
|
||||
},
|
||||
closeDropdown () {
|
||||
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 () {
|
||||
delete this.field.default_value
|
||||
|
||||
@@ -312,7 +370,7 @@ export default {
|
||||
}
|
||||
|
||||
if (['radio', 'multiple', 'select'].includes(this.field.type)) {
|
||||
this.field.options ||= ['']
|
||||
this.field.options ||= [{ value: '', uuid: v4() }]
|
||||
}
|
||||
|
||||
(this.field.areas || []).forEach((area) => {
|
||||
|
||||
@@ -1,5 +1,91 @@
|
||||
<template>
|
||||
<div class="dropdown">
|
||||
<div v-if="mobileView">
|
||||
<div class="flex space-x-2 items-end">
|
||||
<div class="group/contenteditable-container bg-base-100 rounded-md p-2 border border-base-300 w-full flex justify-between items-end">
|
||||
<div class="flex items-center space-x-2">
|
||||
<span
|
||||
class="w-3 h-3 flex-shrink-0 rounded-full"
|
||||
:class="colors[submitters.indexOf(selectedSubmitter)]"
|
||||
/>
|
||||
<Contenteditable
|
||||
v-model="selectedSubmitter.name"
|
||||
class="cursor-text"
|
||||
:icon-inline="true"
|
||||
:editable="editable"
|
||||
:select-on-edit-click="true"
|
||||
:icon-width="18"
|
||||
@update:model-value="$emit('name-change', selectedSubmitter)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dropdown dropdown-top dropdown-end">
|
||||
<label
|
||||
tabindex="0"
|
||||
class="bg-base-100 cursor-pointer rounded-md p-2 border border-base-300 w-full flex justify-center"
|
||||
>
|
||||
<IconChevronUp
|
||||
width="24"
|
||||
height="24"
|
||||
/>
|
||||
</label>
|
||||
<ul
|
||||
v-if="editable"
|
||||
tabindex="0"
|
||||
class="rounded-md min-w-max mb-2"
|
||||
:class="menuClasses"
|
||||
@click="closeDropdown"
|
||||
>
|
||||
<li
|
||||
v-for="(submitter, index) in submitters"
|
||||
:key="submitter.uuid"
|
||||
>
|
||||
<a
|
||||
href="#"
|
||||
class="flex px-2 group justify-between items-center"
|
||||
:class="{ 'active': submitter === selectedSubmitter }"
|
||||
@click.prevent="selectSubmitter(submitter)"
|
||||
>
|
||||
<span class="py-1 flex items-center">
|
||||
<span
|
||||
class="rounded-full w-3 h-3 ml-1 mr-3"
|
||||
:class="colors[index]"
|
||||
/>
|
||||
<span>
|
||||
{{ submitter.name }}
|
||||
</span>
|
||||
</span>
|
||||
<button
|
||||
v-if="submitters.length > 1 && editable"
|
||||
class="px-2"
|
||||
@click.stop="remove(submitter)"
|
||||
>
|
||||
<IconTrashX :width="18" />
|
||||
</button>
|
||||
</a>
|
||||
</li>
|
||||
<li v-if="submitters.length < 10 && editable">
|
||||
<a
|
||||
href="#"
|
||||
class="flex px-2"
|
||||
@click.prevent="addSubmitter"
|
||||
>
|
||||
<IconUserPlus
|
||||
:width="20"
|
||||
:stroke-width="1.6"
|
||||
/>
|
||||
<span class="py-1">
|
||||
Add Submitter
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="dropdown"
|
||||
>
|
||||
<label
|
||||
v-if="compact"
|
||||
tabindex="0"
|
||||
@@ -92,7 +178,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { IconUserPlus, IconTrashX, IconPlus } from '@tabler/icons-vue'
|
||||
import { IconUserPlus, IconTrashX, IconPlus, IconChevronUp } from '@tabler/icons-vue'
|
||||
import Contenteditable from './contenteditable'
|
||||
import { v4 } from 'uuid'
|
||||
|
||||
@@ -102,7 +188,8 @@ export default {
|
||||
IconUserPlus,
|
||||
Contenteditable,
|
||||
IconPlus,
|
||||
IconTrashX
|
||||
IconTrashX,
|
||||
IconChevronUp
|
||||
},
|
||||
props: {
|
||||
submitters: {
|
||||
@@ -119,6 +206,11 @@ export default {
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
mobileView: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
modelValue: {
|
||||
type: String,
|
||||
required: true
|
||||
|
||||
@@ -32,6 +32,35 @@
|
||||
@set-draw="$emit('set-draw', $event)"
|
||||
/>
|
||||
</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
|
||||
v-if="editable"
|
||||
class="grid grid-cols-3 gap-1 pb-2"
|
||||
@@ -45,35 +74,12 @@
|
||||
draggable="true"
|
||||
class="flex items-center justify-center border border-dashed border-base-300 w-full rounded relative"
|
||||
:style="{ backgroundColor: backgroundColor }"
|
||||
@dragstart="onDragstart(type)"
|
||||
@dragstart="onDragstart({ type: type })"
|
||||
@dragend="$emit('drag-end')"
|
||||
@click="addField(type)"
|
||||
>
|
||||
<div class="w-0 absolute left-0">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="cursor-grab"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path
|
||||
stroke="none"
|
||||
d="M0 0h24v24H0z"
|
||||
fill="none"
|
||||
/>
|
||||
<path d="M9 5m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
||||
<path d="M9 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
||||
<path d="M9 19m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
||||
<path d="M15 5m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
||||
<path d="M15 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
||||
<path d="M15 19m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
||||
</svg>
|
||||
<IconDrag class="cursor-grab" />
|
||||
</div>
|
||||
<div class="flex items-center flex-col px-2 py-2">
|
||||
<component :is="icon" />
|
||||
@@ -134,12 +140,15 @@ import { v4 } from 'uuid'
|
||||
import FieldType from './field_type'
|
||||
import FieldSubmitter from './field_submitter'
|
||||
import { IconLock } from '@tabler/icons-vue'
|
||||
import IconDrag from './icon_drag'
|
||||
|
||||
export default {
|
||||
name: 'TemplateFields',
|
||||
components: {
|
||||
Field,
|
||||
FieldType,
|
||||
FieldSubmitter,
|
||||
IconDrag,
|
||||
IconLock
|
||||
},
|
||||
inject: ['save', 'backgroundColor', 'withPhone'],
|
||||
@@ -153,6 +162,11 @@ export default {
|
||||
required: false,
|
||||
default: true
|
||||
},
|
||||
defaultFields: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => []
|
||||
},
|
||||
withStickySubmitters: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
@@ -178,11 +192,16 @@ export default {
|
||||
fieldIcons: FieldType.computed.fieldIcons,
|
||||
submitterFields () {
|
||||
return this.fields.filter((f) => f.submitter_uuid === this.selectedSubmitter.uuid)
|
||||
},
|
||||
submitterDefaultFields () {
|
||||
return this.defaultFields.filter((f) => {
|
||||
return !this.fields.find((field) => field.name === f.name) && (!f.role || f.role === this.selectedSubmitter.name)
|
||||
})
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onDragstart (fieldType) {
|
||||
this.$emit('set-drag', fieldType)
|
||||
onDragstart (field) {
|
||||
this.$emit('set-drag', field)
|
||||
},
|
||||
onFieldDragover (e) {
|
||||
const targetFieldUuid = e.target.closest('[data-uuid]')?.dataset?.uuid
|
||||
@@ -233,7 +252,7 @@ export default {
|
||||
}
|
||||
|
||||
if (['select', 'multiple', 'radio'].includes(type)) {
|
||||
field.options = ['']
|
||||
field.options = [{ value: '', uuid: v4() }]
|
||||
}
|
||||
|
||||
this.fields.push(field)
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path
|
||||
stroke="none"
|
||||
d="M0 0h24v24H0z"
|
||||
fill="none"
|
||||
/>
|
||||
<path d="M9 5m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
||||
<path d="M9 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
||||
<path d="M9 19m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
||||
<path d="M15 5m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
||||
<path d="M15 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
||||
<path d="M15 19m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'DragIcon'
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<div class="fixed text-center w-full bottom-0 pr-6 mb-4">
|
||||
<span class="w-full bg-base-200 px-4 py-2 rounded-md inline-flex space-x-2 mx-auto items-center justify-between mb-2 z-20 md:hidden">
|
||||
<div class="flex items-center space-x-2">
|
||||
<component
|
||||
:is="fieldIcons[drawField.type]"
|
||||
:width="20"
|
||||
:height="20"
|
||||
class="inline"
|
||||
:stroke-width="1.6"
|
||||
/>
|
||||
<span> Draw {{ fieldNames[drawField.type] }} Field </span>
|
||||
</div>
|
||||
<a
|
||||
href="#"
|
||||
class="link block text-center"
|
||||
@click.prevent="$emit('cancel')"
|
||||
>
|
||||
Cancel
|
||||
</a>
|
||||
</span>
|
||||
<FieldSubmitter
|
||||
:model-value="selectedSubmitter.uuid"
|
||||
:submitters="submitters"
|
||||
:editable="editable"
|
||||
:mobile-view="true"
|
||||
@new-submitter="save"
|
||||
@remove="removeSubmitter"
|
||||
@name-change="save"
|
||||
@update:model-value="$emit('change-submitter', submitters.find((s) => s.uuid === $event))"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Field from './field'
|
||||
import FieldType from './field_type'
|
||||
import FieldSubmitter from './field_submitter'
|
||||
|
||||
export default {
|
||||
name: 'MobileDrawField',
|
||||
components: {
|
||||
Field,
|
||||
FieldSubmitter
|
||||
},
|
||||
inject: ['save'],
|
||||
props: {
|
||||
drawField: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
editable: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
},
|
||||
submitters: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
fields: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
selectedSubmitter: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
emits: ['change-submitter', 'cancel'],
|
||||
computed: {
|
||||
fieldNames: FieldType.computed.fieldNames,
|
||||
fieldIcons: FieldType.computed.fieldIcons
|
||||
},
|
||||
methods: {
|
||||
removeSubmitter (submitter) {
|
||||
[...this.fields].forEach((field) => {
|
||||
if (field.submitter_uuid === submitter.uuid) {
|
||||
this.removeField(field)
|
||||
}
|
||||
})
|
||||
|
||||
this.submitters.splice(this.submitters.indexOf(submitter), 1)
|
||||
|
||||
if (this.selectedSubmitter === submitter) {
|
||||
this.$emit('change-submitter', this.submitters[0])
|
||||
}
|
||||
|
||||
this.save()
|
||||
},
|
||||
removeField (field) {
|
||||
this.fields.splice(this.fields.indexOf(field), 1)
|
||||
|
||||
this.save()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -40,8 +40,8 @@
|
||||
v-show="resizeDirection || isMove || isDrag || showMask || (drawField && isMobile)"
|
||||
id="mask"
|
||||
ref="mask"
|
||||
class="top-0 bottom-0 left-0 right-0 absolute z-10"
|
||||
:class="{ 'cursor-grab': isDrag || isMove, 'cursor-nwse-resize': drawField, [resizeDirectionClasses[resizeDirection]]: !!resizeDirectionClasses }"
|
||||
class="top-0 bottom-0 left-0 right-0 absolute"
|
||||
:class="{ 'z-10': !isMobile, 'cursor-grab': isDrag || isMove, 'cursor-nwse-resize': drawField, [resizeDirectionClasses[resizeDirection]]: !!resizeDirectionClasses }"
|
||||
@pointermove="onPointermove"
|
||||
@pointerdown="onStartDraw"
|
||||
@dragover.prevent
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
<div style="width: 26px" />
|
||||
<div class="">
|
||||
<ReplaceButton
|
||||
v-if="withReplaceButton"
|
||||
:is-direct-upload="isDirectUpload"
|
||||
:template-id="template.id"
|
||||
:accept-file-types="acceptFileTypes"
|
||||
@@ -108,6 +109,11 @@ export default {
|
||||
required: false,
|
||||
default: 'image/*, application/pdf'
|
||||
},
|
||||
withReplaceButton: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
default: true
|
||||
},
|
||||
isDirectUpload: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
|
||||
@@ -8,7 +8,7 @@ class SubmitterMailer < ApplicationMailer
|
||||
@submitter = submitter
|
||||
@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 =
|
||||
if @email_config || subject.present?
|
||||
@@ -31,7 +31,7 @@ class SubmitterMailer < ApplicationMailer
|
||||
|
||||
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)
|
||||
|
||||
@@ -58,7 +58,7 @@ class SubmitterMailer < ApplicationMailer
|
||||
|
||||
@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 =
|
||||
if @email_config
|
||||
|
||||
@@ -49,7 +49,7 @@ class AccountConfig < ApplicationRecord
|
||||
'body' => "Hi there,\n\n" \
|
||||
"Please check the copy of your \"{{template.name}}\" submission in the email attachments.\n" \
|
||||
"Alternatively, you can download your copy using:\n\n" \
|
||||
"{{documents.links}}\n\n" \
|
||||
"{{documents.link}}\n\n" \
|
||||
"Thanks,\n" \
|
||||
'{{account.name}}'
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# deleted_at :datetime
|
||||
# slug :string not null
|
||||
# source :text not null
|
||||
# submitters_order :string not null
|
||||
# template_fields :text
|
||||
@@ -19,6 +20,7 @@
|
||||
# Indexes
|
||||
#
|
||||
# index_submissions_on_created_by_user_id (created_by_user_id)
|
||||
# index_submissions_on_slug (slug) UNIQUE
|
||||
# index_submissions_on_template_id (template_id)
|
||||
#
|
||||
# Foreign Keys
|
||||
@@ -41,6 +43,8 @@ class Submission < ApplicationRecord
|
||||
attribute :source, :string, default: 'link'
|
||||
attribute :submitters_order, :string, default: 'random'
|
||||
|
||||
attribute :slug, :string, default: -> { SecureRandom.base58(14) }
|
||||
|
||||
has_one_attached :audit_trail
|
||||
|
||||
has_many :template_schema_documents,
|
||||
|
||||
@@ -61,7 +61,7 @@ class Submitter < ApplicationRecord
|
||||
|
||||
def friendly_name
|
||||
if name.present? && email.present? && email.exclude?(',')
|
||||
"#{name} <#{email}>"
|
||||
%("#{name.delete('"')}" <#{email}>)
|
||||
else
|
||||
email
|
||||
end
|
||||
|
||||
+2
-1
@@ -52,6 +52,7 @@ class User < ApplicationRecord
|
||||
belongs_to :account
|
||||
has_one :access_token, dependent: :destroy
|
||||
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 :encrypted_configs, dependent: :destroy, class_name: 'EncryptedUserConfig'
|
||||
|
||||
@@ -94,7 +95,7 @@ class User < ApplicationRecord
|
||||
|
||||
def friendly_name
|
||||
if full_name.present?
|
||||
"#{full_name} <#{email}>"
|
||||
%("#{full_name.delete('"')}" <#{email}>)
|
||||
else
|
||||
email
|
||||
end
|
||||
|
||||
@@ -16,7 +16,11 @@
|
||||
</div>
|
||||
<% end %>
|
||||
<% 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 %>
|
||||
<%= render 'devise/shared/links' %>
|
||||
</div>
|
||||
|
||||
@@ -20,7 +20,11 @@
|
||||
</div>
|
||||
<% end %>
|
||||
<% 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 %>
|
||||
<%= render 'devise/shared/links' %>
|
||||
</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 %>
|
||||
<% if !Docuseal.demo? && can?(:manage, EncryptedConfig) %>
|
||||
<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 %>
|
||||
Console
|
||||
<%= link_to Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/api") : "#{Docuseal::CONSOLE_URL}/on_premise", class: 'text-base hover:bg-base-300', data: { prefetch: false } do %>
|
||||
<% if Docuseal.multitenant? %> API <% else %> Console <% end %>
|
||||
<% end %>
|
||||
</li>
|
||||
<% if Docuseal.multitenant? %>
|
||||
<li>
|
||||
<%= link_to console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/embedding/form"), class: 'text-base hover:bg-base-300', data: { prefetch: false } do %>
|
||||
Embedding
|
||||
<% end %>
|
||||
</li>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</ul>
|
||||
</menu-active>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="flex absolute text-[1.5vw] lg:text-base" style="width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%">
|
||||
<div class="flex absolute <%= field['readonly'] ? 'text-[1.5vw] lg:text-xs' : 'text-[1.5vw] lg:text-base' %>" style="width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%">
|
||||
<% if field['type'].in?(['signature', 'image', 'initials']) %>
|
||||
<img class="object-contain mx-auto" src="<%= attachments_index[value].url %>" loading="lazy">
|
||||
<% elsif field['type'] == 'file' %>
|
||||
@@ -11,9 +11,16 @@
|
||||
<% end %>
|
||||
</div>
|
||||
<% 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'}") %>
|
||||
</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' %>
|
||||
<% cell_width = area['cell_w'] / area['w'] * 100 %>
|
||||
<div class="w-full flex items-center">
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
<% fields_index.dig(document.uuid, index)&.each do |(area, field)| %>
|
||||
<% value = values[field['uuid']] %>
|
||||
<% 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 %>
|
||||
</div>
|
||||
</div>
|
||||
@@ -109,18 +109,18 @@
|
||||
<div class="flex items-center space-x-1 mt-1">
|
||||
<%= svg_icon('writing', class: 'w-5 h-5') %>
|
||||
<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>
|
||||
</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">
|
||||
<%= 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>
|
||||
<% 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: %>
|
||||
<% 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">
|
||||
<a class="btn btn-sm btn-primary w-full" target="_blank" href="<%= submit_form_path(slug: submitter.slug) %>">
|
||||
Submit Form
|
||||
@@ -160,7 +160,7 @@
|
||||
<% elsif field['type'] == 'checkbox' %>
|
||||
<%= svg_icon('check', class: 'w-6 h-6') %>
|
||||
<% 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 %>
|
||||
<%= Array.wrap(value).join(', ') %>
|
||||
<% end %>
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
<%= render 'docuseal_logo' %>
|
||||
<div class="flex mt-4">
|
||||
<%= render 'docuseal_logo' %>
|
||||
</div>
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
<% if @submitter.submission.template.submitters.size == 1 %>
|
||||
<div class="divider">OR</div>
|
||||
<div>
|
||||
<%= button_to button_title(title: 'Resubmit', disabled_with: 'Resubmit', icon: svg_icon('reload', class: 'w-6 h-6')), start_form_path(@submitter.submission.template.slug), params: { submitter: { email: @submitter.email, phone: @submitter.phone }, resubmit: true }, method: :put, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'white-button w-full' %>
|
||||
<%= button_to button_title(title: 'Resubmit', disabled_with: 'Resubmit', icon: svg_icon('reload', class: 'w-6 h-6')), start_form_path(@submitter.submission.template.slug), params: { submitter: { email: @submitter.email, phone: @submitter.phone, name: @submitter.name }, resubmit: true }, method: :put, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'white-button w-full' %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
@@ -5,9 +5,8 @@
|
||||
<div style="max-height: -webkit-fill-available;">
|
||||
<div id="scrollbox">
|
||||
<div class="mx-auto block pb-72" style="max-width: 1000px">
|
||||
<div class="mt-4 flex">
|
||||
<%= render 'banner' %>
|
||||
</div>
|
||||
<%# 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' %>
|
||||
<% (@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_annots_index = document.metadata.dig('pdf', 'annotations')&.group_by { |e| e['page'] } || {} %>
|
||||
@@ -22,7 +21,7 @@
|
||||
<%= render 'submissions/annotation', annot: %>
|
||||
<% end %>
|
||||
<% fields_index.dig(document.uuid, index)&.each do |(area, field)| %>
|
||||
<% value = values[field['uuid']].presence || field['readonly'] ? Submitters::SubmitValues.template_default_value_for_submitter(field['default_value'], @submitter.submission.submitters.find { |e| e.uuid == field['submitter_uuid'] }, with_time: false) : nil %>
|
||||
<% value = values[field['uuid']].presence || (field['readonly'] ? Submitters::SubmitValues.template_default_value_for_submitter(field['default_value'], @submitter.submission.submitters.find { |e| e.uuid == field['submitter_uuid'] }, with_time: false) : nil) %>
|
||||
<% next if value.blank? %>
|
||||
<% next if !field['readonly'] && field['submitter_uuid'] == @submitter.uuid %>
|
||||
<%= render 'submissions/value', area:, field:, attachments_index:, value:, locale: @submitter.submission.template.account.locale %>
|
||||
|
||||
@@ -3,14 +3,10 @@
|
||||
<% else %>
|
||||
<p>Hi there,</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>
|
||||
<% @documents.each do |document| %>
|
||||
<ul>
|
||||
<li>
|
||||
<%= link_to document.filename.to_s, rails_blob_url(document) %>
|
||||
</li>
|
||||
</ul>
|
||||
<% end %>
|
||||
<p>Alternatively, you can review and download your copy using:</p>
|
||||
<p>
|
||||
<%= link_to @submitter.template.name, submissions_preview_url(@submitter.submission.slug) %>
|
||||
</p>
|
||||
<p>
|
||||
Thanks,<br><%= @current_account.name %>
|
||||
</p>
|
||||
|
||||
@@ -22,7 +22,7 @@ module DocuSeal
|
||||
|
||||
config.active_storage.routes_prefix = ''
|
||||
|
||||
config.i18n.available_locales = %i[en en-US en-GB es-ES pt-PT de-DE]
|
||||
config.i18n.available_locales = %i[en en-US en-GB es-ES fr-FR pt-PT de-DE]
|
||||
config.i18n.fallbacks = [:en]
|
||||
|
||||
config.action_view.frozen_string_literal = true
|
||||
|
||||
@@ -1,2 +1,9 @@
|
||||
en:
|
||||
hello: "Hello world"
|
||||
en-US:
|
||||
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
|
||||
end
|
||||
|
||||
resources :submissions_preview, only: %i[show], path: 'e', param: 'slug'
|
||||
|
||||
resources :send_submission_email, only: %i[create] do
|
||||
get :success, on: :collection
|
||||
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.
|
||||
|
||||
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
|
||||
enable_extension "plpgsql"
|
||||
|
||||
@@ -124,7 +124,9 @@ ActiveRecord::Schema[7.0].define(version: 2023_11_02_171817) do
|
||||
t.text "template_submitters"
|
||||
t.text "source", 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 ["slug"], name: "index_submissions_on_slug", unique: true
|
||||
t.index ["template_id"], name: "index_submissions_on_template_id"
|
||||
end
|
||||
|
||||
|
||||
@@ -17,7 +17,15 @@ module AccountConfigs
|
||||
module_function
|
||||
|
||||
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])
|
||||
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
|
||||
|
||||
@@ -23,7 +23,7 @@ module ActionMailerConfigsInterceptor
|
||||
if 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
|
||||
message.delivery_method(:test)
|
||||
end
|
||||
|
||||
@@ -15,7 +15,5 @@ class AuthWithTokenStrategy < Devise::Strategies::Base
|
||||
else
|
||||
fail!('Invalid token')
|
||||
end
|
||||
rescue JWT::VerificationError
|
||||
fail!('Invalid token')
|
||||
end
|
||||
end
|
||||
|
||||
@@ -9,6 +9,7 @@ module ReplaceEmailVariables
|
||||
SUBMISSION_LINK = '{{submission.link}}'
|
||||
SUBMISSION_SUBMITTERS = '{{submission.submitters}}'
|
||||
DOCUMENTS_LINKS = '{{documents.links}}'
|
||||
DOCUMENTS_LINK = '{{documents.link}}'
|
||||
|
||||
module_function
|
||||
|
||||
@@ -26,6 +27,7 @@ module ReplaceEmailVariables
|
||||
text = text.gsub(SUBMISSION_SUBMITTERS, build_submission_submitters(submitter.submission))
|
||||
end
|
||||
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
|
||||
|
||||
@@ -33,14 +35,9 @@ module ReplaceEmailVariables
|
||||
end
|
||||
|
||||
def build_documents_links_text(submitter)
|
||||
Submitters.select_attachments_for_download(submitter).map do |document|
|
||||
link =
|
||||
Rails.application.routes.url_helpers.rails_blob_url(
|
||||
document, **Docuseal.default_url_options
|
||||
)
|
||||
|
||||
"#{link}\n"
|
||||
end.join
|
||||
Rails.application.routes.url_helpers.submissions_preview_url(
|
||||
submitter.submission.slug, **Docuseal.default_url_options
|
||||
)
|
||||
end
|
||||
|
||||
def build_submitter_link(submitter, tracking_event_type)
|
||||
|
||||
+7
-3
@@ -28,9 +28,7 @@ module Submissions
|
||||
end
|
||||
|
||||
def create_from_emails(template:, user:, emails:, source:, mark_as_sent: false)
|
||||
emails = emails.to_s.scan(User::EMAIL_REGEXP) unless emails.is_a?(Array)
|
||||
|
||||
emails.uniq.map do |email|
|
||||
parse_emails(emails).uniq.map do |email|
|
||||
submission = template.submissions.new(created_by_user: user, source:, template_submitters: template.submitters)
|
||||
submission.submitters.new(email: normalize_email(email),
|
||||
uuid: template.submitters.first['uuid'],
|
||||
@@ -40,6 +38,12 @@ module Submissions
|
||||
end
|
||||
end
|
||||
|
||||
def parse_emails(emails)
|
||||
emails = emails.to_s.scan(User::EMAIL_REGEXP) unless emails.is_a?(Array)
|
||||
|
||||
emails
|
||||
end
|
||||
|
||||
def create_from_submitters(template:, user:, submissions_attrs:, source:, mark_as_sent: false,
|
||||
submitters_order: DEFAULT_SUBMITTERS_ORDER)
|
||||
Submissions::CreateFromSubmitters.call(
|
||||
|
||||
@@ -107,7 +107,7 @@ module Submissions
|
||||
"\n",
|
||||
{ text: 'Generated at: ', font: [FONT_BOLD_NAME, { variant: :bold }] },
|
||||
"#{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
|
||||
)
|
||||
]
|
||||
@@ -232,7 +232,7 @@ module Submissions
|
||||
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)} " \
|
||||
"#{timezone_abbr(account.timezone, event.event_timestamp)}",
|
||||
"#{TimeUtils.timezone_abbr(account.timezone, event.event_timestamp)}",
|
||||
composer.document.layout.formatted_text_box(
|
||||
[
|
||||
{ text: SubmissionEvents::EVENT_NAMES[event.event_type.to_sym],
|
||||
@@ -268,14 +268,6 @@ module Submissions
|
||||
)
|
||||
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)
|
||||
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)
|
||||
.draw(canvas, (area['x'] * width) + TEXT_LEFT_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
|
||||
|
||||
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
|
||||
box_height = lines.sum(&:height)
|
||||
|
||||
if box_height > (area['h'] * height) + 1
|
||||
text = HexaPDF::Layout::TextFragment.create(Array.wrap(value).join(', '),
|
||||
font: pdf.fonts.add(FONT_NAME),
|
||||
font_size: (font_size / 1.4).to_i)
|
||||
|
||||
lines = layouter.fit([text], area['w'] * width, height).lines
|
||||
|
||||
box_height = lines.sum(&:height)
|
||||
end
|
||||
|
||||
height_diff = [0, box_height - (area['h'] * height)].max
|
||||
|
||||
layouter.fit([text], area['w'] * width, height_diff.positive? ? box_height : area['h'] * height)
|
||||
|
||||
@@ -19,7 +19,9 @@ module Submitters
|
||||
fields.each do |field|
|
||||
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
|
||||
|
||||
submitter.save!
|
||||
|
||||
@@ -71,7 +71,7 @@ module Submitters
|
||||
|
||||
return blob if blob
|
||||
|
||||
data = conn.get(url).body
|
||||
data = conn.get(Addressable::URI.parse(url).display_uri.to_s).body
|
||||
|
||||
checksum = Digest::MD5.base64digest(data)
|
||||
|
||||
|
||||
@@ -88,6 +88,7 @@ module Submitters
|
||||
|
||||
def template_default_value_for_submitter(value, submitter, with_time: false)
|
||||
return if value.blank?
|
||||
return if submitter.blank?
|
||||
|
||||
role = submitter.submission.template_submitters.find { |e| e['uuid'] == submitter.uuid }['name']
|
||||
|
||||
@@ -105,12 +106,16 @@ module Submitters
|
||||
when 'time'
|
||||
if with_time
|
||||
I18n.l(Time.current.in_time_zone(template.account.timezone), format: :long, locale: template.account.locale)
|
||||
else
|
||||
e
|
||||
end
|
||||
when 'date'
|
||||
I18n.l(Time.current.in_time_zone(template.account.timezone).to_date) if with_time
|
||||
when 'name', 'full_name'
|
||||
"#{attrs['first_name']} #{attrs['last_name']}".strip.presence || e
|
||||
when 'role', 'email', 'phone'
|
||||
if with_time
|
||||
I18n.l(Time.current.in_time_zone(template.account.timezone).to_date)
|
||||
else
|
||||
e
|
||||
end
|
||||
when 'role', 'email', 'phone', 'name'
|
||||
attrs[key] || e
|
||||
else
|
||||
e
|
||||
|
||||
@@ -17,7 +17,9 @@ module Templates
|
||||
build_external_link_hash(page, annot).merge('page' => index)
|
||||
end
|
||||
end
|
||||
rescue PDF::Reader::MalformedPDFError, OpenSSL::Cipher::CipherError
|
||||
rescue StandardError => e
|
||||
Rollbar.error(e) if defined?(Rollbar)
|
||||
|
||||
[]
|
||||
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