mirror of
https://github.com/docusealco/docuseal
synced 2026-11-05 06:42:32 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
22afd01fd2 | ||
|
|
fd9eb09dd6 | ||
|
|
c53ed7f984 | ||
|
|
1d87e210be | ||
|
|
5fec952b8c | ||
|
|
c4fbcc103a | ||
|
|
9333a1ba45 | ||
|
|
de5cd92dba | ||
|
|
83e32bec9f | ||
|
|
4603efc04e | ||
|
|
6c73c82a27 | ||
|
|
8e71d9f9e9 | ||
|
|
80f60a5d88 | ||
|
|
2c33ec382d | ||
|
|
884865cffc | ||
|
|
9537fe8c91 | ||
|
|
c8fe36a2b6 | ||
|
|
7ec3282b56 | ||
|
|
ab8c41a520 | ||
|
|
4c1c70ff04 | ||
|
|
d7a6e80bb1 | ||
|
|
f87ef670d1 | ||
|
|
daba2505bc | ||
|
|
71528eda45 | ||
|
|
269e2beeaa | ||
|
|
72dd2c6bc5 | ||
|
|
9758f7a15f |
No files matched your search
@@ -45,6 +45,9 @@ Metrics/PerceivedComplexity:
|
|||||||
Style/MultipleComparison:
|
Style/MultipleComparison:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
|
Style/NumericPredicate:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
Naming/PredicateMethod:
|
Naming/PredicateMethod:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ class PersonalizationSettingsController < ApplicationController
|
|||||||
ALLOWED_KEYS = [
|
ALLOWED_KEYS = [
|
||||||
AccountConfig::FORM_COMPLETED_BUTTON_KEY,
|
AccountConfig::FORM_COMPLETED_BUTTON_KEY,
|
||||||
AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY,
|
AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY,
|
||||||
|
AccountConfig::SUBMITTER_INVITATION_REMINDER_EMAIL_KEY,
|
||||||
AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY,
|
AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY,
|
||||||
AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY,
|
AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY,
|
||||||
AccountConfig::FORM_COMPLETED_MESSAGE_KEY,
|
AccountConfig::FORM_COMPLETED_MESSAGE_KEY,
|
||||||
|
|||||||
@@ -3,6 +3,12 @@
|
|||||||
class TemplateDocumentsController < ApplicationController
|
class TemplateDocumentsController < ApplicationController
|
||||||
load_and_authorize_resource :template
|
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
|
def create
|
||||||
if params[:blobs].blank? && params[:files].blank?
|
if params[:blobs].blank? && params[:files].blank?
|
||||||
return render json: { error: I18n.t('file_is_missing') }, status: :unprocessable_content
|
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)
|
fields = Templates::FindAcroFields.call(pdf, attachment, data)
|
||||||
end
|
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'] ||= {}
|
||||||
attachment.metadata['pdf']['fields'] = fields
|
attachment.metadata['pdf']['fields'] = fields
|
||||||
|
|||||||
@@ -3,6 +3,15 @@
|
|||||||
class TemplatesPreferencesController < ApplicationController
|
class TemplatesPreferencesController < ApplicationController
|
||||||
load_and_authorize_resource :template
|
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 show; end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@@ -15,19 +24,38 @@ class TemplatesPreferencesController < ApplicationController
|
|||||||
head :ok
|
head :ok
|
||||||
end
|
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
|
private
|
||||||
|
|
||||||
def template_params
|
def template_params
|
||||||
params.require(:template).permit(
|
params.require(:template).permit(
|
||||||
preferences: %i[bcc_completed request_email_subject request_email_body
|
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_subject documents_copy_email_body
|
||||||
documents_copy_email_enabled documents_copy_email_attach_audit
|
documents_copy_email_enabled documents_copy_email_attach_audit
|
||||||
documents_copy_email_attach_documents documents_copy_email_reply_to
|
documents_copy_email_attach_documents documents_copy_email_reply_to
|
||||||
completed_notification_email_attach_documents
|
completed_notification_email_attach_documents
|
||||||
completed_redirect_url validate_unique_submitters
|
completed_redirect_url validate_unique_submitters
|
||||||
require_all_submitters submitters_order require_phone_2fa
|
require_all_submitters submitters_order require_phone_2fa
|
||||||
default_expire_at_duration shared_link_2fa
|
default_expire_at_duration shared_link_2fa default_expire_at request_email_enabled
|
||||||
default_expire_at request_email_enabled
|
|
||||||
completed_notification_email_subject completed_notification_email_body
|
completed_notification_email_subject completed_notification_email_body
|
||||||
completed_notification_email_enabled completed_notification_email_attach_audit] +
|
completed_notification_email_enabled completed_notification_email_attach_audit] +
|
||||||
[completed_message: %i[title body],
|
[completed_message: %i[title body],
|
||||||
|
|||||||
@@ -24,11 +24,20 @@ class UsersController < ApplicationController
|
|||||||
def edit; end
|
def edit; end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
if User.accessible_by(current_ability).exists?(email: @user.email)
|
existing_user = User.accessible_by(current_ability).find_by(email: @user.email)
|
||||||
|
|
||||||
|
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'))
|
@user.errors.add(:email, I18n.t('already_exists'))
|
||||||
|
|
||||||
return render turbo_stream: turbo_stream.replace(:modal, template: 'users/new'), status: :unprocessable_content
|
return render turbo_stream: turbo_stream.replace(:modal, template: 'users/new'), status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
@user.password = SecureRandom.hex if @user.password.blank?
|
@user.password = SecureRandom.hex if @user.password.blank?
|
||||||
@user.role = User::ADMIN_ROLE unless role_valid?(@user.role)
|
@user.role = User::ADMIN_ROLE unless role_valid?(@user.role)
|
||||||
|
|||||||
@@ -167,6 +167,7 @@ safeRegisterElement('template-builder', class extends HTMLElement {
|
|||||||
withConditions: this.dataset.withConditions === 'true',
|
withConditions: this.dataset.withConditions === 'true',
|
||||||
withGoogleDrive: this.dataset.withGoogleDrive === 'true',
|
withGoogleDrive: this.dataset.withGoogleDrive === 'true',
|
||||||
withReplaceAndCloneUpload: true,
|
withReplaceAndCloneUpload: true,
|
||||||
|
withDownload: true,
|
||||||
currencies: (this.dataset.currencies || '').split(',').filter(Boolean),
|
currencies: (this.dataset.currencies || '').split(',').filter(Boolean),
|
||||||
acceptFileTypes: this.dataset.acceptFileTypes,
|
acceptFileTypes: this.dataset.acceptFileTypes,
|
||||||
showTourStartForm: this.dataset.showTourStartForm === 'true'
|
showTourStartForm: this.dataset.showTourStartForm === 'true'
|
||||||
|
|||||||
@@ -211,7 +211,7 @@
|
|||||||
@input="updateWrittenSignature"
|
@input="updateWrittenSignature"
|
||||||
>
|
>
|
||||||
<select
|
<select
|
||||||
v-if="requireSigningReason && !isOtherReason"
|
v-if="withSigningReason && !isOtherReason"
|
||||||
class="select base-input !text-2xl w-full mt-6 text-center"
|
class="select base-input !text-2xl w-full mt-6 text-center"
|
||||||
:class="{ 'text-gray-300': !reason }"
|
:class="{ 'text-gray-300': !reason }"
|
||||||
required
|
required
|
||||||
@@ -226,6 +226,18 @@
|
|||||||
>
|
>
|
||||||
{{ t('select_a_reason') }}
|
{{ t('select_a_reason') }}
|
||||||
</option>
|
</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
|
<option
|
||||||
v-for="(label, option) in defaultReasons"
|
v-for="(label, option) in defaultReasons"
|
||||||
:key="option"
|
:key="option"
|
||||||
@@ -241,9 +253,10 @@
|
|||||||
>
|
>
|
||||||
{{ t('other') }}
|
{{ t('other') }}
|
||||||
</option>
|
</option>
|
||||||
|
</template>
|
||||||
</select>
|
</select>
|
||||||
<input
|
<input
|
||||||
v-if="requireSigningReason && isOtherReason"
|
v-if="withSigningReason && isOtherReason"
|
||||||
class="base-input !text-2xl w-full mt-6"
|
class="base-input !text-2xl w-full mt-6"
|
||||||
required
|
required
|
||||||
:name="`values[${field.preferences.reason_field_uuid}]`"
|
:name="`values[${field.preferences.reason_field_uuid}]`"
|
||||||
@@ -253,7 +266,7 @@
|
|||||||
@input="$emit('update:reason', $event.target.value)"
|
@input="$emit('update:reason', $event.target.value)"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
v-if="requireSigningReason"
|
v-if="withSigningReason"
|
||||||
hidden
|
hidden
|
||||||
name="with_reason"
|
name="with_reason"
|
||||||
:value="field.preferences.reason_field_uuid"
|
:value="field.preferences.reason_field_uuid"
|
||||||
@@ -406,6 +419,9 @@ export default {
|
|||||||
format () {
|
format () {
|
||||||
return this.field.preferences?.format
|
return this.field.preferences?.format
|
||||||
},
|
},
|
||||||
|
withSigningReason () {
|
||||||
|
return this.requireSigningReason || this.field.preferences?.reasons?.length
|
||||||
|
},
|
||||||
defaultReasons () {
|
defaultReasons () {
|
||||||
return {
|
return {
|
||||||
[this.t('approved_by')]: this.t('approved'),
|
[this.t('approved_by')]: this.t('approved'),
|
||||||
@@ -424,10 +440,11 @@ export default {
|
|||||||
created () {
|
created () {
|
||||||
this.isSignatureStarted = !!this.computedPreviousValue
|
this.isSignatureStarted = !!this.computedPreviousValue
|
||||||
|
|
||||||
if (this.requireSigningReason) {
|
if (this.withSigningReason) {
|
||||||
this.field.preferences ||= {}
|
this.field.preferences ||= {}
|
||||||
this.field.preferences.reason_field_uuid ||= v4()
|
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 () {
|
async mounted () {
|
||||||
|
|||||||
@@ -175,7 +175,10 @@
|
|||||||
{{ t('save') }}
|
{{ t('save') }}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<div class="dropdown dropdown-end">
|
<div
|
||||||
|
class="dropdown dropdown-end"
|
||||||
|
:class="{ 'dropdown-open': isDownloading }"
|
||||||
|
>
|
||||||
<label
|
<label
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
class="base-button !rounded-l-none !pl-1 !pr-2 !border-l-neutral-500"
|
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>
|
<span class="whitespace-nowrap">{{ t('preferences') }}</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
@@ -457,6 +484,7 @@
|
|||||||
:show-tour-start-form="showTourStartForm"
|
:show-tour-start-form="showTourStartForm"
|
||||||
@add-field="addField"
|
@add-field="addField"
|
||||||
@set-draw="[drawField = $event.field, drawOption = $event.option]"
|
@set-draw="[drawField = $event.field, drawOption = $event.option]"
|
||||||
|
@select-submitter="selectedSubmitter = $event"
|
||||||
@set-draw-type="[drawFieldType = $event, showDrawField = true]"
|
@set-draw-type="[drawFieldType = $event, showDrawField = true]"
|
||||||
@set-drag="dragField = $event"
|
@set-drag="dragField = $event"
|
||||||
@set-drag-placeholder="$refs.dragPlaceholder.dragPlaceholder = $event"
|
@set-drag-placeholder="$refs.dragPlaceholder.dragPlaceholder = $event"
|
||||||
@@ -511,7 +539,7 @@ import DocumentPreview from './preview'
|
|||||||
import DocumentControls from './controls'
|
import DocumentControls from './controls'
|
||||||
import MobileFields from './mobile_fields'
|
import MobileFields from './mobile_fields'
|
||||||
import FieldSubmitter from './field_submitter'
|
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 { v4 } from 'uuid'
|
||||||
import { ref, computed, toRaw } from 'vue'
|
import { ref, computed, toRaw } from 'vue'
|
||||||
import * as i18n from './i18n'
|
import * as i18n from './i18n'
|
||||||
@@ -537,6 +565,7 @@ export default {
|
|||||||
Contenteditable,
|
Contenteditable,
|
||||||
IconUsersPlus,
|
IconUsersPlus,
|
||||||
IconChevronDown,
|
IconChevronDown,
|
||||||
|
IconDownload,
|
||||||
IconAdjustments,
|
IconAdjustments,
|
||||||
IconEye,
|
IconEye,
|
||||||
IconDeviceFloppy
|
IconDeviceFloppy
|
||||||
@@ -584,6 +613,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: null
|
default: null
|
||||||
},
|
},
|
||||||
|
withDownload: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
backgroundColor: {
|
backgroundColor: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -805,6 +839,7 @@ export default {
|
|||||||
return {
|
return {
|
||||||
documentRefs: [],
|
documentRefs: [],
|
||||||
isBreakpointLg: false,
|
isBreakpointLg: false,
|
||||||
|
isDownloading: false,
|
||||||
isLoadingBlankPage: false,
|
isLoadingBlankPage: false,
|
||||||
isSaving: false,
|
isSaving: false,
|
||||||
selectedSubmitter: null,
|
selectedSubmitter: null,
|
||||||
@@ -963,6 +998,75 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
toRaw,
|
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) {
|
onDragover (e) {
|
||||||
if (this.$refs.dragPlaceholder?.dragPlaceholder) {
|
if (this.$refs.dragPlaceholder?.dragPlaceholder) {
|
||||||
this.$refs.dragPlaceholder.isMask = e.target.id === 'mask'
|
this.$refs.dragPlaceholder.isMask = e.target.id === 'mask'
|
||||||
|
|||||||
@@ -222,12 +222,21 @@
|
|||||||
width="22"
|
width="22"
|
||||||
class="animate-spin"
|
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_') }}
|
{{ fieldPagesLoaded }} / {{ numberOfPages }} {{ t('processing_') }}
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<IconListSearch width="22" />
|
<IconSparkles width="22" />
|
||||||
<span
|
<span
|
||||||
class="hidden md:inline"
|
class="hidden md:inline"
|
||||||
>
|
>
|
||||||
@@ -259,7 +268,7 @@
|
|||||||
import Field from './field'
|
import Field from './field'
|
||||||
import FieldType from './field_type'
|
import FieldType from './field_type'
|
||||||
import FieldSubmitter from './field_submitter'
|
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'
|
import IconDrag from './icon_drag'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -268,7 +277,7 @@ export default {
|
|||||||
Field,
|
Field,
|
||||||
FieldType,
|
FieldType,
|
||||||
IconCirclePlus,
|
IconCirclePlus,
|
||||||
IconListSearch,
|
IconSparkles,
|
||||||
IconInnerShadowTop,
|
IconInnerShadowTop,
|
||||||
FieldSubmitter,
|
FieldSubmitter,
|
||||||
IconDrag,
|
IconDrag,
|
||||||
@@ -363,10 +372,11 @@ export default {
|
|||||||
default: false
|
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 () {
|
data () {
|
||||||
return {
|
return {
|
||||||
fieldPagesLoaded: null,
|
fieldPagesLoaded: null,
|
||||||
|
analyzingProgress: 0,
|
||||||
defaultFieldsSearch: ''
|
defaultFieldsSearch: ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -448,8 +458,6 @@ export default {
|
|||||||
while (true) {
|
while (true) {
|
||||||
const { value, done } = await reader.read()
|
const { value, done } = await reader.read()
|
||||||
|
|
||||||
if (done) break
|
|
||||||
|
|
||||||
buffer += decoder.decode(value, { stream: true })
|
buffer += decoder.decode(value, { stream: true })
|
||||||
|
|
||||||
const lines = buffer.split('\n\n')
|
const lines = buffer.split('\n\n')
|
||||||
@@ -462,12 +470,27 @@ export default {
|
|||||||
const data = JSON.parse(jsonStr)
|
const data = JSON.parse(jsonStr)
|
||||||
|
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
|
if ((data.fields || fields).length) {
|
||||||
|
this.template.fields = data.fields || fields
|
||||||
|
|
||||||
|
this.save()
|
||||||
|
} else {
|
||||||
alert(data.error)
|
alert(data.error)
|
||||||
|
}
|
||||||
|
|
||||||
break
|
break
|
||||||
|
} else if (data.analyzing) {
|
||||||
|
this.analyzingProgress = data.progress
|
||||||
} else if (data.completed) {
|
} else if (data.completed) {
|
||||||
this.fieldPagesLoaded = null
|
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()
|
this.save()
|
||||||
|
|
||||||
break
|
break
|
||||||
@@ -484,11 +507,14 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (done) break
|
||||||
}
|
}
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
console.error('Error in streaming message: ', error)
|
console.error('Error in streaming message: ', error)
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
this.fieldPagesLoaded = null
|
this.fieldPagesLoaded = null
|
||||||
|
this.analyzingProgress = null
|
||||||
this.isFieldsLoading = false
|
this.isFieldsLoading = false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
const en = {
|
const en = {
|
||||||
|
analyzing_: 'Analyzing...',
|
||||||
|
download: 'Download',
|
||||||
|
downloading_: 'Downloading...',
|
||||||
view: 'View',
|
view: 'View',
|
||||||
autodetect_fields: 'Autodetect fields',
|
autodetect_fields: 'Autodetect fields',
|
||||||
payment_link: 'Payment link',
|
payment_link: 'Payment link',
|
||||||
@@ -185,6 +188,10 @@ const en = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const es = {
|
const es = {
|
||||||
|
autodetect_fields: 'Autodetectar campos',
|
||||||
|
analyzing_: 'Analizando...',
|
||||||
|
download: 'Descargar',
|
||||||
|
downloading_: 'Descargando...',
|
||||||
view: 'Vista',
|
view: 'Vista',
|
||||||
payment_link: 'Enlace de pago',
|
payment_link: 'Enlace de pago',
|
||||||
strikeout: 'Tachar',
|
strikeout: 'Tachar',
|
||||||
@@ -370,6 +377,10 @@ const es = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const it = {
|
const it = {
|
||||||
|
autodetect_fields: 'Rileva campi',
|
||||||
|
analyzing_: 'Analisi...',
|
||||||
|
download: 'Scarica',
|
||||||
|
downloading_: 'Download in corso...',
|
||||||
view: 'Vista',
|
view: 'Vista',
|
||||||
payment_link: 'Link di pagamento',
|
payment_link: 'Link di pagamento',
|
||||||
strikeout: 'Barrato',
|
strikeout: 'Barrato',
|
||||||
@@ -555,6 +566,10 @@ const it = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pt = {
|
const pt = {
|
||||||
|
autodetect_fields: 'Detectar campos',
|
||||||
|
analyzing_: 'Analisando...',
|
||||||
|
download: 'Baixar',
|
||||||
|
downloading_: 'Baixando...',
|
||||||
view: 'Visualizar',
|
view: 'Visualizar',
|
||||||
payment_link: 'Link de pagamento',
|
payment_link: 'Link de pagamento',
|
||||||
strikeout: 'Tachado',
|
strikeout: 'Tachado',
|
||||||
@@ -740,6 +755,10 @@ const pt = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fr = {
|
const fr = {
|
||||||
|
autodetect_fields: 'Détecter les champs',
|
||||||
|
analyzing_: 'Analyse...',
|
||||||
|
download: 'Télécharger',
|
||||||
|
downloading_: 'Téléchargement...',
|
||||||
view: 'Voir',
|
view: 'Voir',
|
||||||
payment_link: 'Lien de paiement',
|
payment_link: 'Lien de paiement',
|
||||||
strikeout: 'Rature',
|
strikeout: 'Rature',
|
||||||
@@ -925,6 +944,10 @@ const fr = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const de = {
|
const de = {
|
||||||
|
autodetect_fields: 'Felder erkennen',
|
||||||
|
analyzing_: 'Analysiere...',
|
||||||
|
download: 'Download',
|
||||||
|
downloading_: 'Download...',
|
||||||
view: 'Anzeigen',
|
view: 'Anzeigen',
|
||||||
payment_link: 'Zahlungslink',
|
payment_link: 'Zahlungslink',
|
||||||
strikeout: 'Durchstreichen',
|
strikeout: 'Durchstreichen',
|
||||||
@@ -1110,6 +1133,10 @@ const de = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const nl = {
|
const nl = {
|
||||||
|
autodetect_fields: 'Velden detecteren',
|
||||||
|
analyzing_: 'Analyseren...',
|
||||||
|
download: 'Downloaden',
|
||||||
|
downloading_: 'Downloaden...',
|
||||||
view: 'Bekijken',
|
view: 'Bekijken',
|
||||||
payment_link: 'Betaallink',
|
payment_link: 'Betaallink',
|
||||||
strikeout: 'Doorhalen',
|
strikeout: 'Doorhalen',
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ class SendSubmitterInvitationEmailJob
|
|||||||
submitter = Submitter.find(params['submitter_id'])
|
submitter = Submitter.find(params['submitter_id'])
|
||||||
|
|
||||||
return if submitter.completed_at?
|
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)
|
return if submitter.submission.source == 'invite' && !Accounts.can_send_emails?(submitter.account, on_events: true)
|
||||||
|
|
||||||
unless Accounts.can_send_invitation_emails?(submitter.account)
|
unless Accounts.can_send_invitation_emails?(submitter.account)
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
#
|
#
|
||||||
class AccountConfig < ApplicationRecord
|
class AccountConfig < ApplicationRecord
|
||||||
SUBMITTER_INVITATION_EMAIL_KEY = 'submitter_invitation_email'
|
SUBMITTER_INVITATION_EMAIL_KEY = 'submitter_invitation_email'
|
||||||
|
SUBMITTER_INVITATION_REMINDER_EMAIL_KEY = 'submitter_invitation_reminder_email'
|
||||||
SUBMITTER_COMPLETED_EMAIL_KEY = 'submitter_completed_email'
|
SUBMITTER_COMPLETED_EMAIL_KEY = 'submitter_completed_email'
|
||||||
SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY = 'submitter_documents_copy_email'
|
SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY = 'submitter_documents_copy_email'
|
||||||
BCC_EMAILS = 'bcc_emails'
|
BCC_EMAILS = 'bcc_emails'
|
||||||
@@ -59,6 +60,12 @@ class AccountConfig < ApplicationRecord
|
|||||||
'body' => I18n.t(:submitter_invitation_email_sign_body)
|
'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 {
|
SUBMITTER_COMPLETED_EMAIL_KEY => lambda {
|
||||||
{
|
{
|
||||||
'subject' => I18n.t(:template_name_has_been_completed_by_submitters),
|
'subject' => I18n.t(:template_name_has_been_completed_by_submitters),
|
||||||
|
|||||||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 %>">
|
<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') %>
|
<%= svg_icon(SubmissionEventsController::SUBMISSION_EVENT_ICONS.fetch(event.event_type, 'circle_dot'), class: 'w-4 h-4') %>
|
||||||
</span>
|
</span>
|
||||||
<p class="text-sm leading-none text-base-content/60 pt-1.5">
|
<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) %>
|
<%= l(event.event_timestamp.in_time_zone(current_account.timezone), format: :long, locale: current_account.locale) %>
|
||||||
</p>
|
</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">
|
<p class="text-base-content/80 mt-1">
|
||||||
<% if event.event_type == 'complete_verification' %>
|
<% if event.event_type == 'complete_verification' %>
|
||||||
<%= t('submission_event_names.complete_verification_by_html', provider: event.data['method'], submitter_name:) %>
|
<%= t('submission_event_names.complete_verification_by_html', provider: event.data['method'], submitter_name:) %>
|
||||||
|
|||||||
@@ -1,116 +1 @@
|
|||||||
<% submitter_preferences_index = template&.preferences&.dig('submitters').to_a.index_by { |e| e['uuid'] } %>
|
<%= render partial: 'submissions/send_email_base', locals: local_assigns %>
|
||||||
<% 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>
|
|
||||||
@@ -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>
|
||||||
@@ -81,7 +81,7 @@
|
|||||||
<% document = @submission.schema_documents.find { |a| item['attachment_uuid'] == a.uuid } %>
|
<% document = @submission.schema_documents.find { |a| item['attachment_uuid'] == a.uuid } %>
|
||||||
<% if document.preview_images.first %>
|
<% if document.preview_images.first %>
|
||||||
<scroll-to data-selector-id="page-<%= document.uuid %>-0" class="block cursor-pointer">
|
<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">
|
<div class="pb-2 pt-1.5 text-center" dir="auto">
|
||||||
<%= item['name'].presence || document.filename.base %>
|
<%= item['name'].presence || document.filename.base %>
|
||||||
</div>
|
</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| %>
|
<% (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 = 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'] %>">
|
<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">
|
<div class="top-0 bottom-0 left-0 right-0 absolute">
|
||||||
<% document_annots_index[index]&.each do |annot| %>
|
<% document_annots_index[index]&.each do |annot| %>
|
||||||
<%= render 'submissions/annotation', 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>
|
||||||
Whitespace-only changes.
@@ -98,157 +98,17 @@
|
|||||||
<%= t('signature_request_email') %>
|
<%= t('signature_request_email') %>
|
||||||
</div>
|
</div>
|
||||||
<div class="collapse-content">
|
<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| %>
|
<%= render 'templates_preferences/submitter_invitation_email_form' %>
|
||||||
<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 %>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<%= render 'templates_preferences/submitter_invitation_reminder_email_collapse' %>
|
||||||
<div class="collapse collapse-arrow join-item border border-base-300">
|
<div class="collapse collapse-arrow join-item border border-base-300">
|
||||||
<input type="checkbox" name="accordion">
|
<input type="checkbox" name="accordion">
|
||||||
<div class="collapse-title text-xl font-medium">
|
<div class="collapse-title text-xl font-medium">
|
||||||
<%= t('documents_copy_email') %>
|
<%= t('documents_copy_email') %>
|
||||||
</div>
|
</div>
|
||||||
<div class="collapse-content">
|
<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| %>
|
<%= render 'templates_preferences/submitter_documents_copy_email_form' %>
|
||||||
<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 %>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="collapse collapse-arrow join-item border border-base-300">
|
<div class="collapse collapse-arrow join-item border border-base-300">
|
||||||
@@ -257,57 +117,7 @@
|
|||||||
<%= t('completed_notification_email') %>
|
<%= t('completed_notification_email') %>
|
||||||
</div>
|
</div>
|
||||||
<div class="collapse-content">
|
<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| %>
|
<%= render 'templates_preferences/submitter_completed_email_form' %>
|
||||||
<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 %>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -290,6 +290,7 @@ en: &en
|
|||||||
invalid_timeserver: Invalid Timeserver
|
invalid_timeserver: Invalid Timeserver
|
||||||
email_templates: Email Templates
|
email_templates: Email Templates
|
||||||
signature_request_email: Signature request email
|
signature_request_email: Signature request email
|
||||||
|
signature_request_reminder_email: Signature request reminder email
|
||||||
signature_request_sms: Signature Request SMS
|
signature_request_sms: Signature Request SMS
|
||||||
verification_code_sms: Verification Code SMS
|
verification_code_sms: Verification Code SMS
|
||||||
completed_notification_email: Completed notification email
|
completed_notification_email: Completed notification email
|
||||||
@@ -837,6 +838,11 @@ en: &en
|
|||||||
connect_google_drive: Connect Google Drive
|
connect_google_drive: Connect Google Drive
|
||||||
google_drive_has_been_connected: Google Drive has been connected
|
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.
|
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:
|
submission_sources:
|
||||||
api: API
|
api: API
|
||||||
bulk: Bulk Send
|
bulk: Bulk Send
|
||||||
@@ -1212,6 +1218,7 @@ es: &es
|
|||||||
invalid_timeserver: Servidor de tiempo inválido
|
invalid_timeserver: Servidor de tiempo inválido
|
||||||
email_templates: Plantillas de correo electrónico
|
email_templates: Plantillas de correo electrónico
|
||||||
signature_request_email: Correo de solicitud de firma
|
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
|
signature_request_sms: SMS de solicitud de firma
|
||||||
verification_code_sms: SMS de código de verificación
|
verification_code_sms: SMS de código de verificación
|
||||||
completed_notification_email: Correo de notificación de formulario completado
|
completed_notification_email: Correo de notificación de formulario completado
|
||||||
@@ -1758,6 +1765,11 @@ es: &es
|
|||||||
connect_google_drive: Conectar Google Drive
|
connect_google_drive: Conectar Google Drive
|
||||||
google_drive_has_been_connected: Google Drive se ha conectado
|
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.
|
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:
|
submission_sources:
|
||||||
api: API
|
api: API
|
||||||
bulk: Envío masivo
|
bulk: Envío masivo
|
||||||
@@ -2133,6 +2145,7 @@ it: &it
|
|||||||
invalid_timeserver: Server di timestamp non valido
|
invalid_timeserver: Server di timestamp non valido
|
||||||
email_templates: Modelli email
|
email_templates: Modelli email
|
||||||
signature_request_email: Email di richiesta di firma
|
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
|
signature_request_sms: SMS di richiesta di firma
|
||||||
verification_code_sms: SMS con codice di verifica
|
verification_code_sms: SMS con codice di verifica
|
||||||
completed_notification_email: Email di notifica di completamento
|
completed_notification_email: Email di notifica di completamento
|
||||||
@@ -2680,6 +2693,11 @@ it: &it
|
|||||||
connect_google_drive: Connetti Google Drive
|
connect_google_drive: Connetti Google Drive
|
||||||
google_drive_has_been_connected: Google Drive è stato connesso
|
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.
|
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:
|
submission_sources:
|
||||||
api: API
|
api: API
|
||||||
bulk: Invio massivo
|
bulk: Invio massivo
|
||||||
@@ -3056,6 +3074,7 @@ fr: &fr
|
|||||||
invalid_timeserver: Serveur d’horodatage invalide
|
invalid_timeserver: Serveur d’horodatage invalide
|
||||||
email_templates: Modèles d’e‑mail
|
email_templates: Modèles d’e‑mail
|
||||||
signature_request_email: E‑mail de demande de signature
|
signature_request_email: E‑mail de demande de signature
|
||||||
|
signature_request_reminder_email: E‑mail de rappel de demande de signature
|
||||||
signature_request_sms: SMS de demande de signature
|
signature_request_sms: SMS de demande de signature
|
||||||
verification_code_sms: SMS de code de vérification
|
verification_code_sms: SMS de code de vérification
|
||||||
completed_notification_email: E‑mail de notification de finalisation
|
completed_notification_email: E‑mail de notification de finalisation
|
||||||
@@ -3599,6 +3618,11 @@ fr: &fr
|
|||||||
connect_google_drive: Connecter Google Drive
|
connect_google_drive: Connecter Google Drive
|
||||||
google_drive_has_been_connected: Google Drive a été connecté
|
google_drive_has_been_connected: Google Drive a été connecté
|
||||||
unable_to_identify_reset_your_password_to_sign_in: Impossible d’identifier. Réinitialisez votre mot de passe pour vous connecter.
|
unable_to_identify_reset_your_password_to_sign_in: Impossible d’identifier. 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:
|
submission_sources:
|
||||||
api: API
|
api: API
|
||||||
bulk: Envoi en masse
|
bulk: Envoi en masse
|
||||||
@@ -3975,6 +3999,7 @@ pt: &pt
|
|||||||
invalid_timeserver: Servidor de carimbo de tempo inválido
|
invalid_timeserver: Servidor de carimbo de tempo inválido
|
||||||
email_templates: Modelos de e-mail
|
email_templates: Modelos de e-mail
|
||||||
signature_request_email: E-mail de solicitação de assinatura
|
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
|
signature_request_sms: SMS de solicitação de assinatura
|
||||||
verification_code_sms: SMS com código de verificação
|
verification_code_sms: SMS com código de verificação
|
||||||
completed_notification_email: E-mail de notificação de submissão concluída
|
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
|
connect_google_drive: Conectar Google Drive
|
||||||
google_drive_has_been_connected: O Google Drive foi conectado
|
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.
|
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:
|
submission_sources:
|
||||||
api: API
|
api: API
|
||||||
bulk: Envio em massa
|
bulk: Envio em massa
|
||||||
@@ -4898,6 +4928,7 @@ de: &de
|
|||||||
invalid_timeserver: Ungültiger Zeitstempelserver
|
invalid_timeserver: Ungültiger Zeitstempelserver
|
||||||
email_templates: E-Mail-Vorlagen
|
email_templates: E-Mail-Vorlagen
|
||||||
signature_request_email: E-Mail für Signaturanfrage
|
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
|
signature_request_sms: SMS für Signaturanfrage
|
||||||
verification_code_sms: SMS mit Verifizierungscode
|
verification_code_sms: SMS mit Verifizierungscode
|
||||||
completed_notification_email: E-Mail-Benachrichtigung bei Abschluss
|
completed_notification_email: E-Mail-Benachrichtigung bei Abschluss
|
||||||
@@ -5445,6 +5476,11 @@ de: &de
|
|||||||
connect_google_drive: Google Drive verbinden
|
connect_google_drive: Google Drive verbinden
|
||||||
google_drive_has_been_connected: Google Drive wurde verbunden
|
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.
|
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:
|
submission_sources:
|
||||||
api: API
|
api: API
|
||||||
bulk: Massenversand
|
bulk: Massenversand
|
||||||
@@ -6186,6 +6222,7 @@ nl: &nl
|
|||||||
invalid_timeserver: Ongeldige tijdserver
|
invalid_timeserver: Ongeldige tijdserver
|
||||||
email_templates: E-mailsjablonen
|
email_templates: E-mailsjablonen
|
||||||
signature_request_email: E-mail voor handtekeningverzoek
|
signature_request_email: E-mail voor handtekeningverzoek
|
||||||
|
signature_request_reminder_email: E-mailherinnering voor handtekeningverzoek
|
||||||
signature_request_sms: SMS voor handtekeningverzoek
|
signature_request_sms: SMS voor handtekeningverzoek
|
||||||
verification_code_sms: Verificatiecode-SMS
|
verification_code_sms: Verificatiecode-SMS
|
||||||
completed_notification_email: E-mailmelding voltooid
|
completed_notification_email: E-mailmelding voltooid
|
||||||
@@ -6729,6 +6766,11 @@ nl: &nl
|
|||||||
connect_google_drive: Verbind Google Drive
|
connect_google_drive: Verbind Google Drive
|
||||||
google_drive_has_been_connected: Google Drive is verbonden
|
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.
|
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:
|
submission_sources:
|
||||||
api: API
|
api: API
|
||||||
bulk: Bulkverzending
|
bulk: Bulkverzending
|
||||||
|
|||||||
+3
-5
@@ -98,11 +98,9 @@ Rails.application.routes.draw do
|
|||||||
resources :submissions_filters, only: %i[show], param: 'name'
|
resources :submissions_filters, only: %i[show], param: 'name'
|
||||||
resources :templates, only: %i[new create edit update show destroy] do
|
resources :templates, only: %i[new create edit update show destroy] do
|
||||||
resource :debug, only: %i[show], controller: 'templates_debug' if Rails.env.development?
|
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'
|
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' unless Docuseal.multitenant?
|
||||||
resources :detect_fields, only: %i[create], controller: 'templates_detect_fields'
|
|
||||||
end
|
|
||||||
resources :restore, only: %i[create], controller: 'templates_restore'
|
resources :restore, only: %i[create], controller: 'templates_restore'
|
||||||
resources :archived, only: %i[index], controller: 'templates_archived_submissions'
|
resources :archived, only: %i[index], controller: 'templates_archived_submissions'
|
||||||
resources :submissions, only: %i[new create]
|
resources :submissions, only: %i[new create]
|
||||||
@@ -110,7 +108,7 @@ Rails.application.routes.draw do
|
|||||||
resource :preview, only: %i[show], controller: 'templates_preview'
|
resource :preview, only: %i[show], controller: 'templates_preview'
|
||||||
resource :form, only: %i[show], controller: 'templates_form_preview'
|
resource :form, only: %i[show], controller: 'templates_form_preview'
|
||||||
resource :code_modal, only: %i[show], controller: 'templates_code_modal'
|
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'
|
resource :share_link, only: %i[show create], controller: 'templates_share_link'
|
||||||
resources :recipients, only: %i[create], controller: 'templates_recipients'
|
resources :recipients, only: %i[create], controller: 'templates_recipients'
|
||||||
resources :prefillable_fields, only: %i[create], controller: 'templates_prefillable_fields'
|
resources :prefillable_fields, only: %i[create], controller: 'templates_prefillable_fields'
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -39,6 +39,7 @@ Puma::Plugin.create do
|
|||||||
configs = Sidekiq.configure_embed do |config|
|
configs = Sidekiq.configure_embed do |config|
|
||||||
config.logger.level = Logger::INFO
|
config.logger.level = Logger::INFO
|
||||||
sidekiq_config = YAML.load_file('config/sidekiq.yml')
|
sidekiq_config = YAML.load_file('config/sidekiq.yml')
|
||||||
|
sidekiq_config['queues'] << 'fields' if ENV['DEMO'] == 'true'
|
||||||
config.queues = sidekiq_config['queues']
|
config.queues = sidekiq_config['queues']
|
||||||
config.concurrency = ENV.fetch('SIDEKIQ_THREADS', 5).to_i
|
config.concurrency = ENV.fetch('SIDEKIQ_THREADS', 5).to_i
|
||||||
config.merge!(sidekiq_config)
|
config.merge!(sidekiq_config)
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ module Submissions
|
|||||||
RTL_REGEXP = TextUtils::RTL_REGEXP
|
RTL_REGEXP = TextUtils::RTL_REGEXP
|
||||||
MAX_IMAGE_HEIGHT = 100
|
MAX_IMAGE_HEIGHT = 100
|
||||||
|
|
||||||
|
CHECKSUM_LIMIT = 30
|
||||||
|
|
||||||
US_TIMEZONES = TimeUtils::US_TIMEZONES
|
US_TIMEZONES = TimeUtils::US_TIMEZONES
|
||||||
|
|
||||||
module_function
|
module_function
|
||||||
@@ -216,7 +218,7 @@ module Submissions
|
|||||||
composer.document.layout.formatted_text_box(
|
composer.document.layout.formatted_text_box(
|
||||||
[
|
[
|
||||||
{ text: "#{I18n.t('original_sha256')}:\n", font: [FONT_NAME, { variant: :bold }] },
|
{ 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",
|
"\n",
|
||||||
{ text: "#{I18n.t('result_sha256')}:\n", font: [FONT_NAME, { variant: :bold }] },
|
{ text: "#{I18n.t('result_sha256')}:\n", font: [FONT_NAME, { variant: :bold }] },
|
||||||
document.metadata['sha256'] || document.checksum,
|
document.metadata['sha256'] || document.checksum,
|
||||||
|
|||||||
@@ -719,20 +719,30 @@ module Submissions
|
|||||||
|
|
||||||
begin
|
begin
|
||||||
pdf.sign(io, write_options: { validate: false }, **sign_params)
|
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)
|
Rollbar.error(e) if defined?(Rollbar)
|
||||||
|
|
||||||
|
begin
|
||||||
pdf.sign(io, write_options: { validate: false, incremental: false }, **sign_params)
|
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
|
end
|
||||||
|
|
||||||
maybe_enable_ltv(io, sign_params)
|
maybe_enable_ltv(io, sign_params)
|
||||||
else
|
else
|
||||||
begin
|
begin
|
||||||
pdf.write(io, incremental: true, validate: false)
|
pdf.write(io, incremental: true, validate: false)
|
||||||
rescue HexaPDF::MalformedPDFError, NoMethodError => e
|
rescue HexaPDF::Error, NoMethodError => e
|
||||||
Rollbar.error(e) if defined?(Rollbar)
|
Rollbar.error(e) if defined?(Rollbar)
|
||||||
|
|
||||||
|
begin
|
||||||
pdf.write(io, incremental: false, validate: false)
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ module Submitters
|
|||||||
def call(text, font: nil)
|
def call(text, font: nil)
|
||||||
font = FONT_ALIASES[font] || font
|
font = FONT_ALIASES[font] || font
|
||||||
|
|
||||||
|
text = ERB::Util.html_escape(text)
|
||||||
|
|
||||||
text_image = Vips::Image.text(text, font:, fontfile: FONTS[font],
|
text_image = Vips::Image.text(text, font:, fontfile: FONTS[font],
|
||||||
width: WIDTH, height: HEIGHT, wrap: :none)
|
width: WIDTH, height: HEIGHT, wrap: :none)
|
||||||
|
|
||||||
|
|||||||
+230
-15
@@ -5,17 +5,63 @@ module Templates
|
|||||||
module_function
|
module_function
|
||||||
|
|
||||||
TextFieldBox = Struct.new(:x, :y, :w, :h, keyword_init: true)
|
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,
|
def call(io, attachment: nil, confidence: 0.3, temperature: 1, inference: Templates::ImageToFields,
|
||||||
nms: 0.1, split_page: false, aspect_ratio: true, padding: 20, &)
|
nms: 0.1, split_page: false, aspect_ratio: true, padding: 20, regexp_type: true, &)
|
||||||
|
fields, head_node =
|
||||||
if attachment&.image?
|
if attachment&.image?
|
||||||
process_image_attachment(io, attachment:, confidence:, nms:, split_page:, inference:,
|
process_image_attachment(io, attachment:, confidence:, nms:, split_page:, inference:,
|
||||||
temperature:, aspect_ratio:, padding:, &)
|
temperature:, aspect_ratio:, padding:, &)
|
||||||
else
|
else
|
||||||
process_pdf_attachment(io, attachment:, confidence:, nms:, split_page:, inference:,
|
process_pdf_attachment(io, attachment:, confidence:, nms:, split_page:, inference:,
|
||||||
temperature:, aspect_ratio:, padding:, &)
|
temperature:, aspect_ratio:, regexp_type:, padding:, &)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
[fields, head_node]
|
||||||
end
|
end
|
||||||
|
|
||||||
def process_image_attachment(io, attachment:, confidence:, nms:, temperature:, inference:,
|
def process_image_attachment(io, attachment:, confidence:, nms:, temperature:, inference:,
|
||||||
@@ -29,7 +75,7 @@ module Templates
|
|||||||
{
|
{
|
||||||
uuid: SecureRandom.uuid,
|
uuid: SecureRandom.uuid,
|
||||||
type: f.type,
|
type: f.type,
|
||||||
required: true,
|
required: f.type != 'checkbox',
|
||||||
preferences: {},
|
preferences: {},
|
||||||
areas: [{
|
areas: [{
|
||||||
x: f.x,
|
x: f.x,
|
||||||
@@ -44,21 +90,24 @@ module Templates
|
|||||||
|
|
||||||
yield [attachment&.uuid, 0, fields] if block_given?
|
yield [attachment&.uuid, 0, fields] if block_given?
|
||||||
|
|
||||||
fields
|
[fields, nil]
|
||||||
end
|
end
|
||||||
|
|
||||||
def process_pdf_attachment(io, attachment:, confidence:, nms:, temperature:, inference:,
|
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 = 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)
|
page = doc.get_page(page_number)
|
||||||
|
|
||||||
data, width, height = page.render_to_bitmap(width: inference::RESOLUTION * 1.5)
|
data, width, height = page.render_to_bitmap(width: inference::RESOLUTION * 1.5)
|
||||||
|
|
||||||
image = Vips::Image.new_from_memory(data, width, height, 4, :uchar)
|
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:)
|
temperature:, aspect_ratio:, padding:)
|
||||||
|
|
||||||
text_fields = extract_text_fields_from_page(page)
|
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, text_fields)
|
||||||
fields = increase_confidence_for_overlapping_fields(fields, line_fields)
|
fields = increase_confidence_for_overlapping_fields(fields, line_fields)
|
||||||
|
|
||||||
fields = fields.filter_map do |f|
|
fields = fields.reject { |f| f.confidence < confidence }
|
||||||
next if 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,
|
uuid: SecureRandom.uuid,
|
||||||
type: f.type,
|
type:,
|
||||||
required: true,
|
required: type != 'checkbox',
|
||||||
preferences: {},
|
preferences: {},
|
||||||
areas: [{
|
areas: [{
|
||||||
x: f.x, y: f.y,
|
x: field.x, y: field.y,
|
||||||
w: f.w, h: f.h,
|
w: field.w, h: field.h,
|
||||||
page: page_number,
|
page: page_number,
|
||||||
attachment_uuid: attachment&.uuid
|
attachment_uuid: attachment&.uuid
|
||||||
}]
|
}]
|
||||||
@@ -90,10 +145,170 @@ module Templates
|
|||||||
ensure
|
ensure
|
||||||
page.close
|
page.close
|
||||||
end
|
end
|
||||||
|
|
||||||
|
print_debug(head_node) if Rails.env.development?
|
||||||
|
|
||||||
|
[fields, head_node]
|
||||||
ensure
|
ensure
|
||||||
doc.close
|
doc.close
|
||||||
end
|
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)
|
def extract_line_fields_from_page(page)
|
||||||
line_thickness = 5.0 / page.height
|
line_thickness = 5.0 / page.height
|
||||||
|
|
||||||
@@ -265,6 +480,6 @@ module Templates
|
|||||||
|
|
||||||
image_fields
|
image_fields
|
||||||
end
|
end
|
||||||
# rubocop:enable Metrics
|
# rubocop:enable Metrics, Style
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -37,10 +37,10 @@ module Templates
|
|||||||
transform_info[:trim_offset_x] = base_offset_x
|
transform_info[:trim_offset_x] = base_offset_x
|
||||||
transform_info[:trim_offset_y] = base_offset_y + r[:offset_y]
|
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]
|
boxes = outputs['dets'][0, true, true]
|
||||||
logits = Numo::SFloat.cast(outputs['labels'])[0, true, true]
|
logits = outputs['labels'][0, true, true]
|
||||||
|
|
||||||
postprocess_outputs(boxes, logits, transform_info, acc, confidence:, temperature:, resolution:)
|
postprocess_outputs(boxes, logits, transform_info, acc, confidence:, temperature:, resolution:)
|
||||||
end
|
end
|
||||||
@@ -50,10 +50,10 @@ module Templates
|
|||||||
transform_info[:trim_offset_x] = base_offset_x
|
transform_info[:trim_offset_x] = base_offset_x
|
||||||
transform_info[:trim_offset_y] = base_offset_y
|
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]
|
boxes = outputs['dets'][0, true, true]
|
||||||
logits = Numo::SFloat.cast(outputs['labels'])[0, true, true]
|
logits = outputs['labels'][0, true, true]
|
||||||
|
|
||||||
detections = postprocess_outputs(boxes, logits, transform_info, confidence:, temperature:, resolution:)
|
detections = postprocess_outputs(boxes, logits, transform_info, confidence:, temperature:, resolution:)
|
||||||
end
|
end
|
||||||
@@ -77,7 +77,7 @@ module Templates
|
|||||||
end
|
end
|
||||||
|
|
||||||
def build_fields_from_detections(detections, image)
|
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]
|
x1 = detections[:xyxy][i, 0]
|
||||||
y1 = detections[:xyxy][i, 1]
|
y1 = detections[:xyxy][i, 1]
|
||||||
x2 = detections[:xyxy][i, 2]
|
x2 = detections[:xyxy][i, 2]
|
||||||
@@ -92,6 +92,12 @@ module Templates
|
|||||||
x1_norm = x2 / image.width.to_f
|
x1_norm = x2 / image.width.to_f
|
||||||
y1_norm = y2 / image.height.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]
|
type_name = ID_TO_CLASS[class_id]
|
||||||
|
|
||||||
Field.new(
|
Field.new(
|
||||||
|
|||||||
Reference in new issue
Block a user