mirror of
https://github.com/docusealco/docuseal.git
synced 2026-08-07 23:32:07 +00:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9ddbcae153 | |||
| 10cd3aa72a | |||
| 6684a27e2f | |||
| b6f7a7dd15 | |||
| 309e43e18e | |||
| d5b8640a0e | |||
| 49b9064d9f | |||
| 698031cfc0 | |||
| 6dc21e66e1 | |||
| 60b32fec94 | |||
| c621166b45 | |||
| 8e2da83f9e | |||
| a39ba5b275 | |||
| edf466a6de | |||
| 3b8bb8c5bb | |||
| 79f1061353 | |||
| 745f2160c5 | |||
| 2f0c79444b | |||
| 69a3782903 | |||
| 3dd7c47558 | |||
| 761bb07ee6 | |||
| 92d7bc65f5 |
@@ -106,7 +106,9 @@ module Api
|
||||
|
||||
def submissions_params
|
||||
params.permit(submission: [{
|
||||
submitters: [[:uuid, :name, :email, :role, :completed, :phone, { values: {} }]]
|
||||
submitters: [[:uuid, :name, :email, :role, :completed, :phone,
|
||||
{ values: {}, readonly_fields: [],
|
||||
fields: [%i[name default_value readonly validation_pattern invalid_message]] }]]
|
||||
}])
|
||||
end
|
||||
|
||||
@@ -115,11 +117,15 @@ module Api
|
||||
|
||||
Array.wrap(submissions_params).each do |submission|
|
||||
submission[:submitters].each_with_index do |submitter, index|
|
||||
next if submitter[:values].blank?
|
||||
default_values = submitter[:values] || {}
|
||||
|
||||
submitter[:fields]&.each { |f| default_values[f[:name]] = f[:default_value] if f[:default_value].present? }
|
||||
|
||||
next if default_values.blank?
|
||||
|
||||
values, new_attachments =
|
||||
Submitters::NormalizeValues.call(template,
|
||||
submitter[:values],
|
||||
default_values,
|
||||
submitter[:role] || template.submitters[index]['name'])
|
||||
|
||||
attachments.push(*new_attachments)
|
||||
|
||||
@@ -33,7 +33,7 @@ class ApplicationController < ActionController::Base
|
||||
private
|
||||
|
||||
def sign_in_for_demo
|
||||
sign_in(User.order('random()').take) unless signed_in?
|
||||
sign_in(User.active.order('random()').take) unless signed_in?
|
||||
end
|
||||
|
||||
def current_account
|
||||
|
||||
@@ -10,7 +10,7 @@ class MfaSetupController < ApplicationController
|
||||
|
||||
current_user.save!
|
||||
|
||||
@provision_url = current_user.otp_provisioning_uri(current_user.email, issuer: Docuseal::PRODUCT_NAME)
|
||||
@provision_url = current_user.otp_provisioning_uri(current_user.email, issuer: Docuseal.product_name)
|
||||
end
|
||||
|
||||
def edit; end
|
||||
@@ -22,7 +22,7 @@ class MfaSetupController < ApplicationController
|
||||
|
||||
redirect_to settings_profile_index_path, notice: '2FA has been configured'
|
||||
else
|
||||
@provision_url = current_user.otp_provisioning_uri(current_user.email, issuer: Docuseal::PRODUCT_NAME)
|
||||
@provision_url = current_user.otp_provisioning_uri(current_user.email, issuer: Docuseal.product_name)
|
||||
|
||||
@error_message = 'Code is invalid'
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ class RegistrationsController < Devise::RegistrationsController
|
||||
|
||||
self.resource = account.users.new(user_params)
|
||||
|
||||
account.name ||= "#{resource.full_name}'s Company" if params[:action] == 'create'
|
||||
account.name ||= resource.full_name if params[:action] == 'create'
|
||||
end
|
||||
|
||||
def user_params
|
||||
|
||||
@@ -47,6 +47,10 @@ select:required:invalid {
|
||||
@apply border-base-content/20;
|
||||
}
|
||||
|
||||
.base-textarea {
|
||||
@apply textarea textarea-bordered bg-white rounded-3xl;
|
||||
}
|
||||
|
||||
.btn {
|
||||
@apply no-animation;
|
||||
}
|
||||
|
||||
@@ -110,9 +110,10 @@
|
||||
<span v-else-if="field.type === 'date'">
|
||||
{{ formattedDate }}
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ modelValue }}
|
||||
</span>
|
||||
<span
|
||||
v-else
|
||||
class="whitespace-pre-wrap"
|
||||
>{{ modelValue }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -58,42 +58,29 @@
|
||||
>
|
||||
<div class="md:mt-4">
|
||||
<div v-if="['cells', 'text'].includes(currentField.type)">
|
||||
<label
|
||||
v-if="currentField.name"
|
||||
:for="currentField.uuid"
|
||||
class="label text-2xl mb-2"
|
||||
>{{ currentField.name }}
|
||||
<template v-if="!currentField.required">({{ t('optional') }})</template>
|
||||
</label>
|
||||
<div
|
||||
v-else
|
||||
class="py-1"
|
||||
<TextStep
|
||||
:key="currentField.uuid"
|
||||
v-model="values[currentField.uuid]"
|
||||
:field="currentField"
|
||||
@focus="$refs.areas.scrollIntoField(currentField)"
|
||||
/>
|
||||
<div>
|
||||
<input
|
||||
:id="currentField.uuid"
|
||||
v-model="values[currentField.uuid]"
|
||||
class="base-input !text-2xl w-full"
|
||||
:required="currentField.required"
|
||||
:placeholder="`${t('type_here')}...${currentField.required ? '' : ` (${t('optional')})`}`"
|
||||
type="text"
|
||||
:name="`values[${currentField.uuid}]`"
|
||||
@focus="$refs.areas.scrollIntoField(currentField)"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="currentField.type === 'date'">
|
||||
<label
|
||||
v-if="currentField.name"
|
||||
:for="currentField.uuid"
|
||||
class="label text-2xl mb-2"
|
||||
>{{ currentField.name }}
|
||||
<template v-if="!currentField.required">({{ t('optional') }})</template>
|
||||
</label>
|
||||
<div
|
||||
v-else
|
||||
class="py-1"
|
||||
/>
|
||||
<div class="flex justify-between items-center w-full mb-2">
|
||||
<label
|
||||
:for="currentField.uuid"
|
||||
class="label text-2xl"
|
||||
>{{ currentField.name || t('date') }}
|
||||
<template v-if="!currentField.required">({{ t('optional') }})</template>
|
||||
</label>
|
||||
<button
|
||||
class="btn btn-outline btn-sm !normal-case font-normal"
|
||||
@click.prevent="setCurrentDate"
|
||||
>
|
||||
<IconCalendarCheck :width="16" />
|
||||
{{ t('set_today') }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<input
|
||||
:id="currentField.uuid"
|
||||
@@ -179,6 +166,7 @@
|
||||
</div>
|
||||
<MultiSelectStep
|
||||
v-else-if="currentField.type === 'multiple'"
|
||||
:key="currentField.uuid"
|
||||
v-model="values[currentField.uuid]"
|
||||
:field="currentField"
|
||||
/>
|
||||
@@ -240,6 +228,7 @@
|
||||
</div>
|
||||
<ImageStep
|
||||
v-else-if="currentField.type === 'image'"
|
||||
:key="currentField.uuid"
|
||||
v-model="values[currentField.uuid]"
|
||||
:field="currentField"
|
||||
:is-direct-upload="isDirectUpload"
|
||||
@@ -250,6 +239,7 @@
|
||||
<SignatureStep
|
||||
v-else-if="currentField.type === 'signature'"
|
||||
ref="currentStep"
|
||||
:key="currentField.uuid"
|
||||
v-model="values[currentField.uuid]"
|
||||
:field="currentField"
|
||||
:is-direct-upload="isDirectUpload"
|
||||
@@ -262,6 +252,7 @@
|
||||
<InitialsStep
|
||||
v-else-if="currentField.type === 'initials'"
|
||||
ref="currentStep"
|
||||
:key="currentField.uuid"
|
||||
v-model="values[currentField.uuid]"
|
||||
:field="currentField"
|
||||
:is-direct-upload="isDirectUpload"
|
||||
@@ -274,6 +265,7 @@
|
||||
/>
|
||||
<AttachmentStep
|
||||
v-else-if="currentField.type === 'file'"
|
||||
:key="currentField.uuid"
|
||||
v-model="values[currentField.uuid]"
|
||||
:is-direct-upload="isDirectUpload"
|
||||
:field="currentField"
|
||||
@@ -284,6 +276,7 @@
|
||||
<PhoneStep
|
||||
v-else-if="currentField.type === 'phone'"
|
||||
ref="currentStep"
|
||||
:key="currentField.uuid"
|
||||
v-model="values[currentField.uuid]"
|
||||
:field="currentField"
|
||||
:default-value="submitter.phone"
|
||||
@@ -294,6 +287,7 @@
|
||||
</div>
|
||||
<div class="mt-6 md:mt-8">
|
||||
<button
|
||||
ref="submitButton"
|
||||
type="submit"
|
||||
class="base-button w-full flex justify-center"
|
||||
:disabled="isButtonDisabled"
|
||||
@@ -314,6 +308,12 @@
|
||||
><span>...</span></span>
|
||||
</span>
|
||||
</button>
|
||||
<div
|
||||
v-if="showFillAllRequiredFields"
|
||||
class="text-center mt-1"
|
||||
>
|
||||
{{ t('please_fill_all_required_fields') }}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<FormCompleted
|
||||
@@ -332,7 +332,7 @@
|
||||
:key="step[0].uuid"
|
||||
href="#"
|
||||
class="inline border border-base-300 h-3 w-3 rounded-full mx-1"
|
||||
:class="{ 'bg-base-300': index === currentStep, 'bg-base-content': index < currentStep || isCompleted, 'bg-white': index > currentStep }"
|
||||
:class="{ 'bg-base-300': index === currentStep, 'bg-base-content': (index < currentStep && stepFields[index].every((f) => !f.required || ![null, undefined, ''].includes(values[f.uuid]))) || isCompleted, 'bg-white': index > currentStep }"
|
||||
@click.prevent="isCompleted ? '' : [saveStep(), goToStep(step, true)]"
|
||||
/>
|
||||
</div>
|
||||
@@ -349,8 +349,9 @@ import InitialsStep from './initials_step'
|
||||
import AttachmentStep from './attachment_step'
|
||||
import MultiSelectStep from './multi_select_step'
|
||||
import PhoneStep from './phone_step'
|
||||
import TextStep from './text_step'
|
||||
import FormCompleted from './completed'
|
||||
import { IconInnerShadowTop, IconArrowsDiagonal, IconArrowsDiagonalMinimize2 } from '@tabler/icons-vue'
|
||||
import { IconInnerShadowTop, IconArrowsDiagonal, IconArrowsDiagonalMinimize2, IconCalendarCheck } from '@tabler/icons-vue'
|
||||
import { t } from './i18n'
|
||||
|
||||
export default {
|
||||
@@ -364,7 +365,9 @@ export default {
|
||||
MultiSelectStep,
|
||||
IconInnerShadowTop,
|
||||
IconArrowsDiagonal,
|
||||
TextStep,
|
||||
PhoneStep,
|
||||
IconCalendarCheck,
|
||||
IconArrowsDiagonalMinimize2,
|
||||
FormCompleted
|
||||
},
|
||||
@@ -454,9 +457,11 @@ export default {
|
||||
return {
|
||||
isCompleted: false,
|
||||
isFormVisible: true,
|
||||
showFillAllRequiredFields: false,
|
||||
currentStep: 0,
|
||||
isSubmitting: false,
|
||||
submittedValues: {},
|
||||
isSecondWalkthrough: false,
|
||||
recalculateButtonDisabledKey: ''
|
||||
}
|
||||
},
|
||||
@@ -484,7 +489,7 @@ export default {
|
||||
return this.currentStepFields[0]
|
||||
},
|
||||
stepFields () {
|
||||
return this.fields.reduce((acc, f) => {
|
||||
return this.fields.filter((f) => !f.readonly).reduce((acc, f) => {
|
||||
const prevStep = acc[acc.length - 1]
|
||||
|
||||
if (f.type === 'checkbox' && Array.isArray(prevStep) && prevStep[0].type === 'checkbox') {
|
||||
@@ -511,10 +516,20 @@ export default {
|
||||
this.submittedValues = JSON.parse(JSON.stringify(this.values))
|
||||
|
||||
if (this.goToLast) {
|
||||
this.currentStep = Math.min(
|
||||
this.stepFields.indexOf([...this.stepFields].reverse().find((fields) => fields.some((f) => !!this.values[f.uuid]))) + 1,
|
||||
this.stepFields.length - 1
|
||||
)
|
||||
const requiredEmptyStepIndex = this.stepFields.indexOf(this.stepFields.find((fields) => fields.some((f) => f.required && !this.values[f.uuid])))
|
||||
const lastFilledStepIndex = this.stepFields.indexOf([...this.stepFields].reverse().find((fields) => fields.some((f) => !!this.values[f.uuid]))) + 1
|
||||
|
||||
const indexesList = [this.stepFields.length - 1]
|
||||
|
||||
if (requiredEmptyStepIndex !== -1) {
|
||||
indexesList.push(requiredEmptyStepIndex)
|
||||
}
|
||||
|
||||
if (lastFilledStepIndex !== -1) {
|
||||
indexesList.push(lastFilledStepIndex)
|
||||
}
|
||||
|
||||
this.currentStep = Math.min(...indexesList)
|
||||
}
|
||||
|
||||
if (/iPhone|iPad|iPod/i.test(navigator.userAgent)) {
|
||||
@@ -605,6 +620,7 @@ export default {
|
||||
},
|
||||
goToStep (step, scrollToArea = false, clickUpload = false) {
|
||||
this.currentStep = this.stepFields.indexOf(step)
|
||||
this.showFillAllRequiredFields = false
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.recalculateButtonDisabledKey = Math.random()
|
||||
@@ -620,6 +636,13 @@ export default {
|
||||
}
|
||||
})
|
||||
},
|
||||
setCurrentDate () {
|
||||
const inputEl = document.getElementById(this.currentField.uuid)
|
||||
|
||||
inputEl.valueAsDate = new Date(new Date().getTime() - new Date().getTimezoneOffset() * 60000)
|
||||
|
||||
inputEl.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
},
|
||||
saveStep (formData) {
|
||||
if (this.isCompleted) {
|
||||
return Promise.resolve({})
|
||||
@@ -639,12 +662,13 @@ export default {
|
||||
|
||||
stepPromise().then(async () => {
|
||||
const emptyRequiredField = this.stepFields.find((fields, index) => {
|
||||
return index < this.currentStep && fields[0].required && (fields[0].type === 'phone' || !this.allowToSkip) && !this.submittedValues[fields[0].uuid]
|
||||
return index < this.currentStep && fields[0].required && (fields[0].type === 'phone' || !this.allowToSkip || !this.isSecondWalkthrough) && !this.submittedValues[fields[0].uuid]
|
||||
})
|
||||
|
||||
const formData = new FormData(this.$refs.form)
|
||||
const isLastStep = this.currentStep === this.stepFields.length - 1
|
||||
|
||||
if (this.currentStep === this.stepFields.length - 1 && !emptyRequiredField) {
|
||||
if (isLastStep && !emptyRequiredField) {
|
||||
formData.append('completed', 'true')
|
||||
}
|
||||
|
||||
@@ -659,10 +683,18 @@ export default {
|
||||
|
||||
this.submittedValues[this.currentField.uuid] = this.values[this.currentField.uuid]
|
||||
|
||||
const nextStep = emptyRequiredField || this.stepFields[this.currentStep + 1]
|
||||
if (isLastStep) {
|
||||
this.isSecondWalkthrough = true
|
||||
}
|
||||
|
||||
const nextStep = (isLastStep && emptyRequiredField) || this.stepFields[this.currentStep + 1]
|
||||
|
||||
if (nextStep) {
|
||||
this.goToStep(nextStep, true)
|
||||
|
||||
if (emptyRequiredField === nextStep) {
|
||||
this.showFillAllRequiredFields = true
|
||||
}
|
||||
} else {
|
||||
this.isCompleted = true
|
||||
}
|
||||
|
||||
@@ -29,6 +29,10 @@ const en = {
|
||||
sending: 'Sending...',
|
||||
resend_code: 'Re-send code',
|
||||
verification_code_has_been_resent: 'Verification code has been re-sent via SMS',
|
||||
please_fill_all_required_fields: 'Please fill all required fields',
|
||||
set_today: 'Set Today',
|
||||
toggle_multiline_text: 'Toggle Multiline Text',
|
||||
date: 'Date',
|
||||
email_has_been_sent: 'Email has been sent'
|
||||
}
|
||||
|
||||
@@ -63,6 +67,10 @@ const es = {
|
||||
sending: 'Enviando...',
|
||||
resend_code: 'Reenviar código',
|
||||
verification_code_has_been_resent: 'El código de verificación ha sido reenviado por SMS',
|
||||
please_fill_all_required_fields: 'Por favor, complete todos los campos obligatorios',
|
||||
set_today: 'Establecer Hoy',
|
||||
date: 'Fecha',
|
||||
toggle_multiline_text: 'Alternar Texto Multilínea',
|
||||
email_has_been_sent: 'El correo electrónico ha sido enviado'
|
||||
}
|
||||
|
||||
@@ -97,6 +105,10 @@ const it = {
|
||||
sending: 'Invio in corso...',
|
||||
resend_code: 'Rinvia codice',
|
||||
verification_code_has_been_resent: 'Il codice di verifica è stato rinviato tramite SMS',
|
||||
please_fill_all_required_fields: 'Si prega di compilare tutti i campi obbligatori',
|
||||
set_today: 'Imposta Oggi',
|
||||
date: 'Data',
|
||||
toggle_multiline_text: 'Attiva Testo Multilinea',
|
||||
email_has_been_sent: "L'email è stata inviata"
|
||||
}
|
||||
|
||||
@@ -131,6 +143,10 @@ const de = {
|
||||
sending: 'Senden...',
|
||||
resend_code: 'Code erneut senden',
|
||||
verification_code_has_been_resent: 'Die Verifizierungscode wurde erneut per SMS gesendet',
|
||||
please_fill_all_required_fields: 'Bitte füllen Sie alle erforderlichen Felder aus',
|
||||
set_today: 'Heute einstellen',
|
||||
date: 'Datum',
|
||||
toggle_multiline_text: 'Mehrzeiligen Text umschalten',
|
||||
email_has_been_sent: 'Die E-Mail wurde gesendet'
|
||||
}
|
||||
|
||||
@@ -165,6 +181,10 @@ const fr = {
|
||||
sending: 'Envoi en cours...',
|
||||
resend_code: 'Renvoyer le code',
|
||||
verification_code_has_been_resent: 'Le code de vérification a été renvoyé par SMS',
|
||||
please_fill_all_required_fields: 'Veuillez remplir tous les champs obligatoires',
|
||||
set_today: "Définir Aujourd'hui",
|
||||
date: 'Date',
|
||||
toggle_multiline_text: 'Basculer le Texte Multiligne',
|
||||
email_has_been_sent: "L'email a été envoyé"
|
||||
}
|
||||
|
||||
@@ -199,6 +219,10 @@ const pl = {
|
||||
sending: 'Wysyłanie...',
|
||||
resend_code: 'Ponownie wyślij kod',
|
||||
verification_code_has_been_resent: 'Kod weryfikacyjny został ponownie wysłany',
|
||||
please_fill_all_required_fields: 'Proszę wypełnić wszystkie wymagane pola',
|
||||
set_today: 'Ustaw Dziś',
|
||||
date: 'Data',
|
||||
toggle_multiline_text: 'Przełącz Tekst Wielolinijkowy',
|
||||
email_has_been_sent: 'E-mail został wysłany'
|
||||
}
|
||||
|
||||
@@ -233,6 +257,10 @@ const uk = {
|
||||
sending: 'Надсилаю...',
|
||||
resend_code: 'Повторно відправити код',
|
||||
verification_code_has_been_resent: 'Код підтвердження був повторно надісланий',
|
||||
please_fill_all_required_fields: "Будь ласка, заповніть всі обов'язкові поля",
|
||||
set_today: 'Задати Сьогодні',
|
||||
date: 'Дата',
|
||||
toggle_multiline_text: 'Перемкнути Багаторядковий Текст',
|
||||
email_has_been_sent: 'Електронний лист був відправлений'
|
||||
}
|
||||
|
||||
@@ -267,6 +295,10 @@ const cs = {
|
||||
sending: 'Odesílání...',
|
||||
resend_code: 'Znovu odeslat kód',
|
||||
verification_code_has_been_resent: 'Ověřovací kód byl znovu odeslán',
|
||||
please_fill_all_required_fields: 'Prosím vyplňte všechny povinné položky',
|
||||
set_today: 'Nastavit Dnes',
|
||||
date: 'Datum',
|
||||
toggle_multiline_text: 'Přepnout Víceřádkový Text',
|
||||
email_has_been_sent: 'E-mail byl odeslán'
|
||||
}
|
||||
|
||||
@@ -301,6 +333,10 @@ const pt = {
|
||||
sending: 'Enviando...',
|
||||
resend_code: 'Reenviar código',
|
||||
verification_code_has_been_resent: 'O código de verificação foi reenviado via SMS',
|
||||
please_fill_all_required_fields: 'Por favor, preencha todos os campos obrigatórios',
|
||||
set_today: 'Definir Hoje',
|
||||
date: 'Data',
|
||||
toggle_multiline_text: 'Alternar Texto Multilinha',
|
||||
email_has_been_sent: 'Email enviado'
|
||||
}
|
||||
|
||||
|
||||
@@ -9,33 +9,37 @@
|
||||
class="tooltip"
|
||||
:data-tip="t('draw_initials')"
|
||||
>
|
||||
<button
|
||||
<a
|
||||
id="type_text_button"
|
||||
href="#"
|
||||
class="btn btn-sm btn-circle"
|
||||
:class="{ 'btn-neutral': isDrawInitials, 'btn-outline': !isDrawInitials }"
|
||||
@click.prevent="toggleTextInput"
|
||||
>
|
||||
<IconSignature :width="16" />
|
||||
</button>
|
||||
</a>
|
||||
</span>
|
||||
<button
|
||||
<a
|
||||
v-if="modelValue"
|
||||
href="#"
|
||||
class="btn btn-outline btn-sm"
|
||||
@click.prevent="remove"
|
||||
>
|
||||
<IconReload :width="16" />
|
||||
{{ t('clear') }}
|
||||
</button>
|
||||
<button
|
||||
</a>
|
||||
<a
|
||||
v-else
|
||||
href="#"
|
||||
class="btn btn-outline btn-sm"
|
||||
@click.prevent="clear"
|
||||
>
|
||||
<IconReload :width="16" />
|
||||
{{ t('clear') }}
|
||||
</button>
|
||||
<button
|
||||
</a>
|
||||
<a
|
||||
title="Minimize"
|
||||
href="#"
|
||||
class="py-1.5 inline md:hidden"
|
||||
@click.prevent="$emit('minimize')"
|
||||
>
|
||||
@@ -43,7 +47,7 @@
|
||||
:width="20"
|
||||
:height="20"
|
||||
/>
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
@@ -66,7 +70,7 @@
|
||||
id="initials_text_input"
|
||||
ref="textInput"
|
||||
class="base-input !text-2xl w-full mt-6 text-center"
|
||||
:required="field.required && !isInitialsStarted"
|
||||
:required="field.required && !isInitialsStarted && !modelValue"
|
||||
:placeholder="`${t('type_initial_here')}...`"
|
||||
type="text"
|
||||
@focus="$emit('focus')"
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
<template>
|
||||
<label
|
||||
v-if="field.name"
|
||||
:for="field.uuid"
|
||||
class="label text-2xl mb-2"
|
||||
>{{ field.name }}
|
||||
<template v-if="!field.required">({{ t('optional') }})</template>
|
||||
</label>
|
||||
<div
|
||||
v-else
|
||||
class="py-1"
|
||||
/>
|
||||
<div class="items-center flex">
|
||||
<input
|
||||
v-if="!isTextArea"
|
||||
:id="field.uuid"
|
||||
v-model="text"
|
||||
class="base-input !text-2xl w-full !pr-11 -mr-10"
|
||||
:required="field.required"
|
||||
:pattern="field.validation?.pattern"
|
||||
:oninvalid="field.validation?.message ? `this.setCustomValidity(${JSON.stringify(field.validation.message)})` : ''"
|
||||
:oninput="field.validation?.message ? `this.setCustomValidity('')` : ''"
|
||||
:placeholder="`${t('type_here')}...${field.required ? '' : ` (${t('optional')})`}`"
|
||||
type="text"
|
||||
:name="`values[${field.uuid}]`"
|
||||
@focus="$emit('focus')"
|
||||
>
|
||||
<textarea
|
||||
v-if="isTextArea"
|
||||
:id="field.uuid"
|
||||
ref="textarea"
|
||||
v-model="text"
|
||||
class="base-textarea !text-2xl w-full"
|
||||
:placeholder="`${t('type_here')}...${field.required ? '' : ` (${t('optional')})`}`"
|
||||
:required="field.required"
|
||||
:name="`values[${field.uuid}]`"
|
||||
@input="resizeTextarea"
|
||||
@focus="$emit('focus')"
|
||||
/>
|
||||
<div
|
||||
v-if="!isTextArea"
|
||||
class="tooltip"
|
||||
:data-tip="t('toggle_multiline_text')"
|
||||
>
|
||||
<a
|
||||
href="#"
|
||||
class="btn btn-ghost btn-circle btn-sm"
|
||||
@click.prevent="toggleTextArea"
|
||||
>
|
||||
<IconAlignBoxLeftTop />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { IconAlignBoxLeftTop } from '@tabler/icons-vue'
|
||||
|
||||
export default {
|
||||
name: 'TextStep',
|
||||
components: {
|
||||
IconAlignBoxLeftTop
|
||||
},
|
||||
inject: ['t'],
|
||||
props: {
|
||||
field: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
modelValue: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
emits: ['update:model-value', 'focus'],
|
||||
data () {
|
||||
return {
|
||||
isTextArea: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
text: {
|
||||
set (value) {
|
||||
this.$emit('update:model-value', value)
|
||||
},
|
||||
get () {
|
||||
return this.modelValue
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.isTextArea = this.modelValue?.includes('\n')
|
||||
|
||||
if (this.isTextArea) {
|
||||
this.$nextTick(() => {
|
||||
this.resizeTextarea()
|
||||
})
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
resizeTextarea () {
|
||||
const textarea = this.$refs.textarea
|
||||
|
||||
textarea.style.height = 'auto'
|
||||
textarea.style.height = textarea.scrollHeight + 'px'
|
||||
},
|
||||
toggleTextArea () {
|
||||
this.isTextArea = true
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.$refs.textarea.focus()
|
||||
this.$refs.textarea.setSelectionRange(this.$refs.textarea.value.length, this.$refs.textarea.value.length)
|
||||
|
||||
this.resizeTextarea()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -12,7 +12,7 @@ class SubmitterMailer < ApplicationMailer
|
||||
if @email_config || subject.present?
|
||||
ReplaceEmailVariables.call(subject.presence || @email_config.value['subject'], submitter:)
|
||||
else
|
||||
'You have been invited to submit a form'
|
||||
'You are invited to submit a form'
|
||||
end
|
||||
|
||||
mail(to: @submitter.friendly_name,
|
||||
@@ -62,7 +62,7 @@ class SubmitterMailer < ApplicationMailer
|
||||
if @email_config
|
||||
ReplaceEmailVariables.call(@email_config.value['subject'], submitter:)
|
||||
else
|
||||
'Your copy of documents'
|
||||
'Your document copy'
|
||||
end
|
||||
|
||||
mail(from: from_address_for_submitter(submitter),
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class UserMailer < ApplicationMailer
|
||||
def invitation_email(user)
|
||||
@current_account = user.account
|
||||
def invitation_email(user, invited_by: nil)
|
||||
@current_account = invited_by&.account || user.account
|
||||
@user = user
|
||||
@token = @user.send(:set_reset_password_token)
|
||||
|
||||
mail(to: @user.friendly_name,
|
||||
subject: 'You have been invited to Docuseal')
|
||||
subject: 'You are invited to DocuSeal')
|
||||
end
|
||||
end
|
||||
|
||||
@@ -30,9 +30,9 @@ class AccountConfig < ApplicationRecord
|
||||
|
||||
DEFAULT_VALUES = {
|
||||
SUBMITTER_INVITATION_EMAIL_KEY => {
|
||||
'subject' => 'You have been invited to submit a form',
|
||||
'subject' => 'You are invited to submit a form',
|
||||
'body' => "Hi there,\n\n" \
|
||||
"You have been invited to submit the \"{{template.name}}\" form:\n\n" \
|
||||
"You have been invited to submit the \"{{template.name}}\" form.\n\n" \
|
||||
"{{submitter.link}}\n\n" \
|
||||
"Please contact us by replying to this email if you didn't request this.\n\n" \
|
||||
"Thanks,\n" \
|
||||
@@ -45,10 +45,10 @@ class AccountConfig < ApplicationRecord
|
||||
'{{submission.link}}'
|
||||
},
|
||||
SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY => {
|
||||
'subject' => 'Your copy of documents',
|
||||
'subject' => 'Your document copy',
|
||||
'body' => "Hi there,\n\n" \
|
||||
"Please check the copy of your \"{{template.name}}\" submission in the email attachments.\n" \
|
||||
"Alternatively, you can download the copy using:\n\n" \
|
||||
"Alternatively, you can download your copy using:\n\n" \
|
||||
"{{documents.links}}\n\n" \
|
||||
"Thanks,\n" \
|
||||
'{{account.name}}'
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
<div class="flex items-center h-full">
|
||||
<div class="mx-auto">
|
||||
<div class="max-w-xl mx-auto">
|
||||
<h1 class="text-5xl font-bold text-base-content">👋 Welcome to DocuSeal</h1>
|
||||
<h1 class="text-5xl font-bold text-base-content">👋 Welcome to <%= Docuseal.product_name %></h1>
|
||||
</div>
|
||||
<div class="max-w-lg mx-auto">
|
||||
<p class="py-6 text-gray-600">Streamline document workflows, from creating customizable templates to filling and signing document forms</p>
|
||||
|
||||
@@ -21,6 +21,5 @@
|
||||
<div class="form-control">
|
||||
<%= f.button button_title(title: 'Change my password', disabled_with: 'Changing password'), class: 'base-button' %>
|
||||
</div>
|
||||
<%= render 'devise/shared/links' %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
@@ -12,5 +12,4 @@
|
||||
<%= f.button button_title(title: 'Reset password', disabled_with: 'Resetting password'), class: 'base-button' %>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= render 'devise/shared/links' %>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div class="max-w-lg mx-auto px-2">
|
||||
<%= render 'devise/shared/select_server' if Docuseal.multitenant? %>
|
||||
<h1 class="text-4xl font-bold text-center mt-8">Log In</h1>
|
||||
<h1 class="text-4xl font-bold text-center mt-8">Sign In</h1>
|
||||
<%= form_for(resource, as: resource_name, html: { class: 'space-y-6' }, data: { turbo: params[:redir].blank? }, url: session_path(resource_name)) do |f| %>
|
||||
<% if params[:redir].present? %>
|
||||
<%= hidden_field_tag :redir, params[:redir] %>
|
||||
@@ -16,11 +16,11 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<%= f.button button_title(title: 'Log In', disabled_with: 'Logging In'), class: 'base-button' %>
|
||||
<%= f.button button_title(title: 'Sign In', disabled_with: 'Signing In'), class: 'base-button' %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% if devise_mapping.omniauthable? %>
|
||||
<%= button_to button_title(title: 'Log 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 %>
|
||||
<%= 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 %>
|
||||
<% end %>
|
||||
<%= render 'devise/shared/links' %>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div class="max-w-lg mx-auto px-2">
|
||||
<h1 class="text-4xl font-bold text-center mt-8">Log In</h1>
|
||||
<h1 class="text-4xl font-bold text-center mt-8">Sign In</h1>
|
||||
<%= form_for(resource, as: resource_name, html: { class: 'space-y-6' }, data: { turbo: params[:redir].blank? }, url: session_path(resource_name)) do |f| %>
|
||||
<%= f.hidden_field :email %>
|
||||
<%= f.hidden_field :password %>
|
||||
@@ -13,7 +13,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<%= f.button button_title(title: 'Log In', disabled_with: 'Logging In'), class: 'base-button' %>
|
||||
<%= f.button button_title(title: 'Sign In', disabled_with: 'Signing In'), class: 'base-button' %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div class="flex justify-between mt-4">
|
||||
<%- if controller_name != 'sessions' %>
|
||||
<%= link_to 'Log in', new_session_path(resource_name), class: 'link link-hover' %>
|
||||
<%= link_to 'Already have an account?', new_session_path(resource_name), class: 'link link-hover mx-auto' %>
|
||||
<% end %>
|
||||
<%- if devise_mapping.registerable? && controller_name != 'registrations' %>
|
||||
<%= link_to 'Create free account', registration_path({ redir: params[:redir] }.compact_blank), class: 'link link-hover' %>
|
||||
@@ -8,10 +8,4 @@
|
||||
<%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %>
|
||||
<%= link_to 'Forgot your password?', new_password_path(resource_name), class: 'link link-hover' %>
|
||||
<% end %>
|
||||
<%- if devise_mapping.confirmable? && controller_name != 'confirmations' %>
|
||||
<%= link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name), class: 'link link-hover' %>
|
||||
<% end %>
|
||||
<%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %>
|
||||
<%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name), class: 'link link-hover' %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div class="max-w-xl mx-auto px-2">
|
||||
<h1 class="text-4xl font-bold text-center my-8">👋 Welcome to Docuseal</h1>
|
||||
<h1 class="text-4xl font-bold text-center my-8">👋 Welcome to <%= Docuseal.product_name %></h1>
|
||||
<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put, class: 'space-y-6' }) do |f| %>
|
||||
<div class="space-y-2">
|
||||
<%= render 'devise/shared/error_messages', resource: %>
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
<%= javascript_pack_tag 'application', defer: true %>
|
||||
<%= stylesheet_pack_tag 'application', media: 'all' %>
|
||||
<%= render 'shared/posthog' if ENV['POSTHOG_TOKEN'] %>
|
||||
<%= render 'shared/plausible' if !signed_in? && ENV['PLAUSIBLE_DOMAIN'] %>
|
||||
</head>
|
||||
<body>
|
||||
<turbo-frame id="modal"></turbo-frame>
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
<div class="w-full text-center">
|
||||
<span class="font-bold">Demo Environment</span>
|
||||
<br>
|
||||
Feel free to
|
||||
<a href="<%= new_template_path %>" data-turbo-frame="modal" class="inline underline font-medium">create a new template</a> document form or
|
||||
<a href="<%= new_template_path %>" data-turbo-frame="modal" class="inline underline font-medium">Create a new template</a> document form or
|
||||
<a href="<%= start_form_url(slug: ::Template.first&.slug) %>" target="_blank" class="inline underline font-medium">submit the existing one</a> 😊
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
---
|
||||
</p>
|
||||
<p>
|
||||
Sent using <a href="<%= Docuseal::PRODUCT_URL %>"><%= Docuseal::PRODUCT_NAME %></a> free document signing.
|
||||
Sent using <a href="<%= Docuseal::PRODUCT_URL %>"><%= Docuseal.product_name %></a> free document signing.
|
||||
</p>
|
||||
|
||||
@@ -8,7 +8,12 @@
|
||||
<% if signed_in? %>
|
||||
<div class="space-x-4 flex items-center">
|
||||
<% if Docuseal.demo? %>
|
||||
<%= render 'shared/github_button' %>
|
||||
<a href="https://docuseal.co/sign_up" class="btn btn-neutral btn-sm btn-outline inline-flex items-center justify-center" style="height: 37px">
|
||||
Sign Up
|
||||
</a>
|
||||
<span class="hidden sm:inline">
|
||||
<%= render 'shared/github_button' %>
|
||||
</span>
|
||||
<% else %>
|
||||
<div class="flex items-center justify-center space-x-4 mr-1">
|
||||
<% if can?(:manage, EncryptedConfig) && !can?(:manage, :tenants) %>
|
||||
@@ -58,18 +63,19 @@
|
||||
</div>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="flex space-x-6">
|
||||
<%= link_to new_user_session_path, class: 'font-medium text-lg' do %>
|
||||
<span class="flex items-center justify-center space-x-1">
|
||||
<%= svg_icon('login', class: 'w-6 h-6') %>
|
||||
<span>Sign in</span>
|
||||
</span>
|
||||
<% end %>
|
||||
<% if Docuseal.multitenant? %>
|
||||
<%= link_to registration_path, class: 'font-medium text-lg hidden md:inline' do %>
|
||||
<div class="flex space-x-2">
|
||||
<% if request.path != new_user_session_path %>
|
||||
<%= link_to new_user_session_path, class: 'font-medium text-lg' do %>
|
||||
<span class="flex items-center justify-center space-x-1">
|
||||
<%= svg_icon('user_plus', class: 'w-6 h-6') %>
|
||||
<span>Try for Free</span>
|
||||
<%= svg_icon('login', class: 'w-6 h-6') %>
|
||||
<span>Sign in</span>
|
||||
</span>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% if Docuseal.multitenant? && !request.path.in?([registration_path, new_registration_path]) %>
|
||||
<%= link_to registration_path, class: 'btn btn-neutral btn-sm btn-outline' do %>
|
||||
<span class="flex items-center justify-center space-x-1">
|
||||
<span>Create free Account</span>
|
||||
</span>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<script defer data-domain="<%= ENV.fetch('PLAUSIBLE_DOMAIN', nil) %>" src="https://plausible.io/js/script.manual.js"></script>
|
||||
<script>window.plausible = window.plausible || function() { (window.plausible.q = window.plausible.q || []).push(arguments) }</script>
|
||||
<script>
|
||||
document.addEventListener("turbo:load", function (e) {
|
||||
if (e.detail.url.match(/sign_in|sign_up|password/)) {
|
||||
plausible('pageview')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -9,5 +9,5 @@
|
||||
<% else %>
|
||||
Powered by
|
||||
<% end %>
|
||||
<a href="<%= Docuseal::PRODUCT_URL %>" class="underline"><%= Docuseal::PRODUCT_NAME %></a> - open source documents software
|
||||
<a href="<%= Docuseal::PRODUCT_URL %>" class="underline"><%= Docuseal.product_name %></a> - open source documents software
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<form action="<%= request.path %>" method="get" class="items-center hidden md:flex">
|
||||
<div class="relative">
|
||||
<% if params[:q].present? %>
|
||||
<% if params[:q].present? %>
|
||||
<div class="relative">
|
||||
<a href="<%= request.path %>" title="Clear" class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-auto text-neutral text-2xl font-extralight">
|
||||
×
|
||||
</a>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<input id="search" name="q" value="<%= params[:q] %>" class="input input-ghost text-lg pr-10 -mr-12 <%= 'pl-8 input-outlined' if params[:q].present? %>">
|
||||
<button type="submit" title="Search" class="btn btn-ghost btn-circle" onclick="window.search.value || document.activeElement === window.search ? null : [event.preventDefault(), window.search.focus()]">
|
||||
<span class="enabled">
|
||||
|
||||
@@ -41,6 +41,13 @@
|
||||
<%= link_to 'Team', settings_users_path, class: 'text-base hover:bg-base-300' %>
|
||||
</li>
|
||||
<% end %>
|
||||
<% if !Docuseal.demo? && can?(:manage, EncryptedConfig) %>
|
||||
<li>
|
||||
<%= link_to Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CONSOLE_URL}/on_premise", class: 'text-base hover:bg-base-300', data: { prefetch: false } do %>
|
||||
Plans
|
||||
<% end %>
|
||||
</li>
|
||||
<% end %>
|
||||
<% if Docuseal.demo? || !Docuseal.multitenant? %>
|
||||
<% if can?(:read, AccessToken) %>
|
||||
<li>
|
||||
|
||||
@@ -28,8 +28,6 @@
|
||||
<%= l(Date.parse(value), format: :long, locale: local_assigns[:locale]) %>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="flex items-center px-0.5">
|
||||
<%= Array.wrap(value).join(', ') %>
|
||||
</div>
|
||||
<div class="flex items-center px-0.5 whitespace-pre-wrap"><%= Array.wrap(value).join(', ') %></div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<a href="<%= root_path %>" class="mx-auto text-2xl md:text-3xl font-bold items-center flex space-x-3">
|
||||
<%= render 'shared/logo', class: 'w-9 h-9 md:w-12 md:h-12' %>
|
||||
<span><%= Docuseal::PRODUCT_NAME %></span>
|
||||
<span><%= Docuseal.product_name %></span>
|
||||
</a>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<% fields_index = Templates.build_field_areas_index(@submitter.submission.template_fields || @submitter.submission.template.fields) %>
|
||||
<% values = @submitter.submission.submitters.where.not(id: @submitter.id).reduce({}) { |acc, sub| acc.merge(sub.values) } %>
|
||||
<% values = @submitter.submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) } %>
|
||||
<% attachments_index = ActiveStorage::Attachment.where(record: @submitter.submission.submitters, name: :attachments).preload(:blob).index_by(&:uuid) %>
|
||||
<div style="max-height: -webkit-fill-available;">
|
||||
<div id="scrollbox">
|
||||
@@ -20,6 +20,7 @@
|
||||
<% fields_index.dig(document.uuid, index)&.each do |(area, field)| %>
|
||||
<% value = values[field['uuid']] %>
|
||||
<% 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 %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<% 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 the copy using:</p>
|
||||
<p>Alternatively, you can download your copy using:</p>
|
||||
<% @documents.each do |document| %>
|
||||
<ul>
|
||||
<li>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<% end %>
|
||||
<% else %>
|
||||
<p>Hi there,</p>
|
||||
<p>You have been invited to submit the "<%= @submitter.submission.template.name %>" form</p>
|
||||
<p>You have been invited to submit the "<%= @submitter.submission.template.name %>" form.</p>
|
||||
<p><%= link_to 'Submit Form', submit_form_url(slug: @submitter.slug, t: SubmissionEvents.build_tracking_param(@submitter, 'click_email')) %></p>
|
||||
<p>Please contact us by replying to this email if you didn't request this.</p>
|
||||
<p>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<p>Hello <%= @user.first_name %>,</p>
|
||||
<p>You have been invited to <%= @user.account.name %> DocuSeal. Please sign up using the link below:</p>
|
||||
<p>You have been invited to <%= @user.account.name %> <%= Docuseal.product_name %>. Please sign up using the link below:</p>
|
||||
<p><%= link_to 'Sign up', invitation_url(reset_password_token: @token) %></p>
|
||||
<p>Please contact us by replying to this email if you didn't request this.</p>
|
||||
<p>
|
||||
|
||||
@@ -6,7 +6,7 @@ class AddUuidToUsers < ActiveRecord::Migration[7.0]
|
||||
end
|
||||
|
||||
def up
|
||||
add_column :users, :uuid, :text
|
||||
add_column :users, :uuid, :string
|
||||
add_index :users, :uuid, unique: true
|
||||
|
||||
MigrationUser.all.each do |user|
|
||||
|
||||
@@ -6,7 +6,7 @@ class AddSourceToSubmissions < ActiveRecord::Migration[7.0]
|
||||
end
|
||||
|
||||
def up
|
||||
add_column :submissions, :source, :text
|
||||
add_column :submissions, :source, :string
|
||||
|
||||
MigrationSubmission.where(source: nil).update_all(source: :invite)
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ class CreateAccessTokens < ActiveRecord::Migration[7.0]
|
||||
create_table :access_tokens do |t|
|
||||
t.references :user, null: false, foreign_key: true, index: true
|
||||
t.text :token, null: false
|
||||
t.text :sha256, null: false, index: { unique: true }
|
||||
t.string :sha256, null: false, index: { unique: true }
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
|
||||
@@ -8,6 +8,7 @@ module Accounts
|
||||
|
||||
new_user = account.users.first.dup
|
||||
|
||||
new_user.uuid = SecureRandom.uuid
|
||||
new_user.account = new_account
|
||||
new_user.encrypted_password = SecureRandom.hex
|
||||
new_user.email = "#{SecureRandom.hex}@docuseal.co"
|
||||
@@ -18,12 +19,14 @@ module Accounts
|
||||
new_template.account = new_account
|
||||
new_template.slug = SecureRandom.base58(14)
|
||||
|
||||
new_template.deleted_at = nil
|
||||
new_template.save!
|
||||
|
||||
Templates::CloneAttachments.call(template: new_template, original_template: template)
|
||||
end
|
||||
|
||||
new_user.save!(validate: false)
|
||||
new_account.templates.update_all(folder_id: new_account.default_template_folder.id)
|
||||
|
||||
new_account
|
||||
end
|
||||
|
||||
@@ -40,6 +40,7 @@ module ActionMailerConfigsInterceptor
|
||||
address: value['host'],
|
||||
port: value['port'],
|
||||
domain: value['domain'],
|
||||
openssl_verify_mode: OpenSSL::SSL::VERIFY_NONE,
|
||||
authentication: value.fetch('authentication', 'plain'),
|
||||
enable_starttls_auto: true,
|
||||
ssl: value['security'] == 'ssl',
|
||||
|
||||
@@ -58,6 +58,10 @@ module Docuseal
|
||||
end
|
||||
end
|
||||
|
||||
def product_name
|
||||
PRODUCT_NAME
|
||||
end
|
||||
|
||||
def refresh_default_url_options!
|
||||
@default_url_options = nil
|
||||
end
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module GenerateCertificate
|
||||
NAME = Docuseal::PRODUCT_NAME
|
||||
SIZE = 2**11
|
||||
|
||||
module_function
|
||||
|
||||
def call(name = NAME)
|
||||
def call(name = Docuseal.product_name)
|
||||
root_cert, root_key = generate_root_ca(name)
|
||||
|
||||
sub_cert, sub_key = generate_sub_ca(name, root_cert, root_key)
|
||||
|
||||
@@ -9,11 +9,10 @@ module Submissions
|
||||
submission = template.submissions.new(created_by_user: user, source:,
|
||||
template_submitters: template.submitters, submitters_order:)
|
||||
|
||||
maybe_set_template_fields(submission, attrs[:submitters])
|
||||
|
||||
attrs[:submitters].each_with_index do |submitter_attrs, index|
|
||||
uuid =
|
||||
submitter_attrs[:uuid].presence ||
|
||||
template.submitters.find { |e| e['name'] == submitter_attrs[:role] }&.dig('uuid') ||
|
||||
template.submitters[index]&.dig('uuid')
|
||||
uuid = find_submitter_uuid(template, submitter_attrs, index)
|
||||
|
||||
next if uuid.blank?
|
||||
|
||||
@@ -26,6 +25,66 @@ module Submissions
|
||||
end
|
||||
end
|
||||
|
||||
def maybe_set_template_fields(submission, submitters_attrs)
|
||||
template_fields = submission.template.fields.deep_dup
|
||||
|
||||
submitters_attrs.each_with_index do |submitter_attrs, index|
|
||||
submitter_uuid = find_submitter_uuid(submission.template, submitter_attrs, index)
|
||||
|
||||
process_readonly_fields_param(submitter_attrs[:readonly_fields], template_fields, submitter_uuid)
|
||||
|
||||
process_fields_param(submitter_attrs[:fields], template_fields, submitter_uuid)
|
||||
end
|
||||
|
||||
if template_fields != submission.template.fields
|
||||
submission.template_fields = template_fields
|
||||
submission.template_schema = submission.template.schema
|
||||
end
|
||||
|
||||
submission
|
||||
end
|
||||
|
||||
def process_readonly_fields_param(readonly_fields, template_fields, submitter_uuid)
|
||||
return if readonly_fields.blank?
|
||||
|
||||
template_fields.each do |f|
|
||||
next if f['submitter_uuid'] != submitter_uuid ||
|
||||
(!f['name'].in?(readonly_fields) &&
|
||||
!f['name'].to_s.parameterize.underscore.in?(readonly_fields))
|
||||
|
||||
f['readonly'] = true
|
||||
end
|
||||
end
|
||||
|
||||
def process_fields_param(fields, template_fields, submitter_uuid)
|
||||
return if fields.blank?
|
||||
|
||||
template_fields.each do |f|
|
||||
next if f['submitter_uuid'] != submitter_uuid
|
||||
|
||||
field_configs = fields.find do |e|
|
||||
e['name'] == f['name'] || e['name'] == f['name'].to_s.parameterize.underscore
|
||||
end
|
||||
|
||||
next if field_configs.blank?
|
||||
|
||||
f['readonly'] = field_configs['readonly'] if field_configs['readonly'].present?
|
||||
|
||||
next if field_configs['validation_pattern'].blank?
|
||||
|
||||
f['validation'] = {
|
||||
'pattern' => field_configs['validation_pattern'],
|
||||
'message' => field_configs['invalid_message']
|
||||
}.compact_blank
|
||||
end
|
||||
end
|
||||
|
||||
def find_submitter_uuid(template, attrs, index)
|
||||
attrs[:uuid].presence ||
|
||||
template.submitters.find { |e| e['name'] == attrs[:role] }&.dig('uuid') ||
|
||||
template.submitters[index]&.dig('uuid')
|
||||
end
|
||||
|
||||
def build_submitter(submission:, attrs:, uuid:, is_order_sent:, mark_as_sent:)
|
||||
email = Submissions.normalize_email(attrs[:email])
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ module Submissions
|
||||
'Helvetica'
|
||||
end
|
||||
|
||||
INFO_CREATOR = "#{Docuseal::PRODUCT_NAME} (#{Docuseal::PRODUCT_URL})".freeze
|
||||
INFO_CREATOR = "#{Docuseal.product_name} (#{Docuseal::PRODUCT_URL})".freeze
|
||||
SIGN_REASON = 'Signed with DocuSeal.co'
|
||||
VERIFIED_TEXT = if Docuseal.multitenant?
|
||||
'Verified by DocuSeal'
|
||||
@@ -62,15 +62,7 @@ module Submissions
|
||||
composer.new_page
|
||||
|
||||
composer.column(columns: 1) do |column|
|
||||
column.image(PdfIcons.logo_io, width: 40, height: 40, position: :float)
|
||||
|
||||
column.formatted_text([{ text: 'DocuSeal',
|
||||
link: Docuseal::PRODUCT_URL }],
|
||||
font_size: 20,
|
||||
font: [FONT_BOLD_NAME, { variant: :bold }],
|
||||
width: 100,
|
||||
padding: [12, 0, 0, 8],
|
||||
position: :float, position_hint: :left)
|
||||
add_logo(column)
|
||||
|
||||
column.text('Audit Log',
|
||||
font_size: 16,
|
||||
@@ -270,6 +262,18 @@ module Submissions
|
||||
record: submission
|
||||
)
|
||||
end
|
||||
|
||||
def add_logo(column)
|
||||
column.image(PdfIcons.logo_io, width: 40, height: 40, position: :float)
|
||||
|
||||
column.formatted_text([{ text: 'DocuSeal',
|
||||
link: Docuseal::PRODUCT_URL }],
|
||||
font_size: 20,
|
||||
font: [FONT_BOLD_NAME, { variant: :bold }],
|
||||
width: 100,
|
||||
padding: [12, 0, 0, 8],
|
||||
position: :float, position_hint: :left)
|
||||
end
|
||||
# rubocop:enable Metrics
|
||||
end
|
||||
end
|
||||
|
||||
@@ -10,7 +10,7 @@ module Submissions
|
||||
'Helvetica'
|
||||
end
|
||||
|
||||
INFO_CREATOR = "#{Docuseal::PRODUCT_NAME} (#{Docuseal::PRODUCT_URL})".freeze
|
||||
INFO_CREATOR = "#{Docuseal.product_name} (#{Docuseal::PRODUCT_URL})".freeze
|
||||
SIGN_REASON = 'Signed by %<email>s with DocuSeal.co'
|
||||
|
||||
TEXT_LEFT_MARGIN = 1
|
||||
@@ -23,7 +23,6 @@ module Submissions
|
||||
|
||||
# rubocop:disable Metrics
|
||||
def call(submitter)
|
||||
layouter = HexaPDF::Layout::TextLayouter.new(valign: :center)
|
||||
cell_layouter = HexaPDF::Layout::TextLayouter.new(valign: :center, align: :center)
|
||||
|
||||
template = submitter.submission.template
|
||||
@@ -38,7 +37,6 @@ module Submissions
|
||||
|
||||
field.fetch('areas', []).each do |area|
|
||||
pdf = pdfs_index[area['attachment_uuid']]
|
||||
pdf.fonts.add(FONT_NAME)
|
||||
|
||||
page = pdf.pages[area['page']]
|
||||
page.rotate(0, flatten: true) if page[:Rotate] != 0
|
||||
@@ -50,6 +48,8 @@ module Submissions
|
||||
height = page.box.height
|
||||
font_size = ((page.box.width / A4_SIZE[0].to_f) * FONT_SIZE).to_i
|
||||
|
||||
layouter = HexaPDF::Layout::TextLayouter.new(valign: :center, font: pdf.fonts.add(FONT_NAME), font_size:)
|
||||
|
||||
value = submitter.values[field['uuid']]
|
||||
|
||||
next if Array.wrap(value).compact_blank.blank?
|
||||
@@ -203,9 +203,9 @@ module Submissions
|
||||
def save_signed_pdf(pdf:, submitter:, pkcs:, uuid:, name:)
|
||||
io = StringIO.new
|
||||
|
||||
pdf.trailer.info[:Creator] = INFO_CREATOR
|
||||
pdf.trailer.info[:Creator] = info_creator
|
||||
|
||||
pdf.sign(io, reason: format(SIGN_REASON, email: submitter.email),
|
||||
pdf.sign(io, reason: sign_reason(submitter.email),
|
||||
certificate: pkcs.certificate,
|
||||
key: pkcs.key,
|
||||
certificate_chain: pkcs.ca_certs || [])
|
||||
@@ -269,6 +269,14 @@ module Submissions
|
||||
pdf
|
||||
end
|
||||
|
||||
def sign_reason(email)
|
||||
format(SIGN_REASON, email:)
|
||||
end
|
||||
|
||||
def info_creator
|
||||
INFO_CREATOR
|
||||
end
|
||||
|
||||
def h
|
||||
Rails.application.routes.url_helpers
|
||||
end
|
||||
|
||||
@@ -41,7 +41,7 @@ module Submitters
|
||||
end
|
||||
|
||||
def build_fields_index(fields)
|
||||
fields.index_by { |e| e['name'] }.merge(fields.index_by { |e| e['name'].parameterize.underscore })
|
||||
fields.index_by { |e| e['name'] }.merge(fields.index_by { |e| e['name'].to_s.parameterize.underscore })
|
||||
end
|
||||
|
||||
def normalize_attachment_value(value, account)
|
||||
|
||||
Reference in New Issue
Block a user