Merge from docusealco/wip

pull/493/merge 2.0.9
Alex Turchyn 3 months ago committed by GitHub
commit 26749a4dbc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -1,9 +1,17 @@
name: Build Docker Images
on:
push:
tags:
- "*.*.*"
workflow_dispatch:
inputs:
version:
description: Version
type: string
required: true
image:
description: QEMU image
type: string
required: false
default: tonistiigi/binfmt:latest
jobs:
build:
@ -16,23 +24,23 @@ jobs:
with:
submodules: recursive
-
name: Docker meta
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
with:
images: |
docuseal/docuseal
tags: |
type=semver,pattern={{version}}
images: docuseal/docuseal
tags: type=semver,pattern={{version}}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
image: ${{ inputs.image }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Create .version file
run: echo ${{ github.ref_name }} > .version
run: echo ${{ inputs.version }} > .version
- name: Login to Docker Hub
uses: docker/login-action@v3

@ -319,7 +319,7 @@ GEM
method_source (1.1.0)
mini_magick (4.13.2)
mini_mime (1.1.5)
mini_portile2 (2.8.8)
mini_portile2 (2.8.9)
minitest (5.25.4)
msgpack (1.7.5)
multi_json (1.15.0)
@ -338,18 +338,18 @@ GEM
net-smtp (0.5.0)
net-protocol
nio4r (2.7.4)
nokogiri (1.18.8)
nokogiri (1.18.9)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
nokogiri (1.18.8-aarch64-linux-gnu)
nokogiri (1.18.9-aarch64-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.8-aarch64-linux-musl)
nokogiri (1.18.9-aarch64-linux-musl)
racc (~> 1.4)
nokogiri (1.18.8-arm64-darwin)
nokogiri (1.18.9-arm64-darwin)
racc (~> 1.4)
nokogiri (1.18.8-x86_64-linux-gnu)
nokogiri (1.18.9-x86_64-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.8-x86_64-linux-musl)
nokogiri (1.18.9-x86_64-linux-musl)
racc (~> 1.4)
oj (3.16.8)
bigdecimal (>= 3.0)
@ -541,7 +541,7 @@ GEM
stringio (3.1.2)
strip_attributes (1.14.1)
activemodel (>= 3.0, < 9.0)
thor (1.3.2)
thor (1.4.0)
timeout (0.4.3)
trailblazer-option (0.1.2)
turbo-rails (2.0.11)

@ -109,7 +109,7 @@ module Api
submitters: [%i[name uuid is_requester invite_by_uuid optional_invite_by_uuid linked_to_uuid email]],
fields: [[:uuid, :submitter_uuid, :name, :type,
:required, :readonly, :default_value,
:title, :description,
:title, :description, :prefillable,
{ preferences: {},
conditions: [%i[field_uuid value action operation]],
options: [%i[value uuid]],

@ -32,10 +32,7 @@ class SubmissionsController < ApplicationController
def create
save_template_message(@template, params) if params[:save_message] == '1'
if params[:is_custom_message] != '1'
params.delete(:subject)
params.delete(:body)
end
[params.delete(:subject), params.delete(:body)] if params[:is_custom_message] != '1'
submissions =
if params[:emails].present?
@ -46,11 +43,16 @@ class SubmissionsController < ApplicationController
emails: params[:emails],
params: params.merge('send_completed_email' => true))
else
submissions_attrs = submissions_params[:submission].to_h.values
submissions_attrs, =
Submissions::NormalizeParamUtils.normalize_submissions_params!(submissions_attrs, @template)
Submissions.create_from_submitters(template: @template,
user: current_user,
source: :invite,
submitters_order: params[:preserve_order] == '1' ? 'preserved' : 'random',
submissions_attrs: submissions_params[:submission].to_h.values,
submissions_attrs:,
params: params.merge('send_completed_email' => true))
end
@ -62,9 +64,8 @@ class SubmissionsController < ApplicationController
redirect_to template_path(@template), notice: I18n.t('new_recipients_have_been_added')
rescue Submissions::CreateFromSubmitters::BaseError => e
render turbo_stream: turbo_stream.replace(:submitters_error,
partial: 'submissions/error',
locals: { error: e.message }),
render turbo_stream: turbo_stream.replace(:submitters_error, partial: 'submissions/error',
locals: { error: e.message }),
status: :unprocessable_entity
end
@ -95,7 +96,7 @@ class SubmissionsController < ApplicationController
end
def submissions_params
params.permit(submission: { submitters: [%i[uuid email phone name]] })
params.permit(submission: { submitters: [:uuid, :email, :phone, :name, { values: {} }] })
end
def load_template

@ -120,7 +120,7 @@ class TemplatesController < ApplicationController
submitters: [%i[name uuid is_requester linked_to_uuid invite_by_uuid optional_invite_by_uuid email]],
fields: [[:uuid, :submitter_uuid, :name, :type,
:required, :readonly, :default_value,
:title, :description,
:title, :description, :prefillable,
{ preferences: {},
conditions: [%i[field_uuid value action operation]],
options: [%i[value uuid]],

@ -27,7 +27,7 @@ class TemplatesPreferencesController < ApplicationController
completed_redirect_url validate_unique_submitters
require_all_submitters submitters_order require_phone_2fa
default_expire_at_duration shared_link_2fa
default_expire_at
default_expire_at request_email_enabled
completed_notification_email_subject completed_notification_email_body
completed_notification_email_enabled completed_notification_email_attach_audit] +
[completed_message: %i[title body],

@ -0,0 +1,26 @@
# frozen_string_literal: true
class TemplatesPrefillableFieldsController < ApplicationController
PREFILLABLE_FIELD_TYPES = %w[text number cells date checkbox select radio phone].freeze
load_and_authorize_resource :template
def create
authorize!(:update, @template)
field = @template.fields.find { |f| f['uuid'] == params[:field_uuid] }
if params[:prefillable] == 'false'
field.delete('prefillable')
field.delete('readonly')
elsif params[:prefillable] == 'true'
field['prefillable'] = true
field['readonly'] = true
end
@template.save!
render turbo_stream: turbo_stream.replace(:prefillable_fields_list, partial: 'list',
locals: { template: @template })
end
end

@ -37,6 +37,7 @@ import AppTour from './elements/app_tour'
import DashboardDropzone from './elements/dashboard_dropzone'
import RequiredCheckboxGroup from './elements/required_checkbox_group'
import PageContainer from './elements/page_container'
import EmailEditor from './elements/email_editor'
import * as TurboInstantClick from './lib/turbo_instant_click'
@ -109,6 +110,7 @@ safeRegisterElement('dashboard-dropzone', DashboardDropzone)
safeRegisterElement('check-on-click', CheckOnClick)
safeRegisterElement('required-checkbox-group', RequiredCheckboxGroup)
safeRegisterElement('page-container', PageContainer)
safeRegisterElement('email-editor', EmailEditor)
safeRegisterElement('template-builder', class extends HTMLElement {
connectedCallback () {

@ -0,0 +1,131 @@
import { target, targetable } from '@github/catalyst/lib/targetable'
let loaderPromise = null
function loadCodeMirror () {
if (!loaderPromise) {
loaderPromise = Promise.all([
import(/* webpackChunkName: "email-editor" */ '@codemirror/view'),
import(/* webpackChunkName: "email-editor" */ '@codemirror/commands'),
import(/* webpackChunkName: "email-editor" */ '@codemirror/language'),
import(/* webpackChunkName: "email-editor" */ '@codemirror/lang-html'),
import(/* webpackChunkName: "email-editor" */ '@specious/htmlflow')
]).then(([view, commands, language, html, htmlflow]) => {
return {
minimalSetup: [
commands.history(),
language.syntaxHighlighting(language.defaultHighlightStyle, { fallback: true }),
view.keymap.of([...commands.defaultKeymap, ...commands.historyKeymap])
],
EditorView: view.EditorView,
html: html.html,
htmlflow: htmlflow.default || htmlflow
}
})
}
return loaderPromise
}
export default targetable(class extends HTMLElement {
static [target.static] = [
'codeViewTab',
'previewViewTab',
'editorContainer',
'previewIframe'
]
connectedCallback () {
this.mount()
if (this.input.value) {
this.showPreviewView()
} else {
this.showCodeView()
}
this.previewViewTab.addEventListener('click', this.showPreviewView)
this.codeViewTab.addEventListener('click', this.showCodeView)
}
showCodeView = () => {
this.editorView.dispatch({
changes: { from: 0, to: this.editorView.state.doc.length, insert: this.input.value }
})
this.previewViewTab.classList.remove('tab-active', 'tab-bordered')
this.previewViewTab.classList.add('pb-[3px]')
this.codeViewTab.classList.remove('pb-[3px]')
this.codeViewTab.classList.add('tab-active', 'tab-bordered')
this.editorContainer.classList.remove('hidden')
this.previewIframe.classList.add('hidden')
}
showPreviewView = () => {
this.previewIframe.srcdoc = this.input.value
this.codeViewTab.classList.remove('tab-active', 'tab-bordered')
this.codeViewTab.classList.add('pb-[3px]')
this.previewViewTab.classList.remove('pb-[3px]')
this.previewViewTab.classList.add('tab-active', 'tab-bordered')
this.editorContainer.classList.add('hidden')
this.previewIframe.classList.remove('hidden')
}
async mount () {
this.input = this.querySelector('input[type="hidden"]')
this.input.style.display = 'none'
const { EditorView, minimalSetup, html, htmlflow } = await loadCodeMirror()
this.editorView = new EditorView({
doc: this.input.value,
parent: this.editorContainer,
extensions: [
html(),
minimalSetup,
EditorView.lineWrapping,
EditorView.updateListener.of(update => {
if (update.docChanged) this.input.value = update.state.doc.toString()
}),
EditorView.theme({
'&': {
backgroundColor: 'white',
color: 'black',
fontSize: '14px',
fontFamily: 'monospace'
},
'&.cm-focused': {
outline: 'none'
},
'&.cm-editor': {
borderRadius: '0.375rem',
border: 'none'
},
'.cm-gutters': {
display: 'none'
}
})
]
})
this.previewIframe.srcdoc = this.editorView.state.doc.toString()
this.previewIframe.onload = () => {
const previewIframeDoc = this.previewIframe.contentDocument
if (previewIframeDoc.body) {
previewIframeDoc.body.contentEditable = true
}
const contentDocument = this.previewIframe.contentDocument || this.previewIframe.contentWindow.document
contentDocument.body.addEventListener('input', async () => {
const html = contentDocument.documentElement.outerHTML.replace(' contenteditable="true"', '')
const prettifiedHtml = await htmlflow(html)
this.input.value = prettifiedHtml
})
}
}
})

@ -7,5 +7,9 @@ export default actionable(class extends HTMLElement {
elementIds.forEach((elementId) => {
document.getElementById(elementId).classList.toggle('hidden', (event.target.dataset.toggleId || event.target.value) !== elementId)
})
if (this.dataset.focusId) {
document.getElementById(this.dataset.focusId)?.focus()
}
}
})

@ -112,11 +112,23 @@ export default {
onSelectFiles (e) {
e.preventDefault()
this.uploadFiles(this.$refs.input.files).then(() => {
if (this.$refs.input) {
this.$refs.input.value = ''
const files = Array.from(this.$refs.input.files).filter((f) => {
if (this.accept === 'image/*') {
return f.type.startsWith('image')
} else {
return true
}
})
if (this.accept === 'image/*' && !files.length) {
alert(this.t('please_upload_an_image_file'))
} else {
this.uploadFiles(files).then(() => {
if (this.$refs.input) {
this.$refs.input.value = ''
}
})
}
},
async uploadFiles (files) {
this.isLoading = true

@ -141,6 +141,7 @@
:with-required="false"
:with-areas="false"
:with-signature-id="withSignatureId"
:with-prefillable="withPrefillable"
@click-formula="isShowFormulaModal = true"
@click-font="isShowFontModal = true"
@click-description="isShowDescriptionModal = true"
@ -353,6 +354,11 @@ export default {
required: false,
default: null
},
withPrefillable: {
type: Boolean,
required: false,
default: false
},
defaultSubmitters: {
type: Array,
required: false,

@ -331,6 +331,7 @@
:default-fields="[...defaultRequiredFields, ...defaultFields]"
:allow-draw="!onlyDefinedFields || drawField"
:with-signature-id="withSignatureId"
:with-prefillable="withPrefillable"
:data-document-uuid="document.uuid"
:default-submitters="defaultSubmitters"
:drag-field-placeholder="fieldsDragFieldRef.value || dragField"
@ -438,6 +439,7 @@
:field-types="fieldTypes"
:with-sticky-submitters="withStickySubmitters"
:with-signature-id="withSignatureId"
:with-prefillable="withPrefillable"
:only-defined-fields="onlyDefinedFields"
:editable="editable"
:show-tour-start-form="showTourStartForm"
@ -543,7 +545,6 @@ export default {
withPayment: this.withPayment,
isPaymentConnected: this.isPaymentConnected,
withFormula: this.withFormula,
withSignatureId: this.withSignatureId,
withConditions: this.withConditions,
isInlineSize: this.isInlineSize,
defaultDrawFieldType: this.defaultDrawFieldType,
@ -802,6 +803,13 @@ export default {
language () {
return this.locale.split('-')[0].toLowerCase()
},
withPrefillable () {
if (this.template.fields) {
return this.template.fields.some((f) => f.prefillable)
} else {
return false
}
},
isInlineSize () {
return CSS.supports('container-type: size')
},

@ -11,6 +11,7 @@
:areas="areasIndex[index]"
:allow-draw="allowDraw"
:with-signature-id="withSignatureId"
:with-prefillable="withPrefillable"
:is-drag="isDrag"
:with-field-placeholder="withFieldPlaceholder"
:default-fields="defaultFields"
@ -72,6 +73,11 @@ export default {
required: false,
default: null
},
withPrefillable: {
type: Boolean,
required: false,
default: false
},
drawFieldType: {
type: String,
required: false,

@ -126,6 +126,7 @@
:default-field="defaultField"
:editable="editable"
:with-signature-id="withSignatureId"
:with-prefillable="withPrefillable"
:background-color="dropdownBgColor"
@click-formula="isShowFormulaModal = true"
@click-font="isShowFontModal = true"
@ -308,6 +309,11 @@ export default {
required: false,
default: null
},
withPrefillable: {
type: Boolean,
required: false,
default: false
},
withOptions: {
type: Boolean,
required: false,

@ -406,6 +406,21 @@
<span class="label-text">{{ t('read_only') }}</span>
</label>
</li>
<li
v-if="withPrefillable && ['text', 'number', 'cells', 'date', 'checkbox', 'select', 'radio', 'phone'].includes(field['type'])"
@click.stop
>
<label class="cursor-pointer py-1.5">
<input
v-model="field.prefillable"
type="checkbox"
:disabled="!editable || (defaultField && [true, false].includes(defaultField.prefillable))"
class="toggle toggle-xs"
@update:model-value="save"
>
<span class="label-text">{{ t('prefillable') }}</span>
</label>
</li>
<hr
v-if="field.type != 'stamp'"
class="pb-0.5 mt-0.5"
@ -555,6 +570,11 @@ export default {
required: false,
default: null
},
withPrefillable: {
type: Boolean,
required: false,
default: false
},
withRequired: {
type: Boolean,
required: false,

@ -27,6 +27,7 @@
:type-index="fields.filter((f) => f.type === field.type).indexOf(field)"
:editable="editable"
:with-signature-id="withSignatureId"
:with-prefillable="withPrefillable"
:default-field="defaultFieldsIndex[field.name]"
:draggable="editable"
@dragstart="[fieldsDragFieldRef.value = field, removeDragOverlay($event), setDragPlaceholder($event)]"
@ -259,6 +260,11 @@ export default {
required: false,
default: null
},
withPrefillable: {
type: Boolean,
required: false,
default: false
},
template: {
type: Object,
required: true

@ -2,6 +2,8 @@
<div
v-if="isDragging || isLoading"
class="modal modal-open"
@dragover.prevent
@drop.prevent="$emit('error')"
>
<div class="flex flex-col gap-2 p-4 items-center bg-base-100 h-full max-h-[85vh] max-w-6xl rounded-2xl w-full">
<Dropzone

@ -1,4 +1,5 @@
const en = {
prefillable: 'Prefillable',
signature_id: 'Signature ID',
error_message: 'Error message',
length: 'Length',
@ -173,6 +174,7 @@ const en = {
}
const es = {
prefillable: 'Rellenable',
signature_id: 'ID de Firma',
error_message: 'Mensaje de error',
length: 'Longitud',
@ -347,6 +349,7 @@ const es = {
}
const it = {
prefillable: 'Precompilabile',
signature_id: 'ID firma',
error_message: 'Messaggio di errore',
length: 'Lunghezza',
@ -521,6 +524,7 @@ const it = {
}
const pt = {
prefillable: 'Pré-preenchível',
signature_id: 'ID da Assinatura',
error_message: 'Mensagem de erro',
length: 'Comprimento',
@ -695,6 +699,7 @@ const pt = {
}
const fr = {
prefillable: 'Pré-remplissable',
signature_id: 'ID de signature',
error_message: 'Message d\'erreur',
length: 'Longueur',
@ -869,6 +874,7 @@ const fr = {
}
const de = {
prefillable: 'Vorausfüllbar',
signature_id: 'Signatur-ID',
error_message: 'Fehlermeldung',
length: 'Länge',

@ -28,6 +28,7 @@
:editable="editable"
:with-field-placeholder="withFieldPlaceholder"
:with-signature-id="withSignatureId"
:with-prefillable="withPrefillable"
:default-field="defaultFieldsIndex[item.field.name]"
:default-submitters="defaultSubmitters"
:max-page="totalPages - 1"
@ -84,6 +85,11 @@ export default {
required: false,
default: null
},
withPrefillable: {
type: Boolean,
required: false,
default: false
},
areas: {
type: Array,
required: false,

@ -6,6 +6,7 @@ class SendSubmitterInvitationEmailJob
def perform(params = {})
submitter = Submitter.find(params['submitter_id'])
return if submitter.completed_at?
return if submitter.submission.source == 'invite' && !Accounts.can_send_emails?(submitter.account, on_events: true)
unless Accounts.can_send_invitation_emails?(submitter.account)

@ -30,6 +30,7 @@ class SubmitterMailer < ApplicationMailer
@submitter.template&.preferences&.dig('request_email_subject').presence
@email_config = AccountConfigs.find_for_account(@current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY)
@body ||= fetch_config_email_body(@email_config, @submitter)
assign_message_metadata('submitter_invitation', @submitter)
@ -71,7 +72,7 @@ class SubmitterMailer < ApplicationMailer
@subject ||= @email_config.value['subject'] if @email_config
@body = template_preferences['completed_notification_email_body'].presence
@body ||= @email_config.value['body'] if @email_config
@body ||= fetch_config_email_body(@email_config, @submitter)
assign_message_metadata('submitter_completed', @submitter)
@ -127,7 +128,7 @@ class SubmitterMailer < ApplicationMailer
@subject ||= @email_config.value['subject'] if @email_config
@body = template_preferences['documents_copy_email_body'].presence
@body ||= @email_config.value['body'] if @email_config
@body ||= fetch_config_email_body(@email_config, @submitter)
assign_message_metadata('submitter_documents_copy', @submitter)
reply_to = build_submitter_reply_to(submitter, email_config: @email_config, documents_copy_email: true)
@ -246,4 +247,8 @@ class SubmitterMailer < ApplicationMailer
user.friendly_name
end
end
def fetch_config_email_body(email_config, _submitter = nil)
email_config ? email_config.value['body'].presence : nil
end
end

@ -141,7 +141,7 @@
<%= t('preferences') %>
</h2>
</div>
<% if can?(:manage, account_config) && (can?(:manage, :personalization_advanced) || !Docuseal.multitenant?) %>
<% if can?(:manage, account_config) && (can?(:manage, :personalization_advanced) || account_config.persisted?) %>
<%= form_for account_config, url: account_configs_path, method: :post do |f| %>
<%= f.hidden_field :key %>
<div class="flex items-center justify-between py-2.5">

@ -18,7 +18,6 @@
<link href="<%= canonical_url %>" rel="canonical">
<% end %>
<%= stylesheet_pack_tag 'application', media: 'all' %>
<%= render 'shared/posthog' if ENV['POSTHOG_TOKEN'] %>
<%= render 'shared/plausible' if !signed_in? && ENV['PLAUSIBLE_DOMAIN'] %>
</head>
<body>

@ -12,7 +12,6 @@
<%= javascript_pack_tag 'form', defer: true %>
<% end %>
<%= stylesheet_pack_tag 'form', media: 'all' %>
<%= render 'shared/posthog' if ENV['POSTHOG_TOKEN'] %>
</head>
<body>
<%= yield %>

@ -12,7 +12,6 @@
<%= javascript_pack_tag 'application', defer: true %>
<% end %>
<%= stylesheet_pack_tag 'application', media: 'all' %>
<%= render 'shared/posthog' if ENV['POSTHOG_TOKEN'] %>
</head>
<body>
<% if flash.present? %><%= render 'shared/flash' %><% end %>

@ -13,17 +13,7 @@
<%= ff.label :subject, t('subject'), class: 'label' %>
<%= ff.text_field :subject, required: true, class: 'base-input', dir: 'auto' %>
</div>
<div class="form-control">
<div class="flex items-center">
<%= ff.label :body, t('body'), class: 'label' %>
<span class="tooltip" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY].call['body'].scan(/{.*?}/).join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<autoresize-textarea>
<%= ff.text_area :body, required: true, class: 'base-input w-full py-2', dir: 'auto' %>
</autoresize-textarea>
</div>
<%= render 'personalization_settings/email_body_field', ff:, config: f.object %>
<% if can?(:manage, :reply_to) || can?(:manage, :personalization_advanced) %>
<div class="form-control">
<%= ff.label :reply_to, t('reply_to'), class: 'label' %>

@ -0,0 +1,11 @@
<div class="form-control">
<div class="flex items-center">
<%= ff.label :body, t('body'), class: 'label' %>
<span class="tooltip" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[local_assigns[:config].key].call['body'].scan(/{.*?}/).join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<autoresize-textarea>
<%= ff.text_area :body, required: true, class: 'base-input w-full !rounded-2xl py-2', dir: 'auto' %>
</autoresize-textarea>
</div>

@ -13,17 +13,7 @@
<%= ff.label :subject, t('subject'), class: 'label' %>
<%= ff.text_field :subject, required: true, class: 'base-input', dir: 'auto' %>
</div>
<div class="form-control">
<div class="flex items-center">
<%= ff.label :body, t('body'), class: 'label' %>
<span class="tooltip" 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, required: true, class: 'base-input w-full py-2', dir: 'auto' %>
</autoresize-textarea>
</div>
<%= render 'personalization_settings/email_body_field', ff:, config: f.object %>
<% end %>
<div class="form-control pt-2">
<%= f.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button' %>

@ -18,17 +18,7 @@
</div>
<%= ff.text_field :subject, required: true, class: 'base-input', dir: 'auto' %>
</div>
<div class="form-control">
<div class="flex items-center">
<%= ff.label :body, t('body'), class: 'label' %>
<span class="tooltip" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY].call['body'].scan(/{.*?}/).join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<autoresize-textarea>
<%= ff.text_area :body, required: true, class: 'base-input w-full py-2', dir: 'auto' %>
</autoresize-textarea>
</div>
<%= render 'personalization_settings/email_body_field', ff:, config: f.object %>
<div class="flex items-center justify-between pt-2.5 mx-1">
<span>
<%= t('attach_documents') %>

@ -1,4 +0,0 @@
<script>
!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.async=!0,p.src=s.api_host+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="capture identify alias people.set people.set_once set_config register register_once unregister opt_out_capturing has_opted_out_capturing opt_in_capturing reset isFeatureEnabled onFeatureFlags getFeatureFlag getFeatureFlagPayload reloadFeatureFlags group updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures getActiveMatchingSurveys getSurveys".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
posthog.init('<%= ENV.fetch('POSTHOG_TOKEN', nil) %>',{api_host:'https://app.posthog.com',autocapture: false,enable_recording_console_log: true,capture_pageview: false,session_recording:{maskAllInputs: false,maskInputOptions: {password: true}}})
</script>

@ -1,3 +1,4 @@
<% has_phone_field = false %>
<%= form_for '', url: template_submissions_path(template), html: { class: 'space-y-4', autocomplete: 'off' }, data: { turbo_frame: :_top } do |f| %>
<% submitters = template.submitters.reject { |e| e['invite_by_uuid'].present? || e['optional_invite_by_uuid'].present? } %>
<dynamic-list class="space-y-4">
@ -11,6 +12,7 @@
</div>
<div class="grid <%= 'md:grid-cols-2' if submitters.size > 1 %> gap-4">
<% submitters.each_with_index do |item, index| %>
<% prefillable_fields = local_assigns[:prefillable_fields].to_a.select { |f| f['submitter_uuid'] == item['uuid'] } %>
<submitter-item class="form-control">
<% if submitters.size > 1 %>
<label class="label pt-0 pb-1">
@ -18,30 +20,62 @@
</label>
<% end %>
<input type="hidden" name="submission[1][submitters][][uuid]" value="<%= item['uuid'] %>">
<submitters-autocomplete data-field="name">
<linked-input data-target-id="<%= "detailed_name_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
<%= tag.input type: 'text', name: 'submission[1][submitters][][name]', autocomplete: 'off', class: 'base-input !h-10 w-full', placeholder: t('name'), required: index.zero? || template.preferences['require_all_submitters'], value: item['email'].present? ? current_account.submitters.accessible_by(current_ability).where.not(name: nil).order(id: :desc).find_by(email: item['email'])&.name : ((params[:selfsign] && index.zero?) || item['is_requester'] ? current_user.full_name : ''), dir: 'auto', id: "detailed_name_#{item['uuid']}" %>
</linked-input>
</submitters-autocomplete>
<div class="grid <%= 'md:grid-cols-2 gap-1' if submitters.size == 1 %>">
<submitters-autocomplete data-field="email">
<linked-input data-target-id="<%= "detailed_email_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
<input type="email" multiple name="submission[1][submitters][][email]" autocomplete="off" class="base-input !h-10 mt-1.5 w-full" placeholder="<%= "#{t('email')} (#{t('optional')})" %>" value="<%= item['email'].presence || ((params[:selfsign] && index.zero?) || item['is_requester'] ? current_user.email : '') %>" id="detailed_email_<%= item['uuid'] %>">
<% if prefillable_fields.blank? %>
<submitters-autocomplete data-field="name">
<linked-input data-target-id="<%= "detailed_name_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
<%= tag.input type: 'text', name: 'submission[1][submitters][][name]', autocomplete: 'off', class: 'base-input !h-10 w-full', placeholder: t('name'), required: index.zero? || template.preferences['require_all_submitters'], value: item['email'].present? ? current_account.submitters.accessible_by(current_ability).where.not(name: nil).order(id: :desc).find_by(email: item['email'])&.name : ((params[:selfsign] && index.zero?) || item['is_requester'] ? current_user.full_name : ''), dir: 'auto', id: "detailed_name_#{item['uuid']}" %>
</linked-input>
</submitters-autocomplete>
<submitters-autocomplete data-field="phone">
<linked-input data-target-id="<%= "detailed_phone_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
<%= tag.input type: 'tel', pattern: '^\+[0-9\s\-]+$', oninvalid: "this.value ? this.setCustomValidity('#{t('use_international_format_1xxx_')}') : ''", oninput: "this.setCustomValidity('')", name: 'submission[1][submitters][][phone]', autocomplete: 'off', class: 'base-input !h-10 mt-1.5 w-full', placeholder: local_assigns[:require_phone_2fa] == true ? t(:phone) : "#{t('phone')} (#{t('optional')})", id: "detailed_phone_#{item['uuid']}", required: local_assigns[:require_phone_2fa] == true %>
<div class="grid <%= 'md:grid-cols-2 gap-1' if submitters.size == 1 %>">
<submitters-autocomplete data-field="email">
<linked-input data-target-id="<%= "detailed_email_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
<input type="email" multiple name="submission[1][submitters][][email]" autocomplete="off" class="base-input !h-10 mt-1.5 w-full" placeholder="<%= "#{t('email')} (#{t('optional')})" %>" value="<%= item['email'].presence || ((params[:selfsign] && index.zero?) || item['is_requester'] ? current_user.email : '') %>" id="detailed_email_<%= item['uuid'] %>">
</linked-input>
</submitters-autocomplete>
<% has_phone_field = true %>
<submitters-autocomplete data-field="phone">
<linked-input data-target-id="<%= "detailed_phone_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
<%= tag.input type: 'tel', pattern: '^\+[0-9\s\-]+$', oninvalid: "this.value ? this.setCustomValidity('#{t('use_international_format_1xxx_')}') : ''", oninput: "this.setCustomValidity('')", name: 'submission[1][submitters][][phone]', autocomplete: 'off', class: 'base-input !h-10 mt-1.5 w-full', placeholder: local_assigns[:require_phone_2fa] == true ? t(:phone) : "#{t('phone')} (#{t('optional')})", id: "detailed_phone_#{item['uuid']}", required: local_assigns[:require_phone_2fa] == true %>
</linked-input>
</submitters-autocomplete>
</div>
<% end %>
<% if prefillable_fields.present? %>
<submitters-autocomplete data-field="email">
<linked-input data-target-id="<%= "detailed_email_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
<input type="email" multiple name="submission[1][submitters][][email]" autocomplete="off" class="base-input !h-10 w-full" placeholder="<%= t('email') %>" value="<%= item['email'].presence || ((params[:selfsign] && index.zero?) || item['is_requester'] ? current_user.email : '') %>" id="detailed_email_<%= item['uuid'] %>" required>
</linked-input>
</submitters-autocomplete>
</div>
<% if local_assigns[:require_phone_2fa] == true || prefillable_fields.any? { |f| f['type'] == 'phone' } %>
<% has_phone_field = true %>
<submitters-autocomplete data-field="phone">
<linked-input data-target-id="<%= "detailed_phone_#{item['linked_to_uuid']}" if item['linked_to_uuid'].present? %>">
<%= tag.input type: 'tel', pattern: '^\+[0-9\s\-]+$', oninvalid: "this.value ? this.setCustomValidity('#{t('use_international_format_1xxx_')}') : ''", oninput: "this.setCustomValidity('')", name: 'submission[1][submitters][][phone]', autocomplete: 'off', class: 'base-input !h-10 mt-1.5 w-full', placeholder: t(:phone), id: "detailed_phone_#{item['uuid']}", required: true %>
</linked-input>
</submitters-autocomplete>
<% end %>
<% prefillable_fields.each do |field| %>
<% if field['type'] == 'checkbox' %>
<label for="detailed_field_<%= field['uuid'] %>" class="flex items-center justify-between mt-1.5 pl-3 pr-2.5 h-10 border border-base-content/20 rounded-full cursor-pointer transition-colors bg-white">
<span class="text-base select-none px-1"> <%= field['title'].presence || field['name'] %></span>
<%= tag.input type: 'checkbox', name: "submission[1][submitters][][values][#{field['uuid']}]", id: "detailed_field_#{field['uuid']}", class: 'toggle toggle-sm', style: 'width: 38px; --handleoffset: 17px', checked: field['default_value'].present? && (field['default_value'] == true || field['default_value'].to_s == '1' || field['default_value'].to_s.downcase == 'true'), required: field['required'], value: 'true' %>
</label>
<% elsif field['type'] == 'select' || field['type'] == 'radio' %>
<%= select_tag "submission[1][submitters][][values][#{field['uuid']}]", options_for_select(field['options'].pluck('value'), field['default_value']), prompt: t(:select), id: "detailed_field_#{field['uuid']}", class: 'select select-sm base-input !h-10 mt-1.5 ', required: field['required'] %>
<% elsif field['type'] == 'date' %>
<%= tag.input type: field['type'], name: "submission[1][submitters][][values][#{field['uuid']}]", autocomplete: 'off', class: 'base-input !h-10 mt-1.5 w-full border rounded p-3', placeholder: (field['required'] ? field['title'].presence || field['name'] : "#{field['title'].presence || field['name']} (#{t('optional')})"), value: field['default_value'], id: "detailed_field_#{field['uuid']}", required: field['required'] %>
<% elsif field['type'] != 'phone' %>
<%= tag.input type: field['type'], name: "submission[1][submitters][][values][#{field['uuid']}]", autocomplete: 'off', class: 'base-input !h-10 mt-1.5 w-full border rounded p-3', placeholder: (field['required'] ? field['title'].presence || field['name'] : "#{field['title'].presence || field['name']} (#{t('optional')})"), value: field['default_value'], id: "detailed_field_#{field['uuid']}", required: field['required'] %>
<% end %>
<% end %>
<% end %>
</submitter-item>
<% end %>
</div>
</div>
</div>
</div>
<% if params[:selfsign].blank? %>
<% if params[:selfsign].blank? && local_assigns[:prefillable_fields].blank? %>
<a href="#" class="btn btn-primary btn-sm w-full flex items-center justify-center" data-action="click:dynamic-list#addItem">
<%= svg_icon('user_plus', class: 'w-4 h-4 stroke-2') %>
<span><%= t('add_new') %></span>
@ -51,7 +85,9 @@
<div>
<%= render('submitters_order', f:, template:) if Accounts.can_send_emails?(current_account) %>
<%= render 'send_email', f:, template: %>
<%= render 'send_sms', f: %>
<% if has_phone_field %>
<%= render 'send_sms', f: %>
<% end %>
</div>
<div class="form-control">
<%= f.button button_title(title: t('add_recipients')), class: 'base-button' %>

@ -4,7 +4,7 @@
<% 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] %>
<%= 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>

@ -1,11 +1,13 @@
<% require_phone_2fa = @template.preferences['require_phone_2fa'] == true %>
<% prefillable_fields = @template.fields.select { |f| f['prefillable'] } %>
<% only_detailed = require_phone_2fa || prefillable_fields.present? %>
<%= render 'shared/turbo_modal_large', title: params[:selfsign] ? t('add_recipients') : t('add_new_recipients') do %>
<% options = [require_phone_2fa ? nil : [t('via_email'), 'email'], require_phone_2fa ? nil : [t('via_phone'), 'phone'], [t('detailed'), 'detailed'], [t('upload_list'), 'list']].compact %>
<% options = [only_detailed ? nil : [t('via_email'), 'email'], only_detailed ? nil : [t('via_phone'), 'phone'], [t('detailed'), 'detailed'], [t('upload_list'), 'list']].compact %>
<toggle-visible data-element-ids="<%= options.map(&:last).to_json %>" class="relative text-center px-2 mt-4 block">
<div class="flex justify-center">
<% options.each_with_index do |(label, value), index| %>
<div>
<%= radio_button_tag 'option', value, value == (require_phone_2fa ? 'detailed' : 'email'), class: 'peer hidden', data: { action: 'change:toggle-visible#trigger' } %>
<%= radio_button_tag 'option', value, value == (only_detailed ? 'detailed' : 'email'), class: 'peer hidden', data: { action: 'change:toggle-visible#trigger' } %>
<label for="option_<%= value %>" class="block bg-base-200 md:min-w-[112px] text-sm font-semibold whitespace-nowrap py-1.5 px-4 peer-checked:bg-base-300 <%= 'hidden sm:inline-block' if value == 'list' %> <%= 'rounded-l-3xl' if index.zero? %> <%= 'rounded-r-3xl sm:rounded-r-none' if value == 'detailed' %> <%= 'rounded-r-3xl' if index == options.size - 1 %>">
<%= label %>
</label>
@ -14,7 +16,7 @@
</div>
</toggle-visible>
<div class="px-5 mb-5 mt-4">
<% unless require_phone_2fa %>
<% unless only_detailed %>
<div id="email">
<%= render 'email_form', template: @template %>
</div>
@ -22,8 +24,8 @@
<%= render 'phone_form', template: @template %>
</div>
<% end %>
<div id="detailed" class="<%= 'hidden' unless require_phone_2fa %>">
<%= render 'detailed_form', template: @template, require_phone_2fa: %>
<div id="detailed" class="<%= 'hidden' unless only_detailed %>">
<%= render 'detailed_form', template: @template, require_phone_2fa:, prefillable_fields: %>
</div>
<div id="list" class="hidden">
<%= render 'list_form', template: @template %>

@ -12,7 +12,6 @@
<%= javascript_pack_tag 'draw', defer: true %>
<% end %>
<%= stylesheet_pack_tag 'form', media: 'all' %>
<%= render 'shared/posthog' if ENV['POSTHOG_TOKEN'] %>
</head>
<body>
<% field = (@submitter.submission.template_fields || @submitter.template.fields).find { |f| f['type'] == 'signature' && f['uuid'].starts_with?(params[:f]) } %>

@ -1,14 +1,13 @@
<% if @email_config || @body.present? %>
<% body = (@body.presence || @email_config.value['body']).to_s %>
<%= render 'custom_content', content: body, submitter: @submitter %>
<% if !body.match?(ReplaceEmailVariables::SUBMITTER_LINK) && !body.match?(ReplaceEmailVariables::SUBMITTER_ID) && !body.match?(ReplaceEmailVariables::SUBMISSION_LINK) && !body.match?(ReplaceEmailVariables::TEMPLATE_ID) && !@submitter.submission.source.in?(%w[api embed]) %>
<% if @body.present? %>
<%= render 'custom_content', content: @body, submitter: @submitter %>
<% if !@body.match?(ReplaceEmailVariables::SUBMITTER_LINK) && !@body.match?(ReplaceEmailVariables::SUBMITTER_ID) && !@body.match?(ReplaceEmailVariables::SUBMISSION_LINK) && !@body.match?(ReplaceEmailVariables::TEMPLATE_ID) && !@submitter.submission.source.in?(%w[api embed]) %>
<p><%= link_to nil, submit_form_url(slug: @submitter.slug, t: SubmissionEvents.build_tracking_param(@submitter, 'click_email')) %></p>
<% end %>
<% else %>
<p><%= t('hi_there') %>,</p>
<p><%= I18n.t(@submitter.with_signature_fields? ? :you_have_been_invited_to_sign_the_name : :you_have_been_invited_to_submit_the_name_form, name: @submitter.submission.name || @submitter.submission.template.name) %></p>
<p><%= link_to I18n.t(@submitter.with_signature_fields? ? :review_and_sign : :review_and_submit), submit_form_url(slug: @submitter.slug, t: SubmissionEvents.build_tracking_param(@submitter, 'click_email'), host: ENV.fetch('EMAIL_HOST', Docuseal.default_url_options[:host])) %></p>
<p><%= t('please_contact_us_by_replying_to_this_email_if_you_didn_t_request_this') %></p>
<p><%= t('please_contact_us_by_replying_to_this_email_if_you_have_any_questions') %></p>
<p>
<%= t('thanks') %>,<br><%= @current_account.name %>
</p>

@ -3,27 +3,23 @@
<% if with_subfolder %>
<%= form_for '', url: template_folder_path(@template), method: :put, data: { turbo_frame: :_top }, html: { id: 'subfolder_form', autocomplete: :off } do |f| %>
<%= f.hidden_field :parent_name, value: @template.folder.parent_folder&.name || @template.folder.name %>
<toggle-visible data-element-ids="<%= %w[folder_form subfolder_form].to_json %>" class="block relative">
<toggle-visible data-element-ids="<%= %w[folder_form subfolder_form].to_json %>" class="block relative" data-focus-id="name">
<div class="flex items-center justify-between mb-2.5">
<label for="is_root_folder" class="flex items-center">
<label for="is_root_folder" class="flex items-center tooltip tooltip-right pr-2 group" data-tip="<%= t('change_parent_folder') %>">
<%= check_box_tag :is_root_folder, 'folder_form', data: { action: 'change:toggle-visible#trigger' }, class: 'hidden' %>
<span class="flex items-center space-x-0.5 mt-1 peer">
<%= svg_icon('folder', class: 'w-6 h-5 flex-shrink-0') %>
<span class="flex items-center mt-1">
<%= svg_icon('folder', class: 'w-5 h-5 flex-shrink-0 group-hover:hidden mr-1') %>
<%= svg_icon('folder_share', class: 'w-5 h-5 flex-shrink-0 hidden group-hover:inline mr-1') %>
<span class="text-md">
<%= @template.folder.parent_folder&.name || @template.folder.name %>
</span>
</span>
<span class="pl-1 tooltip tooltip-right md:opacity-0 hover:opacity-100 peer-hover:opacity-100" data-tip="<%= t('change_parent_folder') %>">
<span href="<%= edit_template_folder_path(@template.id) %>" data-turbo-frame="modal">
<%= svg_icon('pencil', class: 'w-5 h-5') %>
</span>
</span>
</label>
</div>
</toggle-visible>
<div class="form-control mb-6">
<folder-autocomplete class="block" data-submit-on-select="true" data-parent-name="<%= @template.folder.parent_folder&.name || @template.folder.name %>" data-enabled="<%= params[:autocomplete] != 'false' %>">
<%= f.text_field :name, required: true, placeholder: "#{t('new_subfolder_name')}...", class: 'base-input w-full', autofocus: true %>
<%= f.text_field :name, required: true, placeholder: "#{t('new_subfolder_name')}...", class: 'base-input w-full', autofocus: true, id: 'subfolder_name' %>
</folder-autocomplete>
</div>
<div class="form-control">

@ -59,6 +59,7 @@
<%= t('form_preferences') %>
</div>
<div class="collapse-content">
<%= render 'templates_prefillable_fields/form', template: @template %>
<%= 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="form_saved_alert"></toggle-on-submit>
<% configs = AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY).value %>
@ -68,7 +69,7 @@
<%= ff.url_field :completed_redirect_url, required: false, class: 'base-input', dir: 'auto' %>
</div>
<%= ff.fields_for :completed_message, ff.object.completed_message do |fff| %>
<div class="form-control mb-2 border-t mt-4">
<div class="form-control mb-2 mt-4">
<%= fff.label :body, t('completion_message'), class: 'label' %>
<autoresize-textarea>
<%= fff.text_area :body, required: false, class: 'base-input w-full py-2', dir: 'auto' %>
@ -162,6 +163,14 @@
<% 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>
<%= ff.check_box :request_email_enabled, { checked: ff.object.request_email_enabled != false, class: 'toggle', onchange: 'this.form.requestSubmit()' }, 'true', 'false' %>
</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">

@ -0,0 +1,16 @@
<% select_fields = template.fields.filter_map { |f| [f['name'], f['uuid']] if f['name'].present? && f['type'].in?(TemplatesPrefillableFieldsController::PREFILLABLE_FIELD_TYPES) } %>
<% if select_fields.present? %>
<div class="border-b pb-4 mb-4">
<%= form_for '', url: template_prefillable_fields_path(template), method: :post, data: { close_on_submit: false } do |f| %>
<div class="form-control">
<%= f.hidden_field :prefillable, value: 'true' %>
<%= f.label :field_uuid, t(:invite_form_fields), class: 'label' %>
<div class="join w-full">
<%= select_tag :field_uuid, options_for_select(select_fields), prompt: t(:select_field), class: 'base-select w-full join-item', dir: 'auto', required: true %>
<%= f.button button_title(title: t('add'), disabled_with: t('add')), class: 'base-button join-item !px-6' %>
</div>
</div>
<% end %>
<%= render partial: 'templates_prefillable_fields/list', locals: { template: } %>
</div>
<% end %>

@ -0,0 +1,6 @@
<div id="prefillable_fields_list" class="flex gap-1 px-0.5 mt-2 flex-wrap">
<% template.fields.each do |f| %>
<% next unless f['prefillable'] %>
<%= button_to button_title(title: f['name'].presence || f['type'].capitalize, disabled_with: f['name'].presence || f['type'].capitalize, icon: svg_icon('x', class: 'w-4 h-4'), icon_disabled: svg_icon('loader', class: 'w-4 h-4 animate-spin')), template_prefillable_fields_path(template), params: { field_uuid: f['uuid'], prefillable: 'false' }, class: 'badge badge-lg badge-primary space-x-1 pr-3 pl-2', form: { data: { close_on_submit: false } } %>
<% end %>
</div>

@ -1,3 +1,5 @@
<% multiple_submitters = @template.submitters.to_a.length > 1 %>
<% enough_defined_submitters = Templates.filter_undefined_submitters(@template.submitters).size < 2 %>
<%= render 'shared/turbo_modal_large', title: t('share_link') do %>
<div class="mt-2 mb-4 px-5">
<%= form_for @template, url: template_share_link_path(@template), method: :post, html: { id: 'shared_link_form', autocomplete: 'off', class: 'mt-3' }, data: { close_on_submit: false } do |f| %>
@ -12,28 +14,14 @@
</check-on-click>
</div>
<% end %>
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-4 mb-5' }, data: { close_on_submit: false } do |f| %>
<% link_form_fields = @template.preferences.fetch('link_form_fields', ['email']) %>
<label class="pl-1"><%= t('link_form_fields') %></label>
<required-checkbox-group class="flex flex-col md:flex-row items-center gap-2 w-full mt-2">
<% %w[name email phone].each do |field| %>
<%= label_tag "link_form_fields_#{field}", t(field), class: 'relative flex w-full md:w-1/3 items-center h-12 border-base-300 py-3.5 border rounded-xl' do %>
<%= check_box_tag 'template[preferences][link_form_fields][]', field, link_form_fields.include?(field), class: 'absolute !animate-none checkbox left-3', id: "link_form_fields_#{field}" %>
<span class="font-medium w-full text-center"><%= t(field) %></span>
<% end %>
<% end %>
</required-checkbox-group>
<% end %>
<% if Templates.filter_undefined_submitters(@template.submitters).size > 1 %>
<% if multiple_submitters && !enough_defined_submitters %>
<div class="alert items-start bg-base-100 border-base-300 mt-4">
<%= svg_icon('info_circle', class: 'stroke-current shrink-0 h-6 w-6 mt-1') %>
<div><%= t('this_template_has_multiple_parties_which_prevents_the_use_of_a_sharing_link') %></div>
</div>
<% end %>
<% if @template.submitters.to_a.length > 1 %>
<div class="collapse collapse-arrow join-item border border-base-300 mt-4 mb-5">
<input type="checkbox" name="accordion">
<div class="collapse-title text-xl font-medium">
<div class="collapse collapse-arrow join-item border border-base-300 mt-4">
<input type="checkbox" name="accordion" class="min-h-0">
<div class="collapse-title py-4 min-h-0">
<%= t('default_parties') %>
</div>
<div class="collapse-content !pb-0">
@ -41,15 +29,46 @@
</div>
</div>
<% end %>
<% if Docuseal.multitenant? || Accounts.can_send_emails?(current_account) %>
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-3' }, data: { close_on_submit: false } do |f| %>
<%= f.fields_for :preferences, Struct.new(:shared_link_2fa).new(@template.preferences['shared_link_2fa'] == true) do |ff| %>
<label for="template_preferences_shared_link_2fa" class="flex items-center my-4 justify-between gap-1 alert bg-base-100 border-base-300">
<span><%= t('request_email_otp_verification_with_shared_link') %></span>
<%= ff.check_box :shared_link_2fa, { checked: ff.object.shared_link_2fa == true, disabled: !can?(:update, @template), class: 'toggle', onchange: 'this.form.requestSubmit()' }, 'true', 'false' %>
</label>
<div class="collapse collapse-arrow join-item mt-4">
<input id="accordion_checkbox" type="checkbox" name="accordion" class="min-h-0">
<div class="collapse-title min-h-0 after:!right-3 p-1">
<%= t('advanced_settings') %>
</div>
<div class="collapse-content !p-0">
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-4 pt-4 border-t border-base-300' }, data: { close_on_submit: false } do |f| %>
<% link_form_fields = @template.preferences.fetch('link_form_fields', ['email']) %>
<label class="pl-1"><%= t('link_form_fields') %></label>
<required-checkbox-group class="flex flex-col md:flex-row items-center gap-2 w-full mt-2">
<% %w[name email phone].each do |field| %>
<%= label_tag "link_form_fields_#{field}", t(field), class: 'relative flex w-full md:w-1/3 items-center h-14 border-base-300 py-3.5 border rounded-xl' do %>
<%= check_box_tag 'template[preferences][link_form_fields][]', field, link_form_fields.include?(field), class: 'absolute !animate-none checkbox left-3', id: "link_form_fields_#{field}" %>
<span class="font-medium w-full text-center"><%= t(field) %></span>
<% end %>
<% end %>
</required-checkbox-group>
<% end %>
<% end %>
<% end %>
<% if multiple_submitters && enough_defined_submitters %>
<div class="collapse collapse-arrow join-item border border-base-300 mt-4">
<input type="checkbox" name="accordion" class="min-h-0">
<div class="collapse-title py-4 min-h-0">
<%= t('default_parties') %>
</div>
<div class="collapse-content !pb-0">
<%= render 'templates_preferences/recipients', template: @template, close_on_submit: false, with_toggles: false, with_submission_requester: false %>
</div>
</div>
<% end %>
<% if Docuseal.multitenant? || Accounts.can_send_emails?(current_account) %>
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-4' }, data: { close_on_submit: false } do |f| %>
<%= f.fields_for :preferences, Struct.new(:shared_link_2fa).new(@template.preferences['shared_link_2fa'] == true) do |ff| %>
<label for="template_preferences_shared_link_2fa" class="flex items-center mt-4 h-14 justify-between gap-1 alert bg-base-100 border-base-300">
<span><%= t('request_email_otp_verification_with_shared_link') %></span>
<%= ff.check_box :shared_link_2fa, { checked: ff.object.shared_link_2fa == true, disabled: !can?(:update, @template), class: 'toggle', onchange: 'this.form.requestSubmit()' }, 'true', 'false' %>
</label>
<% end %>
<% end %>
<% end %>
</div>
</div>
</div>
<% end %>

@ -1,7 +1,7 @@
<p><%= @user.first_name.present? ? t('hello_name', name: @user.first_name) : t('hi_there') %>,</p>
<p><%= t('you_have_been_invited_to_account_name_product_name_please_sign_up_using_the_link_below_', account_name: @user.account.name, product_name: Docuseal.product_name) %></p>
<p><%= link_to t('sign_up'), invitation_url(reset_password_token: @token) %></p>
<p><%= t('please_contact_us_by_replying_to_this_email_if_you_didn_t_request_this') %></p>
<p><%= t('please_contact_us_by_replying_to_this_email_if_you_have_any_questions') %></p>
<p>
<%= t('thanks') %>,<br><%= @current_account.name %>
</p>

@ -23,6 +23,8 @@ en: &en
pro: Pro
thanks: Thanks
private: Private
select: Select
invite_form_fields: Invite form fields
default_parties: Default parties
authenticate_embedded_form_preview_with_token: Authenticate embedded form preview with token
stripe_integration: Stripe Integration
@ -71,7 +73,7 @@ en: &en
awaiting_completion_by_the_other_party: "Awaiting completion by the other party"
review_and_sign: Review and Sign
review_and_submit: Review and Submit
please_contact_us_by_replying_to_this_email_if_you_didn_t_request_this: "Please contact us by replying to this email if you have any questions."
please_contact_us_by_replying_to_this_email_if_you_have_any_questions: "Please contact us by replying to this email if you have any questions."
submitter_invitation_sms_body_sign: '{account.name} has invited you to sign a document: {submitter.link}'
verification_code_sms_body: 'Verification code: {code}'
you_are_invited_to_submit_a_form: 'You are invited to submit a form'
@ -777,6 +779,19 @@ en: &en
email_verification: Email verification
your_verification_code_to_access_the_name: 'Your verification code to access the "%{name}":'
please_reply_to_this_email_if_you_didnt_request_this: Please reply to this email if you didn't request this.
advanced_settings: Advanced settings
text: Text
code: Code
custom_html_emails: Custom HTML emails
connect_your_email_to_send_html_emails: Connect your email to send HTML emails
connect_your_email_or_outlook_account_or_add_smtp_settings_to_send_custom_html_emails: Connect your Gmail or Outlook account or add SMTP settings to send custom HTML emails.
connect_gmail_or_outlook: Connect Gmail or Outlook
connect_your_email_to_bulk_send: Connect your email to bulk send
connect_your_email_or_outlook_account_or_add_smtp_settings_to_bulk_send: Connect your Gmail or Outlook account or add SMTP settings to bulk send.
are_you_sure_you_want_to_add_recipients_without_sending_to_send_emails_it_requires_to_connect_gmail_or_outlook: Are you sure you want to add recipients without sending? To send emails it requires to connect Gmail or Outlook.
template_name_has_been_completed_by_submitters_html: '"<strong>{template.name}</strong>" has been completed by <strong>{submission.submitters}</strong>'
please_check_the_copy_of_your_template_name_in_the_email_attachments_html: 'Please check the copy of your "<strong>{template.name}</strong>" in the email attachments.'
you_have_been_invited_to_sign_the_template_name_html: 'You have been invited to sign the "<strong>{template.name}</strong>".'
submission_sources:
api: API
bulk: Bulk Send
@ -883,6 +898,8 @@ en: &en
range_without_total: "%{from}-%{to} events"
es: &es
select: Seleccionar
invite_form_fields: Invitar campos del formulario
pro: Pro
default_parties: Partes predeterminadas
authenticate_embedded_form_preview_with_token: Autenticar vista previa del formulario incrustado con token
@ -936,7 +953,7 @@ es: &es
please_check_the_copy_of_your_name_in_the_email_attachments: 'Por favor, revisa la copia de tu "%{name}" en los archivos adjuntos del correo electrónico.'
review_and_sign: Revisar y Firmar
review_and_submit: Revisar y Enviar
please_contact_us_by_replying_to_this_email_if_you_didn_t_request_this: "Por favor, contáctanos respondiendo a este correo si tienes alguna pregunta."
please_contact_us_by_replying_to_this_email_if_you_have_any_questions: "Por favor, contáctanos respondiendo a este correo si tienes alguna pregunta."
submitter_invitation_sms_body_sign: '{account.name} te ha invitado a firmar un documento: {submitter.link}'
verification_code_sms_body: 'Código de verificación: {code}'
you_are_invited_to_submit_a_form: 'Estás invitado/a a enviar un formulario'
@ -965,7 +982,7 @@ es: &es
submitter_documents_copy_email_body: |
Hola,
Por favor, revisa la copia de tu "{template.name}" en los archivos adjuntos.
Por favor, revisa la copia de tu "{template.name}" en los archivos adjuntos del correo electrónico.
Alternativamente, puedes revisar y descargar tu copia usando el enlace a continuación:
[{template.name}]({documents.link})
@ -1640,6 +1657,19 @@ es: &es
email_verification: Verificación por correo electrónico
your_verification_code_to_access_the_name: 'Su código de verificación para acceder a "%{name}":'
please_reply_to_this_email_if_you_didnt_request_this: Por favor, responda este correo si no solicitó esto.
advanced_settings: Configuración avanzada
text: Texto
code: Código
custom_html_emails: Correos electrónicos HTML personalizados
connect_your_email_to_send_html_emails: Conecta tu correo electrónico para enviar correos HTML
connect_your_email_or_outlook_account_or_add_smtp_settings_to_send_custom_html_emails: Conecta tu cuenta de Gmail u Outlook o añade ajustes SMTP para enviar correos HTML personalizados.
connect_gmail_or_outlook: Conectar Gmail u Outlook
connect_your_email_to_bulk_send: Conecta tu correo electrónico para el envío masivo
connect_your_email_or_outlook_account_or_add_smtp_settings_to_bulk_send: Conecta tu cuenta de Gmail u Outlook o añade ajustes SMTP para el envío masivo.
are_you_sure_you_want_to_add_recipients_without_sending_to_send_emails_it_requires_to_connect_gmail_or_outlook: ¿Estás seguro de que quieres añadir destinatarios sin enviar? Para enviar correos electrónicos necesitas conectar Gmail u Outlook.
template_name_has_been_completed_by_submitters_html: '"<strong>{template.name}</strong>" ha sido completado por <strong>{submission.submitters}</strong>'
please_check_the_copy_of_your_template_name_in_the_email_attachments_html: 'Por favor, revisa la copia de tu "<strong>{template.name}</strong>" en los archivos adjuntos del correo electrónico.'
you_have_been_invited_to_sign_the_template_name_html: 'Has sido invitado a firmar el "<strong>{template.name}</strong>".'
submission_sources:
api: API
bulk: Envío masivo
@ -1746,6 +1776,8 @@ es: &es
range_without_total: "%{from}-%{to} eventos"
it: &it
select: Seleziona
invite_form_fields: Invita campi modulo
pro: Pro
default_parties: Parti predefiniti
authenticate_embedded_form_preview_with_token: "Autentica l'anteprima del modulo incorporato con il token"
@ -1798,7 +1830,7 @@ it: &it
please_check_the_copy_of_your_name_in_the_email_attachments: "Per favore, controlla la copia del tuo \"%{name}\" negli allegati dell'email."
review_and_sign: Rivedi e Firma
review_and_submit: Rivedi e Invia
please_contact_us_by_replying_to_this_email_if_you_didn_t_request_this: "Per favore, contattaci rispondendo a questa email se hai domande."
please_contact_us_by_replying_to_this_email_if_you_have_any_questions: "Per favore, contattaci rispondendo a questa email se hai domande."
submitter_invitation_sms_body_sign: '{account.name} ti ha invitato a firmare un documento: {submitter.link}'
verification_code_sms_body: 'Codice di verifica: {code}'
you_are_invited_to_submit_a_form: 'Sei stato invitato a inviare un modulo'
@ -2503,6 +2535,19 @@ it: &it
email_verification: Verifica email
your_verification_code_to_access_the_name: 'Il tuo codice per accedere a "%{name}":'
please_reply_to_this_email_if_you_didnt_request_this: Rispondi a questa email se non hai richiesto questo.
advanced_settings: Impostazioni avanzate
text: Testo
code: Codice
custom_html_emails: Email HTML personalizzate
connect_your_email_to_send_html_emails: Collega la tua email per inviare email HTML
connect_your_email_or_outlook_account_or_add_smtp_settings_to_send_custom_html_emails: Collega il tuo account Gmail o Outlook oppure aggiungi impostazioni SMTP per inviare email HTML personalizzate.
connect_gmail_or_outlook: Collega Gmail o Outlook
connect_your_email_to_bulk_send: Collega la tua email per invio multiplo
connect_your_email_or_outlook_account_or_add_smtp_settings_to_bulk_send: Collega il tuo account Gmail o Outlook oppure aggiungi impostazioni SMTP per invio multiplo.
are_you_sure_you_want_to_add_recipients_without_sending_to_send_emails_it_requires_to_connect_gmail_or_outlook: Sei sicuro di voler aggiungere destinatari senza inviare? Per inviare email devi collegare Gmail o Outlook.
template_name_has_been_completed_by_submitters_html: '"<strong>{template.name}</strong>" è stato completato da <strong>{submission.submitters}</strong>'
please_check_the_copy_of_your_template_name_in_the_email_attachments_html: 'Per favore, controlla la copia del tuo "<strong>{template.name}</strong>" negli allegati dell''email.'
you_have_been_invited_to_sign_the_template_name_html: 'Sei stato invitato a firmare il "<strong>{template.name}</strong>".'
submission_sources:
api: API
bulk: Invio massivo
@ -2609,6 +2654,8 @@ it: &it
range_without_total: "%{from}-%{to} eventi"
fr: &fr
select: Sélectionner
invite_form_fields: Inviter des champs de formulaire
pro: Pro
default_parties: Parties par défaut
authenticate_embedded_form_preview_with_token: Authentifier laperçu du formulaire intégré avec un jeton
@ -2662,7 +2709,7 @@ fr: &fr
please_check_the_copy_of_your_name_in_the_email_attachments: "Veuillez vérifier la copie de votre \"%{name}\" dans les pièces jointes de l'email."
review_and_sign: Consulter et signer
review_and_submit: Consulter et soumettre
please_contact_us_by_replying_to_this_email_if_you_didn_t_request_this: 'Veuillez nous contacter en répondant à cet e-mail si vous avez des questions.'
please_contact_us_by_replying_to_this_email_if_you_have_any_questions: 'Veuillez nous contacter en répondant à cet e-mail si vous avez des questions.'
submitter_invitation_sms_body_sign: '{account.name} vous a invité à signer un document: {submitter.link}'
verification_code_sms_body: 'Code de vérification: {code}'
you_are_invited_to_submit_a_form: Vous êtes invité à soumettre un formulaire
@ -2678,7 +2725,7 @@ fr: &fr
[Consulter et signer]({submitter.link})
Veuillez nous contacter si vous avez des questions.
Veuillez nous contacter en répondant à cet e-mail si vous avez des questions.
Merci,
{account.name}
@ -2691,7 +2738,7 @@ fr: &fr
submitter_documents_copy_email_body: |
Bonjour,
Veuillez vérifier la copie de votre "{template.name}" dans les pièces jointes.
Veuillez vérifier la copie de votre "{template.name}" dans les pièces jointes de l'email.
Vous pouvez également consulter et télécharger votre copie en utilisant le lien ci-dessous:
[{template.name}]({documents.link})
@ -3369,6 +3416,19 @@ fr: &fr
email_verification: Vérification de l'email
your_verification_code_to_access_the_name: 'Votre code pour accéder à "%{name}" :'
please_reply_to_this_email_if_you_didnt_request_this: Veuillez répondre à cet email si vous n'avez pas fait cette demande.
advanced_settings: Paramètres avancés
text: Texte
code: Code
custom_html_emails: E-mails HTML personnalisés
connect_your_email_to_send_html_emails: Connectez votre e-mail pour envoyer des e-mails HTML
connect_your_email_or_outlook_account_or_add_smtp_settings_to_send_custom_html_emails: Connectez votre compte Gmail ou Outlook ou ajoutez des paramètres SMTP pour envoyer des e-mails HTML personnalisés.
connect_gmail_or_outlook: Connecter Gmail ou Outlook
connect_your_email_to_bulk_send: Connectez votre e-mail pour l'envoi groupé
connect_your_email_or_outlook_account_or_add_smtp_settings_to_bulk_send: Connectez votre compte Gmail ou Outlook ou ajoutez des paramètres SMTP pour l'envoi groupé.
are_you_sure_you_want_to_add_recipients_without_sending_to_send_emails_it_requires_to_connect_gmail_or_outlook: Êtes-vous sûr de vouloir ajouter des destinataires sans envoi ? Pour envoyer des e-mails, vous devez connecter Gmail ou Outlook.
template_name_has_been_completed_by_submitters_html: '"<strong>{template.name}</strong>" a été complété par <strong>{submission.submitters}</strong>'
please_check_the_copy_of_your_template_name_in_the_email_attachments_html: 'Veuillez vérifier la copie de votre "<strong>{template.name}</strong>" dans les pièces jointes de le-mail.'
you_have_been_invited_to_sign_the_template_name_html: 'Vous avez été invité à signer le "<strong>{template.name}</strong>".'
submission_sources:
api: API
bulk: Envoi en masse
@ -3475,6 +3535,8 @@ fr: &fr
range_without_total: "%{from} à %{to} événements"
pt: &pt
select: Selecionar
invite_form_fields: Convidar campos do formulário
pro: Pro
default_parties: Partes padrão
authenticate_embedded_form_preview_with_token: Autenticar visualização incorporada do formulário com token
@ -3528,7 +3590,7 @@ pt: &pt
please_check_the_copy_of_your_name_in_the_email_attachments: 'Por favor, verifique a cópia de "%{name}" nos anexos do e-mail.'
review_and_sign: Revisar e assinar
review_and_submit: Revisar e submeter
please_contact_us_by_replying_to_this_email_if_you_didn_t_request_this: 'Por favor, entre em contato conosco respondendo a este e-mail se você tiver alguma dúvida.'
please_contact_us_by_replying_to_this_email_if_you_have_any_questions: 'Por favor, entre em contato conosco respondendo a este e-mail se você tiver alguma dúvida.'
submitter_invitation_sms_body_sign: '{account.name} convidou você para assinar um documento: {submitter.link}'
verification_code_sms_body: 'Código de verificação: {code}'
you_are_invited_to_submit_a_form: Você foi convidado a submeter um formulário
@ -3540,11 +3602,11 @@ pt: &pt
submitter_invitation_email_sign_body: |
Olá,
Você foi convidado a assinar o documento "{template.name}".
Você foi convidado a assinar "{template.name}".
[Revisar e assinar]({submitter.link})
Por favor, entre em contato conosco se tiver alguma dúvida.
Por favor, entre em contato conosco respondendo a este e-mail se você tiver alguma dúvida.
Obrigado,
{account.name}
@ -3557,8 +3619,8 @@ pt: &pt
submitter_documents_copy_email_body: |
Olá,
Por favor, verifique a cópia de seu "{template.name}" nos anexos do e-mail.
Alternativamente, você pode revisar e baixar sua cópia usando o link abaixo:
Por favor, verifique a cópia de "{template.name}" nos anexos do e-mail.
Você pode revisar e baixar sua cópia usando o link abaixo:
[{template.name}]({documents.link})
@ -4233,6 +4295,19 @@ pt: &pt
email_verification: Verificação de e-mail
your_verification_code_to_access_the_name: 'Seu código de verificação para acessar "%{name}":'
please_reply_to_this_email_if_you_didnt_request_this: Responda a este e-mail se você não solicitou isso.
advanced_settings: Configurações avançadas
text: Texto
code: Código
custom_html_emails: E-mails HTML personalizados
connect_your_email_to_send_html_emails: Conecte seu e-mail para enviar e-mails HTML
connect_your_email_or_outlook_account_or_add_smtp_settings_to_send_custom_html_emails: Conecte sua conta Gmail ou Outlook ou adicione configurações SMTP para enviar e-mails HTML personalizados.
connect_gmail_or_outlook: Conectar Gmail ou Outlook
connect_your_email_to_bulk_send: Conecte seu e-mail para envio em massa
connect_your_email_or_outlook_account_or_add_smtp_settings_to_bulk_send: Conecte sua conta Gmail ou Outlook ou adicione configurações SMTP para envio em massa.
are_you_sure_you_want_to_add_recipients_without_sending_to_send_emails_it_requires_to_connect_gmail_or_outlook: Tem certeza de que deseja adicionar destinatários sem enviar? Para enviar e-mails é necessário conectar Gmail ou Outlook.
template_name_has_been_completed_by_submitters_html: '"<strong>{template.name}</strong>" foi concluído por <strong>{submission.submitters}</strong>'
please_check_the_copy_of_your_template_name_in_the_email_attachments_html: 'Por favor, verifique a cópia do seu "<strong>{template.name}</strong>" nos anexos do e-mail.'
you_have_been_invited_to_sign_the_template_name_html: 'Você foi convidado a assinar o "<strong>{template.name}</strong>".'
submission_sources:
api: API
bulk: Envio em massa
@ -4339,6 +4414,8 @@ pt: &pt
range_without_total: "%{from}-%{to} eventos"
de: &de
select: Auswählen
invite_form_fields: Formularfelder einladen
pro: Pro
default_parties: Standardparteien
authenticate_embedded_form_preview_with_token: Authentifizieren Sie die eingebettete Formularvorschau mit Token
@ -4392,7 +4469,7 @@ de: &de
please_check_the_copy_of_your_name_in_the_email_attachments: 'Bitte überprüfe die Kopie von "%{name}" im E-Mail-Anhang.'
review_and_sign: Überprüfen und unterschreiben
review_and_submit: Überprüfen und einreichen
please_contact_us_by_replying_to_this_email_if_you_didn_t_request_this: 'Bitte kontaktiere uns, indem du auf diese E-Mail antwortest, falls du Fragen hast.'
please_contact_us_by_replying_to_this_email_if_you_have_any_questions: 'Bitte kontaktieren Sie uns, indem Sie auf diese E-Mail antworten, falls Sie Fragen haben.'
submitter_invitation_sms_body_sign: '{account.name} hat dich eingeladen, ein Dokument zu unterschreiben: {submitter.link}'
verification_code_sms_body: 'Verifizierungscode: {code}'
you_are_invited_to_submit_a_form: Du wurdest eingeladen, ein Formular einzureichen
@ -4404,11 +4481,11 @@ de: &de
submitter_invitation_email_sign_body: |
Hallo,
Du wurdest eingeladen, das Dokument "{template.name}" zu unterschreiben.
Du wurdest eingeladen, "{template.name}" zu unterschreiben.
[Überprüfen und unterschreiben]({submitter.link})
Bitte kontaktiere uns, falls du Fragen hast.
Bitte kontaktieren Sie uns, indem Sie auf diese E-Mail antworten, falls Sie Fragen haben.
Danke,
{account.name}
@ -4421,8 +4498,8 @@ de: &de
submitter_documents_copy_email_body: |
Hallo,
Bitte überprüfe die Kopie deines "{template.name}" im E-Mail-Anhang.
Alternativ kannst du deine Kopie mit dem untenstehenden Link überprüfen und herunterladen:
Bitte überprüfe die Kopie von "{template.name}" im E-Mail-Anhang.
Du kannst alternativ deine Kopie mit dem untenstehenden Link überprüfen und herunterladen:
[{template.name}]({documents.link})
@ -5097,6 +5174,19 @@ de: &de
email_verification: E-Mail-Verifizierung
your_verification_code_to_access_the_name: 'Ihr Verifizierungscode für den Zugriff auf "%{name}":'
please_reply_to_this_email_if_you_didnt_request_this: Antworten Sie auf diese E-Mail, wenn Sie dies nicht angefordert haben.
advanced_settings: Erweiterte Einstellungen
text: Text
code: Code
custom_html_emails: Benutzerdefinierte HTML-E-Mails
connect_your_email_to_send_html_emails: Verbinden Sie Ihr E-Mail-Konto, um HTML-E-Mails zu senden
connect_your_email_or_outlook_account_or_add_smtp_settings_to_send_custom_html_emails: Verbinden Sie Ihr Gmail- oder Outlook-Konto oder fügen Sie SMTP-Einstellungen hinzu, um benutzerdefinierte HTML-E-Mails zu senden.
connect_gmail_or_outlook: Gmail oder Outlook verbinden
connect_your_email_to_bulk_send: Verbinden Sie Ihr E-Mail-Konto für Massenversand
connect_your_email_or_outlook_account_or_add_smtp_settings_to_bulk_send: Verbinden Sie Ihr Gmail- oder Outlook-Konto oder fügen Sie SMTP-Einstellungen für den Massenversand hinzu.
are_you_sure_you_want_to_add_recipients_without_sending_to_send_emails_it_requires_to_connect_gmail_or_outlook: Möchten Sie wirklich Empfänger hinzufügen, ohne zu senden? Zum Senden von E-Mails ist eine Verbindung zu Gmail oder Outlook erforderlich.
template_name_has_been_completed_by_submitters_html: '"<strong>{template.name}</strong>" wurde von <strong>{submission.submitters}</strong> abgeschlossen'
please_check_the_copy_of_your_template_name_in_the_email_attachments_html: 'Bitte prüfen Sie die Kopie Ihres "<strong>{template.name}</strong>" in den E-Mail-Anhängen.'
you_have_been_invited_to_sign_the_template_name_html: 'Du wurdest eingeladen, "<strong>{template.name}</strong>" zu unterschreiben.'
submission_sources:
api: API
bulk: Massenversand

@ -108,6 +108,7 @@ Rails.application.routes.draw do
resource :preferences, only: %i[show create], controller: 'templates_preferences'
resource :share_link, only: %i[show create], controller: 'templates_share_link'
resources :recipients, only: %i[create], controller: 'templates_recipients'
resources :prefillable_fields, only: %i[create], controller: 'templates_prefillable_fields'
resources :submissions_export, only: %i[index new]
end
resources :preview_document_page, only: %i[show], path: '/preview/:signed_uuid'

@ -4,7 +4,9 @@ module TemplateFolders
module_function
def filter_by_full_name(template_folders, name)
parent_name, name = name.split(' / ', 2).map(&:squish)
return template_folders.none if name.blank?
parent_name, name = name.to_s.split(' / ', 2).map(&:squish)
if name.present?
parent_folder = template_folders.where(parent_folder_id: nil).find_by(name: parent_name)
@ -75,7 +77,7 @@ module TemplateFolders
def find_or_create_by_name(author, name)
return author.account.default_template_folder if name.blank? || name == TemplateFolder::DEFAULT_NAME
parent_name, name = name.split(' / ', 2).map(&:squish)
parent_name, name = name.to_s.split(' / ', 2).map(&:squish)
if name.present?
parent_folder = author.account.template_folders.create_with(author:)

@ -6,10 +6,12 @@
"@babel/plugin-transform-runtime": "7.21.4",
"@babel/preset-env": "7.21.5",
"@babel/runtime": "7.21.5",
"@codemirror/lang-html": "^6.4.9",
"@eid-easy/eideasy-widget": "^2.163.4",
"@github/catalyst": "^2.0.0-beta",
"@hotwired/turbo": "https://github.com/docusealco/turbo#main",
"@hotwired/turbo-rails": "^7.3.0",
"@specious/htmlflow": "^1.1.0",
"@tabler/icons-vue": "^2.47.0",
"autocompleter": "^9.1.0",
"autoprefixer": "^10.4.14",
@ -17,6 +19,7 @@
"babel-plugin-dynamic-import-node": "^2.3.3",
"babel-plugin-macros": "^3.1.0",
"canvas-confetti": "^1.6.0",
"codemirror": "^6.0.2",
"compression-webpack-plugin": "10.0.0",
"css-loader": "^6.7.3",
"css-minimizer-webpack-plugin": "^5.0.0",

@ -1030,6 +1030,112 @@
"@babel/helper-string-parser" "^7.25.9"
"@babel/helper-validator-identifier" "^7.25.9"
"@codemirror/autocomplete@^6.0.0":
version "6.18.6"
resolved "https://registry.yarnpkg.com/@codemirror/autocomplete/-/autocomplete-6.18.6.tgz#de26e864a1ec8192a1b241eb86addbb612964ddb"
integrity sha512-PHHBXFomUs5DF+9tCOM/UoW6XQ4R44lLNNhRaW9PKPTU0D7lIjRg3ElxaJnTwsl/oHiR93WSXDBrekhoUGCPtg==
dependencies:
"@codemirror/language" "^6.0.0"
"@codemirror/state" "^6.0.0"
"@codemirror/view" "^6.17.0"
"@lezer/common" "^1.0.0"
"@codemirror/commands@^6.0.0":
version "6.8.1"
resolved "https://registry.yarnpkg.com/@codemirror/commands/-/commands-6.8.1.tgz#639f5559d2f33f2582a2429c58cb0c1b925c7a30"
integrity sha512-KlGVYufHMQzxbdQONiLyGQDUW0itrLZwq3CcY7xpv9ZLRHqzkBSoteocBHtMCoY7/Ci4xhzSrToIeLg7FxHuaw==
dependencies:
"@codemirror/language" "^6.0.0"
"@codemirror/state" "^6.4.0"
"@codemirror/view" "^6.27.0"
"@lezer/common" "^1.1.0"
"@codemirror/lang-css@^6.0.0":
version "6.3.1"
resolved "https://registry.yarnpkg.com/@codemirror/lang-css/-/lang-css-6.3.1.tgz#763ca41aee81bb2431be55e3cfcc7cc8e91421a3"
integrity sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==
dependencies:
"@codemirror/autocomplete" "^6.0.0"
"@codemirror/language" "^6.0.0"
"@codemirror/state" "^6.0.0"
"@lezer/common" "^1.0.2"
"@lezer/css" "^1.1.7"
"@codemirror/lang-html@^6.4.9":
version "6.4.9"
resolved "https://registry.yarnpkg.com/@codemirror/lang-html/-/lang-html-6.4.9.tgz#d586f2cc9c341391ae07d1d7c545990dfa069727"
integrity sha512-aQv37pIMSlueybId/2PVSP6NPnmurFDVmZwzc7jszd2KAF8qd4VBbvNYPXWQq90WIARjsdVkPbw29pszmHws3Q==
dependencies:
"@codemirror/autocomplete" "^6.0.0"
"@codemirror/lang-css" "^6.0.0"
"@codemirror/lang-javascript" "^6.0.0"
"@codemirror/language" "^6.4.0"
"@codemirror/state" "^6.0.0"
"@codemirror/view" "^6.17.0"
"@lezer/common" "^1.0.0"
"@lezer/css" "^1.1.0"
"@lezer/html" "^1.3.0"
"@codemirror/lang-javascript@^6.0.0":
version "6.2.4"
resolved "https://registry.yarnpkg.com/@codemirror/lang-javascript/-/lang-javascript-6.2.4.tgz#eef2227d1892aae762f3a0f212f72bec868a02c5"
integrity sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA==
dependencies:
"@codemirror/autocomplete" "^6.0.0"
"@codemirror/language" "^6.6.0"
"@codemirror/lint" "^6.0.0"
"@codemirror/state" "^6.0.0"
"@codemirror/view" "^6.17.0"
"@lezer/common" "^1.0.0"
"@lezer/javascript" "^1.0.0"
"@codemirror/language@^6.0.0", "@codemirror/language@^6.4.0", "@codemirror/language@^6.6.0":
version "6.11.2"
resolved "https://registry.yarnpkg.com/@codemirror/language/-/language-6.11.2.tgz#90d2d094cfbd14263bc5354ebd2445ee4e81bdc3"
integrity sha512-p44TsNArL4IVXDTbapUmEkAlvWs2CFQbcfc0ymDsis1kH2wh0gcY96AS29c/vp2d0y2Tquk1EDSaawpzilUiAw==
dependencies:
"@codemirror/state" "^6.0.0"
"@codemirror/view" "^6.23.0"
"@lezer/common" "^1.1.0"
"@lezer/highlight" "^1.0.0"
"@lezer/lr" "^1.0.0"
style-mod "^4.0.0"
"@codemirror/lint@^6.0.0":
version "6.8.5"
resolved "https://registry.yarnpkg.com/@codemirror/lint/-/lint-6.8.5.tgz#9edaa808e764e28e07665b015951934c8ec3a418"
integrity sha512-s3n3KisH7dx3vsoeGMxsbRAgKe4O1vbrnKBClm99PU0fWxmxsx5rR2PfqQgIt+2MMJBHbiJ5rfIdLYfB9NNvsA==
dependencies:
"@codemirror/state" "^6.0.0"
"@codemirror/view" "^6.35.0"
crelt "^1.0.5"
"@codemirror/search@^6.0.0":
version "6.5.11"
resolved "https://registry.yarnpkg.com/@codemirror/search/-/search-6.5.11.tgz#a324ffee36e032b7f67aa31c4fb9f3e6f9f3ed63"
integrity sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA==
dependencies:
"@codemirror/state" "^6.0.0"
"@codemirror/view" "^6.0.0"
crelt "^1.0.5"
"@codemirror/state@^6.0.0", "@codemirror/state@^6.4.0", "@codemirror/state@^6.5.0":
version "6.5.2"
resolved "https://registry.yarnpkg.com/@codemirror/state/-/state-6.5.2.tgz#8eca3a64212a83367dc85475b7d78d5c9b7076c6"
integrity sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==
dependencies:
"@marijn/find-cluster-break" "^1.0.0"
"@codemirror/view@^6.0.0", "@codemirror/view@^6.17.0", "@codemirror/view@^6.23.0", "@codemirror/view@^6.27.0", "@codemirror/view@^6.35.0":
version "6.38.0"
resolved "https://registry.yarnpkg.com/@codemirror/view/-/view-6.38.0.tgz#4486062b791a4247793e0953e05ae71a9e172217"
integrity sha512-yvSchUwHOdupXkd7xJ0ob36jdsSR/I+/C+VbY0ffBiL5NiSTEBDfB1ZGWbbIlDd5xgdUkody+lukAdOxYrOBeg==
dependencies:
"@codemirror/state" "^6.5.0"
crelt "^1.0.6"
style-mod "^4.1.0"
w3c-keyname "^2.2.4"
"@discoveryjs/json-ext@0.5.7", "@discoveryjs/json-ext@^0.5.0":
version "0.5.7"
resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70"
@ -1237,6 +1343,57 @@
resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b"
integrity sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==
"@lezer/common@^1.0.0", "@lezer/common@^1.0.2", "@lezer/common@^1.1.0", "@lezer/common@^1.2.0":
version "1.2.3"
resolved "https://registry.yarnpkg.com/@lezer/common/-/common-1.2.3.tgz#138fcddab157d83da557554851017c6c1e5667fd"
integrity sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==
"@lezer/css@^1.1.0", "@lezer/css@^1.1.7":
version "1.3.0"
resolved "https://registry.yarnpkg.com/@lezer/css/-/css-1.3.0.tgz#296f298814782c2fad42a936f3510042cdcd2034"
integrity sha512-pBL7hup88KbI7hXnZV3PQsn43DHy6TWyzuyk2AO9UyoXcDltvIdqWKE1dLL/45JVZ+YZkHe1WVHqO6wugZZWcw==
dependencies:
"@lezer/common" "^1.2.0"
"@lezer/highlight" "^1.0.0"
"@lezer/lr" "^1.3.0"
"@lezer/highlight@^1.0.0", "@lezer/highlight@^1.1.3":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@lezer/highlight/-/highlight-1.2.1.tgz#596fa8f9aeb58a608be0a563e960c373cbf23f8b"
integrity sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==
dependencies:
"@lezer/common" "^1.0.0"
"@lezer/html@^1.3.0":
version "1.3.10"
resolved "https://registry.yarnpkg.com/@lezer/html/-/html-1.3.10.tgz#1be9a029a6fe835c823b20a98a449a630416b2af"
integrity sha512-dqpT8nISx/p9Do3AchvYGV3qYc4/rKr3IBZxlHmpIKam56P47RSHkSF5f13Vu9hebS1jM0HmtJIwLbWz1VIY6w==
dependencies:
"@lezer/common" "^1.2.0"
"@lezer/highlight" "^1.0.0"
"@lezer/lr" "^1.0.0"
"@lezer/javascript@^1.0.0":
version "1.5.1"
resolved "https://registry.yarnpkg.com/@lezer/javascript/-/javascript-1.5.1.tgz#2a424a6ec29f1d4ef3c34cbccc5447e373618ad8"
integrity sha512-ATOImjeVJuvgm3JQ/bpo2Tmv55HSScE2MTPnKRMRIPx2cLhHGyX2VnqpHhtIV1tVzIjZDbcWQm+NCTF40ggZVw==
dependencies:
"@lezer/common" "^1.2.0"
"@lezer/highlight" "^1.1.3"
"@lezer/lr" "^1.3.0"
"@lezer/lr@^1.0.0", "@lezer/lr@^1.3.0":
version "1.4.2"
resolved "https://registry.yarnpkg.com/@lezer/lr/-/lr-1.4.2.tgz#931ea3dea8e9de84e90781001dae30dea9ff1727"
integrity sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==
dependencies:
"@lezer/common" "^1.0.0"
"@marijn/find-cluster-break@^1.0.0":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz#775374306116d51c0c500b8c4face0f9a04752d8"
integrity sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==
"@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1":
version "5.1.1-v1"
resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz#dbf733a965ca47b1973177dc0bb6c889edcfb129"
@ -1280,6 +1437,13 @@
resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.25.24.tgz#8c7688559979f7079aacaf31aa881c3aa410b718"
integrity sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==
"@specious/htmlflow@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@specious/htmlflow/-/htmlflow-1.1.0.tgz#cc8319c2cc6b5e2e309fa213d5a1f54cc093d9eb"
integrity sha512-g41FgN5kfuGkOrWhhmVvFes2BzQ3msU5oR/8KPdXbkBt8UIHtwL+by9maItDIhu+BR7pxshABtaB/FqpESa9Mg==
dependencies:
htmlparser2 "^8.0.1"
"@tabler/icons-vue@^2.47.0":
version "2.47.0"
resolved "https://registry.yarnpkg.com/@tabler/icons-vue/-/icons-vue-2.47.0.tgz#604608a6df673d035c0e8e33f0565eeffa3adf36"
@ -2172,6 +2336,19 @@ clone-deep@^4.0.1:
kind-of "^6.0.2"
shallow-clone "^3.0.0"
codemirror@^6.0.2:
version "6.0.2"
resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-6.0.2.tgz#4d3fea1ad60b6753f97ca835f2f48c6936a8946e"
integrity sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==
dependencies:
"@codemirror/autocomplete" "^6.0.0"
"@codemirror/commands" "^6.0.0"
"@codemirror/language" "^6.0.0"
"@codemirror/lint" "^6.0.0"
"@codemirror/search" "^6.0.0"
"@codemirror/state" "^6.0.0"
"@codemirror/view" "^6.0.0"
color-convert@^1.9.0:
version "1.9.3"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
@ -2351,6 +2528,11 @@ cosmiconfig@^8.1.3:
parse-json "^5.0.0"
path-type "^4.0.0"
crelt@^1.0.5, crelt@^1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/crelt/-/crelt-1.0.6.tgz#7cc898ea74e190fb6ef9dae57f8f81cf7302df72"
integrity sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==
cross-spawn@^7.0.2, cross-spawn@^7.0.3:
version "7.0.3"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
@ -2711,7 +2893,7 @@ enhanced-resolve@^5.17.1:
graceful-fs "^4.2.4"
tapable "^2.2.0"
entities@^4.2.0:
entities@^4.2.0, entities@^4.4.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48"
integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==
@ -3511,6 +3693,16 @@ html-entities@^2.3.2:
resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.3.3.tgz#117d7626bece327fc8baace8868fa6f5ef856e46"
integrity sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==
htmlparser2@^8.0.1:
version "8.0.2"
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-8.0.2.tgz#f002151705b383e62433b5cf466f5b716edaec21"
integrity sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==
dependencies:
domelementtype "^2.3.0"
domhandler "^5.0.3"
domutils "^3.0.1"
entities "^4.4.0"
http-deceiver@^1.2.7:
version "1.2.7"
resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87"
@ -5438,6 +5630,11 @@ strip-json-comments@^3.1.0, strip-json-comments@^3.1.1:
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
style-mod@^4.0.0, style-mod@^4.1.0:
version "4.1.2"
resolved "https://registry.yarnpkg.com/style-mod/-/style-mod-4.1.2.tgz#ca238a1ad4786520f7515a8539d5a63691d7bf67"
integrity sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==
stylehacks@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-6.0.0.tgz#9fdd7c217660dae0f62e14d51c89f6c01b3cb738"
@ -5845,6 +6042,11 @@ vue@^3.3.2:
"@vue/server-renderer" "3.3.4"
"@vue/shared" "3.3.4"
w3c-keyname@^2.2.4:
version "2.2.8"
resolved "https://registry.yarnpkg.com/w3c-keyname/-/w3c-keyname-2.2.8.tgz#7b17c8c6883d4e8b86ac8aba79d39e880f8869c5"
integrity sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==
watchpack@^2.4.0, watchpack@^2.4.1:
version "2.4.2"
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.2.tgz#2feeaed67412e7c33184e5a79ca738fbd38564da"

Loading…
Cancel
Save