add phone type placeholder
This commit is contained in:
@@ -23,6 +23,8 @@ class SubmitFormController < ApplicationController
|
|||||||
Submitters::SubmitValues.call(submitter, params, request)
|
Submitters::SubmitValues.call(submitter, params, request)
|
||||||
|
|
||||||
head :ok
|
head :ok
|
||||||
|
rescue Submitters::SubmitValues::ValidationError => e
|
||||||
|
render json: { error: e.message }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
def completed
|
def completed
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ window.customElements.define('template-builder', class extends HTMLElement {
|
|||||||
this.app = createApp(TemplateBuilder, {
|
this.app = createApp(TemplateBuilder, {
|
||||||
template: reactive(JSON.parse(this.dataset.template)),
|
template: reactive(JSON.parse(this.dataset.template)),
|
||||||
backgroundColor: '#faf7f5',
|
backgroundColor: '#faf7f5',
|
||||||
|
withPhone: this.dataset.withPhone === 'true',
|
||||||
isDirectUpload: this.dataset.isDirectUpload === 'true'
|
isDirectUpload: this.dataset.isDirectUpload === 'true'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -113,7 +113,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconCheck, IconColumns3 } from '@tabler/icons-vue'
|
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconCheck, IconColumns3, IconPhoneCheck } from '@tabler/icons-vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FieldArea',
|
name: 'FieldArea',
|
||||||
@@ -179,7 +179,8 @@ export default {
|
|||||||
select: 'Select',
|
select: 'Select',
|
||||||
checkbox: 'Checkbox',
|
checkbox: 'Checkbox',
|
||||||
radio: 'Radio',
|
radio: 'Radio',
|
||||||
multiple: 'Multiple Select'
|
multiple: 'Multiple Select',
|
||||||
|
phone: 'Phone'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
fieldIcons () {
|
fieldIcons () {
|
||||||
@@ -193,7 +194,8 @@ export default {
|
|||||||
checkbox: IconCheckbox,
|
checkbox: IconCheckbox,
|
||||||
radio: IconCircleDot,
|
radio: IconCircleDot,
|
||||||
cells: IconColumns3,
|
cells: IconColumns3,
|
||||||
multiple: IconChecks
|
multiple: IconChecks,
|
||||||
|
phone: IconPhoneCheck
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
image () {
|
image () {
|
||||||
|
|||||||
@@ -265,6 +265,16 @@
|
|||||||
:submitter-slug="submitterSlug"
|
:submitter-slug="submitterSlug"
|
||||||
@attached="[attachments.push($event), $refs.areas.scrollIntoField(currentField)]"
|
@attached="[attachments.push($event), $refs.areas.scrollIntoField(currentField)]"
|
||||||
/>
|
/>
|
||||||
|
<PhoneStep
|
||||||
|
v-else-if="currentField.type === 'phone'"
|
||||||
|
ref="currentStep"
|
||||||
|
v-model="values[currentField.uuid]"
|
||||||
|
:field="currentField"
|
||||||
|
:default-value="submitter.phone"
|
||||||
|
:submitter-slug="submitterSlug"
|
||||||
|
@focus="$refs.areas.scrollIntoField(currentField)"
|
||||||
|
@submit="submitStep"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-6 md:mt-8">
|
<div class="mt-6 md:mt-8">
|
||||||
<button
|
<button
|
||||||
@@ -295,7 +305,7 @@
|
|||||||
:is-demo="isDemo"
|
:is-demo="isDemo"
|
||||||
:attribution="attribution"
|
:attribution="attribution"
|
||||||
:with-confetti="withConfetti"
|
:with-confetti="withConfetti"
|
||||||
:can-send-email="canSendEmail && submitter.email"
|
:can-send-email="canSendEmail && !!submitter.email"
|
||||||
:submitter-slug="submitterSlug"
|
:submitter-slug="submitterSlug"
|
||||||
/>
|
/>
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
@@ -320,6 +330,7 @@ import ImageStep from './image_step'
|
|||||||
import SignatureStep from './signature_step'
|
import SignatureStep from './signature_step'
|
||||||
import AttachmentStep from './attachment_step'
|
import AttachmentStep from './attachment_step'
|
||||||
import MultiSelectStep from './multi_select_step'
|
import MultiSelectStep from './multi_select_step'
|
||||||
|
import PhoneStep from './phone_step'
|
||||||
import FormCompleted from './completed'
|
import FormCompleted from './completed'
|
||||||
import { IconInnerShadowTop, IconArrowsDiagonal, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
|
import { IconInnerShadowTop, IconArrowsDiagonal, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
|
||||||
import { t } from './i18n'
|
import { t } from './i18n'
|
||||||
@@ -334,6 +345,7 @@ export default {
|
|||||||
MultiSelectStep,
|
MultiSelectStep,
|
||||||
IconInnerShadowTop,
|
IconInnerShadowTop,
|
||||||
IconArrowsDiagonal,
|
IconArrowsDiagonal,
|
||||||
|
PhoneStep,
|
||||||
IconArrowsDiagonalMinimize2,
|
IconArrowsDiagonalMinimize2,
|
||||||
FormCompleted
|
FormCompleted
|
||||||
},
|
},
|
||||||
@@ -504,28 +516,40 @@ export default {
|
|||||||
async submitStep () {
|
async submitStep () {
|
||||||
this.isSubmitting = true
|
this.isSubmitting = true
|
||||||
|
|
||||||
const stepPromise = this.currentField.type === 'signature'
|
const stepPromise = ['signature', 'phone'].includes(this.currentField.type)
|
||||||
? this.$refs.currentStep.submit
|
? this.$refs.currentStep.submit
|
||||||
: () => Promise.resolve({})
|
: () => Promise.resolve({})
|
||||||
|
|
||||||
await stepPromise()
|
stepPromise().then(() => {
|
||||||
|
const formData = new FormData(this.$refs.form)
|
||||||
|
|
||||||
const formData = new FormData(this.$refs.form)
|
if (this.currentStep === this.stepFields.length - 1) {
|
||||||
|
formData.append('completed', 'true')
|
||||||
if (this.currentStep === this.stepFields.length - 1) {
|
|
||||||
formData.append('completed', 'true')
|
|
||||||
}
|
|
||||||
|
|
||||||
this.saveStep(formData).then(response => {
|
|
||||||
const nextStep = this.stepFields[this.currentStep + 1]
|
|
||||||
|
|
||||||
if (nextStep) {
|
|
||||||
this.goToStep(this.stepFields[this.currentStep + 1], true)
|
|
||||||
} else {
|
|
||||||
this.isCompleted = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.saveStep(formData).then(async (response) => {
|
||||||
|
if (response.status === 422) {
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
alert(data.error || 'Value is invalid')
|
||||||
|
|
||||||
|
return Promise.reject(new Error(data.error))
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextStep = this.stepFields[this.currentStep + 1]
|
||||||
|
|
||||||
|
if (nextStep) {
|
||||||
|
this.goToStep(this.stepFields[this.currentStep + 1], true)
|
||||||
|
} else {
|
||||||
|
this.isCompleted = true
|
||||||
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
console.error('Error submitting form:', error)
|
||||||
|
}).finally(() => {
|
||||||
|
this.isSubmitting = false
|
||||||
|
})
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
console.error('Error submitting form:', error)
|
console.log(error)
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
this.isSubmitting = false
|
this.isSubmitting = false
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -13,7 +13,14 @@ const en = {
|
|||||||
form_has_been_completed: 'Form has been completed!',
|
form_has_been_completed: 'Form has been completed!',
|
||||||
create_a_free_account: 'Create a Free Account',
|
create_a_free_account: 'Create a Free Account',
|
||||||
signed_with: 'Signed with',
|
signed_with: 'Signed with',
|
||||||
open_source_documents_software: 'open source documents software'
|
open_source_documents_software: 'open source documents software',
|
||||||
|
verified_phone_number: 'Verify Phone Number',
|
||||||
|
use_international_format: 'Use internatioanl format: +1xxx',
|
||||||
|
six_digits_code: '6-digit code',
|
||||||
|
change_phone_number: 'Change phone number',
|
||||||
|
sending: 'Sending...',
|
||||||
|
resend_code: 'Re-send code',
|
||||||
|
verification_code_has_been_resent: 'Verification code has been re-sent via SMS'
|
||||||
}
|
}
|
||||||
|
|
||||||
const es = {
|
const es = {
|
||||||
@@ -31,7 +38,14 @@ const es = {
|
|||||||
form_has_been_completed: '¡El formulario ha sido completado!',
|
form_has_been_completed: '¡El formulario ha sido completado!',
|
||||||
create_a_free_account: 'Crear una Cuenta Gratuita',
|
create_a_free_account: 'Crear una Cuenta Gratuita',
|
||||||
signed_with: 'Firmado con',
|
signed_with: 'Firmado con',
|
||||||
open_source_documents_software: 'software de documentos de código abierto'
|
open_source_documents_software: 'software de documentos de código abierto',
|
||||||
|
verified_phone_number: 'Verificar número de teléfono',
|
||||||
|
use_international_format: 'Usar formato internacional: +1xxx',
|
||||||
|
six_digits_code: 'Código de 6 dígitos',
|
||||||
|
change_phone_number: 'Cambiar número de teléfono',
|
||||||
|
sending: 'Enviando...',
|
||||||
|
resend_code: 'Reenviar código',
|
||||||
|
verification_code_has_been_resent: 'El código de verificación ha sido reenviado por SMS'
|
||||||
}
|
}
|
||||||
|
|
||||||
const it = {
|
const it = {
|
||||||
@@ -49,7 +63,14 @@ const it = {
|
|||||||
form_has_been_completed: 'Il modulo è stato completato!',
|
form_has_been_completed: 'Il modulo è stato completato!',
|
||||||
create_a_free_account: 'Crea un Account Gratuito',
|
create_a_free_account: 'Crea un Account Gratuito',
|
||||||
signed_with: 'Firmato con',
|
signed_with: 'Firmato con',
|
||||||
open_source_documents_software: 'software di documenti open source'
|
open_source_documents_software: 'software di documenti open source',
|
||||||
|
verified_phone_number: 'Verifica numero di telefono',
|
||||||
|
use_international_format: 'Usa formato internazionale: +1xxx',
|
||||||
|
six_digits_code: 'Codice a 6 cifre',
|
||||||
|
change_phone_number: 'Cambia numero di telefono',
|
||||||
|
sending: 'Invio in corso...',
|
||||||
|
resend_code: 'Rinvia codice',
|
||||||
|
verification_code_has_been_resent: 'Il codice di verifica è stato rinviato tramite SMS'
|
||||||
}
|
}
|
||||||
|
|
||||||
const de = {
|
const de = {
|
||||||
@@ -67,7 +88,14 @@ const de = {
|
|||||||
form_has_been_completed: 'Formular wurde ausgefüllt!',
|
form_has_been_completed: 'Formular wurde ausgefüllt!',
|
||||||
create_a_free_account: 'Kostenloses Konto erstellen',
|
create_a_free_account: 'Kostenloses Konto erstellen',
|
||||||
signed_with: 'Unterschrieben mit',
|
signed_with: 'Unterschrieben mit',
|
||||||
open_source_documents_software: 'Open-Source-Dokumentensoftware'
|
open_source_documents_software: 'Open-Source-Dokumentensoftware',
|
||||||
|
verified_phone_number: 'Telefonnummer überprüfen',
|
||||||
|
use_international_format: 'Internationales Format verwenden: +1xxx',
|
||||||
|
six_digits_code: '6-stelliger Code',
|
||||||
|
change_phone_number: 'Telefonnummer ändern',
|
||||||
|
sending: 'Senden...',
|
||||||
|
resend_code: 'Code erneut senden',
|
||||||
|
verification_code_has_been_resent: 'Die Verifizierungscode wurde erneut per SMS gesendet'
|
||||||
}
|
}
|
||||||
|
|
||||||
const fr = {
|
const fr = {
|
||||||
@@ -85,7 +113,14 @@ const fr = {
|
|||||||
form_has_been_completed: 'Le formulaire a été complété !',
|
form_has_been_completed: 'Le formulaire a été complété !',
|
||||||
create_a_free_account: 'Créer un Compte Gratuit',
|
create_a_free_account: 'Créer un Compte Gratuit',
|
||||||
signed_with: 'Signé avec',
|
signed_with: 'Signé avec',
|
||||||
open_source_documents_software: 'logiciel de documents open source'
|
open_source_documents_software: 'logiciel de documents open source',
|
||||||
|
verified_phone_number: 'Vérifier le numéro de téléphone',
|
||||||
|
use_international_format: 'Utiliser le format international : +1xxx',
|
||||||
|
six_digits_code: 'Code à 6 chiffres',
|
||||||
|
change_phone_number: 'Changer le numéro de téléphone',
|
||||||
|
sending: 'Envoi en cours...',
|
||||||
|
resend_code: 'Renvoyer le code',
|
||||||
|
verification_code_has_been_resent: 'Le code de vérification a été renvoyé par SMS'
|
||||||
}
|
}
|
||||||
|
|
||||||
const pl = {
|
const pl = {
|
||||||
@@ -103,7 +138,14 @@ const pl = {
|
|||||||
form_has_been_completed: 'Formularz został wypełniony!',
|
form_has_been_completed: 'Formularz został wypełniony!',
|
||||||
create_a_free_account: 'Utwórz darmowe konto',
|
create_a_free_account: 'Utwórz darmowe konto',
|
||||||
signed_with: 'Podpisane za pomocą',
|
signed_with: 'Podpisane za pomocą',
|
||||||
open_source_documents_software: 'oprogramowanie do dokumentów open source'
|
open_source_documents_software: 'oprogramowanie do dokumentów open source',
|
||||||
|
verified_phone_number: 'Zweryfikuj numer telefonu',
|
||||||
|
use_international_format: 'Użyj międzynarodowego formatu: +1xxx',
|
||||||
|
six_digits_code: '6-cyfrowy kod',
|
||||||
|
change_phone_number: 'Zmień numer telefonu',
|
||||||
|
sending: 'Wysyłanie...',
|
||||||
|
resend_code: 'Ponownie wyślij kod',
|
||||||
|
verification_code_has_been_resent: 'Kod weryfikacyjny został ponownie wysłany'
|
||||||
}
|
}
|
||||||
|
|
||||||
const uk = {
|
const uk = {
|
||||||
@@ -121,7 +163,14 @@ const uk = {
|
|||||||
form_has_been_completed: 'Форму заповнено!',
|
form_has_been_completed: 'Форму заповнено!',
|
||||||
create_a_free_account: 'Створити безкоштовний обліковий запис',
|
create_a_free_account: 'Створити безкоштовний обліковий запис',
|
||||||
signed_with: 'Підписано за допомогою',
|
signed_with: 'Підписано за допомогою',
|
||||||
open_source_documents_software: 'відкритий програмний засіб для документів'
|
open_source_documents_software: 'відкритий програмний засіб для документів',
|
||||||
|
verified_phone_number: 'Підтвердіть номер телефону',
|
||||||
|
use_international_format: 'Використовуйте міжнародний формат: +1xxx',
|
||||||
|
six_digits_code: '6-значний код',
|
||||||
|
change_phone_number: 'Змінити номер телефону',
|
||||||
|
sending: 'Надсилаю...',
|
||||||
|
resend_code: 'Повторно відправити код',
|
||||||
|
verification_code_has_been_resent: 'Код підтвердження був повторно надісланий'
|
||||||
}
|
}
|
||||||
|
|
||||||
const cs = {
|
const cs = {
|
||||||
@@ -139,7 +188,14 @@ const cs = {
|
|||||||
form_has_been_completed: 'Formulář byl dokončen!',
|
form_has_been_completed: 'Formulář byl dokončen!',
|
||||||
create_a_free_account: 'Vytvořit bezplatný účet',
|
create_a_free_account: 'Vytvořit bezplatný účet',
|
||||||
signed_with: 'Podepsáno pomocí',
|
signed_with: 'Podepsáno pomocí',
|
||||||
open_source_documents_software: 'open source software pro dokumenty'
|
open_source_documents_software: 'open source software pro dokumenty',
|
||||||
|
verified_phone_number: 'Ověřte telefonní číslo',
|
||||||
|
use_international_format: 'Použijte mezinárodní formát: +1xxx',
|
||||||
|
six_digits_code: '6-místný kód',
|
||||||
|
change_phone_number: 'Změnit telefonní číslo',
|
||||||
|
sending: 'Odesílání...',
|
||||||
|
resend_code: 'Znovu odeslat kód',
|
||||||
|
verification_code_has_been_resent: 'Ověřovací kód byl znovu odeslán'
|
||||||
}
|
}
|
||||||
|
|
||||||
const i18n = { en, es, it, de, fr, pl, uk, cs }
|
const i18n = { en, es, it, de, fr, pl, uk, cs }
|
||||||
|
|||||||
@@ -0,0 +1,155 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
:for="isCodeSent ? 'one_time_code' : field.uuid"
|
||||||
|
class="label text-2xl mb-2"
|
||||||
|
>{{ field.name || t('verified_phone_number') }}
|
||||||
|
<template v-if="!field.required">({{ t('optional') }})</template>
|
||||||
|
</label>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
name="normalize_phone"
|
||||||
|
value="true"
|
||||||
|
>
|
||||||
|
<div v-if="isCodeSent">
|
||||||
|
<input
|
||||||
|
id="one_time_code"
|
||||||
|
class="base-input !text-2xl w-full text-center"
|
||||||
|
name="one_time_code"
|
||||||
|
type="text"
|
||||||
|
autocomplete="one-time-code"
|
||||||
|
:placeholder="t('six_digits_code')"
|
||||||
|
required
|
||||||
|
maxlength="6"
|
||||||
|
autofocus
|
||||||
|
inputmode="decimal"
|
||||||
|
@input="onInputCode"
|
||||||
|
>
|
||||||
|
<div class="flex justify-between mt-2 -mb-2 md:-mb-4">
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="link"
|
||||||
|
@click.prevent="isCodeSent = false"
|
||||||
|
>
|
||||||
|
{{ t('change_phone_number') }}
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="link"
|
||||||
|
@click.prevent="resendCode"
|
||||||
|
>
|
||||||
|
{{ isResendLoading ? t('sending') : t('resend_code') }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
v-show="!isCodeSent"
|
||||||
|
:id="field.uuid"
|
||||||
|
ref="phone"
|
||||||
|
:value="modelValue || defaultValue"
|
||||||
|
class="base-input !text-2xl w-full"
|
||||||
|
autocomplete="tel"
|
||||||
|
pattern="^\+[0-9\s\-]+$"
|
||||||
|
:oninvalid="`this.value ? this.setCustomValidity('${t('use_international_format')}...') : ''`"
|
||||||
|
oninput="this.setCustomValidity('')"
|
||||||
|
type="tel"
|
||||||
|
inputmode="tel"
|
||||||
|
:required="field.required"
|
||||||
|
placeholder="+1 234 567-8900"
|
||||||
|
:name="`values[${field.uuid}]`"
|
||||||
|
@input="$emit('update:model-value', $event.target.value)"
|
||||||
|
@focus="$emit('focus')"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function throttle (func, delay) {
|
||||||
|
let lastCallTime = 0
|
||||||
|
|
||||||
|
return function (...args) {
|
||||||
|
const now = Date.now()
|
||||||
|
|
||||||
|
if (now - lastCallTime >= delay) {
|
||||||
|
func.apply(this, args)
|
||||||
|
lastCallTime = now
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'PhoneStep',
|
||||||
|
inject: ['t', 'baseUrl'],
|
||||||
|
props: {
|
||||||
|
field: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
submitterSlug: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
modelValue: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
defaultValue: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['update:model-value', 'focus', 'submit'],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
isCodeSent: false,
|
||||||
|
isResendLoading: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
emitSubmit: throttle(function (e) {
|
||||||
|
this.$emit('submit')
|
||||||
|
}, 1000),
|
||||||
|
onInputCode (e) {
|
||||||
|
if (e.target.value.length === 6) {
|
||||||
|
this.emitSubmit()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
resendCode () {
|
||||||
|
this.isResendLoading = true
|
||||||
|
|
||||||
|
this.sendVerificationCode().finally(() => {
|
||||||
|
alert(this.t('verification_code_has_been_resent'))
|
||||||
|
|
||||||
|
this.isResendLoading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
sendVerificationCode () {
|
||||||
|
return fetch(this.baseUrl + '/api/send_phone_verification_code', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
submitter_slug: this.submitterSlug,
|
||||||
|
phone: this.$refs.phone.value
|
||||||
|
}),
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
})
|
||||||
|
},
|
||||||
|
async submit () {
|
||||||
|
if (!this.isCodeSent) {
|
||||||
|
this.sendVerificationCode()
|
||||||
|
|
||||||
|
this.$emit('update:model-value', this.$refs.phone.value)
|
||||||
|
|
||||||
|
this.isCodeSent = true
|
||||||
|
|
||||||
|
return Promise.reject(new Error('verify with code'))
|
||||||
|
} else {
|
||||||
|
return Promise.resolve({})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -237,6 +237,7 @@ export default {
|
|||||||
save: this.save,
|
save: this.save,
|
||||||
baseFetch: this.baseFetch,
|
baseFetch: this.baseFetch,
|
||||||
backgroundColor: this.backgroundColor,
|
backgroundColor: this.backgroundColor,
|
||||||
|
withPhone: this.withPhone,
|
||||||
selectedAreaRef: computed(() => this.selectedAreaRef)
|
selectedAreaRef: computed(() => this.selectedAreaRef)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -270,6 +271,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: true
|
default: true
|
||||||
},
|
},
|
||||||
|
withPhone: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
fetchOptions: {
|
fetchOptions: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: false,
|
required: false,
|
||||||
|
|||||||
@@ -18,33 +18,36 @@
|
|||||||
:class="menuClasses"
|
:class="menuClasses"
|
||||||
@click="closeDropdown"
|
@click="closeDropdown"
|
||||||
>
|
>
|
||||||
<li
|
<template
|
||||||
v-for="(icon, type) in fieldIcons"
|
v-for="(icon, type) in fieldIcons"
|
||||||
:key="type"
|
:key="type"
|
||||||
>
|
>
|
||||||
<a
|
<li v-if="withPhone || type !== 'phone'">
|
||||||
href="#"
|
<a
|
||||||
class="text-sm py-1 px-2"
|
href="#"
|
||||||
:class="{ 'active': type === modelValue }"
|
class="text-sm py-1 px-2"
|
||||||
@click.prevent="$emit('update:model-value', type)"
|
:class="{ 'active': type === modelValue }"
|
||||||
>
|
@click.prevent="$emit('update:model-value', type)"
|
||||||
<component
|
>
|
||||||
:is="icon"
|
<component
|
||||||
:stroke-width="1.6"
|
:is="icon"
|
||||||
:width="20"
|
:stroke-width="1.6"
|
||||||
/>
|
:width="20"
|
||||||
{{ fieldNames[type] }}
|
/>
|
||||||
</a>
|
{{ fieldNames[type] }}
|
||||||
</li>
|
</a>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
</ul>
|
</ul>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconColumns3 } from '@tabler/icons-vue'
|
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconColumns3, IconPhoneCheck } from '@tabler/icons-vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FiledTypeDropdown',
|
name: 'FiledTypeDropdown',
|
||||||
|
inject: ['withPhone'],
|
||||||
props: {
|
props: {
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -79,7 +82,8 @@ export default {
|
|||||||
checkbox: 'Checkbox',
|
checkbox: 'Checkbox',
|
||||||
multiple: 'Multiple',
|
multiple: 'Multiple',
|
||||||
radio: 'Radio',
|
radio: 'Radio',
|
||||||
cells: 'Cells'
|
cells: 'Cells',
|
||||||
|
phone: 'Phone'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
fieldIcons () {
|
fieldIcons () {
|
||||||
@@ -93,7 +97,8 @@ export default {
|
|||||||
checkbox: IconCheckbox,
|
checkbox: IconCheckbox,
|
||||||
cells: IconColumns3,
|
cells: IconColumns3,
|
||||||
multiple: IconChecks,
|
multiple: IconChecks,
|
||||||
radio: IconCircleDot
|
radio: IconCircleDot,
|
||||||
|
phone: IconPhoneCheck
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -25,49 +25,79 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-3 gap-1 pb-2">
|
<div class="grid grid-cols-3 gap-1 pb-2">
|
||||||
<button
|
<template
|
||||||
v-for="(icon, type) in fieldIcons"
|
v-for="(icon, type) in fieldIcons"
|
||||||
:key="type"
|
:key="type"
|
||||||
draggable="true"
|
|
||||||
class="flex items-center justify-center border border-dashed border-base-300 w-full rounded relative"
|
|
||||||
:style="{ backgroundColor: backgroundColor }"
|
|
||||||
@dragstart="onDragstart(type)"
|
|
||||||
@dragend="$emit('drag-end')"
|
|
||||||
@click="addField(type)"
|
|
||||||
>
|
>
|
||||||
<div class="w-0 absolute left-0">
|
<button
|
||||||
<svg
|
v-if="withPhone || type != 'phone'"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
draggable="true"
|
||||||
class="cursor-grab"
|
class="flex items-center justify-center border border-dashed border-base-300 w-full rounded relative"
|
||||||
width="18"
|
:style="{ backgroundColor: backgroundColor }"
|
||||||
height="18"
|
@dragstart="onDragstart(type)"
|
||||||
viewBox="0 0 24 24"
|
@dragend="$emit('drag-end')"
|
||||||
stroke-width="1.5"
|
@click="addField(type)"
|
||||||
stroke="currentColor"
|
>
|
||||||
fill="none"
|
<div class="w-0 absolute left-0">
|
||||||
stroke-linecap="round"
|
<svg
|
||||||
stroke-linejoin="round"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
>
|
class="cursor-grab"
|
||||||
<path
|
width="18"
|
||||||
stroke="none"
|
height="18"
|
||||||
d="M0 0h24v24H0z"
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
fill="none"
|
fill="none"
|
||||||
/>
|
stroke-linecap="round"
|
||||||
<path d="M9 5m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
stroke-linejoin="round"
|
||||||
<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
|
||||||
<path d="M15 5m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
stroke="none"
|
||||||
<path d="M15 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
d="M0 0h24v24H0z"
|
||||||
<path d="M15 19m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
|
fill="none"
|
||||||
</svg>
|
/>
|
||||||
|
<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 class="flex items-center flex-col px-2 py-2">
|
||||||
|
<component :is="icon" />
|
||||||
|
<span class="text-xs mt-1">
|
||||||
|
{{ fieldNames[type] }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="tooltip flex"
|
||||||
|
data-tip="Unlock SMS-verified phone number field with paid plan. Use text field for phone numbers without verification."
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="https://www.docuseal.co/pricing"
|
||||||
|
target="_blank"
|
||||||
|
class="opacity-50 flex items-center justify-center border border-dashed border-base-300 w-full rounded relative"
|
||||||
|
:style="{ backgroundColor: backgroundColor }"
|
||||||
|
>
|
||||||
|
<div class="w-0 absolute left-0">
|
||||||
|
<IconLock
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
stroke-width="1.5"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center flex-col px-2 py-2">
|
||||||
|
<component :is="icon" />
|
||||||
|
<span class="text-xs mt-1">
|
||||||
|
{{ fieldNames[type] }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center flex-col px-2 py-2">
|
</template>
|
||||||
<component :is="icon" />
|
|
||||||
<span class="text-xs mt-1">
|
|
||||||
{{ fieldNames[type] }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="fields.length < 4"
|
v-if="fields.length < 4"
|
||||||
@@ -95,14 +125,16 @@ import Field from './field'
|
|||||||
import { v4 } from 'uuid'
|
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'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'TemplateFields',
|
name: 'TemplateFields',
|
||||||
components: {
|
components: {
|
||||||
Field,
|
Field,
|
||||||
FieldSubmitter
|
FieldSubmitter,
|
||||||
|
IconLock
|
||||||
},
|
},
|
||||||
inject: ['save', 'backgroundColor'],
|
inject: ['save', 'backgroundColor', 'withPhone'],
|
||||||
props: {
|
props: {
|
||||||
fields: {
|
fields: {
|
||||||
type: Array,
|
type: Array,
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
<% data_attachments = attachments_index.values.select { |e| e.record_id == submitter.id }.to_json(only: %i[uuid], methods: %i[url filename content_type]) %>
|
<% data_attachments = attachments_index.values.select { |e| e.record_id == submitter.id }.to_json(only: %i[uuid], methods: %i[url filename content_type]) %>
|
||||||
<% data_fields = (submitter.submission.template_fields || submitter.submission.template.fields).select { |f| f['submitter_uuid'] == submitter.uuid }.to_json %>
|
<% data_fields = (submitter.submission.template_fields || submitter.submission.template.fields).select { |f| f['submitter_uuid'] == submitter.uuid }.to_json %>
|
||||||
<submission-form data-is-demo="<%= Docuseal.demo? %>" data-is-direct-upload="<%= Docuseal.active_storage_public? %>" data-submitter="<%= submitter.to_json(only: %i[uuid slug email]) %>" data-can-send-email="<%= Accounts.can_send_emails?(Struct.new(:id).new(@submitter.submission.template.account_id)) %>" data-attachments="<%= data_attachments %>" data-fields="<%= data_fields %>" data-authenticity-token="<%= form_authenticity_token %>" data-values="<%= submitter.values.to_json %>"></submission-form>
|
<submission-form data-is-demo="<%= Docuseal.demo? %>" data-is-direct-upload="<%= Docuseal.active_storage_public? %>" data-submitter="<%= submitter.to_json(only: %i[uuid slug name phone email]) %>" data-can-send-email="<%= Accounts.can_send_emails?(Struct.new(:id).new(@submitter.submission.template.account_id)) %>" data-attachments="<%= data_attachments %>" data-fields="<%= data_fields %>" data-authenticity-token="<%= form_authenticity_token %>" data-values="<%= submitter.values.to_json %>"></submission-form>
|
||||||
|
|||||||
@@ -2,13 +2,15 @@
|
|||||||
|
|
||||||
module Submitters
|
module Submitters
|
||||||
module SubmitValues
|
module SubmitValues
|
||||||
|
ValidationError = Class.new(StandardError)
|
||||||
|
|
||||||
module_function
|
module_function
|
||||||
|
|
||||||
def call(submitter, params, request)
|
def call(submitter, params, request)
|
||||||
update_submitter!(submitter, params, request)
|
|
||||||
|
|
||||||
Submissions.update_template_fields!(submitter.submission) if submitter.submission.template_fields.blank?
|
Submissions.update_template_fields!(submitter.submission) if submitter.submission.template_fields.blank?
|
||||||
|
|
||||||
|
update_submitter!(submitter, params, request)
|
||||||
|
|
||||||
submitter.submission.save!
|
submitter.submission.save!
|
||||||
|
|
||||||
return unless submitter.completed_at?
|
return unless submitter.completed_at?
|
||||||
@@ -27,7 +29,11 @@ module Submitters
|
|||||||
end
|
end
|
||||||
|
|
||||||
def update_submitter!(submitter, params, request)
|
def update_submitter!(submitter, params, request)
|
||||||
submitter.values.merge!(normalized_values(params))
|
values = normalized_values(params)
|
||||||
|
|
||||||
|
validate_values!(values, submitter, params)
|
||||||
|
|
||||||
|
submitter.values.merge!(values)
|
||||||
submitter.opened_at ||= Time.current
|
submitter.opened_at ||= Time.current
|
||||||
|
|
||||||
if params[:completed] == 'true'
|
if params[:completed] == 'true'
|
||||||
@@ -45,10 +51,24 @@ module Submitters
|
|||||||
params.fetch(:values, {}).to_unsafe_h.transform_values do |v|
|
params.fetch(:values, {}).to_unsafe_h.transform_values do |v|
|
||||||
if params[:cast_boolean] == 'true'
|
if params[:cast_boolean] == 'true'
|
||||||
v == 'true'
|
v == 'true'
|
||||||
|
elsif params[:normalize_phone] == 'true'
|
||||||
|
v.to_s.gsub(/[^0-9+]/, '')
|
||||||
else
|
else
|
||||||
v.is_a?(Array) ? v.compact_blank : v
|
v.is_a?(Array) ? v.compact_blank : v
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def validate_values!(values, submitter, params)
|
||||||
|
values.each do |key, value|
|
||||||
|
field = submitter.submission.template_fields.find { |e| e['uuid'] == key }
|
||||||
|
|
||||||
|
validate_value!(value, field, params)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_value!(_value, _field, _params)
|
||||||
|
true
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user