Compare commits

..
34 Commits
Author SHA1 Message Date
DocuSeal ce39494f84 optimize for large files 2023-10-22 22:33:54 +03:00
DocuSeal c639a3f733 do not sign in after reset password if 2fa
https://github.com/devise-two-factor/devise-two-factor#disabling-automatic-login-after-password-resets
thanks Greg Molnar
2023-10-22 13:19:47 +03:00
DocuSeal 74bef133b3 add enqueries checkbox 2023-10-21 15:33:10 +03:00
DocuSeal 33142966dc allow to resubmit form via shared link 2023-10-21 13:06:59 +03:00
DocuSeal 42ec8d5fc9 do not allow to skip required fields 2023-10-21 12:09:50 +03:00
DocuSeal 01d752e811 fix webhook serializer 2023-10-21 12:09:37 +03:00
DocuSeal 99b33d4165 allow to resubmit form via shared link 2023-10-20 22:55:25 +03:00
DocuSeal 11a90d797a add user encrypted configs 2023-10-20 13:16:39 +03:00
DocuSeal 9da7463657 add submission status filters 2023-10-20 00:13:17 +03:00
Alex Turchyn b6d350ebbd fix no error messages when sign in with wrong credentials 2023-10-18 22:56:28 +03:00
DocuSeal 9e87a0373f add application key to submitters 2023-10-18 02:14:44 +03:00
DocuSeal aee55165c5 use start page 2023-10-16 21:21:04 +03:00
DocuSeal 9ddbcae153 fix plans link 2023-10-16 20:19:39 +03:00
DocuSeal 10cd3aa72a smtp ssl verify mode none 2023-10-16 20:19:28 +03:00
DocuSeal 6684a27e2f preserve textarea in the form 2023-10-16 02:04:52 +03:00
DocuSeal b6f7a7dd15 fix initials step 2023-10-16 01:40:17 +03:00
DocuSeal 309e43e18e fix set today date timezone 2023-10-16 01:28:04 +03:00
DocuSeal d5b8640a0e force redraw form steps 2023-10-16 00:51:08 +03:00
DocuSeal 49b9064d9f add ability to toggle textarea 2023-10-16 00:41:14 +03:00
DocuSeal 698031cfc0 add current date button 2023-10-15 22:59:32 +03:00
DocuSeal 6dc21e66e1 return to required fields 2023-10-15 21:41:47 +03:00
DocuSeal 60b32fec94 adjust sign up header buttons 2023-10-15 01:03:29 +03:00
DocuSeal c621166b45 use full name form company name if missing 2023-10-14 22:31:32 +03:00
DocuSeal 8e2da83f9e adjust demo 2023-10-14 21:19:21 +03:00
DocuSeal a39ba5b275 add plausible 2023-10-14 16:00:56 +03:00
DocuSeal edf466a6de add plans link 2023-10-14 15:42:50 +03:00
DocuSeal 3b8bb8c5bb fix api when fields without name 2023-10-13 14:17:28 +03:00
DocuSeal 79f1061353 improve signup form links 2023-10-12 22:59:15 +03:00
Alex Turchyn 745f2160c5 update the wording of emails 2023-10-12 22:42:14 +03:00
DocuSeal 2f0c79444b add fields configs param to submissions api 2023-10-12 22:13:46 +03:00
DocuSeal 69a3782903 add readonly fields via api 2023-10-12 17:23:59 +03:00
DocuSeal 3dd7c47558 refactor result generation 2023-10-11 21:05:42 +03:00
DocuSeal 761bb07ee6 fix migration for mysql 2023-10-11 21:05:29 +03:00
Alex Turchyn 92d7bc65f5 replace 'log in' with 'sign in' 2023-10-11 01:11:36 +03:00
93 changed files with 900 additions and 215 deletions

No files matched your search

+3
View File
@@ -67,6 +67,9 @@ RSpec/MultipleMemoizedHelpers:
Rails/I18nLocaleTexts:
Enabled: false
Rails/FindEach:
Enabled: false
Rails/SkipsModelValidations:
Enabled: false
@@ -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, :application_key,
{ 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)
@@ -4,6 +4,27 @@ module Api
class SubmittersController < ApiBaseController
load_and_authorize_resource :submitter
def index
submitters = Submitters.search(@submitters, params[:q])
submitters = submitters.where(application_key: params[:application_key]) if params[:application_key].present?
submitters = submitters.where(submission_id: params[:submission_id]) if params[:submission_id].present?
submitters = paginate(
submitters.preload(:template, :submission, :submission_events,
documents_attachments: :blob, attachments_attachments: :blob)
)
render json: {
data: submitters.map { |s| Submitters::SerializeForApi.call(s, with_template: true, with_events: true) },
pagination: {
count: submitters.size,
next: submitters.last&.id,
prev: submitters.first&.id
}
}
end
def show
Submissions::EnsureResultGenerated.call(@submitter) if @submitter.completed_at?
@@ -16,6 +16,7 @@ module Api
render json: {
schema:,
documents: documents.as_json(
methods: [:metadata],
include: {
preview_images: { methods: %i[url metadata filename] }
}
+1 -1
View File
@@ -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
+22
View File
@@ -0,0 +1,22 @@
# frozen_string_literal: true
class EnquiriesController < ApplicationController
skip_before_action :authenticate_user!
skip_authorization_check
def create
if params[:talk_to_sales] == 'on'
Faraday.post(Docuseal::ENQUIRIES_URL,
enquiry_params.merge(type: :talk_to_sales).to_json,
'Content-Type' => 'application/json')
end
head :ok
end
private
def enquiry_params
params.require(:user).permit(:email)
end
end
+2 -2
View File
@@ -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'
+13
View File
@@ -0,0 +1,13 @@
# frozen_string_literal: true
class PasswordsController < Devise::PasswordsController
class Current < ActiveSupport::CurrentAttributes
attribute :user
end
def update
super do |resource|
Current.user = resource
end
end
end
@@ -0,0 +1,52 @@
# frozen_string_literal: true
class PreviewDocumentPageController < ActionController::API
include ActiveStorage::SetCurrent
FORMAT = Templates::ProcessDocument::FORMAT
def show
if Docuseal.multitenant?
Rollbar.warning('load page')
return head :not_found
end
attachment = ActiveStorage::Attachment.find_by(uuid: params[:attachment_uuid])
return head :not_found unless attachment
preview_image = attachment.preview_images.joins(:blob).find_by(blob: { filename: "#{params[:id]}#{FORMAT}" })
return redirect_to preview_image.url, allow_other_host: true if preview_image
file_path =
if attachment.service.name == :disk
ActiveStorage::Blob.service.path_for(attachment.key)
else
find_or_create_document_tempfile_path(attachment)
end
io = Templates::ProcessDocument.generate_pdf_preview_from_file(attachment, file_path, params[:id].to_i)
render plain: io.tap(&:rewind).read
end
def find_or_create_document_tempfile_path(attachment)
file_path = "#{Dir.tmpdir}/#{attachment.uuid}"
File.open(file_path, File::RDWR | File::CREAT, 0o644) do |f|
f.flock(File::LOCK_EX)
# rubocop:disable Style/ZeroLengthPredicate
if f.size.zero?
f.binmode
f.write(attachment.download)
end
# rubocop:enable Style/ZeroLengthPredicate
end
file_path
end
end
+4 -4
View File
@@ -31,10 +31,10 @@ class RegistrationsController < Devise::RegistrationsController
redirect_to after_sign_up_path_for(current_user), allow_other_host: true
end
def require_no_authentication
super
def set_flash_message(key, kind, options = {})
return if key == :alert && kind == 'already_authenticated'
flash.clear
super
end
def build_resource(_hash = {})
@@ -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
+3 -3
View File
@@ -32,9 +32,9 @@ class SessionsController < Devise::SessionsController
devise_parameter_sanitizer.permit(:sign_in, keys: [:otp_attempt])
end
def require_no_authentication
super
def set_flash_message(key, kind, options = {})
return if key == :alert && kind == 'already_authenticated'
flash.clear
super
end
end
+3 -2
View File
@@ -14,7 +14,8 @@ class StartFormController < ApplicationController
def update
@submitter = Submitter.where(submission: @template.submissions.where(deleted_at: nil))
.find_or_initialize_by(email: submitter_params[:email])
.then { |rel| params[:resubmit].present? ? rel.where(completed_at: nil) : rel }
.find_or_initialize_by(**submitter_params.compact_blank)
if @submitter.completed_at?
redirect_to start_form_completed_path(@template.slug, email: submitter_params[:email])
@@ -46,7 +47,7 @@ class StartFormController < ApplicationController
private
def submitter_params
params.require(:submitter).permit(:email).tap do |attrs|
params.require(:submitter).permit(:email, :phone).tap do |attrs|
attrs[:email] = Submissions.normalize_email(attrs[:email])
end
end
+13 -1
View File
@@ -6,12 +6,24 @@ class SubmissionsController < ApplicationController
load_and_authorize_resource :submission, only: %i[show destroy]
PRELOAD_ALL_PAGES_AMOUNT = 200
def show
ActiveRecord::Associations::Preloader.new(
records: [@submission],
associations: [:template, { template_schema_documents: [:blob, { preview_images_attachments: :blob }] }]
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 :show, layout: 'plain'
end
+18 -5
View File
@@ -6,15 +6,28 @@ class SubmitFormController < ApplicationController
skip_before_action :authenticate_user!
skip_authorization_check
PRELOAD_ALL_PAGES_AMOUNT = 200
def show
@submitter =
Submitter.preload(submission: [
:template, { template_schema_documents: [:blob, { preview_images_attachments: :blob }] }
])
.find_by!(slug: params[:slug])
@submitter = Submitter.find_by!(slug: params[:slug])
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
ActiveRecord::Associations::Preloader.new(
records: [@submitter],
associations: [submission: [:template, { template_schema_documents: :blob }]]
).call
total_pages =
@submitter.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: @submitter.submission.template_schema_documents,
associations: [:blob, { preview_images_attachments: :blob }]
).call
end
Submitters::MaybeUpdateDefaultValues.call(@submitter, current_user)
cookies[:submitter_sid] = @submitter.signed_id
+13
View File
@@ -10,6 +10,11 @@ class TemplatesController < ApplicationController
submissions = submissions.active if @template.deleted_at.blank?
submissions = Submissions.search(submissions, params[:q])
@base_submissions = submissions
submissions = submissions.pending if params[:status] == 'pending'
submissions = submissions.completed if params[:status] == 'completed'
@pagy, @submissions = pagy(submissions.preload(:submitters).order(id: :desc))
rescue ActiveRecord::RecordNotFound
redirect_to root_path
@@ -25,6 +30,14 @@ class TemplatesController < ApplicationController
associations: [schema_documents: { preview_images_attachments: :blob }]
).call
@template_data =
@template.as_json.merge(
documents: @template.schema_documents.as_json(
methods: [:metadata],
include: { preview_images: { methods: %i[url metadata filename] } }
)
).to_json
render :edit, layout: 'plain'
end
+4
View File
@@ -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;
}
+4 -3
View File
@@ -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>
+1 -1
View File
@@ -78,7 +78,7 @@
>
{{ t('signed_with') }}
<a
href="https://www.docuseal.co"
href="https://www.docuseal.co/start"
target="_blank"
class="underline"
>DocuSeal</a> - {{ t('open_source_documents_software') }}
+74 -43
View File
@@ -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
},
@@ -422,7 +425,7 @@ export default {
allowToSkip: {
type: Boolean,
required: false,
default: true
default: false
},
goToLast: {
type: Boolean,
@@ -454,6 +457,7 @@ export default {
return {
isCompleted: false,
isFormVisible: true,
showFillAllRequiredFields: false,
currentStep: 0,
isSubmitting: false,
submittedValues: {},
@@ -484,7 +488,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 +515,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 +619,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 +635,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({})
@@ -643,8 +665,9 @@ export default {
})
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 +682,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
}
+36
View File
@@ -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>
+19 -1
View File
@@ -56,8 +56,26 @@ export default {
}
},
computed: {
numberOfPages () {
return this.document.metadata?.pdf?.number_of_pages || this.document.preview_images.length
},
sortedPreviewImages () {
return [...this.document.preview_images].sort((a, b) => parseInt(a.filename) - parseInt(b.filename))
const lazyloadMetadata = this.document.preview_images[this.document.preview_images.length - 1].metadata
return [...Array(this.numberOfPages).keys()].map((i) => {
return this.previewImagesIndex[i] || {
metadata: lazyloadMetadata,
id: Math.random().toString(),
url: `/preview/${this.document.uuid}/${i}.jpg`
}
})
},
previewImagesIndex () {
return this.document.preview_images.reduce((acc, e) => {
acc[parseInt(e.filename)] = e
return acc
}, {})
}
},
beforeUpdate () {
+7 -2
View File
@@ -5,11 +5,12 @@
>
<img
ref="image"
loading="lazy"
:src="image.url"
:width="width"
class="border rounded mb-4"
:height="height"
loading="lazy"
class="border rounded mb-4"
@load="onImageLoad"
>
<div
class="top-0 bottom-0 left-0 right-0 absolute"
@@ -117,6 +118,10 @@ export default {
this.areaRefs = []
},
methods: {
onImageLoad (e) {
e.target.setAttribute('width', e.target.naturalWidth)
e.target.setAttribute('height', e.target.naturalHeight)
},
setAreaRefs (el) {
if (el) {
this.areaRefs.push(el)
+1 -1
View File
@@ -114,7 +114,7 @@ export default {
emits: ['scroll-to', 'change', 'remove', 'up', 'down', 'replace'],
computed: {
previewImage () {
return this.document.preview_images[0]
return [...this.document.preview_images].sort((a, b) => parseInt(a.filename) - parseInt(b.filename))[0]
}
},
mounted () {
+20 -6
View File
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class SubmitterMailer < ApplicationMailer
MAX_ATTACHMENTS_SIZE = 10.megabytes
def invitation_email(submitter, body: nil, subject: nil)
@current_account = submitter.submission.template.account
@submitter = submitter
@@ -12,7 +14,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 +64,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),
@@ -75,14 +77,26 @@ class SubmitterMailer < ApplicationMailer
def add_completed_email_attachments!(submitter)
documents = Submitters.select_attachments_for_download(submitter)
documents.each do |attachment|
attachments[attachment.filename.to_s] = attachment.download
end
total_size = 0
audit_trail_data = nil
if submitter.submission.audit_trail.present?
attachments[submitter.submission.audit_trail.filename.to_s] = submitter.submission.audit_trail.download
audit_trail_data = submitter.submission.audit_trail.download
total_size = audit_trail_data.size
end
documents.each do |attachment|
data = attachment.download
total_size += data.size
break if total_size >= MAX_ATTACHMENTS_SIZE
attachments[attachment.filename.to_s] = data
end
attachments[submitter.submission.audit_trail.filename.to_s] = audit_trail_data if audit_trail_data
documents
end
+3 -3
View File
@@ -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
+4 -4
View File
@@ -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}}'
+29
View File
@@ -0,0 +1,29 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: encrypted_user_configs
#
# id :bigint not null, primary key
# key :string not null
# value :text not null
# created_at :datetime not null
# updated_at :datetime not null
# user_id :bigint not null
#
# Indexes
#
# index_encrypted_user_configs_on_user_id (user_id)
# index_encrypted_user_configs_on_user_id_and_key (user_id,key) UNIQUE
#
# Foreign Keys
#
# fk_rails_... (user_id => users.id)
#
class EncryptedUserConfig < ApplicationRecord
belongs_to :user
encrypts :value
serialize :value, JSON
end
+2
View File
@@ -48,6 +48,8 @@ class Submission < ApplicationRecord
through: :template, source: :documents_attachments
scope :active, -> { where(deleted_at: nil) }
scope :pending, -> { joins(:submitters).where(submitters: { completed_at: nil }).distinct }
scope :completed, -> { left_joins(:submitters).where.not(submitters: { completed_at: nil }).distinct }
enum :source, {
invite: 'invite',
+16 -15
View File
@@ -4,21 +4,22 @@
#
# Table name: submitters
#
# id :bigint not null, primary key
# completed_at :datetime
# email :string
# ip :string
# name :string
# opened_at :datetime
# phone :string
# sent_at :datetime
# slug :string not null
# ua :string
# uuid :string not null
# values :text not null
# created_at :datetime not null
# updated_at :datetime not null
# submission_id :bigint not null
# id :bigint not null, primary key
# application_key :string
# completed_at :datetime
# email :string
# ip :string
# name :string
# opened_at :datetime
# phone :string
# sent_at :datetime
# slug :string not null
# ua :string
# uuid :string not null
# values :text not null
# created_at :datetime not null
# updated_at :datetime not null
# submission_id :bigint not null
#
# Indexes
#
+9
View File
@@ -53,6 +53,7 @@ class User < ApplicationRecord
has_one :access_token, dependent: :destroy
has_many :templates, dependent: :destroy, foreign_key: :author_id, inverse_of: :author
has_many :user_configs, dependent: :destroy
has_many :encrypted_configs, dependent: :destroy, class_name: 'EncryptedUserConfig'
devise :two_factor_authenticatable, :recoverable, :rememberable, :validatable, :trackable
devise :registerable, :omniauthable, omniauth_providers: [:google_oauth2] if Docuseal.multitenant?
@@ -75,6 +76,14 @@ class User < ApplicationRecord
true
end
def self.sign_in_after_reset_password
if PasswordsController::Current.user.present?
!PasswordsController::Current.user.otp_required_for_login
else
true
end
end
def initials
[first_name&.first, last_name&.first].compact_blank.join.upcase
end
+1 -1
View File
@@ -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>
-1
View File
@@ -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>
-1
View File
@@ -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>
@@ -41,6 +41,13 @@
</div>
<div class="form-control">
<%= f.button button_title(title: 'Sign up', disabled_with: 'Signing up'), class: 'base-button' %>
<% if Docuseal.multitenant? %>
<button id="talk_to_sales_button" class="hidden" type="submit" formaction="<%= enquiries_path %>" formnovalidate="true" formmethod="post"></button>
<label class="flex items-center cursor-pointer">
<input type="checkbox" name="talk_to_sales" class="base-checkbox" onchange="window.talk_to_sales_button.click()">
<span class="label">I would like to talk to sales</span>
</label>
<% end %>
</div>
<% end %>
<%= render 'devise/shared/links' %>
+3 -3
View File
@@ -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>
+2 -2
View File
@@ -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 -7
View File
@@ -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>
+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" 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="M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0" />
<path d="M12 7v5l3 3" />
</svg>

After

Width:  |  Height:  |  Size: 362 B

+9
View File
@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" 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 6l11 0" />
<path d="M9 12l11 0" />
<path d="M9 18l11 0" />
<path d="M5 6l0 .01" />
<path d="M5 12l0 .01" />
<path d="M5 18l0 .01" />
</svg>

After

Width:  |  Height:  |  Size: 440 B

+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="2" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M19.933 13.041a8 8 0 1 1 -9.925 -8.788c3.899 -1 7.935 1.007 9.425 4.747"></path>
<path d="M20 4v5h-5"></path>
</svg>

After

Width:  |  Height:  |  Size: 409 B

+1 -1
View File
@@ -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: %>
+1
View File
@@ -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>
Whitespace-only changes.
+1
View File
@@ -21,6 +21,7 @@
<%= f.button button_title(title: 'Update', disabled_with: 'Updating'), class: 'base-button' %>
</div>
<% end %>
<%= render 'email_configs' %>
<p class="text-2xl font-bold mt-8 mb-4">Signature</p>
<% signature = UserConfigs.load_signature(current_user) %>
<% if signature %>
@@ -10,4 +10,4 @@
</div>
</div>
</div>
<%= render 'shared/attribution' %>
<%= render 'shared/attribution', link_path: '/start' %>
+1 -1
View File
@@ -1 +1 @@
<%= render 'shared/powered_by', with_counter: local_assigns[:with_counter] %>
<%= render 'shared/powered_by', with_counter: local_assigns[:with_counter], link_path: local_assigns[:link_path] %>
+1 -2
View File
@@ -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>
+1 -1
View File
@@ -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 %>/start"><%= Docuseal.product_name %></a> free document signing.
</p>
+18 -12
View File
@@ -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 %>
+9
View File
@@ -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>
+1 -1
View File
@@ -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 %><%= local_assigns[:link_path] %>" class="underline"><%= Docuseal.product_name %></a> - open source documents software
</div>
+9 -6
View File
@@ -1,11 +1,14 @@
<form action="<%= request.path %>" method="get" class="items-center hidden md:flex">
<div class="relative">
<% if params[:q].present? %>
<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">
<form action="<%= url_for %>" method="get" class="items-center hidden md:flex">
<% if params[:status].present? %>
<input name="status" value="<%= params[:status] %>" class="hidden">
<% end %>
<% if params[:q].present? %>
<div class="relative">
<a href="<%= url_for(params.to_unsafe_h.except(:q)) %>" title="Clear" class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-auto text-neutral text-2xl font-extralight">
&times;
</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">
+7
View File
@@ -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>
+4 -1
View File
@@ -24,6 +24,9 @@
<%= button_to button_title(title: 'Send copy to Email', disabled_with: 'Sending', icon: svg_icon('mail_forward', class: 'w-6 h-6')), send_submission_email_index_path, params: { submitter_slug: @submitter.slug }, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'base-button w-full' %>
</div>
<% end %>
<div>
<%= button_to button_title(title: 'Resubmit', disabled_with: 'Resubmit', icon: svg_icon('reload', class: 'w-6 h-6')), start_form_path(@template.slug), params: { submitter: { email: params[:email] }, resubmit: true }, method: :put, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'white-button w-full' %>
</div>
</div>
</div>
<%= render 'shared/attribution' %>
<%= render 'shared/attribution', link_path: '/start' %>
+2 -2
View File
@@ -24,7 +24,7 @@
</div>
</div>
<% unless @template.deleted_at? %>
<%= form_for @submitter, url: start_form_path(@template.slug), data: { turbo_frame: :_top }, method: :put, html: { class: 'space-y-4' } do |f| %>
<%= form_for @submitter, url: start_form_path(@template.slug), data: { turbo_frame: :_top }, method: :put, html: { class: 'space-y-4', onsubmit: 'event.submitter.disabled = true' } do |f| %>
<div class="form-control !mt-0">
<%= f.label :email, class: 'label' %>
<%= f.email_field :email, value: current_user&.email, required: true, class: 'base-input', placeholder: 'Provide your email to start' %>
@@ -36,4 +36,4 @@
<% end %>
</div>
</div>
<%= render 'shared/attribution' %>
<%= render 'shared/attribution', link_path: '/start' %>
Whitespace-only changes.
@@ -41,6 +41,7 @@
<%= f.text_area :body, value: config.value['body'], required: true, class: 'base-textarea w-full', rows: 10 %>
</autoresize-textarea>
</div>
<%= render 'message_fields' %>
</div>
</div>
</div>
+1 -3
View File
@@ -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
View File
@@ -23,4 +23,5 @@
<%= render 'detailed_form', template: @template %>
</div>
</div>
<%= content_for(:modal_extra) %>
<% end %>
+6 -2
View File
@@ -44,12 +44,16 @@
<% fields_index = Templates.build_field_areas_index(@submission.template_fields || @submission.template.fields) %>
<% values = @submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) } %>
<% attachments_index = ActiveStorage::Attachment.where(record: @submission.submitters, name: :attachments).preload(:blob).index_by(&:uuid) %>
<% page_blob_struct = Struct.new(:url, :metadata, keyword_init: true) %>
<% (@submission.template_schema || @submission.template.schema).each do |item| %>
<% document = @submission.template_schema_documents.find { |e| e.uuid == item['attachment_uuid'] } %>
<% document_annots_index = document.metadata.dig('pdf', 'annotations')&.group_by { |e| e['page'] } || {} %>
<% document.preview_images.sort_by { |a| a.filename.base.to_i }.each_with_index do |page, index| %>
<% preview_images_index = document.preview_images.loaded? ? document.preview_images.index_by { |e| e.filename.base.to_i } : {} %>
<% lazyload_metadata = document.preview_images.first.metadata %>
<% (document.metadata.dig('pdf', 'number_of_pages') || (document.preview_images.loaded? ? preview_images_index.size : document.preview_images.size)).times do |index| %>
<% page = preview_images_index[index] || page_blob_struct.new(metadata: lazyload_metadata, url: preview_document_page_path(document.uuid, "#{index}.jpg")) %>
<div id="<%= "page-#{document.uuid}-#{index}" %>" class="relative">
<img src="<%= page.url %>" width="<%= page.metadata['width'] %>" class="shadow-md mb-4" height="<%= page.metadata['height'] %>" loading="lazy">
<img loading="lazy" src="<%= page.url %>" width="<%= page.metadata['width'] %>" class="shadow-md mb-4" height="<%= page.metadata['height'] %>">
<div class="top-0 bottom-0 left-0 right-0 absolute">
<% document_annots_index[index]&.each do |annot| %>
<%= render 'submissions/annotation', annot: %>
@@ -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 -1
View File
@@ -18,4 +18,4 @@
</div>
</div>
</div>
<%= render 'shared/attribution' %>
<%= render 'shared/attribution', link_path: '/start' %>
+12 -2
View File
@@ -19,7 +19,11 @@
<div>
<% if Accounts.can_send_emails?(@submitter.submission.template.account) && @submitter.email.present? %>
<%= button_to button_title(title: 'Send copy to Email', disabled_with: 'Sending', icon: svg_icon('mail_forward', class: 'w-6 h-6')), send_submission_email_index_path, params: { submitter_slug: @submitter.slug }, form: { onsubmit: 'event.submitter.disabled = true' }, class: 'white-button w-full' %>
<div class="divider">OR</div>
<% if @submitter.submission.template.submitters.size != 1 %>
<div class="divider">OR</div>
<% else %>
<div class="py-2"></div>
<% end %>
<% end %>
<download-button data-src="<%= submitter_download_index_path(@submitter.slug) %>" class="base-button w-full">
<span class="flex items-center justify-center space-x-2" data-target="download-button.defaultButton">
@@ -32,6 +36,12 @@
</span>
</download-button>
</div>
<% 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' %>
</div>
<% end %>
</div>
</div>
<%= render 'shared/attribution' %>
<%= render 'shared/attribution', link_path: '/start' %>
+9 -4
View File
@@ -1,6 +1,7 @@
<% 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) %>
<% page_blob_struct = Struct.new(:url, :metadata, keyword_init: true) %>
<div style="max-height: -webkit-fill-available;">
<div id="scrollbox">
<div class="mx-auto block pb-72" style="max-width: 1000px">
@@ -10,9 +11,12 @@
<% (@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'] } || {} %>
<% document.preview_images.sort_by { |a| a.filename.base.to_i }.each_with_index do |page, index| %>
<% preview_images_index = document.preview_images.loaded? ? document.preview_images.index_by { |e| e.filename.base.to_i } : {} %>
<% lazyload_metadata = document.preview_images.last.metadata %>
<% (document.metadata.dig('pdf', 'number_of_pages') || (document.preview_images.loaded? ? preview_images_index.size : document.preview_images.size)).times do |index| %>
<% page = preview_images_index[index] || page_blob_struct.new(metadata: lazyload_metadata, url: preview_document_page_path(document.uuid, "#{index}.jpg")) %>
<div class="relative my-4 shadow-md">
<img src="<%= page.url %>" width="<%= page.metadata['width'] %>" height="<%= page.metadata['height'] %>" loading="lazy">
<img loading="lazy" src="<%= page.url %>" width="<%= page.metadata['width'] %>" height="<%= page.metadata['height'] %>">
<div id="page-<%= [document.uuid, index].join('-') %>" class="top-0 bottom-0 left-0 right-0 absolute">
<% document_annots_index[index]&.each do |annot| %>
<%= render 'submissions/annotation', annot: %>
@@ -20,13 +24,14 @@
<% 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>
</div>
<% end %>
<% end %>
<%= render 'shared/attribution' %>
<%= render 'shared/attribution', link_path: '/start' %>
</div>
</div>
</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 -1
View File
@@ -1 +1 @@
<template-builder class="grid" data-is-direct-upload="<%= Docuseal.active_storage_public? %>" data-template="<%= @template.as_json.merge(documents: @template.schema_documents.as_json(include: { preview_images: { methods: %i[url metadata filename] } })).to_json %>"></template-builder>
<template-builder class="grid" data-is-direct-upload="<%= Docuseal.active_storage_public? %>" data-template="<%= @template_data %>"></template-builder>
+35 -3
View File
@@ -1,9 +1,10 @@
<%= render 'title', template: @template %>
<% is_show_tabs = @pagy.pages > 1 || params[:status].present? %>
<% if !@pagy.count.zero? || @template.submitters.to_a.size == 1 || params[:q].present? %>
<div class="flex justify-between mb-6 md:items-end flex-col md:flex-row">
<div class="flex justify-between md:items-end flex-col md:flex-row <%= is_show_tabs ? 'mb-4' : 'mb-6' %>">
<p class="text-3xl font-bold">Submissions</p>
<div class="flex space-x-2 mt-3 md:mt-0">
<% if params[:q].present? || @pagy.pages > 1 %>
<div class="flex md:space-x-2 mt-3 md:mt-0">
<% if params[:q].present? || params[:status].present? || @pagy.pages > 1 %>
<%= render 'shared/search_input' %>
<% end %>
<%= link_to new_template_submissions_export_path(@template), class: 'order-3 md:order-1 btn btn-ghost text-base', data: { turbo_frame: 'modal' } do %>
@@ -24,6 +25,37 @@
</div>
</div>
<% end %>
<% if is_show_tabs %>
<div class="flex items-center md:items-end flex-col space-y-2 md:space-y-0 md:flex-row md:space-x-2 mb-4">
<a href="<%= url_for(params.to_unsafe_h.except(:status)) %>" class="<%= params[:status].blank? ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-600">
<div class="flex items-center space-x-1">
<%= svg_icon('list', class: 'w-5 h-5') %>
<span class="font-normal">All</span>
</div>
<div class="badge badge-neutral badge-outline font-medium">
<%= params[:status].blank? ? @pagy.count : @base_submissions.count %>
</div>
</a>
<a href="<%= url_for(params.to_unsafe_h.merge(status: :pending)) %>" class="<%= params[:status] == 'pending' ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-600">
<div class="flex items-center space-x-1">
<%= svg_icon('clock', class: 'w-5 h-5') %>
<span class="font-normal">Pending</span>
</div>
<div class="badge badge-neutral badge-outline font-medium">
<%= params[:status] == 'pending' ? @pagy.count : @base_submissions.pending.count %>
</div>
</a>
<a href="<%= url_for(params.to_unsafe_h.merge(status: :completed)) %>" class="<%= params[:status] == 'completed' ? 'border-neutral-700' : 'border-neutral-300' %> flex h-10 px-2 py-1 text-lg items-center justify-between border text-center text-neutral font-semibold rounded-xl w-full md:w-48 hover:border-neutral-600">
<div class="flex items-center space-x-1">
<%= svg_icon('circle_check', class: 'w-5 h-5') %>
<span class="font-normal">Completed</span>
</div>
<div class="badge badge-neutral badge-outline font-medium">
<%= params[:status] == 'completed' ? @pagy.count : @base_submissions.completed.count %>
</div>
</a>
</div>
<% end %>
<% if @submissions.present? %>
<div class="space-y-4">
<%= render partial: 'submission', collection: @submissions %>
@@ -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>
+4 -2
View File
@@ -9,7 +9,7 @@ Rails.application.routes.draw do
devise_for :users,
path: '/', only: %i[sessions passwords omniauth_callbacks],
controllers: begin
options = { sessions: 'sessions' }
options = { sessions: 'sessions', passwords: 'passwords' }
options[:omniauth_callbacks] = 'omniauth_callbacks' if Docuseal.multitenant?
options
end
@@ -35,7 +35,7 @@ Rails.application.routes.draw do
resources :template_folders_autocomplete, only: %i[index]
resources :submitter_email_clicks, only: %i[create]
resources :submitter_form_views, only: %i[create]
resources :submitters, only: %i[show]
resources :submitters, only: %i[index show]
resources :submissions, only: %i[index show create destroy] do
collection do
resources :emails, only: %i[create], controller: 'submissions', as: :submissions_emails
@@ -52,6 +52,7 @@ Rails.application.routes.draw do
resources :dashboard, only: %i[index]
resources :setup, only: %i[index create]
resource :newsletter, only: %i[show update]
resources :enquiries, only: %i[create]
resources :users, only: %i[new create edit update destroy]
resource :user_signature, only: %i[edit update]
resources :submissions, only: %i[show destroy]
@@ -66,6 +67,7 @@ Rails.application.routes.draw do
resource :folder, only: %i[edit update], controller: 'templates_folders'
resources :submissions_export, only: %i[index new]
end
resources :preview_document_page, only: %i[show], path: '/preview/:attachment_uuid'
resources :start_form, only: %i[show update], path: 'd', param: 'slug' do
get :completed
@@ -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
@@ -0,0 +1,15 @@
# frozen_string_literal: true
class CreateEncryptedUserConfigs < ActiveRecord::Migration[7.0]
def change
create_table :encrypted_user_configs do |t|
t.references :user, null: false, foreign_key: true, index: true
t.string :key, null: false
t.text :value, null: false
t.index %i[user_id key], unique: true
t.timestamps
end
end
end
@@ -0,0 +1,7 @@
# frozen_string_literal: true
class AddApplicationIdToSubmitters < ActiveRecord::Migration[7.0]
def change
add_column :submitters, :application_key, :string
end
end
+13 -1
View File
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.0].define(version: 2023_10_07_052818) do
ActiveRecord::Schema[7.0].define(version: 2023_10_17_213639) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -91,6 +91,16 @@ ActiveRecord::Schema[7.0].define(version: 2023_10_07_052818) do
t.index ["account_id"], name: "index_encrypted_configs_on_account_id"
end
create_table "encrypted_user_configs", force: :cascade do |t|
t.bigint "user_id", null: false
t.string "key", null: false
t.text "value", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["user_id", "key"], name: "index_encrypted_user_configs_on_user_id_and_key", unique: true
t.index ["user_id"], name: "index_encrypted_user_configs_on_user_id"
end
create_table "submission_events", force: :cascade do |t|
t.bigint "submission_id", null: false
t.bigint "submitter_id"
@@ -133,6 +143,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_10_07_052818) do
t.datetime "updated_at", null: false
t.string "name"
t.string "phone"
t.string "application_key"
t.index ["email"], name: "index_submitters_on_email"
t.index ["slug"], name: "index_submitters_on_slug", unique: true
t.index ["submission_id"], name: "index_submitters_on_submission_id"
@@ -216,6 +227,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_10_07_052818) do
add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
add_foreign_key "document_generation_events", "submitters"
add_foreign_key "encrypted_configs", "accounts"
add_foreign_key "encrypted_user_configs", "users"
add_foreign_key "submission_events", "submissions"
add_foreign_key "submission_events", "submitters"
add_foreign_key "submissions", "templates"
+1
View File
@@ -10,6 +10,7 @@ class Ability
can :manage, Submitter, template: { account_id: user.account_id }
can :manage, User, account_id: user.account_id
can :manage, EncryptedConfig, account_id: user.account_id
can :manage, EncryptedUserConfig, user_id: user.id
can :manage, AccountConfig, account_id: user.account_id
can :manage, UserConfig, user_id: user.id
can :manage, Account, id: user.account_id
+3
View File
@@ -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
+1
View File
@@ -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',
+5
View File
@@ -3,6 +3,7 @@
module Docuseal
PRODUCT_URL = 'https://www.docuseal.co'
NEWSLETTER_URL = "#{PRODUCT_URL}/newsletters".freeze
ENQUIRIES_URL = "#{PRODUCT_URL}/enquiries".freeze
PRODUCT_NAME = 'DocuSeal'
DEFAULT_APP_URL = 'http://localhost:3000'
GITHUB_URL = 'https://github.com/docusealco/docuseal'
@@ -58,6 +59,10 @@ module Docuseal
end
end
def product_name
PRODUCT_NAME
end
def refresh_default_url_options!
@default_url_options = nil
end
+1 -2
View File
@@ -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)
+64 -4
View File
@@ -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])
@@ -33,6 +92,7 @@ module Submissions
email:,
phone: attrs[:phone].to_s.gsub(/[^0-9+]/, ''),
name: attrs[:name],
application_key: attrs[:application_key],
completed_at: attrs[:completed] ? Time.current : nil,
sent_at: mark_as_sent && email.present? && is_order_sent ? Time.current : nil,
values: attrs[:values] || {},
+1 -1
View File
@@ -4,7 +4,7 @@ module Submissions
module EnsureResultGenerated
WAIT_FOR_RETRY = 2.seconds
CHECK_EVENT_INTERVAL = 1.second
CHECK_COMPLETE_TIMEOUT = 20.seconds
CHECK_COMPLETE_TIMEOUT = 90.seconds
WaitForCompleteTimeout = Class.new(StandardError)
+14 -10
View File
@@ -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
+13 -5
View File
@@ -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
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -18,7 +18,7 @@ module Submitters
serialize_params = {
include: {},
only: %i[id slug uuid name email phone completed_at
only: %i[id slug uuid name email phone completed_at application_key
opened_at sent_at created_at updated_at]
}
+2 -2
View File
@@ -24,8 +24,8 @@ module Submitters
end
def build_values_array(submitter)
fields_index = (submitter.submission.template_fields ||
submitter.submission.template.fields).index_by { |e| e['uuid'] }
fields_index = (submitter.submission.template.fields +
submitter.submission.template_fields.to_a).index_by { |e| e['uuid'] }
attachments_index = submitter.attachments.index_by(&:uuid)
submitter_field_counters = Hash.new { 0 }
+9 -7
View File
@@ -13,13 +13,15 @@ module Templates
record: template
)
document.preview_images_attachments.each do |preview_image|
ActiveStorage::Attachment.create!(
uuid: preview_image.uuid,
blob_id: preview_image.blob_id,
name: 'preview_images',
record: new_document
)
ApplicationRecord.no_touching do
document.preview_images_attachments.each do |preview_image|
ActiveStorage::Attachment.create!(
uuid: preview_image.uuid,
blob_id: preview_image.blob_id,
name: 'preview_images',
record: new_document
)
end
end
end
end
+7 -2
View File
@@ -3,6 +3,7 @@
module Templates
module CreateAttachments
PDF_CONTENT_TYPE = 'application/pdf'
ANNOTATIONS_SIZE_LIMIT = 6.megabytes
InvalidFileType = Class.new(StandardError)
module_function
@@ -18,7 +19,10 @@ module Templates
document = template.documents.create!(blob:)
if blob.content_type == PDF_CONTENT_TYPE && blob.metadata['pdf'].nil?
blob.metadata['pdf'] = { 'annotations' => Templates::BuildAnnotations.call(document_data) }
annotations =
document_data.size > ANNOTATIONS_SIZE_LIMIT ? [] : Templates::BuildAnnotations.call(document_data)
blob.metadata['pdf'] = { 'annotations' => annotations }
blob.metadata['sha256'] = Base64.urlsafe_encode64(Digest::SHA256.digest(document_data))
end
@@ -37,9 +41,10 @@ module Templates
data = file.read
if file.content_type == PDF_CONTENT_TYPE
annotations = data.size > ANNOTATIONS_SIZE_LIMIT ? [] : Templates::BuildAnnotations.call(data)
metadata = { 'identified' => true, 'analyzed' => true,
'sha256' => Base64.urlsafe_encode64(Digest::SHA256.digest(data)),
'pdf' => { 'annotations' => Templates::BuildAnnotations.call(data) } }
'pdf' => { 'annotations' => annotations } }
end
ActiveStorage::Blob.create_and_upload!(
+34 -2
View File
@@ -9,6 +9,12 @@ module Templates
PDF_CONTENT_TYPE = 'application/pdf'
Q = 35
MAX_WIDTH = 1400
MAX_NUMBER_OF_PAGES_PROCESSED =
if Docuseal.multitenant?
70
else
40
end
module_function
@@ -42,14 +48,38 @@ module Templates
def generate_pdf_preview_images(attachment, data)
ActiveStorage::Attachment.where(name: ATTACHMENT_NAME, record: attachment).destroy_all
number_of_pages = HexaPDF::Document.new(io: StringIO.new(data)).pages.size - 1
number_of_pages = HexaPDF::Document.new(io: StringIO.new(data)).pages.size
(0..number_of_pages).each do |page_number|
(attachment.metadata['pdf'] ||= {})[:number_of_pages] = number_of_pages
attachment.save!
(0..[number_of_pages - 1, MAX_NUMBER_OF_PAGES_PROCESSED].min).each do |page_number|
page = Vips::Image.new_from_buffer(data, '', dpi: DPI, page: page_number)
page = page.resize(MAX_WIDTH / page.width.to_f)
io = StringIO.new(page.write_to_buffer(FORMAT, Q: Q, interlace: true))
ApplicationRecord.no_touching do
ActiveStorage::Attachment.create!(
blob: ActiveStorage::Blob.create_and_upload!(
io:, filename: "#{page_number}#{FORMAT}",
metadata: { analyzed: true, identified: true, width: page.width, height: page.height }
),
name: ATTACHMENT_NAME,
record: attachment
)
end
end
end
def generate_pdf_preview_from_file(attachment, file_path, page_number)
page = Vips::Image.new_from_file(file_path, dpi: DPI, page: page_number)
page = page.resize(MAX_WIDTH / page.width.to_f)
io = StringIO.new(page.write_to_buffer(FORMAT, Q: Q, interlace: true))
ApplicationRecord.no_touching do
ActiveStorage::Attachment.create!(
blob: ActiveStorage::Blob.create_and_upload!(
io:, filename: "#{page_number}#{FORMAT}",
@@ -59,6 +89,8 @@ module Templates
record: attachment
)
end
io
end
end
end