Compare commits

...

27 Commits

Author SHA1 Message Date
Alex Turchyn 22afd01fd2 Merge from docusealco/wip 2025-11-17 13:20:44 +02:00
Pete Matsyburka fd9eb09dd6 use can? 2025-11-17 10:21:38 +02:00
Alex Turchyn c53ed7f984 add custom invitation reminder emails 2025-11-17 10:05:52 +02:00
Pete Matsyburka 1d87e210be optimize detect fields 2025-11-16 19:05:54 +02:00
Pete Matsyburka 5fec952b8c adjust detections 2025-11-15 19:48:42 +02:00
Pete Matsyburka c4fbcc103a autocorrect pdf 2025-11-15 10:46:54 +02:00
Pete Matsyburka 9333a1ba45 fix fields error alert 2025-11-14 16:35:02 +02:00
Pete Matsyburka de5cd92dba adjust send email 2025-11-13 19:36:06 +02:00
Pete Matsyburka 83e32bec9f fix preferences form 2025-11-13 10:34:52 +02:00
Pete Matsyburka 4603efc04e refactor toggles form 2025-11-13 09:50:26 +02:00
Alex Turchyn 6c73c82a27 add ability to reset custom template emails to default 2025-11-13 08:56:09 +02:00
Pete Matsyburka 8e71d9f9e9 skip email 2025-11-13 08:29:27 +02:00
Pete Matsyburka 80f60a5d88 re-invite archived user 2025-11-12 20:33:52 +02:00
Pete Matsyburka 2c33ec382d fix device icon 2025-11-12 16:48:33 +02:00
Pete Matsyburka 884865cffc adjust size 2025-11-12 16:39:29 +02:00
Alex Turchyn 9537fe8c91 update constants 2025-11-12 16:33:51 +02:00
Alex Turchyn c8fe36a2b6 add desktop/mobile/tablet badges to events log 2025-11-12 16:33:51 +02:00
Pete Matsyburka 7ec3282b56 i18n 2025-11-11 17:34:50 +02:00
Pete Matsyburka ab8c41a520 add signature reasons param 2025-11-11 13:42:01 +02:00
Pete Matsyburka 4c1c70ff04 fix audit generation 2025-11-11 12:08:16 +02:00
Pete Matsyburka d7a6e80bb1 html escape font signature 2025-11-11 11:11:07 +02:00
Alex Turchyn f87ef670d1 Merge from docusealco/wip 2025-11-10 13:08:28 +02:00
Pete Matsyburka daba2505bc fix detection 2025-11-09 21:33:47 +02:00
Pete Matsyburka 71528eda45 map page nodes 2025-11-09 08:51:00 +02:00
Pete Matsyburka 269e2beeaa fix preview 2025-11-07 10:20:41 +02:00
Pete Matsyburka 72dd2c6bc5 add template download 2025-11-05 12:07:11 +02:00
Pete Matsyburka 9758f7a15f adjust detection 2025-11-04 16:17:13 +02:00
34 changed files with 1029 additions and 393 deletions
+3
View File
@@ -45,6 +45,9 @@ Metrics/PerceivedComplexity:
Style/MultipleComparison:
Enabled: false
Style/NumericPredicate:
Enabled: false
Naming/PredicateMethod:
Enabled: false
@@ -4,6 +4,7 @@ class PersonalizationSettingsController < ApplicationController
ALLOWED_KEYS = [
AccountConfig::FORM_COMPLETED_BUTTON_KEY,
AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY,
AccountConfig::SUBMITTER_INVITATION_REMINDER_EMAIL_KEY,
AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY,
AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY,
AccountConfig::FORM_COMPLETED_MESSAGE_KEY,
@@ -3,6 +3,12 @@
class TemplateDocumentsController < ApplicationController
load_and_authorize_resource :template
FILES_TTL = 5.minutes
def index
render json: @template.schema_documents.map { |d| ActiveStorage::Blob.proxy_url(d.blob, expires_at: FILES_TTL.from_now.to_i) }
end
def create
if params[:blobs].blank? && params[:files].blank?
return render json: { error: I18n.t('file_is_missing') }, status: :unprocessable_content
@@ -17,7 +17,7 @@ class TemplatesDebugController < ApplicationController
fields = Templates::FindAcroFields.call(pdf, attachment, data)
end
fields = Templates::DetectFields.call(StringIO.new(data), attachment:) if fields.blank?
fields, = Templates::DetectFields.call(StringIO.new(data), attachment:) if fields.blank?
attachment.metadata['pdf'] ||= {}
attachment.metadata['pdf']['fields'] = fields
@@ -3,6 +3,15 @@
class TemplatesPreferencesController < ApplicationController
load_and_authorize_resource :template
RESETTABLE_PREFERENCE_KEYS = {
AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY => %w[request_email_subject request_email_body submitters],
AccountConfig::SUBMITTER_INVITATION_REMINDER_EMAIL_KEY => %w[invitation_reminder_email_subject
invitation_reminder_email_body],
AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY => %w[documents_copy_email_subject documents_copy_email_body],
AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY => %w[completed_notification_email_subject
completed_notification_email_body]
}.freeze
def show; end
def create
@@ -15,19 +24,38 @@ class TemplatesPreferencesController < ApplicationController
head :ok
end
def destroy
authorize!(:update, @template)
config_key = params[:config_key]
preferences_to_delete = RESETTABLE_PREFERENCE_KEYS[config_key]
return head :ok if preferences_to_delete.blank?
preferences_to_delete.each do |key|
@template.preferences.delete(key)
end
@template.save!
render turbo_stream: turbo_stream.replace("#{config_key}_form",
partial: "templates_preferences/#{config_key}_form"),
status: :ok
end
private
def template_params
params.require(:template).permit(
preferences: %i[bcc_completed request_email_subject request_email_body
invitation_reminder_email_subject invitation_reminder_email_body
documents_copy_email_subject documents_copy_email_body
documents_copy_email_enabled documents_copy_email_attach_audit
documents_copy_email_attach_documents documents_copy_email_reply_to
completed_notification_email_attach_documents
completed_redirect_url validate_unique_submitters
require_all_submitters submitters_order require_phone_2fa
default_expire_at_duration shared_link_2fa
default_expire_at request_email_enabled
default_expire_at_duration shared_link_2fa default_expire_at request_email_enabled
completed_notification_email_subject completed_notification_email_body
completed_notification_email_enabled completed_notification_email_attach_audit] +
[completed_message: %i[title body],
+12 -3
View File
@@ -24,10 +24,19 @@ class UsersController < ApplicationController
def edit; end
def create
if User.accessible_by(current_ability).exists?(email: @user.email)
@user.errors.add(:email, I18n.t('already_exists'))
existing_user = User.accessible_by(current_ability).find_by(email: @user.email)
return render turbo_stream: turbo_stream.replace(:modal, template: 'users/new'), status: :unprocessable_content
if existing_user
if existing_user.archived_at? &&
current_ability.can?(:manage, existing_user) && current_ability.can?(:manage, @user.account)
existing_user.assign_attributes(@user.slice(:first_name, :last_name, :role, :account_id))
existing_user.archived_at = nil
@user = existing_user
else
@user.errors.add(:email, I18n.t('already_exists'))
return render turbo_stream: turbo_stream.replace(:modal, template: 'users/new'), status: :unprocessable_content
end
end
@user.password = SecureRandom.hex if @user.password.blank?
+1
View File
@@ -167,6 +167,7 @@ safeRegisterElement('template-builder', class extends HTMLElement {
withConditions: this.dataset.withConditions === 'true',
withGoogleDrive: this.dataset.withGoogleDrive === 'true',
withReplaceAndCloneUpload: true,
withDownload: true,
currencies: (this.dataset.currencies || '').split(',').filter(Boolean),
acceptFileTypes: this.dataset.acceptFileTypes,
showTourStartForm: this.dataset.showTourStartForm === 'true'
@@ -211,7 +211,7 @@
@input="updateWrittenSignature"
>
<select
v-if="requireSigningReason && !isOtherReason"
v-if="withSigningReason && !isOtherReason"
class="select base-input !text-2xl w-full mt-6 text-center"
:class="{ 'text-gray-300': !reason }"
required
@@ -226,24 +226,37 @@
>
{{ t('select_a_reason') }}
</option>
<option
v-for="(label, option) in defaultReasons"
:key="option"
:value="option"
:selected="reason === option"
class="text-base-content"
>
{{ label }}
</option>
<option
value="other"
class="text-base-content"
>
{{ t('other') }}
</option>
<template v-if="field.preferences?.reasons">
<option
v-for="option in field.preferences.reasons"
:key="option"
:value="option"
:selected="reason === option"
class="text-base-content"
>
{{ option }}
</option>
</template>
<template v-else>
<option
v-for="(label, option) in defaultReasons"
:key="option"
:value="option"
:selected="reason === option"
class="text-base-content"
>
{{ label }}
</option>
<option
value="other"
class="text-base-content"
>
{{ t('other') }}
</option>
</template>
</select>
<input
v-if="requireSigningReason && isOtherReason"
v-if="withSigningReason && isOtherReason"
class="base-input !text-2xl w-full mt-6"
required
:name="`values[${field.preferences.reason_field_uuid}]`"
@@ -253,7 +266,7 @@
@input="$emit('update:reason', $event.target.value)"
>
<input
v-if="requireSigningReason"
v-if="withSigningReason"
hidden
name="with_reason"
:value="field.preferences.reason_field_uuid"
@@ -406,6 +419,9 @@ export default {
format () {
return this.field.preferences?.format
},
withSigningReason () {
return this.requireSigningReason || this.field.preferences?.reasons?.length
},
defaultReasons () {
return {
[this.t('approved_by')]: this.t('approved'),
@@ -424,10 +440,11 @@ export default {
created () {
this.isSignatureStarted = !!this.computedPreviousValue
if (this.requireSigningReason) {
if (this.withSigningReason) {
this.field.preferences ||= {}
this.field.preferences.reason_field_uuid ||= v4()
this.isOtherReason = this.reason && !this.defaultReasons[this.reason]
this.isOtherReason = this.reason && !this.defaultReasons[this.reason] &&
(!this.field.preferences?.reasons?.length || !this.field.preferences.reasons.includes(this.reason))
}
},
async mounted () {
+106 -2
View File
@@ -175,7 +175,10 @@
{{ t('save') }}
</span>
</button>
<div class="dropdown dropdown-end">
<div
class="dropdown dropdown-end"
:class="{ 'dropdown-open': isDownloading }"
>
<label
tabindex="0"
class="base-button !rounded-l-none !pl-1 !pr-2 !border-l-neutral-500"
@@ -209,6 +212,30 @@
<span class="whitespace-nowrap">{{ t('preferences') }}</span>
</a>
</li>
<li v-if="withDownload">
<button
class="flex space-x-2"
:disabled="isDownloading"
@click.stop.prevent="download"
>
<IconInnerShadowTop
v-if="isDownloading"
class="animate-spin w-6 h-6 flex-shrink-0"
/>
<IconDownload
v-else
class="w-6 h-6 flex-shrink-0"
/>
<span
v-if="isDownloading"
class="whitespace-nowrap"
>{{ t('downloading_') }}</span>
<span
v-else
class="whitespace-nowrap"
>{{ t('download') }}</span>
</button>
</li>
</ul>
</div>
</span>
@@ -457,6 +484,7 @@
:show-tour-start-form="showTourStartForm"
@add-field="addField"
@set-draw="[drawField = $event.field, drawOption = $event.option]"
@select-submitter="selectedSubmitter = $event"
@set-draw-type="[drawFieldType = $event, showDrawField = true]"
@set-drag="dragField = $event"
@set-drag-placeholder="$refs.dragPlaceholder.dragPlaceholder = $event"
@@ -511,7 +539,7 @@ import DocumentPreview from './preview'
import DocumentControls from './controls'
import MobileFields from './mobile_fields'
import FieldSubmitter from './field_submitter'
import { IconPlus, IconUsersPlus, IconDeviceFloppy, IconChevronDown, IconEye, IconWritingSign, IconInnerShadowTop, IconInfoCircle, IconAdjustments } from '@tabler/icons-vue'
import { IconPlus, IconUsersPlus, IconDeviceFloppy, IconChevronDown, IconEye, IconWritingSign, IconInnerShadowTop, IconInfoCircle, IconAdjustments, IconDownload } from '@tabler/icons-vue'
import { v4 } from 'uuid'
import { ref, computed, toRaw } from 'vue'
import * as i18n from './i18n'
@@ -537,6 +565,7 @@ export default {
Contenteditable,
IconUsersPlus,
IconChevronDown,
IconDownload,
IconAdjustments,
IconEye,
IconDeviceFloppy
@@ -584,6 +613,11 @@ export default {
required: false,
default: null
},
withDownload: {
type: Boolean,
required: false,
default: false
},
backgroundColor: {
type: String,
required: false,
@@ -805,6 +839,7 @@ export default {
return {
documentRefs: [],
isBreakpointLg: false,
isDownloading: false,
isLoadingBlankPage: false,
isSaving: false,
selectedSubmitter: null,
@@ -963,6 +998,75 @@ export default {
},
methods: {
toRaw,
download () {
this.isDownloading = true
this.baseFetch(`/templates/${this.template.id}/documents`).then(async (response) => {
if (response.ok) {
const urls = await response.json()
const isMobileSafariIos = 'ontouchstart' in window && navigator.maxTouchPoints > 0 && /AppleWebKit/i.test(navigator.userAgent)
const isSafariIos = isMobileSafariIos || /iPhone|iPad|iPod/i.test(navigator.userAgent)
if (isSafariIos && urls.length > 1) {
this.downloadSafariIos(urls)
} else {
this.downloadUrls(urls)
}
} else {
alert(this.t('failed_to_download_files'))
}
})
},
downloadUrls (urls) {
const fileRequests = urls.map((url) => {
return () => {
return fetch(url).then(async (resp) => {
const blobUrl = URL.createObjectURL(await resp.blob())
const link = document.createElement('a')
link.href = blobUrl
link.setAttribute('download', decodeURI(url.split('/').pop()))
link.click()
URL.revokeObjectURL(blobUrl)
})
}
})
fileRequests.reduce(
(prevPromise, request) => prevPromise.then(() => request()),
Promise.resolve()
).finally(() => {
this.isDownloading = false
})
},
downloadSafariIos (urls) {
const fileRequests = urls.map((url) => {
return fetch(url).then(async (resp) => {
const blob = await resp.blob()
const blobUrl = URL.createObjectURL(blob.slice(0, blob.size, 'application/octet-stream'))
const link = document.createElement('a')
link.href = blobUrl
link.setAttribute('download', decodeURI(url.split('/').pop()))
return link
})
})
Promise.all(fileRequests).then((links) => {
links.forEach((link, index) => {
setTimeout(() => {
link.click()
URL.revokeObjectURL(link.href)
}, index * 50)
})
}).finally(() => {
this.isDownloading = false
})
},
onDragover (e) {
if (this.$refs.dragPlaceholder?.dragPlaceholder) {
this.$refs.dragPlaceholder.isMask = e.target.id === 'mask'
+35 -9
View File
@@ -222,12 +222,21 @@
width="22"
class="animate-spin"
/>
<span class="hidden md:inline">
<span
v-if="analyzingProgress"
class="hidden md:inline"
>
{{ Math.round(analyzingProgress * 100) }}% {{ t('analyzing_') }}
</span>
<span
v-else
class="hidden md:inline"
>
{{ fieldPagesLoaded }} / {{ numberOfPages }} {{ t('processing_') }}
</span>
</template>
<template v-else>
<IconListSearch width="22" />
<IconSparkles width="22" />
<span
class="hidden md:inline"
>
@@ -259,7 +268,7 @@
import Field from './field'
import FieldType from './field_type'
import FieldSubmitter from './field_submitter'
import { IconLock, IconCirclePlus, IconInnerShadowTop, IconListSearch } from '@tabler/icons-vue'
import { IconLock, IconCirclePlus, IconInnerShadowTop, IconSparkles } from '@tabler/icons-vue'
import IconDrag from './icon_drag'
export default {
@@ -268,7 +277,7 @@ export default {
Field,
FieldType,
IconCirclePlus,
IconListSearch,
IconSparkles,
IconInnerShadowTop,
FieldSubmitter,
IconDrag,
@@ -363,10 +372,11 @@ export default {
default: false
}
},
emits: ['add-field', 'set-draw', 'set-draw-type', 'set-drag', 'drag-end', 'scroll-to-area', 'change-submitter', 'set-drag-placeholder'],
emits: ['add-field', 'set-draw', 'set-draw-type', 'set-drag', 'drag-end', 'scroll-to-area', 'change-submitter', 'set-drag-placeholder', 'select-submitter'],
data () {
return {
fieldPagesLoaded: null,
analyzingProgress: 0,
defaultFieldsSearch: ''
}
},
@@ -448,8 +458,6 @@ export default {
while (true) {
const { value, done } = await reader.read()
if (done) break
buffer += decoder.decode(value, { stream: true })
const lines = buffer.split('\n\n')
@@ -462,12 +470,27 @@ export default {
const data = JSON.parse(jsonStr)
if (data.error) {
alert(data.error)
if ((data.fields || fields).length) {
this.template.fields = data.fields || fields
this.save()
} else {
alert(data.error)
}
break
} else if (data.analyzing) {
this.analyzingProgress = data.progress
} else if (data.completed) {
this.fieldPagesLoaded = null
this.template.fields = fields
if (data.submitters) {
this.template.submitters = data.submitters
this.$emit('select-submitter', this.template.submitters[0])
}
this.template.fields = data.fields || fields
this.save()
break
@@ -484,11 +507,14 @@ export default {
}
}
}
if (done) break
}
}).catch(error => {
console.error('Error in streaming message: ', error)
}).finally(() => {
this.fieldPagesLoaded = null
this.analyzingProgress = null
this.isFieldsLoading = false
})
},
+27
View File
@@ -1,4 +1,7 @@
const en = {
analyzing_: 'Analyzing...',
download: 'Download',
downloading_: 'Downloading...',
view: 'View',
autodetect_fields: 'Autodetect fields',
payment_link: 'Payment link',
@@ -185,6 +188,10 @@ const en = {
}
const es = {
autodetect_fields: 'Autodetectar campos',
analyzing_: 'Analizando...',
download: 'Descargar',
downloading_: 'Descargando...',
view: 'Vista',
payment_link: 'Enlace de pago',
strikeout: 'Tachar',
@@ -370,6 +377,10 @@ const es = {
}
const it = {
autodetect_fields: 'Rileva campi',
analyzing_: 'Analisi...',
download: 'Scarica',
downloading_: 'Download in corso...',
view: 'Vista',
payment_link: 'Link di pagamento',
strikeout: 'Barrato',
@@ -555,6 +566,10 @@ const it = {
}
const pt = {
autodetect_fields: 'Detectar campos',
analyzing_: 'Analisando...',
download: 'Baixar',
downloading_: 'Baixando...',
view: 'Visualizar',
payment_link: 'Link de pagamento',
strikeout: 'Tachado',
@@ -740,6 +755,10 @@ const pt = {
}
const fr = {
autodetect_fields: 'Détecter les champs',
analyzing_: 'Analyse...',
download: 'Télécharger',
downloading_: 'Téléchargement...',
view: 'Voir',
payment_link: 'Lien de paiement',
strikeout: 'Rature',
@@ -925,6 +944,10 @@ const fr = {
}
const de = {
autodetect_fields: 'Felder erkennen',
analyzing_: 'Analysiere...',
download: 'Download',
downloading_: 'Download...',
view: 'Anzeigen',
payment_link: 'Zahlungslink',
strikeout: 'Durchstreichen',
@@ -1110,6 +1133,10 @@ const de = {
}
const nl = {
autodetect_fields: 'Velden detecteren',
analyzing_: 'Analyseren...',
download: 'Downloaden',
downloading_: 'Downloaden...',
view: 'Bekijken',
payment_link: 'Betaallink',
strikeout: 'Doorhalen',
@@ -7,6 +7,8 @@ class SendSubmitterInvitationEmailJob
submitter = Submitter.find(params['submitter_id'])
return if submitter.completed_at?
return if submitter.submission.archived_at?
return if submitter.template&.archived_at?
return if submitter.submission.source == 'invite' && !Accounts.can_send_emails?(submitter.account, on_events: true)
unless Accounts.can_send_invitation_emails?(submitter.account)
+7
View File
@@ -22,6 +22,7 @@
#
class AccountConfig < ApplicationRecord
SUBMITTER_INVITATION_EMAIL_KEY = 'submitter_invitation_email'
SUBMITTER_INVITATION_REMINDER_EMAIL_KEY = 'submitter_invitation_reminder_email'
SUBMITTER_COMPLETED_EMAIL_KEY = 'submitter_completed_email'
SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY = 'submitter_documents_copy_email'
BCC_EMAILS = 'bcc_emails'
@@ -59,6 +60,12 @@ class AccountConfig < ApplicationRecord
'body' => I18n.t(:submitter_invitation_email_sign_body)
}
},
SUBMITTER_INVITATION_REMINDER_EMAIL_KEY => lambda {
{
'subject' => I18n.t(:you_are_invited_to_sign_a_document),
'body' => I18n.t(:submitter_invitation_email_sign_body)
}
},
SUBMITTER_COMPLETED_EMAIL_KEY => lambda {
{
'subject' => I18n.t(:template_name_has_been_completed_by_submitters),
+8
View File
@@ -0,0 +1,8 @@
<svg class="<%= local_assigns[:class] %>" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M3 5a1 1 0 0 1 1 -1h16a1 1 0 0 1 1 1v10a1 1 0 0 1 -1 1h-16a1 1 0 0 1 -1 -1v-10z" />
<path d="M7 20h10" /><path d="M9 16v4" />
<path d="M15 16v4" /></svg>
<path d="M9 16v4" />
<path d="M15 16v4" />
</svg>

After

Width:  |  Height:  |  Size: 497 B

+6
View File
@@ -0,0 +1,6 @@
<svg class="<%= local_assigns[:class] %>" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M6 5a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v14a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2v-14z" />
<path d="M11 4h2" />
<path d="M12 17v.01" />
</svg>

After

Width:  |  Height:  |  Size: 423 B

+5
View File
@@ -0,0 +1,5 @@
<svg class="<%= local_assigns[:class] %>" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M5 4a1 1 0 0 1 1 -1h12a1 1 0 0 1 1 1v16a1 1 0 0 1 -1 1h-12a1 1 0 0 1 -1 -1v-16z" />
<path d="M11 17a1 1 0 1 0 2 0a1 1 0 0 0 -2 0" />
</svg>

After

Width:  |  Height:  |  Size: 427 B

+10 -3
View File
@@ -28,9 +28,16 @@
<span class="absolute flex items-center justify-center w-7 h-7 rounded-full -start-3.5 ring-8 ring-base-100 text-base-content <%= bg_class %>">
<%= svg_icon(SubmissionEventsController::SUBMISSION_EVENT_ICONS.fetch(event.event_type, 'circle_dot'), class: 'w-4 h-4') %>
</span>
<p class="text-sm leading-none text-base-content/60 pt-1.5">
<%= l(event.event_timestamp.in_time_zone(current_account.timezone), format: :long, locale: current_account.locale) %>
</p>
<div class="flex items-center space-x-1 pt-1.5">
<span class="text-sm leading-none text-base-content/60">
<%= l(event.event_timestamp.in_time_zone(current_account.timezone), format: :long, locale: current_account.locale) %>
</span>
<% if (device = DetectBrowserDevice.call(event.data['ua'])) %>
<span class="tooltip tooltip-top" data-tip="<%= t(device) %>">
<%= svg_icon("device_#{device}", class: 'w-4 h-4') %>
</span>
<% end %>
</div>
<p class="text-base-content/80 mt-1">
<% if event.event_type == 'complete_verification' %>
<%= t('submission_event_names.complete_verification_by_html', provider: event.data['method'], submitter_name:) %>
+1 -116
View File
@@ -1,116 +1 @@
<% submitter_preferences_index = template&.preferences&.dig('submitters').to_a.index_by { |e| e['uuid'] } %>
<% template_submitters = local_assigns[:submitter]&.submission&.template_submitters || template.submitters %>
<% message_field_id = "message_field_#{SecureRandom.hex(3)}" %>
<div class="form-control">
<% can_send_emails = Accounts.can_send_emails?(current_account) %>
<div class="flex justify-between items-center">
<%= f.label :send_email, for: uuid = SecureRandom.uuid, class: 'flex items-center cursor-pointer' do %>
<%= f.check_box :send_email, id: uuid, class: 'base-checkbox', disabled: !can_send_emails || local_assigns[:disable_email], checked: can_send_emails && !local_assigns.key?(:resend_email) && !local_assigns[:disable_email] && template&.preferences&.dig('request_email_enabled') != false %>
<span class="label"><%= local_assigns[:resend_email] ? t('re_send_email') : t('send_email') %></span>
<% end %>
<div>
<% if can_send_emails %>
<%= render 'submissions/email_stats' %>
<%= content_for(:edit_button) || capture do %>
<toggle-visible data-element-ids="<%= [message_field_id].to_json %>" class="flex">
<label>
<%= f.check_box :is_custom_message, checked: false, class: 'hidden peer', data: { action: 'change:toggle-visible#trigger', type: 'checkbox' } %>
<span class="link peer-checked:hidden"><%= t('edit_message') %></span>
</label>
</toggle-visible>
<% end %>
<% end %>
</div>
</div>
<% if !Docuseal.multitenant? && !can_send_emails %>
<div class="alert my-4">
<%= svg_icon('info_circle', class: 'w-6 h-6') %>
<div>
<p class="font-bold"><%= t('smtp_not_configured') %></p>
<p class="text-gray-700">
<%= t('configure_smtp_settings_in_order_to_send_emails_') %>
<br>
<a class="link font-medium" data-turbo-frame="_top" href="<%= settings_email_index_path %>">
<%= t('go_to_smtp_settings') %>
</a>
</p>
</div>
</div>
<% end %>
</div>
<% config = AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY) %>
<div id="<%= message_field_id %>" class="card card-compact bg-base-300/40 hidden">
<div class="card-body">
<%= tag.input id: toggle_uuid = SecureRandom.uuid, value: '1', name: 'request_email_per_submitter', class: 'peer', type: 'checkbox', hidden: true, checked: local_assigns[:message_per_submitter] != false && template&.preferences&.dig('submitters').to_a.size > 1 %>
<div class="peer-checked:hidden form-control space-y-2">
<div class="form-control">
<div class="flex justify-between">
<%= f.label :subject, t('subject'), class: 'label' %>
<% if template_submitters.size > 1 && template_submitters.size < 5 && local_assigns[:message_per_submitter] != false %>
<label for="<%= toggle_uuid %>" class="label underline">
<%= t('edit_per_party') %>
</label>
<% end %>
</div>
<%= f.text_field :subject, value: local_assigns[:submitter_email_message]&.subject.presence || submitter_preferences_index.dig(local_assigns[:submitter]&.uuid, 'request_email_subject').presence || template&.preferences&.dig('request_email_subject').presence || config.value['subject'], required: true, class: '!text-sm base-input w-full', dir: 'auto' %>
</div>
<div class="form-control">
<div class="flex items-center">
<%= f.label :message, t('body'), class: 'label' %>
<span class="tooltip tooltip-right" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY].call['body'].scan(/{.*?}/).join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<autoresize-textarea>
<%= f.text_area :body, value: local_assigns[:submitter_email_message]&.body.presence || submitter_preferences_index.dig(local_assigns[:submitter]&.uuid, 'request_email_body').presence || template&.preferences&.dig('request_email_body').presence || config.value['body'], required: true, class: 'base-textarea w-full', rows: 10, dir: 'auto' %>
</autoresize-textarea>
<% unless local_assigns.fetch(:disable_save_as_default_template_option, false) %>
<label for="<%= uuid = SecureRandom.uuid %>" class="flex items-center cursor-pointer">
<%= check_box_tag :save_message, id: uuid, class: 'base-checkbox', checked: false %>
<span class="label"><%= t('save_as_default_template_message') %></span>
</label>
<% end %>
</div>
<%= render 'submissions/message_fields' %>
</div>
<% if template_submitters.size > 1 && template_submitters.size < 5 && local_assigns[:message_per_submitter] != false %>
<div class="hidden peer-checked:block form-control space-y-2">
<% uuid = SecureRandom.uuid %>
<% options = template_submitters.map { |e| [e['name'], "request_email_#{uuid}_#{e['uuid']}"] } %>
<toggle-visible data-element-ids="<%= options.map(&:last).to_json %>" class="flex relative px-1">
<ul class="tabs w-full flex flex-nowrap">
<% options.each_with_index do |(label, val), index| %>
<div class="w-full">
<%= f.radio_button :selected, val, checked: index.zero?, id: "#{val}_radio", data: { action: 'click:toggle-visible#trigger' }, class: 'hidden peer' %>
<%= f.label :selected, label, value: val, for: "#{val}_radio", class: 'tab w-full tab-lifted peer-checked:tab-active !bg-transparent' %>
</div>
<% end %>
</ul>
</toggle-visible>
<% template_submitters.each_with_index do |submitter, index| %>
<%= fields_for :submitter_preferences, nil, index: submitter['uuid'] do |ff| %>
<div id="request_email_<%= uuid %>_<%= submitter['uuid'] %>" class="<%= 'hidden' if index != 0 %>">
<div class="form-control">
<div class="flex justify-between">
<%= ff.label :subject, t('subject'), class: 'label' %>
</div>
<%= ff.text_field :subject, value: local_assigns[:submitter_email_message]&.subject.presence || submitter_preferences_index.dig(submitter['uuid'], 'request_email_subject').presence || template&.preferences&.dig('request_email_subject').presence || config.value['subject'], required: true, class: '!text-sm base-input w-full', dir: 'auto' %>
</div>
<div class="form-control">
<div class="flex items-center">
<%= ff.label :message, t('body'), class: 'label' %>
<span class="tooltip tooltip-right" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY].call['body'].scan(/{.*?}/).join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<autoresize-textarea>
<%= ff.text_area :body, value: local_assigns[:submitter_email_message]&.body.presence || submitter_preferences_index.dig(submitter['uuid'], 'request_email_body').presence || template&.preferences&.dig('request_email_body').presence || config.value['body'], required: true, class: 'base-textarea w-full', rows: 10, dir: 'auto' %>
</autoresize-textarea>
</div>
</div>
<% end %>
<% end %>
</div>
<% end %>
</div>
</div>
<%= render partial: 'submissions/send_email_base', locals: local_assigns %>
@@ -0,0 +1,116 @@
<% submitter_preferences_index = template&.preferences&.dig('submitters').to_a.index_by { |e| e['uuid'] } %>
<% template_submitters = local_assigns[:submitter]&.submission&.template_submitters || template.submitters %>
<% message_field_id = "message_field_#{SecureRandom.hex(3)}" %>
<div class="form-control">
<% can_send_emails = Accounts.can_send_emails?(current_account) %>
<div class="flex justify-between items-center">
<%= f.label :send_email, for: uuid = SecureRandom.uuid, class: 'flex items-center cursor-pointer' do %>
<%= f.check_box :send_email, id: uuid, class: 'base-checkbox', disabled: !can_send_emails || local_assigns[:disable_email], checked: can_send_emails && !local_assigns.key?(:resend_email) && !local_assigns[:disable_email] && template&.preferences&.dig('request_email_enabled') != false %>
<span class="label"><%= local_assigns[:resend_email] ? t('re_send_email') : t('send_email') %></span>
<% end %>
<div>
<% if can_send_emails %>
<%= render 'submissions/email_stats' %>
<%= content_for(:edit_button) || capture do %>
<toggle-visible data-element-ids="<%= [message_field_id].to_json %>" class="flex">
<label>
<%= f.check_box :is_custom_message, checked: false, class: 'hidden peer', data: { action: 'change:toggle-visible#trigger', type: 'checkbox' } %>
<span class="link peer-checked:hidden"><%= t('edit_message') %></span>
</label>
</toggle-visible>
<% end %>
<% end %>
</div>
</div>
<% if !Docuseal.multitenant? && !can_send_emails %>
<div class="alert my-4">
<%= svg_icon('info_circle', class: 'w-6 h-6') %>
<div>
<p class="font-bold"><%= t('smtp_not_configured') %></p>
<p class="text-gray-700">
<%= t('configure_smtp_settings_in_order_to_send_emails_') %>
<br>
<a class="link font-medium" data-turbo-frame="_top" href="<%= settings_email_index_path %>">
<%= t('go_to_smtp_settings') %>
</a>
</p>
</div>
</div>
<% end %>
</div>
<% config = AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY) %>
<div id="<%= message_field_id %>" class="card card-compact bg-base-300/40 hidden">
<div class="card-body">
<%= tag.input id: toggle_uuid = SecureRandom.uuid, value: '1', name: 'request_email_per_submitter', class: 'peer', type: 'checkbox', hidden: true, checked: local_assigns[:message_per_submitter] != false && template&.preferences&.dig('submitters').to_a.size > 1 %>
<div class="peer-checked:hidden form-control space-y-2">
<div class="form-control">
<div class="flex justify-between">
<%= f.label :subject, t('subject'), class: 'label' %>
<% if template_submitters.size > 1 && template_submitters.size < 5 && local_assigns[:message_per_submitter] != false %>
<label for="<%= toggle_uuid %>" class="label underline">
<%= t('edit_per_party') %>
</label>
<% end %>
</div>
<%= f.text_field :subject, value: local_assigns[:submitter_email_message]&.subject.presence || submitter_preferences_index.dig(local_assigns[:submitter]&.uuid, 'request_email_subject').presence || template&.preferences&.dig('request_email_subject').presence || config.value['subject'], required: true, class: '!text-sm base-input w-full', dir: 'auto' %>
</div>
<div class="form-control">
<div class="flex items-center">
<%= f.label :message, t('body'), class: 'label' %>
<span class="tooltip tooltip-right" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY].call['body'].scan(/{.*?}/).join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<autoresize-textarea>
<%= f.text_area :body, value: local_assigns[:submitter_email_message]&.body.presence || submitter_preferences_index.dig(local_assigns[:submitter]&.uuid, 'request_email_body').presence || template&.preferences&.dig('request_email_body').presence || config.value['body'], required: true, class: 'base-textarea w-full', rows: 10, dir: 'auto' %>
</autoresize-textarea>
<% unless local_assigns.fetch(:disable_save_as_default_template_option, false) %>
<label for="<%= uuid = SecureRandom.uuid %>" class="flex items-center cursor-pointer">
<%= check_box_tag :save_message, id: uuid, class: 'base-checkbox', checked: false %>
<span class="label"><%= t('save_as_default_template_message') %></span>
</label>
<% end %>
</div>
<%= render 'submissions/message_fields' %>
</div>
<% if template_submitters.size > 1 && template_submitters.size < 5 && local_assigns[:message_per_submitter] != false %>
<div class="hidden peer-checked:block form-control space-y-2">
<% uuid = SecureRandom.uuid %>
<% options = template_submitters.map { |e| [e['name'], "request_email_#{uuid}_#{e['uuid']}"] } %>
<toggle-visible data-element-ids="<%= options.map(&:last).to_json %>" class="flex relative px-1">
<ul class="tabs w-full flex flex-nowrap">
<% options.each_with_index do |(label, val), index| %>
<div class="w-full">
<%= f.radio_button :selected, val, checked: index.zero?, id: "#{val}_radio", data: { action: 'click:toggle-visible#trigger' }, class: 'hidden peer' %>
<%= f.label :selected, label, value: val, for: "#{val}_radio", class: 'tab w-full tab-lifted peer-checked:tab-active !bg-transparent' %>
</div>
<% end %>
</ul>
</toggle-visible>
<% template_submitters.each_with_index do |submitter, index| %>
<%= fields_for :submitter_preferences, nil, index: submitter['uuid'] do |ff| %>
<div id="request_email_<%= uuid %>_<%= submitter['uuid'] %>" class="<%= 'hidden' if index != 0 %>">
<div class="form-control">
<div class="flex justify-between">
<%= ff.label :subject, t('subject'), class: 'label' %>
</div>
<%= ff.text_field :subject, value: local_assigns[:submitter_email_message]&.subject.presence || submitter_preferences_index.dig(submitter['uuid'], 'request_email_subject').presence || template&.preferences&.dig('request_email_subject').presence || config.value['subject'], required: true, class: '!text-sm base-input w-full', dir: 'auto' %>
</div>
<div class="form-control">
<div class="flex items-center">
<%= ff.label :message, t('body'), class: 'label' %>
<span class="tooltip tooltip-right" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY].call['body'].scan(/{.*?}/).join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<autoresize-textarea>
<%= ff.text_area :body, value: local_assigns[:submitter_email_message]&.body.presence || submitter_preferences_index.dig(submitter['uuid'], 'request_email_body').presence || template&.preferences&.dig('request_email_body').presence || config.value['body'], required: true, class: 'base-textarea w-full', rows: 10, dir: 'auto' %>
</autoresize-textarea>
</div>
</div>
<% end %>
<% end %>
</div>
<% end %>
</div>
</div>
+2 -2
View File
@@ -81,7 +81,7 @@
<% document = @submission.schema_documents.find { |a| item['attachment_uuid'] == a.uuid } %>
<% if document.preview_images.first %>
<scroll-to data-selector-id="page-<%= document.uuid %>-0" class="block cursor-pointer">
<img src="<%= Docuseal::URL_CACHE.fetch([document.id, document.uuid, 0].join(':'), expires_in: 10.minutes) { document.preview_images.first.url } %>" width="<%= document.preview_images.first.metadata['width'] %>" height="<%= document.preview_images.first.metadata['height'] %>" class="rounded border" loading="lazy">
<img src="<%= (document.preview_images.find { |e| e.filename.base.to_i.zero? } || document.preview_images.first).url %>" width="<%= document.preview_images.first.metadata['width'] %>" height="<%= document.preview_images.first.metadata['height'] %>" class="rounded border" loading="lazy">
<div class="pb-2 pt-1.5 text-center" dir="auto">
<%= item['name'].presence || document.filename.base %>
</div>
@@ -103,7 +103,7 @@
<% (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.signed_uuid, "#{index}.jpg")) %>
<page-container id="<%= "page-#{document.uuid}-#{index}" %>" class="block before:border before:absolute before:top-0 before:bottom-0 before:left-0 before:right-0 before:rounded relative mb-4" style="container-type: size; aspect-ratio: <%= width = page.metadata['width'] %> / <%= height = page.metadata['height'] %>">
<img loading="lazy" src="<%= Docuseal::URL_CACHE.fetch([document.id, document.uuid, index].join(':'), expires_in: 10.minutes) { page.url } %>" width="<%= width %>" class="rounded" height="<%= height %>">
<img loading="lazy" src="<%= page.url %>" width="<%= width %>" class="rounded" height="<%= height %>">
<div class="top-0 bottom-0 left-0 right-0 absolute">
<% document_annots_index[index]&.each do |annot| %>
<%= render 'submissions/annotation', annot: %>
@@ -0,0 +1,70 @@
<div id="<%= AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY %>_form">
<% configs = AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY).value %>
<% template_email_preferences_values = @template.preferences.values_at('completed_notification_email_subject', 'completed_notification_email_body').compact_blank.presence %>
<% is_custom_template_email = template_email_preferences_values.present? %>
<% if is_custom_template_email %>
<%= button_to nil, template_preferences_path(@template), id: 'submitter_completed_email_reset_link', method: :delete, class: 'hidden', params: { config_key: AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY }, data: { turbo_confirm: t('are_you_sure_') }, form: { data: { close_on_submit: false } } %>
<% end %>
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-1', id: 'submitter_completed_email_template_form' }, data: { close_on_submit: false } do |f| %>
<toggle-on-submit data-element-id="email_saved_alert3"></toggle-on-submit>
<%= f.fields_for :preferences, Struct.new(:completed_notification_email_subject, :completed_notification_email_body).new(@template.preferences['completed_notification_email_subject'].presence || configs['subject'], @template.preferences['completed_notification_email_body'].presence || configs['body']) do |ff| %>
<div class="form-control">
<div class="flex justify-between">
<%= ff.label :completed_notification_email_subject, t('email_subject'), class: 'label' %>
<% if is_custom_template_email %>
<label for="submitter_completed_email_reset_link" class="label underline">
<%= t('reset_default') %>
</label>
<% end %>
</div>
<%= ff.text_field :completed_notification_email_subject, required: true, class: 'base-input', dir: 'auto' %>
</div>
<div class="form-control">
<div class="flex items-center">
<%= ff.label :completed_notification_email_body, t('email_body'), class: 'label' %>
<span class="tooltip tooltip-right" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY].call['body'].scan(/{.*?}/).join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<autoresize-textarea>
<%= ff.text_area :completed_notification_email_body, required: true, class: 'base-input w-full py-2 !rounded-2xl', dir: 'auto' %>
</autoresize-textarea>
</div>
<% end %>
<% end %>
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-1' }, data: { close_on_submit: false } do |f| %>
<toggle-on-submit data-element-id="email_saved_alert3"></toggle-on-submit>
<%= f.fields_for :preferences, Struct.new(:completed_notification_email_enabled, :completed_notification_email_attach_audit, :completed_notification_email_attach_documents).new(@template.preferences['completed_notification_email_enabled'], configs['attach_audit_log'] != false && @template.preferences['completed_notification_email_attach_audit'] != false, configs['attach_documents'] != false && @template.preferences['completed_notification_email_attach_documents'] != false) do |ff| %>
<div class="flex items-center justify-between pt-2.5 px-1 mb-2">
<span>
<%= t('attach_documents_to_the_email') %>
</span>
<submit-form data-on="change" class="flex">
<%= ff.check_box :completed_notification_email_attach_documents, { checked: ff.object.completed_notification_email_attach_documents != false, class: 'toggle', disabled: configs['attach_documents'] == false }, 'true', 'false' %>
</submit-form>
</div>
<div class="flex items-center justify-between pt-2.5 px-1 mb-2">
<span>
<%= t('attach_audit_log_pdf_to_the_email') %>
</span>
<submit-form data-on="change" class="flex">
<%= ff.check_box :completed_notification_email_attach_audit, { checked: ff.object.completed_notification_email_attach_audit != false, class: 'toggle', disabled: configs['attach_audit_log'] == false }, 'true', 'false' %>
</submit-form>
</div>
<div class="flex items-center justify-between py-2.5 px-1 mb-2">
<span>
<%= t('send_emails_automatically_on_completion') %>
</span>
<submit-form data-on="change" class="flex">
<%= ff.check_box :completed_notification_email_enabled, { checked: ff.object.completed_notification_email_enabled != false, class: 'toggle', disabled: configs['enabled'] == false }, 'true', 'false' %>
</submit-form>
</div>
<% end %>
<% end %>
<div class="form-control pt-2">
<%= button_tag button_title(title: t('save'), disabled_with: t('saving')), form: 'submitter_completed_email_template_form', class: 'base-button' %>
<div class="flex justify-center">
<span id="email_saved_alert3" class="text-sm invisible font-normal mt-0.5"><%= t('changes_have_been_saved') %></span>
</div>
</div>
</div>
@@ -0,0 +1,75 @@
<div id="<%= AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY %>_form">
<% configs = AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY).value %>
<% template_email_preferences_values = @template.preferences.values_at('documents_copy_email_subject', 'documents_copy_email_body').compact_blank.presence %>
<% is_custom_template_email = template_email_preferences_values.present? %>
<% if is_custom_template_email %>
<%= button_to nil, template_preferences_path(@template), id: 'submitter_documents_copy_email_reset_link', method: :delete, class: 'hidden', params: { config_key: AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY }, data: { turbo_confirm: t('are_you_sure_') }, form: { data: { close_on_submit: false } } %>
<% end %>
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-1', id: 'submitter_documents_copy_email_template_form' }, data: { close_on_submit: false } do |f| %>
<toggle-on-submit data-element-id="email_saved_alert2"></toggle-on-submit>
<%= f.fields_for :preferences, Struct.new(:documents_copy_email_reply_to, :documents_copy_email_subject, :documents_copy_email_body).new(@template.preferences['documents_copy_email_reply_to'].presence || configs['reply_to'], @template.preferences['documents_copy_email_subject'].presence || configs['subject'], @template.preferences['documents_copy_email_body'].presence || configs['body']) do |ff| %>
<div class="form-control">
<div class="flex justify-between">
<%= ff.label :documents_copy_email_subject, t('email_subject'), class: 'label' %>
<% if is_custom_template_email %>
<label for="submitter_documents_copy_email_reset_link" class="label underline">
<%= t('reset_default') %>
</label>
<% end %>
</div>
<%= ff.text_field :documents_copy_email_subject, required: true, class: 'base-input', dir: 'auto' %>
</div>
<div class="form-control">
<div class="flex items-center">
<%= ff.label :documents_copy_email_body, t('email_body'), class: 'label' %>
<span class="tooltip tooltip-right" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY].call['body'].scan(/{.*?}/).join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<autoresize-textarea>
<%= ff.text_area :documents_copy_email_body, required: true, class: 'base-input w-full py-2 !rounded-2xl', dir: 'auto' %>
</autoresize-textarea>
</div>
<% if can?(:manage, :reply_to) %>
<div class="form-control">
<%= ff.label :documents_copy_email_reply_to, t('reply_to'), class: 'label' %>
<%= ff.email_field :documents_copy_email_reply_to, class: 'base-input', dir: 'auto', placeholder: t(:email) %>
</div>
<% end %>
<% end %>
<% end %>
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-1', id: 'submitter_documents_copy_email_template_form' }, data: { close_on_submit: false } do |f| %>
<%= f.fields_for :preferences, Struct.new(:documents_copy_email_enabled, :documents_copy_email_attach_audit, :documents_copy_email_attach_documents).new(@template.preferences['documents_copy_email_enabled'], configs['attach_audit_log'] != false && @template.preferences['documents_copy_email_attach_audit'] != false, configs['attach_documents'] != false && @template.preferences['documents_copy_email_attach_documents'] != false) do |ff| %>
<div class="flex items-center justify-between pt-2.5 px-1 mb-2">
<span>
<%= t('attach_documents_to_the_email') %>
</span>
<submit-form data-on="change" class="flex">
<%= ff.check_box :documents_copy_email_attach_documents, { checked: ff.object.documents_copy_email_attach_documents != false, class: 'toggle', disabled: configs['attach_documents'] == false }, 'true', 'false' %>
</submit-form>
</div>
<div class="flex items-center justify-between pt-2.5 px-1 mb-2">
<span>
<%= t('attach_audit_log_pdf_to_the_email') %>
</span>
<submit-form data-on="change" class="flex">
<%= ff.check_box :documents_copy_email_attach_audit, { checked: ff.object.documents_copy_email_attach_audit != false, class: 'toggle', disabled: configs['attach_audit_log'] == false }, 'true', 'false' %>
</submit-form>
</div>
<div class="flex items-center justify-between py-2.5 px-1 mb-2">
<span>
<%= t('send_emails_automatically_on_completion') %>
</span>
<submit-form data-on="change" class="flex">
<%= ff.check_box :documents_copy_email_enabled, { checked: ff.object.documents_copy_email_enabled != false && configs['enabled'] != false, class: 'toggle', disabled: configs['enabled'] == false }, 'true', 'false' %>
</submit-form>
</div>
<% end %>
<% end %>
<div class="form-control pt-2">
<%= button_tag button_title(title: t('save'), disabled_with: t('saving')), form: 'submitter_documents_copy_email_template_form', class: 'base-button' %>
<div class="flex justify-center">
<span id="email_saved_alert2" class="text-sm invisible font-normal mt-0.5"><%= t('changes_have_been_saved') %></span>
</div>
</div>
</div>
@@ -0,0 +1,110 @@
<div id="<%= AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY %>_form">
<% template_email_preferences_values = @template.preferences.values_at('request_email_subject', 'request_email_body').compact_blank %>
<% default_template_email_preferences_values = AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY).value.values_at('subject', 'body') %>
<% is_custom_template_email = template_email_preferences_values.present? %>
<% multiple_submitters = @template.submitters.size > 1 && @template.submitters.size < 5 %>
<% if is_custom_template_email || @template.preferences['submitters'].to_a.any? %>
<%= button_to nil, template_preferences_path(@template), id: 'submitter_invitation_email_reset_link', method: :delete, class: 'hidden', params: { config_key: AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY }, data: { turbo_confirm: t('are_you_sure_') }, form: { data: { close_on_submit: false } } %>
<% end %>
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-1', id: 'submitter_invitation_email_template_form' }, data: { close_on_submit: false } do |f| %>
<toggle-on-submit data-element-id="email_saved_alert1"></toggle-on-submit>
<%= tag.input id: 'request_email_per_submitter', value: '1', name: 'request_email_per_submitter', class: 'peer', type: 'checkbox', hidden: true, checked: @template.preferences['submitters'].to_a.size > 1 %>
<div class="peer-checked:hidden">
<%= f.fields_for :preferences, Struct.new(:request_email_subject, :request_email_body).new(*(template_email_preferences_values.presence || default_template_email_preferences_values)) do |ff| %>
<div class="form-control">
<div class="flex justify-between">
<%= ff.label :request_email_subject, t('email_subject'), class: 'label' %>
<% if is_custom_template_email %>
<label for="submitter_invitation_email_reset_link" class="label underline">
<%= t('reset_default') %>
</label>
<% elsif multiple_submitters %>
<label for="request_email_per_submitter" class="label underline">
<%= t('edit_per_party') %>
</label>
<% end %>
</div>
<%= ff.text_field :request_email_subject, required: true, class: 'base-input', dir: 'auto' %>
</div>
<div class="form-control">
<div class="flex items-center">
<%= ff.label :request_email_body, t('email_body'), class: 'label' %>
<span class="tooltip tooltip-right" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY].call['body'].scan(/{.*?}/).join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<autoresize-textarea>
<%= ff.text_area :request_email_body, required: true, class: 'base-input w-full py-2 !rounded-2xl', dir: 'auto' %>
</autoresize-textarea>
</div>
<% end %>
</div>
<% if multiple_submitters %>
<div class="hidden peer-checked:block">
<% options = @template.submitters.map { |e| [e['name'], "request_email_#{e['uuid']}"] } %>
<toggle-visible data-element-ids="<%= options.map(&:last).to_json %>" class="flex relative px-1">
<ul class="tabs w-full flex flex-nowrap mb-2">
<% options.each_with_index do |(label, val), index| %>
<div class="w-full">
<%= f.radio_button :selected, val, checked: index.zero?, id: "#{val}_radio", data: { action: 'click:toggle-visible#trigger' }, class: 'hidden peer' %>
<%= f.label :selected, label, value: val, for: "#{val}_radio", class: 'tab w-full tab-lifted peer-checked:tab-active' %>
</div>
<% end %>
</ul>
</toggle-visible>
<%= f.fields_for :preferences do |ff| %>
<% @template.submitters.each_with_index do |submitter, index| %>
<div id="request_email_<%= submitter['uuid'] %>" class="<%= 'hidden' if index != 0 %>">
<% submitter_preferences = f.object.preferences['submitters'].to_a.find { |e| e['uuid'] == submitter['uuid'] } || {} %>
<% submitter_email_preferences_values = submitter_preferences.values_at('request_email_subject', 'request_email_body').compact_blank.presence %>
<%= ff.fields_for :submitters, Struct.new(:request_email_subject, :request_email_body).new(*(submitter_preferences.values_at('request_email_subject', 'request_email_body').compact_blank.presence || template_email_preferences_values.presence || default_template_email_preferences_values)), index: nil do |fff| %>
<%= fff.hidden_field :uuid, value: submitter['uuid'] %>
<div class="form-control">
<div class="flex justify-between">
<%= fff.label :request_email_subject, t('email_subject'), class: 'label' %>
<% if submitter_email_preferences_values.present? %>
<label for="submitter_invitation_email_reset_link" class="label underline">
<%= t('reset_default') %>
</label>
<% end %>
</div>
<%= fff.text_field :request_email_subject, required: true, class: 'base-input', dir: 'auto' %>
</div>
<div class="form-control">
<div class="flex items-center">
<%= fff.label :request_email_body, t('email_body'), class: 'label' %>
<span class="tooltip tooltip-right" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY].call['body'].scan(/{.*?}/).join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<autoresize-textarea>
<%= fff.text_area :request_email_body, required: true, class: 'base-input w-full py-2', dir: 'auto' %>
</autoresize-textarea>
</div>
<% end %>
</div>
<% end %>
<% end %>
</div>
<% end %>
<% end %>
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-1' }, data: { close_on_submit: false } do |f| %>
<toggle-on-submit data-element-id="email_saved_alert1"></toggle-on-submit>
<%= f.fields_for :preferences, Struct.new(:request_email_enabled).new(@template.preferences['request_email_enabled']) do |ff| %>
<div class="flex items-center justify-between py-2.5 px-1 mb-2">
<span>
<%= t('send_signature_request_email') %>
</span>
<submit-form data-on="change" class="flex">
<%= ff.check_box :request_email_enabled, { checked: ff.object.request_email_enabled != false, class: 'toggle' }, 'true', 'false' %>
</submit-form>
</div>
<% end %>
<% end %>
<div class="form-control pt-2">
<%= button_tag button_title(title: t('save'), disabled_with: t('saving')), form: 'submitter_invitation_email_template_form', class: 'base-button' %>
<div class="flex justify-center">
<span id="email_saved_alert1" class="text-sm invisible font-normal mt-0.5"><%= t('changes_have_been_saved') %></span>
</div>
</div>
</div>
+7 -197
View File
@@ -98,157 +98,17 @@
<%= t('signature_request_email') %>
</div>
<div class="collapse-content">
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-1' }, data: { close_on_submit: false } do |f| %>
<toggle-on-submit data-element-id="email_saved_alert1"></toggle-on-submit>
<%= tag.input id: 'request_email_per_submitter', value: '1', name: 'request_email_per_submitter', class: 'peer', type: 'checkbox', hidden: true, checked: @template.preferences['submitters'].to_a.size > 1 %>
<div class="peer-checked:hidden">
<%= f.fields_for :preferences, Struct.new(:request_email_subject, :request_email_body).new(*(@template.preferences.values_at('request_email_subject', 'request_email_body').compact_blank.presence || AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY).value.values_at('subject', 'body'))) do |ff| %>
<div class="form-control">
<div class="flex justify-between">
<%= ff.label :request_email_subject, t('email_subject'), class: 'label' %>
<% if @template.submitters.size > 1 && @template.submitters.size < 5 %>
<label for="request_email_per_submitter" class="label underline">
<%= t('edit_per_party') %>
</label>
<% end %>
</div>
<%= ff.text_field :request_email_subject, required: true, class: 'base-input', dir: 'auto' %>
</div>
<div class="form-control">
<div class="flex items-center">
<%= ff.label :request_email_body, t('email_body'), class: 'label' %>
<span class="tooltip tooltip-right" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY].call['body'].scan(/{.*?}/).join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<autoresize-textarea>
<%= ff.text_area :request_email_body, required: true, class: 'base-input w-full py-2', dir: 'auto' %>
</autoresize-textarea>
</div>
<% end %>
</div>
<% if @template.submitters.size > 1 && @template.submitters.size < 5 %>
<div class="hidden peer-checked:block">
<% options = @template.submitters.map { |e| [e['name'], "request_email_#{e['uuid']}"] } %>
<toggle-visible data-element-ids="<%= options.map(&:last).to_json %>" class="flex relative px-1">
<ul class="tabs w-full flex flex-nowrap mb-2">
<% options.each_with_index do |(label, val), index| %>
<div class="w-full">
<%= f.radio_button :selected, val, checked: index.zero?, id: "#{val}_radio", data: { action: 'click:toggle-visible#trigger' }, class: 'hidden peer' %>
<%= f.label :selected, label, value: val, for: "#{val}_radio", class: 'tab w-full tab-lifted peer-checked:tab-active' %>
</div>
<% end %>
</ul>
</toggle-visible>
<%= f.fields_for :preferences do |ff| %>
<% @template.submitters.each_with_index do |submitter, index| %>
<div id="request_email_<%= submitter['uuid'] %>" class="<%= 'hidden' if index != 0 %>">
<% submitter_preferences = f.object.preferences['submitters'].to_a.find { |e| e['uuid'] == submitter['uuid'] } || {} %>
<%= ff.fields_for :submitters, Struct.new(:request_email_subject, :request_email_body).new(*(submitter_preferences.values_at('request_email_subject', 'request_email_body').compact_blank.presence || @template.preferences.values_at('request_email_subject', 'request_email_body').compact_blank.presence || AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY).value.values_at('subject', 'body'))), index: nil do |fff| %>
<%= fff.hidden_field :uuid, value: submitter['uuid'] %>
<div class="form-control">
<%= fff.label :request_email_subject, t('email_subject'), class: 'label' %>
<%= fff.text_field :request_email_subject, required: true, class: 'base-input', dir: 'auto' %>
</div>
<div class="form-control">
<div class="flex items-center">
<%= fff.label :request_email_body, t('email_body'), class: 'label' %>
<span class="tooltip tooltip-right" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY].call['body'].scan(/{.*?}/).join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<autoresize-textarea>
<%= fff.text_area :request_email_body, required: true, class: 'base-input w-full py-2', dir: 'auto' %>
</autoresize-textarea>
</div>
<% end %>
</div>
<% end %>
<% end %>
</div>
<% end %>
<%= f.fields_for :preferences, Struct.new(:request_email_enabled).new(@template.preferences['request_email_enabled']) do |ff| %>
<div class="flex items-center justify-between py-2.5 px-1 mb-2">
<span>
<%= 'Send signature request email' %>
</span>
<submit-form data-on="change" class="flex">
<%= ff.check_box :request_email_enabled, { checked: ff.object.request_email_enabled != false, class: 'toggle' }, 'true', 'false' %>
</submit-form>
</div>
<% end %>
<div class="form-control pt-2">
<%= f.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button' %>
<div class="flex justify-center">
<span id="email_saved_alert1" class="text-sm invisible font-normal mt-0.5"><%= t('changes_have_been_saved') %></span>
</div>
</div>
<% end %>
<%= render 'templates_preferences/submitter_invitation_email_form' %>
</div>
</div>
<%= render 'templates_preferences/submitter_invitation_reminder_email_collapse' %>
<div class="collapse collapse-arrow join-item border border-base-300">
<input type="checkbox" name="accordion">
<div class="collapse-title text-xl font-medium">
<%= t('documents_copy_email') %>
</div>
<div class="collapse-content">
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-1' }, data: { close_on_submit: false } do |f| %>
<toggle-on-submit data-element-id="email_saved_alert2"></toggle-on-submit>
<% configs = AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY).value %>
<%= f.fields_for :preferences, Struct.new(:documents_copy_email_reply_to, :documents_copy_email_subject, :documents_copy_email_body, :documents_copy_email_enabled, :documents_copy_email_attach_audit, :documents_copy_email_attach_documents).new(@template.preferences['documents_copy_email_reply_to'].presence || configs['reply_to'], @template.preferences['documents_copy_email_subject'].presence || configs['subject'], @template.preferences['documents_copy_email_body'].presence || configs['body'], @template.preferences['documents_copy_email_enabled'], configs['attach_audit_log'] != false && @template.preferences['documents_copy_email_attach_audit'] != false, configs['attach_documents'] != false && @template.preferences['documents_copy_email_attach_documents'] != false) do |ff| %>
<div class="form-control">
<%= ff.label :documents_copy_email_subject, t('email_subject'), class: 'label' %>
<%= ff.text_field :documents_copy_email_subject, required: true, class: 'base-input', dir: 'auto' %>
</div>
<div class="form-control">
<div class="flex items-center">
<%= ff.label :documents_copy_email_body, t('email_body'), class: 'label' %>
<span class="tooltip tooltip-right" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY].call['body'].scan(/{.*?}/).join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<autoresize-textarea>
<%= ff.text_area :documents_copy_email_body, required: true, class: 'base-input w-full py-2', dir: 'auto' %>
</autoresize-textarea>
</div>
<% if can?(:manage, :reply_to) %>
<div class="form-control">
<%= ff.label :documents_copy_email_reply_to, t('reply_to'), class: 'label' %>
<%= ff.email_field :documents_copy_email_reply_to, class: 'base-input', dir: 'auto', placeholder: t(:email) %>
</div>
<% end %>
<div class="flex items-center justify-between pt-2.5 px-1 mb-2">
<span>
<%= t('attach_documents_to_the_email') %>
</span>
<submit-form data-on="change" class="flex">
<%= ff.check_box :documents_copy_email_attach_documents, { checked: ff.object.documents_copy_email_attach_documents != false, class: 'toggle', disabled: configs['attach_documents'] == false }, 'true', 'false' %>
</submit-form>
</div>
<div class="flex items-center justify-between pt-2.5 px-1 mb-2">
<span>
<%= t('attach_audit_log_pdf_to_the_email') %>
</span>
<submit-form data-on="change" class="flex">
<%= ff.check_box :documents_copy_email_attach_audit, { checked: ff.object.documents_copy_email_attach_audit != false, class: 'toggle', disabled: configs['attach_audit_log'] == false }, 'true', 'false' %>
</submit-form>
</div>
<div class="flex items-center justify-between py-2.5 px-1 mb-2">
<span>
<%= t('send_emails_automatically_on_completion') %>
</span>
<submit-form data-on="change" class="flex">
<%= ff.check_box :documents_copy_email_enabled, { checked: ff.object.documents_copy_email_enabled != false && configs['enabled'] != false, class: 'toggle', disabled: configs['enabled'] == false }, 'true', 'false' %>
</submit-form>
</div>
<% end %>
<div class="form-control pt-2">
<%= f.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button' %>
<div class="flex justify-center">
<span id="email_saved_alert2" class="text-sm invisible font-normal mt-0.5"><%= t('changes_have_been_saved') %></span>
</div>
</div>
<% end %>
<%= render 'templates_preferences/submitter_documents_copy_email_form' %>
</div>
</div>
<div class="collapse collapse-arrow join-item border border-base-300">
@@ -257,57 +117,7 @@
<%= t('completed_notification_email') %>
</div>
<div class="collapse-content">
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-1' }, data: { close_on_submit: false } do |f| %>
<toggle-on-submit data-element-id="email_saved_alert3"></toggle-on-submit>
<% configs = AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY).value %>
<%= f.fields_for :preferences, Struct.new(:completed_notification_email_subject, :completed_notification_email_body, :completed_notification_email_enabled, :completed_notification_email_attach_audit, :completed_notification_email_attach_documents).new(@template.preferences['completed_notification_email_subject'].presence || configs['subject'], @template.preferences['completed_notification_email_body'].presence || configs['body'], @template.preferences['completed_notification_email_enabled'], configs['attach_audit_log'] != false && @template.preferences['completed_notification_email_attach_audit'] != false, configs['attach_documents'] != false && @template.preferences['completed_notification_email_attach_documents'] != false) do |ff| %>
<div class="form-control">
<%= ff.label :completed_notification_email_subject, t('email_subject'), class: 'label' %>
<%= ff.text_field :completed_notification_email_subject, required: true, class: 'base-input', dir: 'auto' %>
</div>
<div class="form-control">
<div class="flex items-center">
<%= ff.label :completed_notification_email_body, t('email_body'), class: 'label' %>
<span class="tooltip tooltip-right" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY].call['body'].scan(/{.*?}/).join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<autoresize-textarea>
<%= ff.text_area :completed_notification_email_body, required: true, class: 'base-input w-full py-2', dir: 'auto' %>
</autoresize-textarea>
</div>
<div class="flex items-center justify-between pt-2.5 px-1 mb-2">
<span>
<%= t('attach_documents_to_the_email') %>
</span>
<submit-form data-on="change" class="flex">
<%= ff.check_box :completed_notification_email_attach_documents, { checked: ff.object.completed_notification_email_attach_documents != false, class: 'toggle', disabled: configs['attach_documents'] == false }, 'true', 'false' %>
</submit-form>
</div>
<div class="flex items-center justify-between pt-2.5 px-1 mb-2">
<span>
<%= t('attach_audit_log_pdf_to_the_email') %>
</span>
<submit-form data-on="change" class="flex">
<%= ff.check_box :completed_notification_email_attach_audit, { checked: ff.object.completed_notification_email_attach_audit != false, class: 'toggle', disabled: configs['attach_audit_log'] == false }, 'true', 'false' %>
</submit-form>
</div>
<div class="flex items-center justify-between py-2.5 px-1 mb-2">
<span>
<%= t('send_emails_automatically_on_completion') %>
</span>
<submit-form data-on="change" class="flex">
<%= ff.check_box :completed_notification_email_enabled, { checked: ff.object.completed_notification_email_enabled != false, class: 'toggle', disabled: configs['enabled'] == false }, 'true', 'false' %>
</submit-form>
</div>
<% end %>
<div class="form-control pt-2">
<%= f.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button' %>
<div class="flex justify-center">
<span id="email_saved_alert3" class="text-sm invisible font-normal mt-0.5"><%= t('changes_have_been_saved') %></span>
</div>
</div>
<% end %>
<%= render 'templates_preferences/submitter_completed_email_form' %>
</div>
</div>
</div>
@@ -359,9 +169,9 @@
<span>
<%= t('share_template_with_test_mode') %>
</span>
<submit-form data-on="change" class="flex">
<%= f.check_box :value, class: 'toggle', checked: @template.template_sharings.exists?(account_id: current_account.testing_accounts) %>
</submit-form>
<submit-form data-on="change" class="flex">
<%= f.check_box :value, class: 'toggle', checked: @template.template_sharings.exists?(account_id: current_account.testing_accounts) %>
</submit-form>
</div>
<% end %>
<div class="mb-4">
+42
View File
@@ -290,6 +290,7 @@ en: &en
invalid_timeserver: Invalid Timeserver
email_templates: Email Templates
signature_request_email: Signature request email
signature_request_reminder_email: Signature request reminder email
signature_request_sms: Signature Request SMS
verification_code_sms: Verification Code SMS
completed_notification_email: Completed notification email
@@ -837,6 +838,11 @@ en: &en
connect_google_drive: Connect Google Drive
google_drive_has_been_connected: Google Drive has been connected
unable_to_identify_reset_your_password_to_sign_in: Unable to identify. Reset your password to sign in.
desktop: Desktop
mobile: Mobile
tablet: Tablet
reset_default: Reset default
send_signature_request_email: Send signature request email
submission_sources:
api: API
bulk: Bulk Send
@@ -1212,6 +1218,7 @@ es: &es
invalid_timeserver: Servidor de tiempo inválido
email_templates: Plantillas de correo electrónico
signature_request_email: Correo de solicitud de firma
signature_request_reminder_email: Correo de recordatorio de solicitud de firma
signature_request_sms: SMS de solicitud de firma
verification_code_sms: SMS de código de verificación
completed_notification_email: Correo de notificación de formulario completado
@@ -1758,6 +1765,11 @@ es: &es
connect_google_drive: Conectar Google Drive
google_drive_has_been_connected: Google Drive se ha conectado
unable_to_identify_reset_your_password_to_sign_in: No se pudo identificar. Restablece tu contraseña para iniciar sesión.
desktop: Escritorio
mobile: Móvil
tablet: Tableta
reset_default: Restablecer por defecto
send_signature_request_email: Enviar correo de solicitud de firma
submission_sources:
api: API
bulk: Envío masivo
@@ -2133,6 +2145,7 @@ it: &it
invalid_timeserver: Server di timestamp non valido
email_templates: Modelli email
signature_request_email: Email di richiesta di firma
signature_request_reminder_email: Email di promemoria di richiesta di firma
signature_request_sms: SMS di richiesta di firma
verification_code_sms: SMS con codice di verifica
completed_notification_email: Email di notifica di completamento
@@ -2680,6 +2693,11 @@ it: &it
connect_google_drive: Connetti Google Drive
google_drive_has_been_connected: Google Drive è stato connesso
unable_to_identify_reset_your_password_to_sign_in: Impossibile identificare. Reimposta la password per accedere.
desktop: Desktop
mobile: Mobile
tablet: Tablet
reset_default: Reimposta predefinito
send_signature_request_email: Invia email di richiesta firma
submission_sources:
api: API
bulk: Invio massivo
@@ -3056,6 +3074,7 @@ fr: &fr
invalid_timeserver: Serveur dhorodatage invalide
email_templates: Modèles demail
signature_request_email: Email de demande de signature
signature_request_reminder_email: Email de rappel de demande de signature
signature_request_sms: SMS de demande de signature
verification_code_sms: SMS de code de vérification
completed_notification_email: Email de notification de finalisation
@@ -3599,6 +3618,11 @@ fr: &fr
connect_google_drive: Connecter Google Drive
google_drive_has_been_connected: Google Drive a été connecté
unable_to_identify_reset_your_password_to_sign_in: Impossible didentifier. Réinitialisez votre mot de passe pour vous connecter.
desktop: Bureau
mobile: Mobile
tablet: Tablette
reset_default: Réinitialiser par défaut
send_signature_request_email: Envoyer un e-mail de demande de signature
submission_sources:
api: API
bulk: Envoi en masse
@@ -3975,6 +3999,7 @@ pt: &pt
invalid_timeserver: Servidor de carimbo de tempo inválido
email_templates: Modelos de e-mail
signature_request_email: E-mail de solicitação de assinatura
signature_request_reminder_email: E-mail de lembrete de solicitação de assinatura
signature_request_sms: SMS de solicitação de assinatura
verification_code_sms: SMS com código de verificação
completed_notification_email: E-mail de notificação de submissão concluída
@@ -4522,6 +4547,11 @@ pt: &pt
connect_google_drive: Conectar Google Drive
google_drive_has_been_connected: O Google Drive foi conectado
unable_to_identify_reset_your_password_to_sign_in: Não foi possível identificar. Redefina sua senha para fazer login.
desktop: Computador
mobile: Celular
tablet: Tablet
reset_default: Redefinir para padrão
send_signature_request_email: Enviar e-mail de solicitação de assinatura
submission_sources:
api: API
bulk: Envio em massa
@@ -4898,6 +4928,7 @@ de: &de
invalid_timeserver: Ungültiger Zeitstempelserver
email_templates: E-Mail-Vorlagen
signature_request_email: E-Mail für Signaturanfrage
signature_request_reminder_email: E-Mail-Erinnerung für Signaturanfrage
signature_request_sms: SMS für Signaturanfrage
verification_code_sms: SMS mit Verifizierungscode
completed_notification_email: E-Mail-Benachrichtigung bei Abschluss
@@ -5445,6 +5476,11 @@ de: &de
connect_google_drive: Google Drive verbinden
google_drive_has_been_connected: Google Drive wurde verbunden
unable_to_identify_reset_your_password_to_sign_in: Identifizierung nicht möglich. Setzen Sie Ihr Passwort zurück, um sich anzumelden.
desktop: Desktop
mobile: Mobil
tablet: Tablet
reset_default: Standard zurücksetzen
send_signature_request_email: Signaturanfrage-E-Mail senden
submission_sources:
api: API
bulk: Massenversand
@@ -6186,6 +6222,7 @@ nl: &nl
invalid_timeserver: Ongeldige tijdserver
email_templates: E-mailsjablonen
signature_request_email: E-mail voor handtekeningverzoek
signature_request_reminder_email: E-mailherinnering voor handtekeningverzoek
signature_request_sms: SMS voor handtekeningverzoek
verification_code_sms: Verificatiecode-SMS
completed_notification_email: E-mailmelding voltooid
@@ -6729,6 +6766,11 @@ nl: &nl
connect_google_drive: Verbind Google Drive
google_drive_has_been_connected: Google Drive is verbonden
unable_to_identify_reset_your_password_to_sign_in: Kan niet worden geïdentificeerd. Stel je wachtwoord opnieuw in om in te loggen.
desktop: Desktop
mobile: Mobiel
tablet: Tablet
reset_default: Standaard herstellen
send_signature_request_email: E-mail met handtekeningaanvraag verzenden
submission_sources:
api: API
bulk: Bulkverzending
+3 -5
View File
@@ -98,11 +98,9 @@ Rails.application.routes.draw do
resources :submissions_filters, only: %i[show], param: 'name'
resources :templates, only: %i[new create edit update show destroy] do
resource :debug, only: %i[show], controller: 'templates_debug' if Rails.env.development?
resources :documents, only: %i[create], controller: 'template_documents'
resources :documents, only: %i[index create], controller: 'template_documents'
resources :clone_and_replace, only: %i[create], controller: 'templates_clone_and_replace'
if !Docuseal.multitenant? || Docuseal.demo?
resources :detect_fields, only: %i[create], controller: 'templates_detect_fields'
end
resources :detect_fields, only: %i[create], controller: 'templates_detect_fields' unless Docuseal.multitenant?
resources :restore, only: %i[create], controller: 'templates_restore'
resources :archived, only: %i[index], controller: 'templates_archived_submissions'
resources :submissions, only: %i[new create]
@@ -110,7 +108,7 @@ Rails.application.routes.draw do
resource :preview, only: %i[show], controller: 'templates_preview'
resource :form, only: %i[show], controller: 'templates_form_preview'
resource :code_modal, only: %i[show], controller: 'templates_code_modal'
resource :preferences, only: %i[show create], controller: 'templates_preferences'
resource :preferences, only: %i[show create destroy], controller: 'templates_preferences'
resource :share_link, only: %i[show create], controller: 'templates_share_link'
resources :recipients, only: %i[create], controller: 'templates_recipients'
resources :prefillable_fields, only: %i[create], controller: 'templates_prefillable_fields'
+37
View File
@@ -0,0 +1,37 @@
# frozen_string_literal: true
module DetectBrowserDevice
module_function
MOBILE_USER_AGENT_REGEXP = /
iPhone |
iPod |
Android.*Mobile|
Opera\ Mini |
Opera\ Mobi |
webOS |
IEMobile |
Windows\ Phone |
BlackBerry |
BB10 |
Mobile
/ix
TABLET_USER_AGENT_REGEXP = /
iPad |
Android(?!.*Mobile)|
Tablet |
Kindle |
PlayBook |
Silk
/ix
def call(user_agent)
return if user_agent.blank?
return 'mobile' if MOBILE_USER_AGENT_REGEXP.match?(user_agent)
return 'tablet' if TABLET_USER_AGENT_REGEXP.match?(user_agent)
'desktop'
end
end
+1
View File
@@ -39,6 +39,7 @@ Puma::Plugin.create do
configs = Sidekiq.configure_embed do |config|
config.logger.level = Logger::INFO
sidekiq_config = YAML.load_file('config/sidekiq.yml')
sidekiq_config['queues'] << 'fields' if ENV['DEMO'] == 'true'
config.queues = sidekiq_config['queues']
config.concurrency = ENV.fetch('SIDEKIQ_THREADS', 5).to_i
config.merge!(sidekiq_config)
+3 -1
View File
@@ -23,6 +23,8 @@ module Submissions
RTL_REGEXP = TextUtils::RTL_REGEXP
MAX_IMAGE_HEIGHT = 100
CHECKSUM_LIMIT = 30
US_TIMEZONES = TimeUtils::US_TIMEZONES
module_function
@@ -216,7 +218,7 @@ module Submissions
composer.document.layout.formatted_text_box(
[
{ text: "#{I18n.t('original_sha256')}:\n", font: [FONT_NAME, { variant: :bold }] },
original_documents.map { |d| d.metadata['sha256'] || d.checksum }.join("\n"),
original_documents.map { |d| d.metadata['sha256'] || d.checksum }.first(CHECKSUM_LIMIT).join("\n"),
"\n",
{ text: "#{I18n.t('result_sha256')}:\n", font: [FONT_NAME, { variant: :bold }] },
document.metadata['sha256'] || document.checksum,
+14 -4
View File
@@ -719,20 +719,30 @@ module Submissions
begin
pdf.sign(io, write_options: { validate: false }, **sign_params)
rescue HexaPDF::MalformedPDFError, NoMethodError => e
rescue HexaPDF::Error, NoMethodError => e
Rollbar.error(e) if defined?(Rollbar)
pdf.sign(io, write_options: { validate: false, incremental: false }, **sign_params)
begin
pdf.sign(io, write_options: { validate: false, incremental: false }, **sign_params)
rescue HexaPDF::Error
pdf.validate(auto_correct: true)
pdf.sign(io, write_options: { validate: false, incremental: false }, **sign_params)
end
end
maybe_enable_ltv(io, sign_params)
else
begin
pdf.write(io, incremental: true, validate: false)
rescue HexaPDF::MalformedPDFError, NoMethodError => e
rescue HexaPDF::Error, NoMethodError => e
Rollbar.error(e) if defined?(Rollbar)
pdf.write(io, incremental: false, validate: false)
begin
pdf.write(io, incremental: false, validate: false)
rescue HexaPDF::Error
pdf.validate(auto_correct: true)
pdf.write(io, incremental: false, validate: false)
end
end
end
+2
View File
@@ -20,6 +20,8 @@ module Submitters
def call(text, font: nil)
font = FONT_ALIASES[font] || font
text = ERB::Util.html_escape(text)
text_image = Vips::Image.text(text, font:, fontfile: FONTS[font],
width: WIDTH, height: HEIGHT, wrap: :none)
+236 -21
View File
@@ -5,17 +5,63 @@ module Templates
module_function
TextFieldBox = Struct.new(:x, :y, :w, :h, keyword_init: true)
PageNode = Struct.new(:prev, :next, :elem, :page, :attachment_uuid, keyword_init: true)
# rubocop:disable Metrics
DATE_REGEXP = /
(?:
date
| signed\sat
| datum
)
\s*[:-]?\s*\z
/ix
NUMBER_REGEXP = /
(?:
price
| \$
| €
| total
| quantity
| prix
| quantité
| preis
| summe
| gesamt(?:betrag)?
| menge
| anzahl
| stückzahl
)
\s*[:-]?\s*\z
/ix
SIGNATURE_REGEXP = /
(?:
signature
| sign\shere
| sign
| signez\sici
| signer\sici
| unterschrift
| unterschreiben
| unterzeichnen
)
\s*[:-]?\s*\z
/ix
# rubocop:disable Metrics, Style
def call(io, attachment: nil, confidence: 0.3, temperature: 1, inference: Templates::ImageToFields,
nms: 0.1, split_page: false, aspect_ratio: true, padding: 20, &)
if attachment&.image?
process_image_attachment(io, attachment:, confidence:, nms:, split_page:, inference:,
temperature:, aspect_ratio:, padding:, &)
else
process_pdf_attachment(io, attachment:, confidence:, nms:, split_page:, inference:,
temperature:, aspect_ratio:, padding:, &)
end
nms: 0.1, split_page: false, aspect_ratio: true, padding: 20, regexp_type: true, &)
fields, head_node =
if attachment&.image?
process_image_attachment(io, attachment:, confidence:, nms:, split_page:, inference:,
temperature:, aspect_ratio:, padding:, &)
else
process_pdf_attachment(io, attachment:, confidence:, nms:, split_page:, inference:,
temperature:, aspect_ratio:, regexp_type:, padding:, &)
end
[fields, head_node]
end
def process_image_attachment(io, attachment:, confidence:, nms:, temperature:, inference:,
@@ -29,7 +75,7 @@ module Templates
{
uuid: SecureRandom.uuid,
type: f.type,
required: true,
required: f.type != 'checkbox',
preferences: {},
areas: [{
x: f.x,
@@ -44,21 +90,24 @@ module Templates
yield [attachment&.uuid, 0, fields] if block_given?
fields
[fields, nil]
end
def process_pdf_attachment(io, attachment:, confidence:, nms:, temperature:, inference:,
split_page: false, aspect_ratio: false, padding: nil)
split_page: false, aspect_ratio: false, padding: nil, regexp_type: false)
doc = Pdfium::Document.open_bytes(io.read)
doc.page_count.times.flat_map do |page_number|
head_node = PageNode.new(elem: ''.b, page: 0, attachment_uuid: attachment&.uuid)
tail_node = head_node
fields = doc.page_count.times.flat_map do |page_number|
page = doc.get_page(page_number)
data, width, height = page.render_to_bitmap(width: inference::RESOLUTION * 1.5)
image = Vips::Image.new_from_memory(data, width, height, 4, :uchar)
fields = inference.call(image, confidence: 0.05, nms:, split_page:,
fields = inference.call(image, confidence: confidence / 4.0, nms:, split_page:,
temperature:, aspect_ratio:, padding:)
text_fields = extract_text_fields_from_page(page)
@@ -67,17 +116,23 @@ module Templates
fields = increase_confidence_for_overlapping_fields(fields, text_fields)
fields = increase_confidence_for_overlapping_fields(fields, line_fields)
fields = fields.filter_map do |f|
next if f.confidence < confidence
fields = fields.reject { |f| f.confidence < confidence }
field_nodes, tail_node = build_page_nodes(page, fields, tail_node, attachment_uuid: attachment&.uuid)
fields = field_nodes.map do |node|
field = node.elem
type = regexp_type ? type_from_page_node(node) : field.type
{
uuid: SecureRandom.uuid,
type: f.type,
required: true,
type:,
required: type != 'checkbox',
preferences: {},
areas: [{
x: f.x, y: f.y,
w: f.w, h: f.h,
x: field.x, y: field.y,
w: field.w, h: field.h,
page: page_number,
attachment_uuid: attachment&.uuid
}]
@@ -90,10 +145,170 @@ module Templates
ensure
page.close
end
print_debug(head_node) if Rails.env.development?
[fields, head_node]
ensure
doc.close
end
def print_debug(head_node)
current_node = head_node
index = 0
string = ''.b
loop do
string <<
if current_node.elem.is_a?(String)
current_node.elem
else
"[#{current_node.elem.type == 'checkbox' ? 'Checkbox' : 'Field'}_#{index += 1}]"
end
current_node = current_node.next
break unless current_node
end
Rails.logger.info(string)
end
def type_from_page_node(node)
return node.elem.type unless node.prev.elem.is_a?(String)
return node.elem.type unless node.elem.type == 'text'
string = node.prev.elem
return 'date' if string.match?(DATE_REGEXP)
return 'signature' if string.match?(SIGNATURE_REGEXP)
return 'number' if string.match?(NUMBER_REGEXP)
return 'text'
end
def build_page_nodes(page, fields, tail_node, attachment_uuid: nil)
field_nodes = []
current_text = ''.b
text_nodes = page.text_nodes
text_idx = 0
field_idx = 0
while text_idx < text_nodes.length || field_idx < fields.length
text_node = text_nodes[text_idx]
field = fields[field_idx]
process_text_node = false
process_field_node = false
if text_node && field
text_y_center = text_node.y + (text_node.h / 2.0)
field_y_center = field.y + (field.h / 2.0)
y_threshold = text_node.h / 2.0
vertical_distance = (text_y_center - field_y_center).abs
if vertical_distance < y_threshold
is_underscore = text_node.content == '_'
is_left_of_field = text_node.x < field.x
if is_underscore && is_left_of_field
text_x_end = text_node.x + text_node.w
distance = field.x - text_x_end
proximity_threshold = text_node.w * 3.0
if distance < proximity_threshold
process_field_node = true
else
process_text_node = true
end
elsif is_left_of_field
process_text_node = true
else
process_field_node = true
end
elsif text_node.y < field.y
process_text_node = true
else
process_field_node = true
end
elsif text_node
process_text_node = true
elsif field
process_field_node = true
end
if process_field_node
unless current_text.empty?
new_text_node = PageNode.new(prev: tail_node, elem: current_text, page: page.page_index, attachment_uuid:)
tail_node.next = new_text_node
tail_node = new_text_node
current_text = ''.b
end
new_field_node = PageNode.new(prev: tail_node, elem: field, page: page.page_index, attachment_uuid:)
tail_node.next = new_field_node
tail_node = new_field_node
field_nodes << tail_node
while text_idx < text_nodes.length
text_node_to_check = text_nodes[text_idx]
is_part_of_field = false
if text_node_to_check.content == '_'
check_y_center = text_node_to_check.y + (text_node_to_check.h / 2.0)
check_y_dist = (check_y_center - field_y_center).abs
check_y_thresh = text_node_to_check.h / 2.0
if check_y_dist < check_y_thresh
padding = text_node_to_check.w * 3.0
field_x_start = field.x - padding
field_x_end = field.x + field.w + padding
text_x_start = text_node_to_check.x
text_x_end = text_node_to_check.x + text_node_to_check.w
is_part_of_field = true if text_x_start <= field_x_end && field_x_start <= text_x_end
end
end
break unless is_part_of_field
text_idx += 1
end
field_idx += 1
elsif process_text_node
if text_idx > 0
prev_text_node = text_nodes[text_idx - 1]
x_gap = text_node.x - (prev_text_node.x + prev_text_node.w)
gap_w = text_node.w > prev_text_node.w ? text_node.w : prev_text_node.w
current_text << ' ' if x_gap > gap_w * 2
end
current_text << text_node.content
text_idx += 1
end
end
unless current_text.empty?
new_text_node = PageNode.new(prev: tail_node, elem: current_text, page: page.page_index, attachment_uuid:)
tail_node.next = new_text_node
tail_node = new_text_node
end
[field_nodes, tail_node]
end
def extract_line_fields_from_page(page)
line_thickness = 5.0 / page.height
@@ -265,6 +480,6 @@ module Templates
image_fields
end
# rubocop:enable Metrics
# rubocop:enable Metrics, Style
end
end
+13 -7
View File
@@ -37,10 +37,10 @@ module Templates
transform_info[:trim_offset_x] = base_offset_x
transform_info[:trim_offset_y] = base_offset_y + r[:offset_y]
outputs = model.predict({ 'input' => input_tensor })
outputs = model.predict({ 'input' => input_tensor }, output_type: :numo)
boxes = Numo::SFloat.cast(outputs['dets'])[0, true, true]
logits = Numo::SFloat.cast(outputs['labels'])[0, true, true]
boxes = outputs['dets'][0, true, true]
logits = outputs['labels'][0, true, true]
postprocess_outputs(boxes, logits, transform_info, acc, confidence:, temperature:, resolution:)
end
@@ -50,10 +50,10 @@ module Templates
transform_info[:trim_offset_x] = base_offset_x
transform_info[:trim_offset_y] = base_offset_y
outputs = model.predict({ 'input' => input_tensor })
outputs = model.predict({ 'input' => input_tensor }, output_type: :numo)
boxes = Numo::SFloat.cast(outputs['dets'])[0, true, true]
logits = Numo::SFloat.cast(outputs['labels'])[0, true, true]
boxes = outputs['dets'][0, true, true]
logits = outputs['labels'][0, true, true]
detections = postprocess_outputs(boxes, logits, transform_info, confidence:, temperature:, resolution:)
end
@@ -77,7 +77,7 @@ module Templates
end
def build_fields_from_detections(detections, image)
Array.new(detections[:xyxy].shape[0]) do |i|
detections[:xyxy].shape[0].times.filter_map do |i|
x1 = detections[:xyxy][i, 0]
y1 = detections[:xyxy][i, 1]
x2 = detections[:xyxy][i, 2]
@@ -92,6 +92,12 @@ module Templates
x1_norm = x2 / image.width.to_f
y1_norm = y2 / image.height.to_f
x1_norm = 1 if x1_norm > 1
y1_norm = 1 if y1_norm > 1
next if x0_norm < 0 || x0_norm > 1
next if y0_norm < 0 || y0_norm > 1
type_name = ID_TO_CLASS[class_id]
Field.new(