Compare commits

..

67 Commits

Author SHA1 Message Date
Alex Turchyn 94d1fb7dcb Merge from docusealco/wip 2025-12-08 15:31:19 +02:00
Alex Turchyn 6e43a42274 add email 2FA 2025-12-08 14:51:49 +02:00
Pete Matsyburka 0e0aa0259e add validate prop 2025-12-07 15:31:20 +02:00
Pete Matsyburka 75ab6d39ef adjust unarchive 2025-12-07 14:57:21 +02:00
Pete Matsyburka 6f84d84f96 use newer model 2025-12-05 15:17:50 +02:00
Pete Matsyburka cf46593e9b fix required checbox 2025-12-04 21:41:36 +02:00
Pete Matsyburka 1808346348 fix build 2025-12-04 15:38:55 +02:00
Pete Matsyburka ea59a68499 adjust active job log 2025-12-04 14:54:55 +02:00
Pete Matsyburka cba2d02990 skip webhook not found 2025-12-03 13:20:52 +02:00
Pete Matsyburka e92fc0d3e1 adjust mobile field types 2025-12-03 10:33:22 +02:00
Pete Matsyburka f1eaa77092 password reset i18n 2025-12-02 14:25:45 +02:00
Alex Turchyn 5934bdec55 use set_reset_password_token 2025-12-02 14:00:51 +02:00
Alex Turchyn 7a7d3c6ee3 add translations for reset password instructions email 2025-12-02 14:00:51 +02:00
Pete Matsyburka cb2f58c04c fix i18n 2025-12-02 13:31:35 +02:00
Pete Matsyburka a1c117f1ca add i18n 2025-12-02 12:50:58 +02:00
Alex Turchyn 1aacd98460 Merge from docusealco/wip 2025-12-01 17:09:47 +02:00
Pete Matsyburka 840162c9ca fix sort 2025-12-01 10:37:58 +02:00
Pete Matsyburka 3519b0e009 fix typo 2025-12-01 10:15:23 +02:00
Pete Matsyburka 53fae56c51 fix regexp field types 2025-12-01 10:09:27 +02:00
Pete Matsyburka f4552dbaa8 skip reconfirmation 2025-11-30 16:30:56 +02:00
Pete Matsyburka dfab4c331c send confirmation 2025-11-30 16:04:30 +02:00
Pete Matsyburka d2a0937038 fix pdf text emoji 2025-11-30 14:37:05 +02:00
Pete Matsyburka fa1ef44b22 add tabs 2025-11-30 12:41:16 +02:00
Pete Matsyburka c1a5c91299 adjust detection 2025-11-30 11:11:08 +02:00
Pete Matsyburka e6280e8f5a optimize text nodes 2025-11-29 18:28:10 +02:00
Pete Matsyburka fd2ba57325 adjust invitation 2025-11-29 10:34:13 +02:00
Pete Matsyburka dc178ef03f adjust router 2025-11-28 16:15:57 +02:00
Alex Turchyn 68936e10c5 add confirmation columns 2025-11-28 15:57:36 +02:00
Pete Matsyburka 41f16e7d39 adjust detection 2025-11-27 17:00:26 +02:00
Pete Matsyburka 453ebbf90a adjust start form 2025-11-27 12:23:45 +02:00
Pete Matsyburka 7ca290c080 adjust detection 2025-11-25 13:43:35 +02:00
Alex Turchyn dd5b6a6aa6 Merge from docusealco/wip 2025-11-24 17:43:59 +02:00
Pete Matsyburka bf2ebb995b add tz info 2025-11-24 15:46:50 +02:00
Alex Turchyn 3e929758fb add the reports page 2025-11-24 15:46:04 +02:00
Pete Matsyburka 4a53116586 adjust links 2025-11-24 12:28:13 +02:00
Pete Matsyburka 2d1981acfa adjust billing link 2025-11-24 11:13:00 +02:00
Pete Matsyburka c610df269f adjust populate 2025-11-22 13:02:58 +02:00
Pete Matsyburka 96dac635f5 add us timezones 2025-11-21 17:50:04 +02:00
Pete Matsyburka abd579103d add index 2025-11-21 13:41:51 +02:00
Pete Matsyburka 7ed27bb413 populate columns 2025-11-21 12:33:43 +02:00
Pete Matsyburka 180c542a9f add index 2025-11-21 11:45:19 +02:00
Pete Matsyburka 89613113de set is first completed submitter 2025-11-21 11:31:02 +02:00
Pete Matsyburka 3102900776 add is first to completed submitters 2025-11-21 11:27:28 +02:00
Pete Matsyburka 1a68053f4e set submission event account id 2025-11-21 11:18:28 +02:00
Pete Matsyburka 5d4a032763 add submission event account id 2025-11-21 11:17:20 +02:00
Pete Matsyburka 7481d27822 add tsa timeout 2025-11-18 22:17:05 +02:00
Alex Turchyn 22afd01fd2 Merge from docusealco/wip 2025-11-17 13:20:44 +02:00
Pete Matsyburka fd9eb09dd6 use can? 2025-11-17 10:21:38 +02:00
Alex Turchyn c53ed7f984 add custom invitation reminder emails 2025-11-17 10:05:52 +02:00
Pete Matsyburka 1d87e210be optimize detect fields 2025-11-16 19:05:54 +02:00
Pete Matsyburka 5fec952b8c adjust detections 2025-11-15 19:48:42 +02:00
Pete Matsyburka c4fbcc103a autocorrect pdf 2025-11-15 10:46:54 +02:00
Pete Matsyburka 9333a1ba45 fix fields error alert 2025-11-14 16:35:02 +02:00
Pete Matsyburka de5cd92dba adjust send email 2025-11-13 19:36:06 +02:00
Pete Matsyburka 83e32bec9f fix preferences form 2025-11-13 10:34:52 +02:00
Pete Matsyburka 4603efc04e refactor toggles form 2025-11-13 09:50:26 +02:00
Alex Turchyn 6c73c82a27 add ability to reset custom template emails to default 2025-11-13 08:56:09 +02:00
Pete Matsyburka 8e71d9f9e9 skip email 2025-11-13 08:29:27 +02:00
Pete Matsyburka 80f60a5d88 re-invite archived user 2025-11-12 20:33:52 +02:00
Pete Matsyburka 2c33ec382d fix device icon 2025-11-12 16:48:33 +02:00
Pete Matsyburka 884865cffc adjust size 2025-11-12 16:39:29 +02:00
Alex Turchyn 9537fe8c91 update constants 2025-11-12 16:33:51 +02:00
Alex Turchyn c8fe36a2b6 add desktop/mobile/tablet badges to events log 2025-11-12 16:33:51 +02:00
Pete Matsyburka 7ec3282b56 i18n 2025-11-11 17:34:50 +02:00
Pete Matsyburka ab8c41a520 add signature reasons param 2025-11-11 13:42:01 +02:00
Pete Matsyburka 4c1c70ff04 fix audit generation 2025-11-11 12:08:16 +02:00
Pete Matsyburka d7a6e80bb1 html escape font signature 2025-11-11 11:11:07 +02:00
93 changed files with 1910 additions and 648 deletions
+3
View File
@@ -45,6 +45,9 @@ Metrics/PerceivedComplexity:
Style/MultipleComparison:
Enabled: false
Style/NumericPredicate:
Enabled: false
Naming/PredicateMethod:
Enabled: false
+2 -2
View File
@@ -9,7 +9,7 @@ RUN apk --no-cache add fontforge wget && \
wget https://cdn.jsdelivr.net/gh/notofonts/notofonts.github.io/fonts/NotoSansSymbols2/hinted/ttf/NotoSansSymbols2-Regular.ttf && \
wget https://github.com/Maxattax97/gnu-freefont/raw/master/ttf/FreeSans.ttf && \
wget https://github.com/impallari/DancingScript/raw/master/OFL.txt && \
wget -O /model.onnx "https://github.com/docusealco/fields-detection/releases/download/1.0.0/model_704_int8.onnx" && \
wget -O /model.onnx "https://github.com/docusealco/fields-detection/releases/download/1.1.0/model_704_int8.onnx" && \
wget -O pdfium-linux.tgz "https://github.com/docusealco/pdfium-binaries/releases/latest/download/pdfium-linux-$(uname -m | sed 's/x86_64/x64/;s/aarch64/arm64/').tgz" && \
mkdir -p /pdfium-linux && \
tar -xzf pdfium-linux.tgz -C /pdfium-linux
@@ -90,7 +90,7 @@ COPY --from=download /model.onnx /app/tmp/model.onnx
COPY --from=webpack /app/public/packs ./public/packs
RUN ln -s /fonts /app/public/fonts
RUN bundle exec bootsnap precompile --gemfile app/ lib/
RUN bundle exec bootsnap precompile -j 1 --gemfile app/ lib/
WORKDIR /data/docuseal
ENV WORKDIR=/data/docuseal
+1
View File
@@ -46,6 +46,7 @@ class AccountsController < ApplicationController
def destroy
authorize!(:manage, current_account)
true_user.skip_reconfirmation!
true_user.update!(locked_at: Time.current, email: true_user.email.sub('@', '+removed@'))
true_user.account.update!(archived_at: Time.current)
@@ -37,7 +37,7 @@ module Api
rescue Submitters::MaliciousFileExtension => e
Rollbar.error(e) if defined?(Rollbar)
render json: { error: e.message }, status: :unprocessable_entity
render json: { error: e.message }, status: :unprocessable_content
end
def build_new_cookie_signatures_json(submitter, attachment)
@@ -1,4 +1,11 @@
# frozen_string_literal: true
class InvitationsController < Devise::PasswordsController
def update
super do |resource|
resource.confirmed_at ||= Time.current if resource.errors.empty?
PasswordsController::Current.user = resource
end
end
end
+2
View File
@@ -5,6 +5,8 @@ class PasswordsController < Devise::PasswordsController
skip_before_action :require_no_authentication, only: %i[edit update]
# rubocop:enable Rails/LexicallyScopedActionFilter
around_action :with_browser_locale
class Current < ActiveSupport::CurrentAttributes
attribute :user
end
@@ -4,6 +4,7 @@ class PersonalizationSettingsController < ApplicationController
ALLOWED_KEYS = [
AccountConfig::FORM_COMPLETED_BUTTON_KEY,
AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY,
AccountConfig::SUBMITTER_INVITATION_REMINDER_EMAIL_KEY,
AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY,
AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY,
AccountConfig::FORM_COMPLETED_MESSAGE_KEY,
+8 -1
View File
@@ -9,7 +9,14 @@ class ProfileController < ApplicationController
def update_contact
if current_user.update(contact_params)
redirect_to settings_profile_index_path, notice: I18n.t('contact_information_has_been_update')
if current_user.try(:pending_reconfirmation?) && current_user.previous_changes.key?(:unconfirmed_email)
SendConfirmationInstructionsJob.perform_async('user_id' => current_user.id)
redirect_to settings_profile_index_path,
notice: I18n.t('a_confirmation_email_has_been_sent_to_the_new_email_address')
else
redirect_to settings_profile_index_path, notice: I18n.t('contact_information_has_been_update')
end
else
render :index, status: :unprocessable_content
end
+4 -2
View File
@@ -16,7 +16,9 @@ class StartFormController < ApplicationController
COOKIES_DEFAULTS = { httponly: true, secure: Rails.env.production? }.freeze
def show
raise ActionController::RoutingError, I18n.t('not_found') if @template.preferences['require_phone_2fa']
if @template.preferences['require_phone_2fa'] || @template.preferences['require_email_2fa']
raise ActionController::RoutingError, I18n.t('not_found')
end
if @template.shared_link?
@submitter = @template.submissions.new(account_id: @template.account_id)
@@ -81,7 +83,7 @@ class StartFormController < ApplicationController
@submitter = Submitter.where(submission: @template.submissions)
.where.not(completed_at: nil)
.find_by!(required_params)
.find_by!(required_params.except('name'))
end
private
@@ -10,6 +10,7 @@ class SubmissionEventsController < ApplicationController
'api_complete_form' => 'check',
'send_reminder_email' => 'mail_forward',
'send_2fa_sms' => '2fa',
'send_2fa_email' => '2fa',
'send_sms' => 'send',
'phone_verified' => 'phone_check',
'email_verified' => 'email_check',
@@ -53,8 +53,11 @@ class SubmissionsPreviewController < ApplicationController
def use_signature?(submission)
return false if current_user && can?(:read, submission)
return true if submission.submitters.any? { |e| e.preferences['require_phone_2fa'] }
return true if submission.submitters.any? do |e|
e.preferences['require_phone_2fa'] || e.preferences['require_email_2fa']
end
return true if submission.template&.preferences&.dig('require_phone_2fa')
return true if submission.template&.preferences&.dig('require_email_2fa')
!submission_valid_ttl?(submission)
end
+16
View File
@@ -17,6 +17,7 @@ class SubmitFormController < ApplicationController
submission = @submitter.submission
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
return render :email_2fa if require_email_2fa?(@submitter)
@form_configs = Submitters::FormConfigs.call(@submitter, CONFIG_KEYS)
@@ -47,6 +48,11 @@ class SubmitFormController < ApplicationController
end
def update
if require_email_2fa?(@submitter)
return render json: { error: I18n.t('verification_required_refresh_the_page_and_pass_2fa') },
status: :unprocessable_content
end
if @submitter.completed_at?
return render json: { error: I18n.t('form_has_been_completed_already') }, status: :unprocessable_content
end
@@ -77,6 +83,8 @@ class SubmitFormController < ApplicationController
def completed
raise ActionController::RoutingError, I18n.t('not_found') if @submitter.account.archived_at?
redirect_to submit_form_path(params[:submit_form_slug]) if require_email_2fa?(@submitter)
end
def success; end
@@ -109,4 +117,12 @@ class SubmitFormController < ApplicationController
ActiveStorage::Attachment.where(record: submission.submitters, name: :attachments)
.preload(:blob).index_by(&:uuid)
end
def require_email_2fa?(submitter)
return false if submitter.submission.template&.preferences&.dig('require_email_2fa') != true &&
submitter.preferences['require_email_2fa'] != true
return false if cookies.encrypted[:email_2fa_slug] == submitter.slug
true
end
end
@@ -0,0 +1,52 @@
# frozen_string_literal: true
class SubmitFormEmail2fasController < ApplicationController
around_action :with_browser_locale
skip_before_action :authenticate_user!
skip_authorization_check
before_action :load_submitter
COOKIES_TTL = 12.hours
COOKIES_DEFAULTS = { httponly: true, secure: Rails.env.production? }.freeze
def create
RateLimit.call("verify-2fa-code-#{@submitter.id}", limit: 2, ttl: 45.seconds, enabled: true)
value = [@submitter.email.downcase.strip, @submitter.slug].join(':')
if EmailVerificationCodes.verify(params[:one_time_code].to_s.gsub(/\D/, ''), value)
SubmissionEvents.create_with_tracking_data(@submitter, 'email_verified', request, { email: @submitter.email })
cookies.encrypted[:email_2fa_slug] =
{ value: @submitter.slug, expires: COOKIES_TTL.from_now, **COOKIES_DEFAULTS }
redirect_to submit_form_path(@submitter.slug)
else
redirect_to submit_form_path(@submitter.slug, status: :error), alert: I18n.t(:invalid_code)
end
rescue RateLimit::LimitApproached
redirect_to submit_form_path(@submitter.slug, status: :error), alert: I18n.t(:too_many_attempts)
end
def update
if @submitter.submission_events.where(event_type: 'send_2fa_email').exists?(created_at: 15.seconds.ago..)
return redirect_to submit_form_path(@submitter.slug, status: :error), alert: I18n.t(:rate_limit_exceeded)
end
RateLimit.call("send-email-code-#{@submitter.id}", limit: 2, ttl: 45.seconds, enabled: true)
SendSubmitterVerificationEmailJob.perform_async('submitter_id' => @submitter.id, 'locale' => I18n.locale.to_s)
redir_params = params[:resend] ? { alert: I18n.t(:code_has_been_resent) } : {}
redirect_to submit_form_path(@submitter.slug, status: :sent), **redir_params
rescue RateLimit::LimitApproached
redirect_to submit_form_path(@submitter.slug, status: :error), alert: I18n.t(:too_many_attempts)
end
def load_submitter
@submitter = Submitter.find_by!(slug: params[:submitter_slug])
end
end
@@ -3,6 +3,15 @@
class TemplatesPreferencesController < ApplicationController
load_and_authorize_resource :template
RESETTABLE_PREFERENCE_KEYS = {
AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY => %w[request_email_subject request_email_body submitters],
AccountConfig::SUBMITTER_INVITATION_REMINDER_EMAIL_KEY => %w[invitation_reminder_email_subject
invitation_reminder_email_body],
AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY => %w[documents_copy_email_subject documents_copy_email_body],
AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY => %w[completed_notification_email_subject
completed_notification_email_body]
}.freeze
def show; end
def create
@@ -15,19 +24,38 @@ class TemplatesPreferencesController < ApplicationController
head :ok
end
def destroy
authorize!(:update, @template)
config_key = params[:config_key]
preferences_to_delete = RESETTABLE_PREFERENCE_KEYS[config_key]
return head :ok if preferences_to_delete.blank?
preferences_to_delete.each do |key|
@template.preferences.delete(key)
end
@template.save!
render turbo_stream: turbo_stream.replace("#{config_key}_form",
partial: "templates_preferences/#{config_key}_form"),
status: :ok
end
private
def template_params
params.require(:template).permit(
preferences: %i[bcc_completed request_email_subject request_email_body
invitation_reminder_email_subject invitation_reminder_email_body
documents_copy_email_subject documents_copy_email_body
documents_copy_email_enabled documents_copy_email_attach_audit
documents_copy_email_attach_documents documents_copy_email_reply_to
completed_notification_email_attach_documents
completed_redirect_url validate_unique_submitters
require_all_submitters submitters_order require_phone_2fa
default_expire_at_duration shared_link_2fa
default_expire_at request_email_enabled
require_all_submitters submitters_order require_phone_2fa require_email_2fa
default_expire_at_duration shared_link_2fa default_expire_at request_email_enabled
completed_notification_email_subject completed_notification_email_body
completed_notification_email_enabled completed_notification_email_attach_audit] +
[completed_message: %i[title body],
+22 -5
View File
@@ -24,10 +24,19 @@ class UsersController < ApplicationController
def edit; end
def create
if User.accessible_by(current_ability).exists?(email: @user.email)
@user.errors.add(:email, I18n.t('already_exists'))
existing_user = User.accessible_by(current_ability).find_by(email: @user.email)
return render turbo_stream: turbo_stream.replace(:modal, template: 'users/new'), status: :unprocessable_content
if existing_user
if existing_user.archived_at? &&
current_ability.can?(:manage, existing_user) && current_ability.can?(:manage, @user.account)
existing_user.assign_attributes(@user.slice(:first_name, :last_name, :role, :account_id))
existing_user.archived_at = nil
@user = existing_user
else
@user.errors.add(:email, I18n.t('already_exists'))
return render turbo_stream: turbo_stream.replace(:modal, template: 'users/new'), status: :unprocessable_content
end
end
@user.password = SecureRandom.hex if @user.password.blank?
@@ -45,7 +54,8 @@ class UsersController < ApplicationController
def update
return redirect_to settings_users_path, notice: I18n.t('unable_to_update_user') if Docuseal.demo?
attrs = user_params.compact_blank.merge(user_params.slice(:archived_at))
attrs = user_params.compact_blank
attrs = attrs.merge(user_params.slice(:archived_at)) if current_ability.can?(:create, @user)
if params.dig(:user, :account_id).present?
account = Account.accessible_by(current_ability).find(params.dig(:user, :account_id))
@@ -56,7 +66,14 @@ class UsersController < ApplicationController
end
if @user.update(attrs.except(*(current_user == @user ? %i[password otp_required_for_login role] : %i[password])))
redirect_back fallback_location: settings_users_path, notice: I18n.t('user_has_been_updated')
if @user.try(:pending_reconfirmation?) && @user.previous_changes.key?(:unconfirmed_email)
SendConfirmationInstructionsJob.perform_async('user_id' => @user.id)
redirect_back fallback_location: settings_users_path,
notice: I18n.t('a_confirmation_email_has_been_sent_to_the_new_email_address')
else
redirect_back fallback_location: settings_users_path, notice: I18n.t('user_has_been_updated')
end
else
render turbo_stream: turbo_stream.replace(:modal, template: 'users/edit'), status: :unprocessable_content
end
+2
View File
@@ -51,6 +51,7 @@ import ToggleClasses from './elements/toggle_classes'
import AutosizeField from './elements/autosize_field'
import GoogleDriveFilePicker from './elements/google_drive_file_picker'
import OpenModal from './elements/open_modal'
import BarChart from './elements/bar_chart'
import * as TurboInstantClick from './lib/turbo_instant_click'
@@ -140,6 +141,7 @@ safeRegisterElement('toggle-classes', ToggleClasses)
safeRegisterElement('autosize-field', AutosizeField)
safeRegisterElement('google-drive-file-picker', GoogleDriveFilePicker)
safeRegisterElement('open-modal', OpenModal)
safeRegisterElement('bar-chart', BarChart)
safeRegisterElement('template-builder', class extends HTMLElement {
connectedCallback () {
+50
View File
@@ -0,0 +1,50 @@
export default class extends HTMLElement {
connectedCallback () {
this.chartLabels = JSON.parse(this.dataset.labels || '[]')
this.chartDatasets = JSON.parse(this.dataset.datasets || '[]')
this.initChart()
}
disconnectedCallback () {
if (this.chartInstance) {
this.chartInstance.destroy()
this.chartInstance = null
}
}
async initChart () {
const { default: Chart } = await import(/* webpackChunkName: "chartjs" */ 'chart.js/auto')
const canvas = this.querySelector('canvas')
const ctx = canvas.getContext('2d')
this.chartInstance = new Chart(ctx, {
type: 'bar',
data: {
labels: this.chartLabels,
datasets: this.chartDatasets
},
options: {
responsive: true,
maintainAspectRatio: true,
animation: false,
scales: {
y: {
beginAtZero: true,
grace: '20%',
ticks: {
precision: 0
}
}
},
plugins: {
legend: {
display: false
}
}
}
})
}
}
+11 -2
View File
@@ -133,6 +133,12 @@
name="_method"
type="hidden"
>
<input
v-if="validate === false"
value="false"
name="validate"
type="hidden"
>
<div class="md:mt-4">
<div v-if="['cells', 'text'].includes(currentField.type)">
<TextStep
@@ -351,8 +357,6 @@
class="base-checkbox !h-7 !w-7"
:required="field.required"
:checked="!!values[field.uuid]"
@invalid="$event.target.setCustomValidity(t('please_check_the_box_to_continue'))"
@change="$event.target.setCustomValidity($event.target.validity.valueMissing ? t('please_check_the_box_to_continue') : '')"
@click="[scrollIntoField(field), values[field.uuid] = !values[field.uuid]]"
>
<span
@@ -718,6 +722,11 @@ export default {
required: false,
default: false
},
validate: {
type: Boolean,
required: false,
default: true
},
withDisclosure: {
type: Boolean,
required: false,
@@ -211,7 +211,7 @@
@input="updateWrittenSignature"
>
<select
v-if="requireSigningReason && !isOtherReason"
v-if="withSigningReason && !isOtherReason"
class="select base-input !text-2xl w-full mt-6 text-center"
:class="{ 'text-gray-300': !reason }"
required
@@ -226,24 +226,37 @@
>
{{ t('select_a_reason') }}
</option>
<option
v-for="(label, option) in defaultReasons"
:key="option"
:value="option"
:selected="reason === option"
class="text-base-content"
>
{{ label }}
</option>
<option
value="other"
class="text-base-content"
>
{{ t('other') }}
</option>
<template v-if="field.preferences?.reasons">
<option
v-for="option in field.preferences.reasons"
:key="option"
:value="option"
:selected="reason === option"
class="text-base-content"
>
{{ option }}
</option>
</template>
<template v-else>
<option
v-for="(label, option) in defaultReasons"
:key="option"
:value="option"
:selected="reason === option"
class="text-base-content"
>
{{ label }}
</option>
<option
value="other"
class="text-base-content"
>
{{ t('other') }}
</option>
</template>
</select>
<input
v-if="requireSigningReason && isOtherReason"
v-if="withSigningReason && isOtherReason"
class="base-input !text-2xl w-full mt-6"
required
:name="`values[${field.preferences.reason_field_uuid}]`"
@@ -253,7 +266,7 @@
@input="$emit('update:reason', $event.target.value)"
>
<input
v-if="requireSigningReason"
v-if="withSigningReason"
hidden
name="with_reason"
:value="field.preferences.reason_field_uuid"
@@ -406,6 +419,9 @@ export default {
format () {
return this.field.preferences?.format
},
withSigningReason () {
return this.requireSigningReason || this.field.preferences?.reasons?.length
},
defaultReasons () {
return {
[this.t('approved_by')]: this.t('approved'),
@@ -424,10 +440,11 @@ export default {
created () {
this.isSignatureStarted = !!this.computedPreviousValue
if (this.requireSigningReason) {
if (this.withSigningReason) {
this.field.preferences ||= {}
this.field.preferences.reason_field_uuid ||= v4()
this.isOtherReason = this.reason && !this.defaultReasons[this.reason]
this.isOtherReason = this.reason && !this.defaultReasons[this.reason] &&
(!this.field.preferences?.reasons?.length || !this.field.preferences.reasons.includes(this.reason))
}
},
async mounted () {
+9 -5
View File
@@ -236,7 +236,7 @@
</span>
</template>
<template v-else>
<IconListSearch width="22" />
<IconSparkles width="22" />
<span
class="hidden md:inline"
>
@@ -268,7 +268,7 @@
import Field from './field'
import FieldType from './field_type'
import FieldSubmitter from './field_submitter'
import { IconLock, IconCirclePlus, IconInnerShadowTop, IconListSearch } from '@tabler/icons-vue'
import { IconLock, IconCirclePlus, IconInnerShadowTop, IconSparkles } from '@tabler/icons-vue'
import IconDrag from './icon_drag'
export default {
@@ -277,7 +277,7 @@ export default {
Field,
FieldType,
IconCirclePlus,
IconListSearch,
IconSparkles,
IconInnerShadowTop,
FieldSubmitter,
IconDrag,
@@ -470,9 +470,13 @@ export default {
const data = JSON.parse(jsonStr)
if (data.error) {
alert(data.error)
if ((data.fields || fields).length) {
this.template.fields = data.fields || fields
this.template.fields = data.fields || fields
this.save()
} else {
alert(data.error)
}
break
} else if (data.analyzing) {
+6
View File
@@ -188,6 +188,7 @@ const en = {
}
const es = {
autodetect_fields: 'Autodetectar campos',
analyzing_: 'Analizando...',
download: 'Descargar',
downloading_: 'Descargando...',
@@ -376,6 +377,7 @@ const es = {
}
const it = {
autodetect_fields: 'Rileva campi',
analyzing_: 'Analisi...',
download: 'Scarica',
downloading_: 'Download in corso...',
@@ -564,6 +566,7 @@ const it = {
}
const pt = {
autodetect_fields: 'Detectar campos',
analyzing_: 'Analisando...',
download: 'Baixar',
downloading_: 'Baixando...',
@@ -752,6 +755,7 @@ const pt = {
}
const fr = {
autodetect_fields: 'Détecter les champs',
analyzing_: 'Analyse...',
download: 'Télécharger',
downloading_: 'Téléchargement...',
@@ -940,6 +944,7 @@ const fr = {
}
const de = {
autodetect_fields: 'Felder erkennen',
analyzing_: 'Analysiere...',
download: 'Download',
downloading_: 'Download...',
@@ -1128,6 +1133,7 @@ const de = {
}
const nl = {
autodetect_fields: 'Velden detecteren',
analyzing_: 'Analyseren...',
download: 'Downloaden',
downloading_: 'Downloaden...',
@@ -49,7 +49,12 @@
</li>
</template>
</template>
<template v-else>
<div
v-if="fieldTypes.length && submitterDefaultFields.length"
class="bg-base-300"
style="height: 1px; margin-top: 1px; margin-bottom: 1px"
/>
<template v-if="fieldTypes.length || !submitterDefaultFields.length">
<template
v-for="(icon, type) in fieldIconsSorted"
:key="type"
+3 -1
View File
@@ -4,7 +4,9 @@ class ProcessSubmissionExpiredJob
include Sidekiq::Job
def perform(params = {})
submission = Submission.find(params['submission_id'])
submission = Submission.find_by(id: params['submission_id'])
return unless submission
return if submission.archived_at?
return if submission.template&.archived_at?
@@ -47,6 +47,7 @@ class ProcessSubmitterCompletionJob
completed_submitter.assign_attributes(
submission_id: submitter.submission_id,
account_id: submission.account_id,
is_first: !CompletedSubmitter.exists?(submission: submitter.submission_id, is_first: true),
template_id: submission.template_id,
source: submission.source,
sms_count: sms_events.sum { |e| e.data['segments'] || 1 },
@@ -0,0 +1,11 @@
# frozen_string_literal: true
class SendConfirmationInstructionsJob
include Sidekiq::Job
def perform(params = {})
user = User.find(params['user_id'])
user.send_confirmation_instructions
end
end
@@ -8,8 +8,13 @@ class SendFormCompletedWebhookRequestJob
MAX_ATTEMPTS = 12
def perform(params = {})
submitter = Submitter.find(params['submitter_id'])
webhook_url = WebhookUrl.find(params['webhook_url_id'])
submitter = Submitter.find_by(id: params['submitter_id'])
return unless submitter
webhook_url = WebhookUrl.find_by(id: params['webhook_url_id'])
return unless webhook_url
attempt = params['attempt'].to_i
@@ -8,8 +8,13 @@ class SendFormDeclinedWebhookRequestJob
MAX_ATTEMPTS = 10
def perform(params = {})
submitter = Submitter.find(params['submitter_id'])
webhook_url = WebhookUrl.find(params['webhook_url_id'])
submitter = Submitter.find_by(id: params['submitter_id'])
return unless submitter
webhook_url = WebhookUrl.find_by(id: params['webhook_url_id'])
return unless webhook_url
attempt = params['attempt'].to_i
@@ -8,8 +8,13 @@ class SendFormStartedWebhookRequestJob
MAX_ATTEMPTS = 10
def perform(params = {})
submitter = Submitter.find(params['submitter_id'])
webhook_url = WebhookUrl.find(params['webhook_url_id'])
submitter = Submitter.find_by(id: params['submitter_id'])
return unless submitter
webhook_url = WebhookUrl.find_by(id: params['webhook_url_id'])
return unless webhook_url
attempt = params['attempt'].to_i
@@ -8,8 +8,13 @@ class SendFormViewedWebhookRequestJob
MAX_ATTEMPTS = 10
def perform(params = {})
submitter = Submitter.find(params['submitter_id'])
webhook_url = WebhookUrl.find(params['webhook_url_id'])
submitter = Submitter.find_by(id: params['submitter_id'])
return unless submitter
webhook_url = WebhookUrl.find_by(id: params['webhook_url_id'])
return unless webhook_url
attempt = params['attempt'].to_i
@@ -8,8 +8,13 @@ class SendSubmissionArchivedWebhookRequestJob
MAX_ATTEMPTS = 10
def perform(params = {})
submission = Submission.find(params['submission_id'])
webhook_url = WebhookUrl.find(params['webhook_url_id'])
submission = Submission.find_by(id: params['submission_id'])
return unless submission
webhook_url = WebhookUrl.find_by(id: params['webhook_url_id'])
return unless webhook_url
attempt = params['attempt'].to_i
@@ -8,8 +8,13 @@ class SendSubmissionCompletedWebhookRequestJob
MAX_ATTEMPTS = 10
def perform(params = {})
submission = Submission.find(params['submission_id'])
webhook_url = WebhookUrl.find(params['webhook_url_id'])
submission = Submission.find_by(id: params['submission_id'])
return unless submission
webhook_url = WebhookUrl.find_by(id: params['webhook_url_id'])
return unless webhook_url
attempt = params['attempt'].to_i
@@ -8,8 +8,13 @@ class SendSubmissionCreatedWebhookRequestJob
MAX_ATTEMPTS = 10
def perform(params = {})
submission = Submission.find(params['submission_id'])
webhook_url = WebhookUrl.find(params['webhook_url_id'])
submission = Submission.find_by(id: params['submission_id'])
return unless submission
webhook_url = WebhookUrl.find_by(id: params['webhook_url_id'])
return unless webhook_url
attempt = params['attempt'].to_i
@@ -8,8 +8,13 @@ class SendSubmissionExpiredWebhookRequestJob
MAX_ATTEMPTS = 10
def perform(params = {})
submission = Submission.find(params['submission_id'])
webhook_url = WebhookUrl.find(params['webhook_url_id'])
submission = Submission.find_by(id: params['submission_id'])
return unless submission
webhook_url = WebhookUrl.find_by(id: params['webhook_url_id'])
return unless webhook_url
attempt = params['attempt'].to_i
@@ -7,6 +7,8 @@ class SendSubmitterInvitationEmailJob
submitter = Submitter.find(params['submitter_id'])
return if submitter.completed_at?
return if submitter.submission.archived_at?
return if submitter.template&.archived_at?
return if submitter.submission.source == 'invite' && !Accounts.can_send_emails?(submitter.account, on_events: true)
unless Accounts.can_send_invitation_emails?(submitter.account)
@@ -0,0 +1,15 @@
# frozen_string_literal: true
class SendSubmitterVerificationEmailJob
include Sidekiq::Job
def perform(params = {})
submitter = Submitter.find(params['submitter_id'])
SubmitterMailer.otp_verification_email(submitter).deliver_now!
SubmissionEvent.create!(submitter_id: params['submitter_id'],
event_type: 'send_2fa_email',
data: { email: submitter.email })
end
end
@@ -8,8 +8,13 @@ class SendTemplateCreatedWebhookRequestJob
MAX_ATTEMPTS = 10
def perform(params = {})
template = Template.find(params['template_id'])
webhook_url = WebhookUrl.find(params['webhook_url_id'])
template = Template.find_by(id: params['template_id'])
return unless template
webhook_url = WebhookUrl.find_by(id: params['webhook_url_id'])
return unless webhook_url
attempt = params['attempt'].to_i
@@ -8,8 +8,13 @@ class SendTemplateUpdatedWebhookRequestJob
MAX_ATTEMPTS = 10
def perform(params = {})
template = Template.find(params['template_id'])
webhook_url = WebhookUrl.find(params['webhook_url_id'])
template = Template.find_by(id: params['template_id'])
return unless template
webhook_url = WebhookUrl.find_by(id: params['webhook_url_id'])
return unless webhook_url
attempt = params['attempt'].to_i
+6 -3
View File
@@ -8,10 +8,13 @@ class SendTestWebhookRequestJob
USER_AGENT = 'DocuSeal.com Webhook'
def perform(params = {})
submitter = Submitter.find(params['submitter_id'])
webhook_url = WebhookUrl.find(params['webhook_url_id'])
submitter = Submitter.find_by(id: params['submitter_id'])
return unless webhook_url && submitter
return unless submitter
webhook_url = WebhookUrl.find_by(id: params['webhook_url_id'])
return unless webhook_url
Faraday.post(webhook_url.url,
{
+11
View File
@@ -144,6 +144,17 @@ class SubmitterMailer < ApplicationMailer
end
end
def otp_verification_email(submitter)
@submitter = submitter
@otp_code = EmailVerificationCodes.generate([submitter.email.downcase.strip, submitter.slug].join(':'))
assign_message_metadata('otp_verification_email', submitter)
I18n.with_locale(submitter.account.locale) do
mail(to: submitter.email, subject: I18n.t('email_verification'))
end
end
private
def build_submitter_reply_to(submitter, email_config: nil, documents_copy_email: nil)
+4
View File
@@ -60,6 +60,10 @@ class Account < ApplicationRecord
linked_account_account&.testing?
end
def tz_info
@tz_info ||= TZInfo::Timezone.get(ActiveSupport::TimeZone::MAPPING[timezone] || timezone)
end
def default_template_folder
super || build_default_template_folder(name: TemplateFolder::DEFAULT_NAME,
author_id: users.minimum(:id)).tap(&:save!)
+7
View File
@@ -22,6 +22,7 @@
#
class AccountConfig < ApplicationRecord
SUBMITTER_INVITATION_EMAIL_KEY = 'submitter_invitation_email'
SUBMITTER_INVITATION_REMINDER_EMAIL_KEY = 'submitter_invitation_reminder_email'
SUBMITTER_COMPLETED_EMAIL_KEY = 'submitter_completed_email'
SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY = 'submitter_documents_copy_email'
BCC_EMAILS = 'bcc_emails'
@@ -59,6 +60,12 @@ class AccountConfig < ApplicationRecord
'body' => I18n.t(:submitter_invitation_email_sign_body)
}
},
SUBMITTER_INVITATION_REMINDER_EMAIL_KEY => lambda {
{
'subject' => I18n.t(:you_are_invited_to_sign_a_document),
'body' => I18n.t(:submitter_invitation_email_sign_body)
}
},
SUBMITTER_COMPLETED_EMAIL_KEY => lambda {
{
'subject' => I18n.t(:template_name_has_been_completed_by_submitters),
+5 -2
View File
@@ -6,6 +6,7 @@
#
# id :bigint not null, primary key
# completed_at :datetime not null
# is_first :boolean
# sms_count :integer not null
# source :string not null
# verification_method :string
@@ -18,8 +19,10 @@
#
# Indexes
#
# index_completed_submitters_on_account_id (account_id)
# index_completed_submitters_on_submitter_id (submitter_id) UNIQUE
# index_completed_submitters_account_id_completed_at_is_first (account_id,completed_at) WHERE (is_first = true)
# index_completed_submitters_on_account_id_and_completed_at (account_id,completed_at)
# index_completed_submitters_on_submission_id (submission_id) UNIQUE WHERE (is_first = true)
# index_completed_submitters_on_submitter_id (submitter_id) UNIQUE
#
class CompletedSubmitter < ApplicationRecord
belongs_to :submitter
+13 -3
View File
@@ -10,17 +10,21 @@
# event_type :string not null
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint
# submission_id :bigint not null
# submitter_id :bigint
#
# Indexes
#
# index_submission_events_on_created_at (created_at)
# index_submission_events_on_submission_id (submission_id)
# index_submission_events_on_submitter_id (submitter_id)
# index_submission_events_on_account_id (account_id)
# index_submission_events_on_created_at (created_at)
# index_submission_events_on_submission_id (submission_id)
# index_submission_events_on_submitter_id (submitter_id)
# index_submissions_events_on_sms_event_types (account_id,created_at) WHERE ((event_type)::text = ANY ((ARRAY['send_sms'::character varying, 'send_2fa_sms'::character varying])::text[]))
#
# Foreign Keys
#
# fk_rails_... (account_id => accounts.id)
# fk_rails_... (submission_id => submissions.id)
# fk_rails_... (submitter_id => submitters.id)
#
@@ -35,6 +39,7 @@ class SubmissionEvent < ApplicationRecord
serialize :data, coder: JSON
before_validation :set_submission_id, on: :create
before_validation :set_account_id, on: :create
enum :event_type, {
send_email: 'send_email',
@@ -43,6 +48,7 @@ class SubmissionEvent < ApplicationRecord
send_reminder_email: 'send_reminder_email',
send_sms: 'send_sms',
send_2fa_sms: 'send_2fa_sms',
send_2fa_email: 'send_2fa_email',
open_email: 'open_email',
click_email: 'click_email',
click_sms: 'click_sms',
@@ -63,4 +69,8 @@ class SubmissionEvent < ApplicationRecord
def set_submission_id
self.submission_id = submitter&.submission_id
end
def set_account_id
self.account_id = submitter&.account_id
end
end
+5 -1
View File
@@ -6,6 +6,9 @@
#
# id :bigint not null, primary key
# archived_at :datetime
# confirmation_sent_at :datetime
# confirmation_token :string
# confirmed_at :datetime
# consumed_timestep :integer
# current_sign_in_at :datetime
# current_sign_in_ip :string
@@ -24,8 +27,9 @@
# reset_password_token :string
# role :string not null
# sign_in_count :integer default(0), not null
# unconfirmed_email :string
# unlock_token :string
# uuid :text not null
# uuid :string not null
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint not null
@@ -1,5 +1,10 @@
<p>Hello <%= @resource.email %>!</p>
<p>Someone has requested a link to change your password. You can do this through the link below.</p>
<p><%= link_to 'Change my password', edit_password_url(@resource, reset_password_token: @token) %></p>
<p>If you didn't request this, please ignore this email.</p>
<p>Your password won't change until you access the link above and create a new one.</p>
<p><%= @resource.first_name.present? ? t('hello_name', name: @resource.first_name) : t('hi_there') %>,</p>
<p><%= t('you_requested_to_reset_your_password_use_the_link_below_to_continue') %>:</p>
<p><%= link_to t('change_my_password'), edit_password_url(@resource, reset_password_token: @token) %></p>
<p><%= t('your_password_wont_change_until_you_open_the_link_above_and_set_a_new_one') %></p>
<p><%= t('if_you_didnt_request_this_you_can_ignore_this_email') %></p>
<p>
<%= t('thanks') %>,<br>
<%= Docuseal.product_name %>
</p>
<% content_for(:remove_attribution, true) %>
+8
View File
@@ -0,0 +1,8 @@
<svg class="<%= local_assigns[:class] %>" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M3 5a1 1 0 0 1 1 -1h16a1 1 0 0 1 1 1v10a1 1 0 0 1 -1 1h-16a1 1 0 0 1 -1 -1v-10z" />
<path d="M7 20h10" /><path d="M9 16v4" />
<path d="M15 16v4" /></svg>
<path d="M9 16v4" />
<path d="M15 16v4" />
</svg>

After

Width:  |  Height:  |  Size: 497 B

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

After

Width:  |  Height:  |  Size: 423 B

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

After

Width:  |  Height:  |  Size: 427 B

+1 -1
View File
@@ -3,7 +3,7 @@
<%= svg_icon('waving_hand', class: 'h-10 w-10') %>
<span><%= t('welcome_to_product_name', product_name: Docuseal.product_name) %></span>
</h1>
<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put, class: 'space-y-6' }) do |f| %>
<%= form_for(resource, as: resource_name, url: invitation_path, html: { method: :put, class: 'space-y-6' }) do |f| %>
<div class="space-y-2">
<%= render 'devise/shared/error_messages', resource: %>
<%= f.hidden_field :reset_password_token %>
+5
View File
@@ -18,6 +18,11 @@
<div class="form-control">
<%= f.label :email, t('email'), class: 'label' %>
<%= f.email_field :email, autocomplete: 'off', class: 'base-input' %>
<% if current_user.try(:pending_reconfirmation?) %>
<label class="label">
<span class="label-text-alt"><%= t('email_address_is_awaiting_confirmation_follow_the_link_in_the_email_to_confirm', email: f.object.unconfirmed_email) %></span>
</label>
<% end %>
</div>
<div class="form-control pt-2">
<%= f.button button_title(title: t('update'), disabled_with: t('updating')), class: 'base-button' %>
+3 -3
View File
@@ -66,7 +66,7 @@
<% end %>
<% if !Docuseal.demo? && can?(:manage, EncryptedConfig) && (current_user != true_user || !current_account.linked_account_account) %>
<li>
<%= link_to Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CLOUD_URL}/sign_up?#{{ redir: "#{Docuseal::CONSOLE_URL}/on_premises" }.to_query}", class: 'text-base hover:bg-base-300', data: { prefetch: false } do %>
<%= content_for(:pro_link) || link_to(Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}/plans") : "#{Docuseal::CLOUD_URL}/sign_up?#{{ redir: "#{Docuseal::CONSOLE_URL}/on_premises" }.to_query}", class: 'text-base hover:bg-base-300', data: { turbo: false }) do %>
<%= t('plans') %>
<span class="badge badge-warning"><%= t('pro') %></span>
<% end %>
@@ -74,13 +74,13 @@
<% end %>
<% if !Docuseal.demo? && can?(:manage, EncryptedConfig) && (current_user == true_user || current_account.testing?) %>
<li>
<%= link_to Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}#{'/test' if current_account.testing?}/api") : "#{Docuseal::CONSOLE_URL}/on_premises", class: 'text-base hover:bg-base-300', data: { prefetch: false } do %>
<%= link_to Docuseal.multitenant? ? console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}#{'/test' if current_account.testing?}/api") : "#{Docuseal::CONSOLE_URL}/on_premises", class: 'text-base hover:bg-base-300', data: { turbo: false } do %>
<% if Docuseal.multitenant? %> API <% else %> <%= t('console') %> <% end %>
<% end %>
</li>
<% if Docuseal.multitenant? %>
<li>
<%= link_to console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}#{'/test' if current_account.testing?}/embedding/form"), class: 'text-base hover:bg-base-300', data: { prefetch: false } do %>
<%= link_to console_redirect_index_path(redir: "#{Docuseal::CONSOLE_URL}#{'/test' if current_account.testing?}/embedding/form"), class: 'text-base hover:bg-base-300', data: { turbo: false } do %>
<%= t('embedding') %>
<% end %>
</li>
+10 -3
View File
@@ -28,9 +28,16 @@
<span class="absolute flex items-center justify-center w-7 h-7 rounded-full -start-3.5 ring-8 ring-base-100 text-base-content <%= bg_class %>">
<%= svg_icon(SubmissionEventsController::SUBMISSION_EVENT_ICONS.fetch(event.event_type, 'circle_dot'), class: 'w-4 h-4') %>
</span>
<p class="text-sm leading-none text-base-content/60 pt-1.5">
<%= l(event.event_timestamp.in_time_zone(current_account.timezone), format: :long, locale: current_account.locale) %>
</p>
<div class="flex items-center space-x-1 pt-1.5">
<span class="text-sm leading-none text-base-content/60">
<%= l(event.event_timestamp.in_time_zone(current_account.timezone), format: :long, locale: current_account.locale) %>
</span>
<% if (device = DetectBrowserDevice.call(event.data['ua'])) %>
<span class="tooltip tooltip-top" data-tip="<%= t(device) %>">
<%= svg_icon("device_#{device}", class: 'w-4 h-4') %>
</span>
<% end %>
</div>
<p class="text-base-content/80 mt-1">
<% if event.event_type == 'complete_verification' %>
<%= t('submission_event_names.complete_verification_by_html', provider: event.data['method'], submitter_name:) %>
@@ -29,7 +29,7 @@
<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'] %>">
<%= tag.input type: 'email', multiple: true, name: 'submission[1][submitters][][email]', autocomplete: 'off', class: 'base-input !h-10 mt-1.5 w-full', placeholder: (local_assigns[:require_email_2fa] == true ? t(:email) : "#{t('email')} (#{t('optional')})"), value: item['email'].presence || ((params[:selfsign] && index.zero?) || item['is_requester'] ? current_user.email : ''), id: "detailed_email_#{item['uuid']}", required: local_assigns[:require_email_2fa] == true %>
</linked-input>
</submitters-autocomplete>
<% has_phone_field = true %>
+1 -116
View File
@@ -1,116 +1 @@
<% submitter_preferences_index = template&.preferences&.dig('submitters').to_a.index_by { |e| e['uuid'] } %>
<% template_submitters = local_assigns[:submitter]&.submission&.template_submitters || template.submitters %>
<% message_field_id = "message_field_#{SecureRandom.hex(3)}" %>
<div class="form-control">
<% can_send_emails = Accounts.can_send_emails?(current_account) %>
<div class="flex justify-between items-center">
<%= f.label :send_email, for: uuid = SecureRandom.uuid, class: 'flex items-center cursor-pointer' do %>
<%= f.check_box :send_email, id: uuid, class: 'base-checkbox', disabled: !can_send_emails || local_assigns[:disable_email], checked: can_send_emails && !local_assigns.key?(:resend_email) && !local_assigns[:disable_email] && template&.preferences&.dig('request_email_enabled') != false %>
<span class="label"><%= local_assigns[:resend_email] ? t('re_send_email') : t('send_email') %></span>
<% end %>
<div>
<% if can_send_emails %>
<%= render 'submissions/email_stats' %>
<%= content_for(:edit_button) || capture do %>
<toggle-visible data-element-ids="<%= [message_field_id].to_json %>" class="flex">
<label>
<%= f.check_box :is_custom_message, checked: false, class: 'hidden peer', data: { action: 'change:toggle-visible#trigger', type: 'checkbox' } %>
<span class="link peer-checked:hidden"><%= t('edit_message') %></span>
</label>
</toggle-visible>
<% end %>
<% end %>
</div>
</div>
<% if !Docuseal.multitenant? && !can_send_emails %>
<div class="alert my-4">
<%= svg_icon('info_circle', class: 'w-6 h-6') %>
<div>
<p class="font-bold"><%= t('smtp_not_configured') %></p>
<p class="text-gray-700">
<%= t('configure_smtp_settings_in_order_to_send_emails_') %>
<br>
<a class="link font-medium" data-turbo-frame="_top" href="<%= settings_email_index_path %>">
<%= t('go_to_smtp_settings') %>
</a>
</p>
</div>
</div>
<% end %>
</div>
<% config = AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY) %>
<div id="<%= message_field_id %>" class="card card-compact bg-base-300/40 hidden">
<div class="card-body">
<%= tag.input id: toggle_uuid = SecureRandom.uuid, value: '1', name: 'request_email_per_submitter', class: 'peer', type: 'checkbox', hidden: true, checked: local_assigns[:message_per_submitter] != false && template&.preferences&.dig('submitters').to_a.size > 1 %>
<div class="peer-checked:hidden form-control space-y-2">
<div class="form-control">
<div class="flex justify-between">
<%= f.label :subject, t('subject'), class: 'label' %>
<% if template_submitters.size > 1 && template_submitters.size < 5 && local_assigns[:message_per_submitter] != false %>
<label for="<%= toggle_uuid %>" class="label underline">
<%= t('edit_per_party') %>
</label>
<% end %>
</div>
<%= f.text_field :subject, value: local_assigns[:submitter_email_message]&.subject.presence || submitter_preferences_index.dig(local_assigns[:submitter]&.uuid, 'request_email_subject').presence || template&.preferences&.dig('request_email_subject').presence || config.value['subject'], required: true, class: '!text-sm base-input w-full', dir: 'auto' %>
</div>
<div class="form-control">
<div class="flex items-center">
<%= f.label :message, t('body'), class: 'label' %>
<span class="tooltip tooltip-right" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY].call['body'].scan(/{.*?}/).join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<autoresize-textarea>
<%= f.text_area :body, value: local_assigns[:submitter_email_message]&.body.presence || submitter_preferences_index.dig(local_assigns[:submitter]&.uuid, 'request_email_body').presence || template&.preferences&.dig('request_email_body').presence || config.value['body'], required: true, class: 'base-textarea w-full', rows: 10, dir: 'auto' %>
</autoresize-textarea>
<% unless local_assigns.fetch(:disable_save_as_default_template_option, false) %>
<label for="<%= uuid = SecureRandom.uuid %>" class="flex items-center cursor-pointer">
<%= check_box_tag :save_message, id: uuid, class: 'base-checkbox', checked: false %>
<span class="label"><%= t('save_as_default_template_message') %></span>
</label>
<% end %>
</div>
<%= render 'submissions/message_fields' %>
</div>
<% if template_submitters.size > 1 && template_submitters.size < 5 && local_assigns[:message_per_submitter] != false %>
<div class="hidden peer-checked:block form-control space-y-2">
<% uuid = SecureRandom.uuid %>
<% options = template_submitters.map { |e| [e['name'], "request_email_#{uuid}_#{e['uuid']}"] } %>
<toggle-visible data-element-ids="<%= options.map(&:last).to_json %>" class="flex relative px-1">
<ul class="tabs w-full flex flex-nowrap">
<% options.each_with_index do |(label, val), index| %>
<div class="w-full">
<%= f.radio_button :selected, val, checked: index.zero?, id: "#{val}_radio", data: { action: 'click:toggle-visible#trigger' }, class: 'hidden peer' %>
<%= f.label :selected, label, value: val, for: "#{val}_radio", class: 'tab w-full tab-lifted peer-checked:tab-active !bg-transparent' %>
</div>
<% end %>
</ul>
</toggle-visible>
<% template_submitters.each_with_index do |submitter, index| %>
<%= fields_for :submitter_preferences, nil, index: submitter['uuid'] do |ff| %>
<div id="request_email_<%= uuid %>_<%= submitter['uuid'] %>" class="<%= 'hidden' if index != 0 %>">
<div class="form-control">
<div class="flex justify-between">
<%= ff.label :subject, t('subject'), class: 'label' %>
</div>
<%= ff.text_field :subject, value: local_assigns[:submitter_email_message]&.subject.presence || submitter_preferences_index.dig(submitter['uuid'], 'request_email_subject').presence || template&.preferences&.dig('request_email_subject').presence || config.value['subject'], required: true, class: '!text-sm base-input w-full', dir: 'auto' %>
</div>
<div class="form-control">
<div class="flex items-center">
<%= ff.label :message, t('body'), class: 'label' %>
<span class="tooltip tooltip-right" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY].call['body'].scan(/{.*?}/).join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<autoresize-textarea>
<%= ff.text_area :body, value: local_assigns[:submitter_email_message]&.body.presence || submitter_preferences_index.dig(submitter['uuid'], 'request_email_body').presence || template&.preferences&.dig('request_email_body').presence || config.value['body'], required: true, class: 'base-textarea w-full', rows: 10, dir: 'auto' %>
</autoresize-textarea>
</div>
</div>
<% end %>
<% end %>
</div>
<% end %>
</div>
</div>
<%= render partial: 'submissions/send_email_base', locals: local_assigns %>
@@ -0,0 +1,116 @@
<% submitter_preferences_index = template&.preferences&.dig('submitters').to_a.index_by { |e| e['uuid'] } %>
<% template_submitters = local_assigns[:submitter]&.submission&.template_submitters || template.submitters %>
<% message_field_id = "message_field_#{SecureRandom.hex(3)}" %>
<div class="form-control">
<% can_send_emails = Accounts.can_send_emails?(current_account) %>
<div class="flex justify-between items-center">
<%= f.label :send_email, for: uuid = SecureRandom.uuid, class: 'flex items-center cursor-pointer' do %>
<%= f.check_box :send_email, id: uuid, class: 'base-checkbox', disabled: !can_send_emails || local_assigns[:disable_email], checked: can_send_emails && !local_assigns.key?(:resend_email) && !local_assigns[:disable_email] && template&.preferences&.dig('request_email_enabled') != false %>
<span class="label"><%= local_assigns[:resend_email] ? t('re_send_email') : t('send_email') %></span>
<% end %>
<div>
<% if can_send_emails %>
<%= render 'submissions/email_stats' %>
<%= content_for(:edit_button) || capture do %>
<toggle-visible data-element-ids="<%= [message_field_id].to_json %>" class="flex">
<label>
<%= f.check_box :is_custom_message, checked: false, class: 'hidden peer', data: { action: 'change:toggle-visible#trigger', type: 'checkbox' } %>
<span class="link peer-checked:hidden"><%= t('edit_message') %></span>
</label>
</toggle-visible>
<% end %>
<% end %>
</div>
</div>
<% if !Docuseal.multitenant? && !can_send_emails %>
<div class="alert my-4">
<%= svg_icon('info_circle', class: 'w-6 h-6') %>
<div>
<p class="font-bold"><%= t('smtp_not_configured') %></p>
<p class="text-gray-700">
<%= t('configure_smtp_settings_in_order_to_send_emails_') %>
<br>
<a class="link font-medium" data-turbo-frame="_top" href="<%= settings_email_index_path %>">
<%= t('go_to_smtp_settings') %>
</a>
</p>
</div>
</div>
<% end %>
</div>
<% config = AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY) %>
<div id="<%= message_field_id %>" class="card card-compact bg-base-300/40 hidden">
<div class="card-body">
<%= tag.input id: toggle_uuid = SecureRandom.uuid, value: '1', name: 'request_email_per_submitter', class: 'peer', type: 'checkbox', hidden: true, checked: local_assigns[:message_per_submitter] != false && template&.preferences&.dig('submitters').to_a.size > 1 %>
<div class="peer-checked:hidden form-control space-y-2">
<div class="form-control">
<div class="flex justify-between">
<%= f.label :subject, t('subject'), class: 'label' %>
<% if template_submitters.size > 1 && template_submitters.size < 5 && local_assigns[:message_per_submitter] != false %>
<label for="<%= toggle_uuid %>" class="label underline">
<%= t('edit_per_party') %>
</label>
<% end %>
</div>
<%= f.text_field :subject, value: local_assigns[:submitter_email_message]&.subject.presence || submitter_preferences_index.dig(local_assigns[:submitter]&.uuid, 'request_email_subject').presence || template&.preferences&.dig('request_email_subject').presence || config.value['subject'], required: true, class: '!text-sm base-input w-full', dir: 'auto' %>
</div>
<div class="form-control">
<div class="flex items-center">
<%= f.label :message, t('body'), class: 'label' %>
<span class="tooltip tooltip-right" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY].call['body'].scan(/{.*?}/).join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<autoresize-textarea>
<%= f.text_area :body, value: local_assigns[:submitter_email_message]&.body.presence || submitter_preferences_index.dig(local_assigns[:submitter]&.uuid, 'request_email_body').presence || template&.preferences&.dig('request_email_body').presence || config.value['body'], required: true, class: 'base-textarea w-full', rows: 10, dir: 'auto' %>
</autoresize-textarea>
<% unless local_assigns.fetch(:disable_save_as_default_template_option, false) %>
<label for="<%= uuid = SecureRandom.uuid %>" class="flex items-center cursor-pointer">
<%= check_box_tag :save_message, id: uuid, class: 'base-checkbox', checked: false %>
<span class="label"><%= t('save_as_default_template_message') %></span>
</label>
<% end %>
</div>
<%= render 'submissions/message_fields' %>
</div>
<% if template_submitters.size > 1 && template_submitters.size < 5 && local_assigns[:message_per_submitter] != false %>
<div class="hidden peer-checked:block form-control space-y-2">
<% uuid = SecureRandom.uuid %>
<% options = template_submitters.map { |e| [e['name'], "request_email_#{uuid}_#{e['uuid']}"] } %>
<toggle-visible data-element-ids="<%= options.map(&:last).to_json %>" class="flex relative px-1">
<ul class="tabs w-full flex flex-nowrap">
<% options.each_with_index do |(label, val), index| %>
<div class="w-full">
<%= f.radio_button :selected, val, checked: index.zero?, id: "#{val}_radio", data: { action: 'click:toggle-visible#trigger' }, class: 'hidden peer' %>
<%= f.label :selected, label, value: val, for: "#{val}_radio", class: 'tab w-full tab-lifted peer-checked:tab-active !bg-transparent' %>
</div>
<% end %>
</ul>
</toggle-visible>
<% template_submitters.each_with_index do |submitter, index| %>
<%= fields_for :submitter_preferences, nil, index: submitter['uuid'] do |ff| %>
<div id="request_email_<%= uuid %>_<%= submitter['uuid'] %>" class="<%= 'hidden' if index != 0 %>">
<div class="form-control">
<div class="flex justify-between">
<%= ff.label :subject, t('subject'), class: 'label' %>
</div>
<%= ff.text_field :subject, value: local_assigns[:submitter_email_message]&.subject.presence || submitter_preferences_index.dig(submitter['uuid'], 'request_email_subject').presence || template&.preferences&.dig('request_email_subject').presence || config.value['subject'], required: true, class: '!text-sm base-input w-full', dir: 'auto' %>
</div>
<div class="form-control">
<div class="flex items-center">
<%= ff.label :message, t('body'), class: 'label' %>
<span class="tooltip tooltip-right" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY].call['body'].scan(/{.*?}/).join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<autoresize-textarea>
<%= ff.text_area :body, value: local_assigns[:submitter_email_message]&.body.presence || submitter_preferences_index.dig(submitter['uuid'], 'request_email_body').presence || template&.preferences&.dig('request_email_body').presence || config.value['body'], required: true, class: 'base-textarea w-full', rows: 10, dir: 'auto' %>
</autoresize-textarea>
</div>
</div>
<% end %>
<% end %>
</div>
<% end %>
</div>
</div>
+3 -2
View File
@@ -1,6 +1,7 @@
<% require_phone_2fa = @template.preferences['require_phone_2fa'] == true %>
<% require_email_2fa = @template.preferences['require_email_2fa'] == true %>
<% prefillable_fields = @template.fields.select { |f| f['prefillable'] } %>
<% only_detailed = require_phone_2fa || prefillable_fields.present? %>
<% only_detailed = require_phone_2fa || require_email_2fa || prefillable_fields.present? %>
<%= render 'shared/turbo_modal_large', title: params[:selfsign] ? t('add_recipients') : t('add_new_recipients') do %>
<% 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">
@@ -25,7 +26,7 @@
</div>
<% end %>
<div id="detailed" class="<%= 'hidden' unless only_detailed %>">
<%= render 'detailed_form', template: @template, require_phone_2fa:, prefillable_fields: %>
<%= render 'detailed_form', template: @template, require_phone_2fa:, require_email_2fa:, prefillable_fields: %>
</div>
<div id="list" class="hidden">
<%= render 'list_form', template: @template %>
+72
View File
@@ -0,0 +1,72 @@
<% content_for(:html_title, "#{@submitter.submission.name || @submitter.submission.template.name} | DocuSeal") %>
<% I18n.with_locale(@submitter.account.locale) do %>
<% content_for(:html_description, t('account_name_has_invited_you_to_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution', account_name: @submitter.account.name)) %>
<% end %>
<div class="max-w-md mx-auto px-2 mt-12 mb-4">
<div class="space-y-6 mx-auto">
<div class="space-y-6">
<div class="flex items-center justify-center">
<%= render 'start_form/banner' %>
</div>
<div class="flex items-center bg-base-200 rounded-xl p-4 mb-4">
<div class="flex items-center">
<div class="mr-3">
<%= svg_icon('writing_sign', class: 'w-10 h-10') %>
</div>
<div>
<p dir="auto" class="text-lg font-bold mb-1"><%= @submitter.submission.name || @submitter.submission.template.name %></p>
<% last_submitter = @submitter.submission.submitters.completed.order(:completed_at).last %>
<% if last_submitter %>
<p dir="auto" class="text-sm">
<%= t(last_submitter.with_signature_fields? ? 'signed_on_time' : 'completed_on_time', time: l(last_submitter.completed_at.to_date, format: :long)) %>
</p>
<% end %>
</div>
</div>
</div>
</div>
<% if @submitter.email.present? %>
<div>
<%= t('the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_email_html', email: TextUtils.mask_email(@submitter.email, 2)) %>
</div>
<% if params[:status] == 'sent' || params[:status] == 'error' %>
<%= form_for '', url: submit_form_email_2fa_path, method: :post, html: { class: 'space-y-4', id: 'code_form' } do |f| %>
<div dir="auto" class="form-control !mt-0">
<%= f.hidden_field :submitter_slug, value: @submitter.slug %>
<%= f.text_field :one_time_code, required: true, class: 'base-input text-center', placeholder: 'XXX-XXX' %>
<div class="flex justify-between items-center mt-1">
<span>
<% if flash[:alert] %>
<span class="text-red-500">
<%= flash[:alert] %>
</span>
<% end %>
<% if flash[:notice] %>
<%= flash[:notice] %>
<% end %>
</span>
<span>
<label for="resend_code" id="resend_label" class="link"><%= t(:re_send_email) %></label>
</span>
</div>
</div>
<toggle-submit dir="auto" class="form-control">
<%= f.button button_title(title: t('submit')), class: 'base-button' %>
</toggle-submit>
<% end %>
<%= button_to t(:re_send_email), submit_form_email_2fa_path, params: { submitter_slug: @submitter.slug, resend: true }, method: :put, id: 'resend_code', class: 'hidden' %>
<% else %>
<% if params[:t] %>
<fetch-form data-onload="true">
<%= button_to '', api_submitter_email_clicks_path, params: { submitter_slug: @submitter.slug, t: params[:t] }, method: :post, class: 'hidden' %>
</fetch-form>
<% end %>
<toggle-submit dir="auto" class="form-control">
<%= button_to button_title(title: t('send_verification_code')), submit_form_email_2fa_path, params: { submitter_slug: @submitter.slug }, method: :put, class: 'base-button w-full' %>
</toggle-submit>
<% end %>
<% else %>
<div><%= t('please_contact_the_requester_to_specify_your_email_for_two_factor_authentication') %></div>
<% end %>
</div>
</div>
@@ -0,0 +1,3 @@
<p><%= t('your_verification_code_to_access_the_name', name: @submitter.submission.name || @submitter.submission.template.name) %></p>
<p><b><%= @otp_code %></b></p>
<p><%= t('please_reply_to_this_email_if_you_didnt_request_this') %></p>
@@ -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>
+16 -198
View File
@@ -67,7 +67,7 @@
<%= 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 %>
<%= f.fields_for :preferences, Struct.new(:completed_redirect_url, :completed_message, :require_phone_2fa).new(@template.preferences['completed_redirect_url'].presence, Struct.new(:title, :body).new(*(@template.preferences['completed_message'] || {}).values_at('title', 'body')), @template.preferences['require_phone_2fa'] == true) do |ff| %>
<%= f.fields_for :preferences, Struct.new(:completed_redirect_url, :completed_message, :require_phone_2fa, :require_email_2fa).new(@template.preferences['completed_redirect_url'].presence, Struct.new(:title, :body).new(*(@template.preferences['completed_message'] || {}).values_at('title', 'body')), @template.preferences['require_phone_2fa'] == true, @template.preferences['require_email_2fa'] == true) do |ff| %>
<div class="form-control mb-2">
<%= ff.label :completed_redirect_url, t('redirect_on_completion_url'), class: 'label' %>
<%= ff.url_field :completed_redirect_url, required: false, class: 'base-input', dir: 'auto' %>
@@ -81,6 +81,14 @@
</div>
<% end %>
<%= render 'templates_preferences/form_fields', ff: %>
<div class="flex items-center justify-between py-2.5 px-1 mb-2">
<span>
<%= t('require_email_2fa_to_open') %>
</span>
<submit-form data-on="change" class="flex">
<%= ff.check_box :require_email_2fa, { checked: ff.object.require_email_2fa == true, 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' %>
@@ -98,157 +106,17 @@
<%= t('signature_request_email') %>
</div>
<div class="collapse-content">
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-1' }, data: { close_on_submit: false } do |f| %>
<toggle-on-submit data-element-id="email_saved_alert1"></toggle-on-submit>
<%= tag.input id: 'request_email_per_submitter', value: '1', name: 'request_email_per_submitter', class: 'peer', type: 'checkbox', hidden: true, checked: @template.preferences['submitters'].to_a.size > 1 %>
<div class="peer-checked:hidden">
<%= f.fields_for :preferences, Struct.new(:request_email_subject, :request_email_body).new(*(@template.preferences.values_at('request_email_subject', 'request_email_body').compact_blank.presence || AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY).value.values_at('subject', 'body'))) do |ff| %>
<div class="form-control">
<div class="flex justify-between">
<%= ff.label :request_email_subject, t('email_subject'), class: 'label' %>
<% if @template.submitters.size > 1 && @template.submitters.size < 5 %>
<label for="request_email_per_submitter" class="label underline">
<%= t('edit_per_party') %>
</label>
<% end %>
</div>
<%= ff.text_field :request_email_subject, required: true, class: 'base-input', dir: 'auto' %>
</div>
<div class="form-control">
<div class="flex items-center">
<%= ff.label :request_email_body, t('email_body'), class: 'label' %>
<span class="tooltip tooltip-right" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY].call['body'].scan(/{.*?}/).join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<autoresize-textarea>
<%= ff.text_area :request_email_body, required: true, class: 'base-input w-full py-2', dir: 'auto' %>
</autoresize-textarea>
</div>
<% end %>
</div>
<% if @template.submitters.size > 1 && @template.submitters.size < 5 %>
<div class="hidden peer-checked:block">
<% options = @template.submitters.map { |e| [e['name'], "request_email_#{e['uuid']}"] } %>
<toggle-visible data-element-ids="<%= options.map(&:last).to_json %>" class="flex relative px-1">
<ul class="tabs w-full flex flex-nowrap mb-2">
<% options.each_with_index do |(label, val), index| %>
<div class="w-full">
<%= f.radio_button :selected, val, checked: index.zero?, id: "#{val}_radio", data: { action: 'click:toggle-visible#trigger' }, class: 'hidden peer' %>
<%= f.label :selected, label, value: val, for: "#{val}_radio", class: 'tab w-full tab-lifted peer-checked:tab-active' %>
</div>
<% end %>
</ul>
</toggle-visible>
<%= f.fields_for :preferences do |ff| %>
<% @template.submitters.each_with_index do |submitter, index| %>
<div id="request_email_<%= submitter['uuid'] %>" class="<%= 'hidden' if index != 0 %>">
<% submitter_preferences = f.object.preferences['submitters'].to_a.find { |e| e['uuid'] == submitter['uuid'] } || {} %>
<%= ff.fields_for :submitters, Struct.new(:request_email_subject, :request_email_body).new(*(submitter_preferences.values_at('request_email_subject', 'request_email_body').compact_blank.presence || @template.preferences.values_at('request_email_subject', 'request_email_body').compact_blank.presence || AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY).value.values_at('subject', 'body'))), index: nil do |fff| %>
<%= fff.hidden_field :uuid, value: submitter['uuid'] %>
<div class="form-control">
<%= fff.label :request_email_subject, t('email_subject'), class: 'label' %>
<%= fff.text_field :request_email_subject, required: true, class: 'base-input', dir: 'auto' %>
</div>
<div class="form-control">
<div class="flex items-center">
<%= fff.label :request_email_body, t('email_body'), class: 'label' %>
<span class="tooltip tooltip-right" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY].call['body'].scan(/{.*?}/).join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<autoresize-textarea>
<%= fff.text_area :request_email_body, required: true, class: 'base-input w-full py-2', dir: 'auto' %>
</autoresize-textarea>
</div>
<% end %>
</div>
<% end %>
<% end %>
</div>
<% end %>
<%= f.fields_for :preferences, Struct.new(:request_email_enabled).new(@template.preferences['request_email_enabled']) do |ff| %>
<div class="flex items-center justify-between py-2.5 px-1 mb-2">
<span>
<%= 'Send signature request email' %>
</span>
<submit-form data-on="change" class="flex">
<%= ff.check_box :request_email_enabled, { checked: ff.object.request_email_enabled != false, class: 'toggle' }, 'true', 'false' %>
</submit-form>
</div>
<% end %>
<div class="form-control pt-2">
<%= f.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button' %>
<div class="flex justify-center">
<span id="email_saved_alert1" class="text-sm invisible font-normal mt-0.5"><%= t('changes_have_been_saved') %></span>
</div>
</div>
<% end %>
<%= render 'templates_preferences/submitter_invitation_email_form' %>
</div>
</div>
<%= render 'templates_preferences/submitter_invitation_reminder_email_collapse' %>
<div class="collapse collapse-arrow join-item border border-base-300">
<input type="checkbox" name="accordion">
<div class="collapse-title text-xl font-medium">
<%= t('documents_copy_email') %>
</div>
<div class="collapse-content">
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-1' }, data: { close_on_submit: false } do |f| %>
<toggle-on-submit data-element-id="email_saved_alert2"></toggle-on-submit>
<% configs = AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY).value %>
<%= f.fields_for :preferences, Struct.new(:documents_copy_email_reply_to, :documents_copy_email_subject, :documents_copy_email_body, :documents_copy_email_enabled, :documents_copy_email_attach_audit, :documents_copy_email_attach_documents).new(@template.preferences['documents_copy_email_reply_to'].presence || configs['reply_to'], @template.preferences['documents_copy_email_subject'].presence || configs['subject'], @template.preferences['documents_copy_email_body'].presence || configs['body'], @template.preferences['documents_copy_email_enabled'], configs['attach_audit_log'] != false && @template.preferences['documents_copy_email_attach_audit'] != false, configs['attach_documents'] != false && @template.preferences['documents_copy_email_attach_documents'] != false) do |ff| %>
<div class="form-control">
<%= ff.label :documents_copy_email_subject, t('email_subject'), class: 'label' %>
<%= ff.text_field :documents_copy_email_subject, required: true, class: 'base-input', dir: 'auto' %>
</div>
<div class="form-control">
<div class="flex items-center">
<%= ff.label :documents_copy_email_body, t('email_body'), class: 'label' %>
<span class="tooltip tooltip-right" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY].call['body'].scan(/{.*?}/).join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<autoresize-textarea>
<%= ff.text_area :documents_copy_email_body, required: true, class: 'base-input w-full py-2', dir: 'auto' %>
</autoresize-textarea>
</div>
<% if can?(:manage, :reply_to) %>
<div class="form-control">
<%= ff.label :documents_copy_email_reply_to, t('reply_to'), class: 'label' %>
<%= ff.email_field :documents_copy_email_reply_to, class: 'base-input', dir: 'auto', placeholder: t(:email) %>
</div>
<% end %>
<div class="flex items-center justify-between pt-2.5 px-1 mb-2">
<span>
<%= t('attach_documents_to_the_email') %>
</span>
<submit-form data-on="change" class="flex">
<%= ff.check_box :documents_copy_email_attach_documents, { checked: ff.object.documents_copy_email_attach_documents != false, class: 'toggle', disabled: configs['attach_documents'] == false }, 'true', 'false' %>
</submit-form>
</div>
<div class="flex items-center justify-between pt-2.5 px-1 mb-2">
<span>
<%= t('attach_audit_log_pdf_to_the_email') %>
</span>
<submit-form data-on="change" class="flex">
<%= ff.check_box :documents_copy_email_attach_audit, { checked: ff.object.documents_copy_email_attach_audit != false, class: 'toggle', disabled: configs['attach_audit_log'] == false }, 'true', 'false' %>
</submit-form>
</div>
<div class="flex items-center justify-between py-2.5 px-1 mb-2">
<span>
<%= t('send_emails_automatically_on_completion') %>
</span>
<submit-form data-on="change" class="flex">
<%= ff.check_box :documents_copy_email_enabled, { checked: ff.object.documents_copy_email_enabled != false && configs['enabled'] != false, class: 'toggle', disabled: configs['enabled'] == false }, 'true', 'false' %>
</submit-form>
</div>
<% end %>
<div class="form-control pt-2">
<%= f.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button' %>
<div class="flex justify-center">
<span id="email_saved_alert2" class="text-sm invisible font-normal mt-0.5"><%= t('changes_have_been_saved') %></span>
</div>
</div>
<% end %>
<%= render 'templates_preferences/submitter_documents_copy_email_form' %>
</div>
</div>
<div class="collapse collapse-arrow join-item border border-base-300">
@@ -257,57 +125,7 @@
<%= t('completed_notification_email') %>
</div>
<div class="collapse-content">
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-1' }, data: { close_on_submit: false } do |f| %>
<toggle-on-submit data-element-id="email_saved_alert3"></toggle-on-submit>
<% configs = AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY).value %>
<%= f.fields_for :preferences, Struct.new(:completed_notification_email_subject, :completed_notification_email_body, :completed_notification_email_enabled, :completed_notification_email_attach_audit, :completed_notification_email_attach_documents).new(@template.preferences['completed_notification_email_subject'].presence || configs['subject'], @template.preferences['completed_notification_email_body'].presence || configs['body'], @template.preferences['completed_notification_email_enabled'], configs['attach_audit_log'] != false && @template.preferences['completed_notification_email_attach_audit'] != false, configs['attach_documents'] != false && @template.preferences['completed_notification_email_attach_documents'] != false) do |ff| %>
<div class="form-control">
<%= ff.label :completed_notification_email_subject, t('email_subject'), class: 'label' %>
<%= ff.text_field :completed_notification_email_subject, required: true, class: 'base-input', dir: 'auto' %>
</div>
<div class="form-control">
<div class="flex items-center">
<%= ff.label :completed_notification_email_body, t('email_body'), class: 'label' %>
<span class="tooltip tooltip-right" data-tip="<%= t('use_following_placeholders_text_') %> <%= AccountConfig::DEFAULT_VALUES[AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY].call['body'].scan(/{.*?}/).join(', ') %>">
<%= svg_icon('info_circle', class: 'w-4 h-4') %>
</span>
</div>
<autoresize-textarea>
<%= ff.text_area :completed_notification_email_body, required: true, class: 'base-input w-full py-2', dir: 'auto' %>
</autoresize-textarea>
</div>
<div class="flex items-center justify-between pt-2.5 px-1 mb-2">
<span>
<%= t('attach_documents_to_the_email') %>
</span>
<submit-form data-on="change" class="flex">
<%= ff.check_box :completed_notification_email_attach_documents, { checked: ff.object.completed_notification_email_attach_documents != false, class: 'toggle', disabled: configs['attach_documents'] == false }, 'true', 'false' %>
</submit-form>
</div>
<div class="flex items-center justify-between pt-2.5 px-1 mb-2">
<span>
<%= t('attach_audit_log_pdf_to_the_email') %>
</span>
<submit-form data-on="change" class="flex">
<%= ff.check_box :completed_notification_email_attach_audit, { checked: ff.object.completed_notification_email_attach_audit != false, class: 'toggle', disabled: configs['attach_audit_log'] == false }, 'true', 'false' %>
</submit-form>
</div>
<div class="flex items-center justify-between py-2.5 px-1 mb-2">
<span>
<%= t('send_emails_automatically_on_completion') %>
</span>
<submit-form data-on="change" class="flex">
<%= ff.check_box :completed_notification_email_enabled, { checked: ff.object.completed_notification_email_enabled != false, class: 'toggle', disabled: configs['enabled'] == false }, 'true', 'false' %>
</submit-form>
</div>
<% end %>
<div class="form-control pt-2">
<%= f.button button_title(title: t('save'), disabled_with: t('saving')), class: 'base-button' %>
<div class="flex justify-center">
<span id="email_saved_alert3" class="text-sm invisible font-normal mt-0.5"><%= t('changes_have_been_saved') %></span>
</div>
</div>
<% end %>
<%= render 'templates_preferences/submitter_completed_email_form' %>
</div>
</div>
</div>
@@ -359,9 +177,9 @@
<span>
<%= t('share_template_with_test_mode') %>
</span>
<submit-form data-on="change" class="flex">
<%= f.check_box :value, class: 'toggle', checked: @template.template_sharings.exists?(account_id: current_account.testing_accounts) %>
</submit-form>
<submit-form data-on="change" class="flex">
<%= f.check_box :value, class: 'toggle', checked: @template.template_sharings.exists?(account_id: current_account.testing_accounts) %>
</submit-form>
</div>
<% end %>
<div class="mb-4">
+54 -45
View File
@@ -3,12 +3,19 @@
<%= 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| %>
<label for="template_shared_link" class="flex items-center my-4 justify-between gap-1 alert bg-base-100 border-base-300">
<span><%= t('enable_shared_link') %></span>
<submit-form data-on="change" class="flex">
<%= f.check_box :shared_link, { disabled: !can?(:update, @template), class: 'toggle' }, 'true', 'false' %>
</submit-form>
</label>
<% if @template.preferences&.dig('require_email_2fa') || @template.preferences&.dig('require_phone_2fa') %>
<label for="template_shared_link" class="tooltip tooltip-bottom flex items-center my-4 justify-between gap-1 alert bg-base-100 border-base-300" data-tip="<%= t(:templates_that_require_email_or_phone_2fa_cannot_be_used_via_a_shared_link) %>">
<span><%= t('enable_shared_link') %></span>
<%= check_box_tag 'shared_link', 'true', false, disabled: true, class: 'toggle' %>
</label>
<% else %>
<label for="template_shared_link" class="flex items-center my-4 justify-between gap-1 alert bg-base-100 border-base-300">
<span><%= t('enable_shared_link') %></span>
<submit-form data-on="change" class="flex">
<%= f.check_box :shared_link, { disabled: !can?(:update, @template), class: 'toggle' }, 'true', 'false' %>
</submit-form>
</label>
<% end %>
<div class="flex gap-2 mt-3">
<input id="embedding_url" type="text" value="<%= start_form_url(slug: @template.slug) %>" class="base-input w-full" autocomplete="off" readonly>
<check-on-click data-element-id="template_shared_link">
@@ -31,48 +38,50 @@
</div>
</div>
<% end %>
<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>
<% if @template.preferences&.dig('require_email_2fa') != true && @template.preferences&.dig('require_phone_2fa') != true %>
<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 %>
<% 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>
<submit-form data-on="change" class="flex">
<%= ff.check_box :shared_link_2fa, { checked: ff.object.shared_link_2fa == true, disabled: !can?(:update, @template), class: 'toggle' }, 'true', 'false' %>
</submit-form>
</label>
<% end %>
<% end %>
</required-checkbox-group>
<% 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>
<submit-form data-on="change" class="flex">
<%= ff.check_box :shared_link_2fa, { checked: ff.object.shared_link_2fa == true, disabled: !can?(:update, @template), class: 'toggle' }, 'true', 'false' %>
</submit-form>
</label>
<% end %>
<% end %>
<% end %>
</div>
</div>
</div>
</div>
<% end %>
<% end %>
+5
View File
@@ -13,6 +13,11 @@
<div class="form-control">
<%= f.label :email, t('email'), class: 'label' %>
<%= f.email_field :email, required: true, class: 'base-input' %>
<% if user.try(:pending_reconfirmation?) %>
<label class="label">
<span class="label-text-alt"><%= t('email_address_is_awaiting_confirmation_follow_the_link_in_the_email_to_confirm', email: f.object.unconfirmed_email) %></span>
</label>
<% end %>
<% if user.persisted? && Accounts.can_send_emails?(current_account) %>
<span class="label-text-alt mt-2 mx-1">
<%= t('click_here_to_send_a_reset_password_email_html') %>
+25 -10
View File
@@ -12,15 +12,24 @@
<% end %>
</h1>
<div class="flex flex-col md:flex-row gap-y-2 gap-x-4 md:items-center">
<% if params[:status].blank? && can?(:create, User.new(account: current_account)) %>
<%= render 'users/extra_buttons' %>
<% if content_for(:add_user_button) %>
<%= content_for(:add_user_button) %>
<% else %>
<%= link_to new_user_path, class: 'btn btn-primary btn-md gap-2 w-full md:w-fit', data: { turbo_frame: 'modal' } do %>
<%= svg_icon('plus', class: 'w-6 h-6') %>
<span><%= t('new_user') %></span>
<% if params[:status].blank? %>
<% if can?(:create, User.new(account: current_account)) %>
<%= render 'users/extra_buttons' %>
<% if content_for(:add_user_button) %>
<%= content_for(:add_user_button) %>
<% else %>
<%= link_to new_user_path, class: 'btn btn-primary btn-md gap-2 w-full md:w-fit', data: { turbo_frame: 'modal' } do %>
<%= svg_icon('plus', class: 'w-6 h-6') %>
<span><%= t('new_user') %></span>
<% end %>
<% end %>
<% else %>
<div class="tooltip" data-tip="<%= t('contact_your_administrator_to_add_new_users') %>">
<%= link_to '#', class: 'btn btn-primary btn-md gap-2 w-full md:w-fit btn-disabled', data: { turbo_frame: 'modal' } do %>
<%= svg_icon('plus', class: 'w-6 h-6') %>
<span><%= t('new_user') %></span>
<% end %>
</div>
<% end %>
<% end %>
</div>
@@ -52,7 +61,13 @@
<%= user.full_name %>
</td>
<td>
<%= user.email %>
<% if user.try(:pending_reconfirmation?) %>
<%= user.unconfirmed_email %>
<br>
<span class="label-text-alt">(<%= t('unconfirmed') %>)</span>
<% else %>
<%= user.email %>
<% end %>
</td>
<td>
<span class="badge badge-info badge-outline whitespace-nowrap">
@@ -73,7 +88,7 @@
<%= t('remove') %>
<% end %>
<% end %>
<% if params[:status] == 'archived' && can?(:manage, user) && user != current_user && user.archived_at? %>
<% if params[:status] == 'archived' && can?(:create, user) && user != current_user && user.archived_at? %>
<%= button_to user_path(user), method: :put, params: { user: { archived_at: nil } }, class: 'btn btn-outline btn-xs', title: t('unarchive'), data: { turbo_confirm: t('are_you_sure_') } do %>
<%= t('unarchive') %>
<% end %>
+9
View File
@@ -0,0 +1,9 @@
# frozen_string_literal: true
ActiveSupport.on_load(:active_job) do
ActiveJob::LogSubscriber.class_eval do
def args_info(_job)
''
end
end
end
+7 -2
View File
@@ -17,7 +17,10 @@ module Devise
assign_message_metadata(action, record)
initialize_from_record(record)
mail(headers_for(action, opts), &)
I18n.with_locale(record.account.locale) do
mail(headers_for(action, opts), &)
end
end
end
end
@@ -31,6 +34,7 @@ end
#
# Use this hook to configure devise mailer, warden hooks and so forth.
# Many of these configuration options can be set straight in your model.
# rubocop:disable Metrics/BlockLength
Devise.setup do |config|
config.warden do |manager|
manager.default_strategies(scope: :user).unshift(:two_factor_authenticatable)
@@ -166,7 +170,7 @@ Devise.setup do |config|
# without confirming their account.
# Default is 0.days, meaning the user cannot access the website without
# confirming their account.
# config.allow_unconfirmed_access_for = 2.days
config.allow_unconfirmed_access_for = nil
# A period that the user is allowed to confirm their account before their
# token becomes invalid. For example, if set to 3.days, the user can confirm
@@ -332,3 +336,4 @@ Devise.setup do |config|
ActiveSupport.run_load_hooks(:devise_config, config)
end
# rubocop:enable Metrics/BlockLength
+377 -1
View File
@@ -23,7 +23,9 @@ en: &en
add_from_google_drive: Add from Google Drive
or_add_from: Or add from
upload_a_new_document: Upload a New Document
billing: Billing
hi_there: Hi there
templates_that_require_email_or_phone_2fa_cannot_be_used_via_a_shared_link: Templates that require email or phone 2FA cannot be used via a shared link.
pro: Pro
thanks: Thanks
private: Private
@@ -31,6 +33,7 @@ en: &en
enabled: Enabled
disabled: Disabled
party: Party
make_owner: Make Owner
use_direct_file_attachment_links_in_the_documents: Use direct file attachment links in the documents
click_here_to_send_a_reset_password_email_html: '<label class="link" for="resend_password_button">Click here</label> to send a reset password email.'
edit_order: Edit Order
@@ -70,7 +73,7 @@ en: &en
team_access: Team access
document_download_filename_format: Document download filename format
docuseal_trusted_signature: DocuSeal Trusted Signature
hello_name: Hello %{name}
hello_name: Hi %{name}
you_are_invited_to_product_name: You are invited to %{product_name}
you_have_been_invited_to_account_name_product_name_please_sign_up_using_the_link_below_: 'You have been invited to %{account_name} %{product_name}. Please sign up using the link below:'
sent_using_product_name_in_testing_mode_html: 'Sent using <a href="%{product_url}">%{product_name}</a> in testing mode'
@@ -290,6 +293,7 @@ en: &en
invalid_timeserver: Invalid Timeserver
email_templates: Email Templates
signature_request_email: Signature request email
signature_request_reminder_email: Signature request reminder email
signature_request_sms: Signature Request SMS
verification_code_sms: Verification Code SMS
completed_notification_email: Completed notification email
@@ -837,6 +841,49 @@ en: &en
connect_google_drive: Connect Google Drive
google_drive_has_been_connected: Google Drive has been connected
unable_to_identify_reset_your_password_to_sign_in: Unable to identify. Reset your password to sign in.
desktop: Desktop
mobile: Mobile
tablet: Tablet
reset_default: Reset default
send_signature_request_email: Send signature request email
last_3_months: Last 3 months
last_6_months: Last 6 months
last_year: Last year
all_time: All time
everyone: Everyone
daily: Daily
weekly: Weekly
monthly: Monthly
api: API
embed: Embed
bulk: Bulk
invite: Invite
api_and_embed: API and Embed
period: Period
reports: Reports
completed_submissions: Completed submissions
sms: SMS
require_email_2fa_to_open: Require email 2FA to open
verification_required_refresh_the_page_and_pass_2fa: Verification required, refresh the page and pass 2FA.
the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_email_html: The sender has requested two-factor authentication via a one-time password sent to your <b>%{email}</b> email.
please_contact_the_requester_to_specify_your_email_for_two_factor_authentication: Please contact the requester to specify your email for two-factor authentication.
rate_limit_exceeded: Rate limit exceeded
a_confirmation_email_has_been_sent_to_the_new_email_address: A confirmation email has been sent to the new email address.
email_address_is_awaiting_confirmation_follow_the_link_in_the_email_to_confirm: "%{email} email address is awaiting confirmation. Follow the link in the email to confirm."
please_confirm_your_email_address_using_the_link_below_: 'Please confirm your email address using the link below:'
confirm_email: Confirm email
unconfirmed: Unconfirmed
you_requested_to_reset_your_password_use_the_link_below_to_continue: You requested to reset your password. Use the link below to continue
if_you_didnt_request_this_you_can_ignore_this_email: "If you didn't request this, please ignore this email."
your_password_wont_change_until_you_open_the_link_above_and_set_a_new_one: "Your password won't change until you open the link above and set a new one."
devise:
confirmations:
confirmed: Your email address has been successfully confirmed.
failure:
unconfirmed: You have to confirm your email address before continuing.
mailer:
confirmation_instructions:
subject: Confirm your email address
submission_sources:
api: API
bulk: Bulk Send
@@ -850,6 +897,7 @@ en: &en
send_reminder_email_to_html: '<b>Reminder email sent</b> to %{submitter_name}'
send_sms_to_html: '<b>SMS sent</b> to %{submitter_name}'
send_2fa_sms_to_html: '<b>Verification SMS sent</b> to %{submitter_name}'
send_2fa_email_to_html: '<b>Verification email sent</b> to %{submitter_name}'
open_email_by_html: '<b>Email opened</b> by %{submitter_name}'
click_email_by_html: '<b>Email link clicked</b> by %{submitter_name}'
click_sms_by_html: '<b>SMS link clicked</b> by %{submitter_name}'
@@ -943,10 +991,14 @@ en: &en
range_without_total: "%{from}-%{to} events"
es: &es
templates_that_require_email_or_phone_2fa_cannot_be_used_via_a_shared_link: Las plantillas que requieren autenticación en dos pasos por correo electrónico o teléfono no se pueden usar mediante un enlace compartido.
make_owner: Hacer propietario
billing: Facturación
add_from_google_drive: Agregar desde Google Drive
or_add_from: O agregar desde
upload_a_new_document: Subir nuevo documento
use_direct_file_attachment_links_in_the_documents: Usar enlaces directos de archivos adjuntos en los documentos
click_here_to_send_a_reset_password_email_html: '<label class="link" for="resend_password_button">Haz clic aquí</label> para enviar un correo electrónico de restablecimiento de contraseña.'
enabled: Habilitado
disabled: Deshabilitado
expirable_file_download_links: Enlaces de descarga de archivos con vencimiento
@@ -1212,6 +1264,7 @@ es: &es
invalid_timeserver: Servidor de tiempo inválido
email_templates: Plantillas de correo electrónico
signature_request_email: Correo de solicitud de firma
signature_request_reminder_email: Correo de recordatorio de solicitud de firma
signature_request_sms: SMS de solicitud de firma
verification_code_sms: SMS de código de verificación
completed_notification_email: Correo de notificación de formulario completado
@@ -1587,6 +1640,7 @@ es: &es
image_field: Campo de Imagen
file_field: Campo de Archivo
select_field: Campo de Selección
checkbox_field: Campo de Casilla
multiple_field: Campo Múltiple
radio_field: Campo de Grupo Radio
cells_field: Campo de Celdas
@@ -1758,6 +1812,49 @@ es: &es
connect_google_drive: Conectar Google Drive
google_drive_has_been_connected: Google Drive se ha conectado
unable_to_identify_reset_your_password_to_sign_in: No se pudo identificar. Restablece tu contraseña para iniciar sesión.
desktop: Escritorio
mobile: Móvil
tablet: Tableta
reset_default: Restablecer por defecto
send_signature_request_email: Enviar correo de solicitud de firma
last_3_months: Últimos 3 meses
last_6_months: Últimos 6 meses
last_year: Último año
all_time: Todo el tiempo
everyone: Todos
daily: Diario
weekly: Semanal
monthly: Mensual
api: API
embed: Integrar
bulk: Masivo
invite: Invitación
api_and_embed: API e Integrar
period: Período
reports: Informes
completed_submissions: Envíos completados
sms: SMS
require_email_2fa_to_open: Requerir 2FA por correo electrónico para abrir
verification_required_refresh_the_page_and_pass_2fa: Verificación requerida, actualiza la página y completa la autenticación de dos factores.
the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_email_html: El remitente ha solicitado autenticación de dos factores mediante una contraseña de un solo uso enviada a tu correo electrónico <b>%{email}</b>.
please_contact_the_requester_to_specify_your_email_for_two_factor_authentication: Por favor, contacta al remitente para especificar tu correo electrónico para la autenticación de dos factores.
rate_limit_exceeded: Límite de velocidad excedido
a_confirmation_email_has_been_sent_to_the_new_email_address: Se ha enviado un correo electrónico de confirmación a la nueva dirección de correo electrónico.
email_address_is_awaiting_confirmation_follow_the_link_in_the_email_to_confirm: "%{email} está pendiente de confirmación. Sigue el enlace en el correo para confirmarla."
please_confirm_your_email_address_using_the_link_below_: 'Por favor, confirma tu dirección de correo electrónico utilizando el enlace a continuación:'
confirm_email: Confirmar correo
unconfirmed: No confirmado
you_requested_to_reset_your_password_use_the_link_below_to_continue: Solicitaste restablecer tu contraseña. Usa el enlace a continuación para continuar.
if_you_didnt_request_this_you_can_ignore_this_email: "Si no solicitaste esto, puedes ignorar este correo electrónico."
your_password_wont_change_until_you_open_the_link_above_and_set_a_new_one: "Tu contraseña no cambiará hasta que abras el enlace anterior y establezcas una nueva."
devise:
confirmations:
confirmed: Tu dirección de correo electrónico ha sido confirmada correctamente.
failure:
unconfirmed: Debes confirmar tu dirección de correo electrónico antes de continuar.
mailer:
confirmation_instructions:
subject: Confirma tu dirección de correo electrónico
submission_sources:
api: API
bulk: Envío masivo
@@ -1771,6 +1868,7 @@ es: &es
send_reminder_email_to_html: '<b>Correo de recordatorio enviado</b> a %{submitter_name}'
send_sms_to_html: '<b>SMS enviado</b> a %{submitter_name}'
send_2fa_sms_to_html: '<b>SMS de verificación enviado</b> a %{submitter_name}'
send_2fa_email_to_html: '<b>Correo electrónico de verificación enviado</b> a %{submitter_name}'
open_email_by_html: '<b>Correo electrónico abierto</b> por %{submitter_name}'
click_email_by_html: '<b>Enlace del correo electrónico clicado</b> por %{submitter_name}'
click_sms_by_html: '<b>Enlace del SMS clicado</b> por %{submitter_name}'
@@ -1864,6 +1962,9 @@ es: &es
range_without_total: "%{from}-%{to} eventos"
it: &it
templates_that_require_email_or_phone_2fa_cannot_be_used_via_a_shared_link: I modelli che richiedono 2FA via email o telefono non possono essere utilizzati tramite un link condiviso.
make_owner: Rendi proprietario
billing: Fatturazione
add_from_google_drive: Aggiungi da Google Drive
or_add_from: Oppure aggiungi da
upload_a_new_document: Carica nuovo documento
@@ -1897,6 +1998,7 @@ it: &it
edit_per_party: Modifica per partito
signed: Firmato
reply_to: Rispondi a
partially_completed: Parzialmente completato
pending_by_me: In sospeso da me
add: Aggiungi
adding: Aggiungendo
@@ -2133,6 +2235,7 @@ it: &it
invalid_timeserver: Server di timestamp non valido
email_templates: Modelli email
signature_request_email: Email di richiesta di firma
signature_request_reminder_email: Email di promemoria di richiesta di firma
signature_request_sms: SMS di richiesta di firma
verification_code_sms: SMS con codice di verifica
completed_notification_email: Email di notifica di completamento
@@ -2341,6 +2444,7 @@ it: &it
upload_signature: Carica firma
integration: Integrazione
admin: Amministratore
tenant_admin: Amministratore tenant
editor: Editor
viewer: Visualizzatore
member: Membro
@@ -2680,6 +2784,49 @@ it: &it
connect_google_drive: Connetti Google Drive
google_drive_has_been_connected: Google Drive è stato connesso
unable_to_identify_reset_your_password_to_sign_in: Impossibile identificare. Reimposta la password per accedere.
desktop: Desktop
mobile: Mobile
tablet: Tablet
reset_default: Reimposta predefinito
send_signature_request_email: Invia email di richiesta firma
last_3_months: Ultimi 3 mesi
last_6_months: Ultimi 6 mesi
last_year: Ultimo anno
all_time: Tutto il tempo
everyone: Tutti
daily: Giornaliero
weekly: Settimanale
monthly: Mensile
api: API
embed: Incorporare
bulk: Massivo
invite: Invito
api_and_embed: API e Incorporare
period: Periodo
reports: Rapporti
completed_submissions: Invii completati
sms: SMS
require_email_2fa_to_open: Richiedi 2FA email per aprire
verification_required_refresh_the_page_and_pass_2fa: Verifica richiesta, aggiorna la pagina e completa l'autenticazione a due fattori.
the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_email_html: Il mittente ha richiesto l'autenticazione a due fattori tramite una password monouso inviata al tuo indirizzo email <b>%{email}</b>.
please_contact_the_requester_to_specify_your_email_for_two_factor_authentication: Si prega di contattare il mittente per specificare il tuo indirizzo email per l'autenticazione a due fattori.
rate_limit_exceeded: Limite di velocità superato
a_confirmation_email_has_been_sent_to_the_new_email_address: È stata inviata un'email di conferma al nuovo indirizzo email.
email_address_is_awaiting_confirmation_follow_the_link_in_the_email_to_confirm: "%{email} è in attesa di conferma. Segui il link nell'email per confermare."
please_confirm_your_email_address_using_the_link_below_: 'Conferma il tuo indirizzo email utilizzando il link qui sotto:'
confirm_email: Conferma email
unconfirmed: Non confermato
you_requested_to_reset_your_password_use_the_link_below_to_continue: Hai richiesto di reimpostare la tua password. Usa il link qui sotto per continuare.
if_you_didnt_request_this_you_can_ignore_this_email: "Se non hai richiesto questo, puoi ignorare questa email."
your_password_wont_change_until_you_open_the_link_above_and_set_a_new_one: "La tua password non cambierà finché non apri il link sopra e ne imposti una nuova."
devise:
confirmations:
confirmed: Il tuo indirizzo email è stato confermato con successo.
failure:
unconfirmed: Devi confermare il tuo indirizzo email prima di continuare.
mailer:
confirmation_instructions:
subject: Conferma il tuo indirizzo email
submission_sources:
api: API
bulk: Invio massivo
@@ -2693,6 +2840,7 @@ it: &it
send_reminder_email_to_html: '<b>E-mail di promemoria inviato</b> a %{submitter_name}'
send_sms_to_html: '<b>SMS inviato</b> a %{submitter_name}'
send_2fa_sms_to_html: '<b>SMS di verifica inviato</b> a %{submitter_name}'
send_2fa_email_to_html: '<b>Email di verifica inviata</b> a %{submitter_name}'
open_email_by_html: '<b>E-mail aperta</b> da %{submitter_name}'
click_email_by_html: "<b>Link dell'e-mail cliccato</b> da %{submitter_name}"
click_sms_by_html: "<b>Link dell'SMS cliccato</b> da %{submitter_name}"
@@ -2786,6 +2934,9 @@ it: &it
range_without_total: "%{from}-%{to} eventi"
fr: &fr
templates_that_require_email_or_phone_2fa_cannot_be_used_via_a_shared_link: Les modèles nécessitant une authentification à deux facteurs par e-mail ou téléphone ne peuvent pas être utilisés via un lien partagé.
make_owner: Rendre propriétaire
billing: Facturation
add_from_google_drive: Ajouter depuis Google Drive
or_add_from: Ou ajouter depuis
upload_a_new_document: Téléverser un nouveau document
@@ -3056,6 +3207,7 @@ fr: &fr
invalid_timeserver: Serveur dhorodatage invalide
email_templates: Modèles demail
signature_request_email: Email de demande de signature
signature_request_reminder_email: Email de rappel de demande de signature
signature_request_sms: SMS de demande de signature
verification_code_sms: SMS de code de vérification
completed_notification_email: Email de notification de finalisation
@@ -3599,6 +3751,50 @@ fr: &fr
connect_google_drive: Connecter Google Drive
google_drive_has_been_connected: Google Drive a été connecté
unable_to_identify_reset_your_password_to_sign_in: Impossible didentifier. Réinitialisez votre mot de passe pour vous connecter.
desktop: Bureau
mobile: Mobile
tablet: Tablette
reset_default: Réinitialiser par défaut
send_signature_request_email: Envoyer un e-mail de demande de signature
last_month: Mois dernier
last_3_months: 3 derniers mois
last_6_months: 6 derniers mois
last_year: Année dernière
all_time: Tout le temps
everyone: Tout le monde
daily: Quotidien
weekly: Hebdomadaire
monthly: Mensuel
api: API
embed: Intégrer
bulk: En masse
invite: Invitation
api_and_embed: API et Intégrer
period: Période
reports: Rapports
completed_submissions: Soumissions terminées
sms: SMS
require_email_2fa_to_open: Exiger la 2FA par email pour ouvrir
verification_required_refresh_the_page_and_pass_2fa: Vérification requise, actualisez la page et effectuez l'authentification à deux facteurs.
the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_email_html: L'expéditeur a demandé l'authentification à deux facteurs par mot de passe à usage unique envoyé à votre adresse e-mail <b>%{email}</b>.
please_contact_the_requester_to_specify_your_email_for_two_factor_authentication: Veuillez contacter l'expéditeur pour spécifier votre adresse e-mail pour l'authentification à deux facteurs.
rate_limit_exceeded: Limite de débit dépassée
a_confirmation_email_has_been_sent_to_the_new_email_address: Un e-mail de confirmation a été envoyé à la nouvelle adresse e-mail.
email_address_is_awaiting_confirmation_follow_the_link_in_the_email_to_confirm: "%{email} est en attente de confirmation. Suivez le lien dans l'e-mail pour la confirmer."
please_confirm_your_email_address_using_the_link_below_: 'Veuillez confirmer votre adresse e-mail en utilisant le lien ci-dessous :'
confirm_email: "Confirmer l'e-mail"
unconfirmed: Non confirmé
you_requested_to_reset_your_password_use_the_link_below_to_continue: Vous avez demandé à réinitialiser votre mot de passe. Utilisez le lien ci-dessous pour continuer.
if_you_didnt_request_this_you_can_ignore_this_email: "Si vous n'avez pas fait cette demande, veuillez ignorer cet e-mail."
your_password_wont_change_until_you_open_the_link_above_and_set_a_new_one: "Votre mot de passe ne changera pas tant que vous naurez pas ouvert le lien ci-dessus et défini un nouveau mot de passe."
devise:
confirmations:
confirmed: Votre adresse e-mail a été confirmée avec succès.
failure:
unconfirmed: Vous devez confirmer votre adresse e-mail avant de continuer.
mailer:
confirmation_instructions:
subject: Confirmez votre adresse e-mail
submission_sources:
api: API
bulk: Envoi en masse
@@ -3612,6 +3808,7 @@ fr: &fr
send_reminder_email_to_html: "<b>Email de rappel envoyé</b> à %{submitter_name}"
send_sms_to_html: "<b>SMS envoyé</b> à %{submitter_name}"
send_2fa_sms_to_html: "<b>SMS de vérification envoyé</b> à %{submitter_name}"
send_2fa_email_to_html: "<b>Email de vérification envoyé</b> à %{submitter_name}"
open_email_by_html: "<b>Email ouvert</b> par %{submitter_name}"
click_email_by_html: "<b>Lien email cliqué</b> par %{submitter_name}"
click_sms_by_html: "<b>Lien SMS cliqué</b> par %{submitter_name}"
@@ -3705,6 +3902,9 @@ fr: &fr
range_without_total: "%{from}-%{to} événements"
pt: &pt
templates_that_require_email_or_phone_2fa_cannot_be_used_via_a_shared_link: Modelos que exigem 2FA por e-mail ou telefone não podem ser usados por meio de um link compartilhado.
make_owner: Tornar proprietário
billing: Pagamentos
add_from_google_drive: Adicionar do Google Drive
or_add_from: Ou adicionar de
upload_a_new_document: Enviar novo documento
@@ -3975,6 +4175,7 @@ pt: &pt
invalid_timeserver: Servidor de carimbo de tempo inválido
email_templates: Modelos de e-mail
signature_request_email: E-mail de solicitação de assinatura
signature_request_reminder_email: E-mail de lembrete de solicitação de assinatura
signature_request_sms: SMS de solicitação de assinatura
verification_code_sms: SMS com código de verificação
completed_notification_email: E-mail de notificação de submissão concluída
@@ -4522,6 +4723,49 @@ pt: &pt
connect_google_drive: Conectar Google Drive
google_drive_has_been_connected: O Google Drive foi conectado
unable_to_identify_reset_your_password_to_sign_in: Não foi possível identificar. Redefina sua senha para fazer login.
desktop: Computador
mobile: Celular
tablet: Tablet
reset_default: Redefinir para padrão
send_signature_request_email: Enviar e-mail de solicitação de assinatura
last_3_months: Últimos 3 meses
last_6_months: Últimos 6 meses
last_year: Último ano
all_time: Todo o tempo
everyone: Todos
daily: Diário
weekly: Semanal
monthly: Mensal
api: API
embed: Incorporar
bulk: Em massa
invite: Convite
api_and_embed: API e Incorporar
period: Período
reports: Relatórios
completed_submissions: Envios concluídos
sms: SMS
require_email_2fa_to_open: Exigir 2FA por e-mail para abrir
verification_required_refresh_the_page_and_pass_2fa: Verificação necessária, atualize a página e conclua a autenticação de dois fatores.
the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_email_html: O remetente solicitou autenticação de dois fatores por meio de uma senha de uso único enviada para o seu e-mail <b>%{email}</b>.
please_contact_the_requester_to_specify_your_email_for_two_factor_authentication: Por favor, entre em contato com o remetente para especificar seu e-mail para autenticação de dois fatores.
rate_limit_exceeded: Limite de taxa excedido
a_confirmation_email_has_been_sent_to_the_new_email_address: Um e-mail de confirmação foi enviado para o novo endereço de e-mail.
email_address_is_awaiting_confirmation_follow_the_link_in_the_email_to_confirm: "%{email} está aguardando confirmação. Siga o link enviado para esse endereço de e-mail para confirmar."
please_confirm_your_email_address_using_the_link_below_: 'Por favor, confirme seu endereço de e-mail usando o link abaixo:'
confirm_email: Confirmar e-mail
unconfirmed: Não confirmado
you_requested_to_reset_your_password_use_the_link_below_to_continue: Você solicitou a redefinição da sua senha. Use o link abaixo para continuar.
if_you_didnt_request_this_you_can_ignore_this_email: "Se você não solicitou isso, pode ignorar este e-mail."
your_password_wont_change_until_you_open_the_link_above_and_set_a_new_one: "Sua senha não será alterada até que você abra o link acima e defina uma nova."
devise:
confirmations:
confirmed: Seu endereço de e-mail foi confirmado com sucesso.
failure:
unconfirmed: Você deve confirmar seu endereço de e-mail antes de continuar.
mailer:
confirmation_instructions:
subject: Confirme seu endereço de e-mail
submission_sources:
api: API
bulk: Envio em massa
@@ -4535,6 +4779,7 @@ pt: &pt
send_reminder_email_to_html: '<b>Email de lembrete enviado</b> para %{submitter_name}'
send_sms_to_html: '<b>SMS enviado</b> para %{submitter_name}'
send_2fa_sms_to_html: '<b>SMS de verificação enviado</b> para %{submitter_name}'
send_2fa_email_to_html: '<b>Email de verificação enviado</b> para %{submitter_name}'
open_email_by_html: '<b>E-mail aberto</b> por %{submitter_name}'
click_email_by_html: '<b>Link do e-mail clicado</b> por %{submitter_name}'
click_sms_by_html: '<b>Link do SMS clicado</b> por %{submitter_name}'
@@ -4628,6 +4873,9 @@ pt: &pt
range_without_total: "%{from}-%{to} eventos"
de: &de
templates_that_require_email_or_phone_2fa_cannot_be_used_via_a_shared_link: Vorlagen, die eine Zwei-Faktor-Authentifizierung per E-Mail oder Telefon erfordern, können nicht über einen geteilten Link verwendet werden.
make_owner: Eigentümer machen
billing: Abrechnung
add_from_google_drive: Aus Google Drive hinzufügen
or_add_from: Oder hinzufügen aus
upload_a_new_document: Neues Dokument hochladen
@@ -4898,6 +5146,7 @@ de: &de
invalid_timeserver: Ungültiger Zeitstempelserver
email_templates: E-Mail-Vorlagen
signature_request_email: E-Mail für Signaturanfrage
signature_request_reminder_email: E-Mail-Erinnerung für Signaturanfrage
signature_request_sms: SMS für Signaturanfrage
verification_code_sms: SMS mit Verifizierungscode
completed_notification_email: E-Mail-Benachrichtigung bei Abschluss
@@ -5445,6 +5694,49 @@ de: &de
connect_google_drive: Google Drive verbinden
google_drive_has_been_connected: Google Drive wurde verbunden
unable_to_identify_reset_your_password_to_sign_in: Identifizierung nicht möglich. Setzen Sie Ihr Passwort zurück, um sich anzumelden.
desktop: Desktop
mobile: Mobil
tablet: Tablet
reset_default: Standard zurücksetzen
send_signature_request_email: Signaturanfrage-E-Mail senden
last_3_months: Letzte 3 Monate
last_6_months: Letzte 6 Monate
last_year: Letztes Jahr
all_time: Gesamte Zeit
everyone: Alle
daily: Täglich
weekly: Wöchentlich
monthly: Monatlich
api: API
embed: Einbetten
bulk: Massenversand
invite: Einladung
api_and_embed: API und Einbetten
period: Zeitraum
reports: Berichte
completed_submissions: Abgeschlossene Übermittlungen
sms: SMS
require_email_2fa_to_open: E-Mail-2FA erforderlich, um zu öffnen
verification_required_refresh_the_page_and_pass_2fa: Verifikation erforderlich. Aktualisieren Sie die Seite und führen Sie die Zwei-Faktor-Authentifizierung durch.
the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_email_html: Der Absender hat die Zwei-Faktor-Authentifizierung per Einmalpasswort angefordert, das an Ihre E-Mail-Adresse <b>%{email}</b> gesendet wurde.
please_contact_the_requester_to_specify_your_email_for_two_factor_authentication: Bitte wenden Sie sich an den Absender, um Ihre E-Mail-Adresse für die Zwei-Faktor-Authentifizierung anzugeben.
rate_limit_exceeded: Limit überschritten
a_confirmation_email_has_been_sent_to_the_new_email_address: Eine Bestätigungs-E-Mail wurde an die neue E-Mail-Adresse gesendet.
email_address_is_awaiting_confirmation_follow_the_link_in_the_email_to_confirm: "%{email} wartet auf Bestätigung. Folgen Sie dem Link in der E-Mail, um sie zu bestätigen."
please_confirm_your_email_address_using_the_link_below_: 'Bitte bestätigen Sie Ihre E-Mail-Adresse über den folgenden Link:'
confirm_email: E-Mail bestätigen
unconfirmed: Unbestätigt
you_requested_to_reset_your_password_use_the_link_below_to_continue: Sie haben angefordert, Ihr Passwort zurückzusetzen. Verwenden Sie den untenstehenden Link, um fortzufahren.
if_you_didnt_request_this_you_can_ignore_this_email: "Wenn Sie dies nicht angefordert haben, können Sie diese E-Mail ignorieren."
your_password_wont_change_until_you_open_the_link_above_and_set_a_new_one: "Ihr Passwort wird erst geändert, wenn Sie den obigen Link öffnen und ein neues festlegen."
devise:
confirmations:
confirmed: Ihre E-Mail-Adresse wurde erfolgreich bestätigt.
failure:
unconfirmed: Sie müssen Ihre E-Mail-Adresse bestätigen, bevor Sie fortfahren.
mailer:
confirmation_instructions:
subject: Bestätigen Sie Ihre E-Mail-Adresse
submission_sources:
api: API
bulk: Massenversand
@@ -5458,6 +5750,7 @@ de: &de
send_reminder_email_to_html: '<b>Erinnerungs-E-Mail gesendet</b> an %{submitter_name}'
send_sms_to_html: '<b>SMS gesendet</b> an %{submitter_name}'
send_2fa_sms_to_html: '<b>Verifizierungs-SMS gesendet</b> an %{submitter_name}'
send_2fa_email_to_html: '<b>Verifizierungs-E-Mail gesendet</b> an %{submitter_name}'
open_email_by_html: '<b>E-Mail geöffnet</b> von %{submitter_name}'
click_email_by_html: '<b>E-Mail-Link angeklickt</b> von %{submitter_name}'
click_sms_by_html: '<b>SMS-Link angeklickt</b> von %{submitter_name}'
@@ -5590,6 +5883,7 @@ pl:
completed_on_time: 'Zakończono %{time}'
document_has_been_signed_already: 'Dokument został już podpisany'
form_has_been_submitted_already: Formularz został już przesłany
re_send_email: Wyślij wiadomość e-mail ponownie
send_copy_to_email: Wyślij kopię na Email
sending: Wysyłanie
resubmit: Prześlij ponownie
@@ -5640,6 +5934,10 @@ pl:
please_reply_to_this_email_if_you_didnt_request_this: Odpowiedz na ten e-mail, jeśli nie prosiłeś o to.
your_user_account_has_been_archived_contact_your_administrator_to_restore_access_to_your_account: Twoje konto użytkownika zostało zarchiwizowane. Skontaktuj się z administratorem, aby przywrócić dostęp do konta.
your_email_could_not_be_reached_this_may_happen_if_there_was_a_typo_in_your_address_or_if_your_mailbox_is_not_available_please_contact_support_email_to_log_in: Nie udało się uzyskać dostępu do Twojego adresu e-mail. Może to być spowodowane literówką w adresie lub niedostępnością skrzynki. Skontaktuj się z support@docuseal.com, aby się zalogować.
verification_required_refresh_the_page_and_pass_2fa: Weryfikacja wymagana, odśwież stronę i przejdź uwierzytelnienie dwuskładnikowe.
the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_email_html: Nadawca zażądał uwierzytelniania dwuskładnikowego za pośrednictwem hasła jednorazowego wysłanego na Twój adres e-mail <b>%{email}</b>.
please_contact_the_requester_to_specify_your_email_for_two_factor_authentication: Skontaktuj się z nadawcą, aby podać swój adres e-mail do uwierzytelniania dwuskładnikowego.
rate_limit_exceeded: Przekroczono limit
uk:
require_phone_2fa_to_open: Вимагати двофакторну автентифікацію через телефон для відкриття
@@ -5681,6 +5979,7 @@ uk:
completed_on_time: 'Завершено %{time}'
document_has_been_signed_already: 'Документ уже був підписаний'
form_has_been_submitted_already: Форма вже була подана
re_send_email: Надіслати e-mail знову
send_copy_to_email: Надіслати копію на Email
sending: Надсилання
resubmit: Подати знову
@@ -5731,6 +6030,10 @@ uk:
please_reply_to_this_email_if_you_didnt_request_this: Відповідайте на цей лист, якщо ви цього не запитували.
your_user_account_has_been_archived_contact_your_administrator_to_restore_access_to_your_account: Ваш обліковий запис було архівовано. Зверніться до адміністратора, щоб відновити доступ.
your_email_could_not_be_reached_this_may_happen_if_there_was_a_typo_in_your_address_or_if_your_mailbox_is_not_available_please_contact_support_email_to_log_in: Не вдалося отримати доступ до вашої електронної пошти. Це може статися, якщо була допущена помилка в адресі або ваша скринька недоступна. Зверніться до support@docuseal.com, щоб увійти.
verification_required_refresh_the_page_and_pass_2fa: Необхідна верифікація, оновіть сторінку та пройдіть двофакторну автентифікацію.
the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_email_html: Відправник запросив двофакторну автентифікацію за допомогою одноразового пароля, відправленого на вашу електронну пошту <b>%{email}</b>.
please_contact_the_requester_to_specify_your_email_for_two_factor_authentication: Будь ласка, зв'яжіться з відправником, щоб вказати вашу електронну пошту для двофакторної автентифікації.
rate_limit_exceeded: Перевищено ліміт
cs:
require_phone_2fa_to_open: Vyžadovat otevření pomocí telefonního 2FA
@@ -5772,6 +6075,7 @@ cs:
completed_on_time: 'Dokončeno %{time}'
document_has_been_signed_already: 'Dokument již byl podepsán'
form_has_been_submitted_already: Formulář již byl odeslán
re_send_email: Znovu odeslat e-mail
send_copy_to_email: Odeslat kopii na Email
sending: Odesílání
resubmit: Odeslat znovu
@@ -5822,6 +6126,10 @@ cs:
please_reply_to_this_email_if_you_didnt_request_this: Odpovězte na tento e-mail, pokud jste o to nežádali.
your_user_account_has_been_archived_contact_your_administrator_to_restore_access_to_your_account: Váš uživatelský účet byl archivován. Kontaktujte svého administrátora pro obnovení přístupu.
your_email_could_not_be_reached_this_may_happen_if_there_was_a_typo_in_your_address_or_if_your_mailbox_is_not_available_please_contact_support_email_to_log_in: Nepodařilo se dosáhnout na váš e-mail. To se může stát, pokud je v adrese překlep nebo vaše schránka není dostupná. Kontaktujte support@docuseal.com pro přihlášení.
verification_required_refresh_the_page_and_pass_2fa: Ověření vyžadováno. Aktualizujte stránku a dokončete dvoufaktorové ověření.
the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_email_html: Odesílatel požádal o dvoufaktorové ověření pomocí jednorázového hesla odeslaného na vaši e-mailovou adresu <b>%{email}</b>.
please_contact_the_requester_to_specify_your_email_for_two_factor_authentication: Prosím kontaktujte odesílatele a uveďte svůj e-mail pro dvoufaktorové ověření.
rate_limit_exceeded: Překročena hranice
he:
require_phone_2fa_to_open: דרוש אימות דו-שלבי באמצעות טלפון לפתיחה
@@ -5863,6 +6171,7 @@ he:
completed_on_time: 'הושלם ב-%{time}'
document_has_been_signed_already: 'המסמך כבר נחתם'
form_has_been_submitted_already: הטופס כבר נשלח
re_send_email: שלח דוא"ל שוב
send_copy_to_email: שלח עותק לדוא"ל
sending: שולח
resubmit: שלח מחדש
@@ -5913,8 +6222,15 @@ he:
please_reply_to_this_email_if_you_didnt_request_this: 'השב למייל זה אם לא ביקשת זאת.'
your_user_account_has_been_archived_contact_your_administrator_to_restore_access_to_your_account: החשבון שלך הועבר לארכיון. פנה למנהל המערכת כדי לשחזר את הגישה.
your_email_could_not_be_reached_this_may_happen_if_there_was_a_typo_in_your_address_or_if_your_mailbox_is_not_available_please_contact_support_email_to_log_in: לא ניתן היה לגשת לדוא"ל שלך. ייתכן שזה קרה עקב שגיאת כתיב בכתובת או אם תיבת הדואר אינה זמינה. אנא פנה ל־support@docuseal.com כדי להתחבר.
verification_required_refresh_the_page_and_pass_2fa: נדרשת אימות. רענן את הדף והשלם אימות דו-שלבי.
the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_email_html: השולח ביקש אימות דו-שלבי באמצעות סיסמה חד-פעמית שנשלחה לכתובת הדוא"ל שלך <b>%{email}</b>.
please_contact_the_requester_to_specify_your_email_for_two_factor_authentication: אנא פנה לשולח וציין את כתובת הדוא"ל שלך לאימות דו-שלבי.
rate_limit_exceeded: חריגה ממגבלת
nl: &nl
templates_that_require_email_or_phone_2fa_cannot_be_used_via_a_shared_link: Sjablonen waarvoor e-mail- of telefoon-2FA vereist is, kunnen niet via een gedeelde link worden gebruikt.
make_owner: Eigenaar maken
billing: Facturatie
add_from_google_drive: Toevoegen vanuit Google Drive
or_add_from: Of toevoegen vanuit
upload_a_new_document: Nieuw document uploaden
@@ -6186,6 +6502,7 @@ nl: &nl
invalid_timeserver: Ongeldige tijdserver
email_templates: E-mailsjablonen
signature_request_email: E-mail voor handtekeningverzoek
signature_request_reminder_email: E-mailherinnering voor handtekeningverzoek
signature_request_sms: SMS voor handtekeningverzoek
verification_code_sms: Verificatiecode-SMS
completed_notification_email: E-mailmelding voltooid
@@ -6729,6 +7046,49 @@ nl: &nl
connect_google_drive: Verbind Google Drive
google_drive_has_been_connected: Google Drive is verbonden
unable_to_identify_reset_your_password_to_sign_in: Kan niet worden geïdentificeerd. Stel je wachtwoord opnieuw in om in te loggen.
desktop: Desktop
mobile: Mobiel
tablet: Tablet
reset_default: Standaard herstellen
send_signature_request_email: E-mail met handtekeningaanvraag verzenden
last_3_months: Afgelopen 3 maanden
last_6_months: Afgelopen 6 maanden
last_year: Afgelopen jaar
all_time: Altijd
everyone: Iedereen
daily: Dagelijks
weekly: Wekelijks
monthly: Maandelijks
api: API
embed: Insluiten
bulk: Bulk
invite: Uitnodiging
api_and_embed: API en Insluiten
period: Periode
reports: Rapporten
completed_submissions: Voltooide inzendingen
sms: SMS
require_email_2fa_to_open: E-mail 2FA vereisen om te openen
verification_required_refresh_the_page_and_pass_2fa: Verificatie vereist. Vernieuw de pagina en voer twee-factor-authenticatie uit.
the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_email_html: De afzender heeft twee-factor-authenticatie aangevraagd via een eenmalig wachtwoord dat naar uw e-mailadres <b>%{email}</b> is verzonden.
please_contact_the_requester_to_specify_your_email_for_two_factor_authentication: Neem alstublieft contact op met de afzender om uw e-mailadres op te geven voor twee-factor-authenticatie.
rate_limit_exceeded: Snelheidslimiet overschreden
a_confirmation_email_has_been_sent_to_the_new_email_address: Er is een bevestigingsmail verzonden naar het nieuwe e-mailadres.
email_address_is_awaiting_confirmation_follow_the_link_in_the_email_to_confirm: "%{email} wacht op bevestiging. Volg de link in de e-mail om te bevestigen."
please_confirm_your_email_address_using_the_link_below_: 'Bevestig je e-mailadres via de onderstaande link:'
confirm_email: E-mailadres bevestigen
unconfirmed: Onbevestigd
you_requested_to_reset_your_password_use_the_link_below_to_continue: Je hebt gevraagd je wachtwoord te resetten. Gebruik de onderstaande link om verder te gaan.
if_you_didnt_request_this_you_can_ignore_this_email: "Als je dit niet hebt aangevraagd, kun je deze e-mail negeren."
your_password_wont_change_until_you_open_the_link_above_and_set_a_new_one: "Je wachtwoord wordt niet gewijzigd totdat je de bovenstaande link opent en een nieuw wachtwoord instelt."
devise:
confirmations:
confirmed: Je e-mailadres is succesvol bevestigd.
failure:
unconfirmed: Je moet je e-mailadres bevestigen voordat je verdergaat.
mailer:
confirmation_instructions:
subject: Bevestig je e-mailadres
submission_sources:
api: API
bulk: Bulkverzending
@@ -6742,6 +7102,7 @@ nl: &nl
send_reminder_email_to_html: "<b>Herinneringsmail verzonden</b> naar %{submitter_name}"
send_sms_to_html: "<b>SMS verzonden</b> naar %{submitter_name}"
send_2fa_sms_to_html: "<b>Verificatie-SMS verzonden</b> naar %{submitter_name}"
send_2fa_email_to_html: "<b>Verificatie-e-mail verzonden</b> naar %{submitter_name}"
open_email_by_html: "<b>E-mail geopend</b> door %{submitter_name}"
click_email_by_html: "<b>E-maillink aangeklikt</b> door %{submitter_name}"
click_sms_by_html: "<b>SMS-link aangeklikt</b> door %{submitter_name}"
@@ -6873,6 +7234,7 @@ ar:
completed_on_time: 'تم إكماله في %{time}'
document_has_been_signed_already: 'تم توقيع الوثيقة بالفعل'
form_has_been_submitted_already: تم تقديم الاستمارة بالفعل
re_send_email: إعادة إرسال البريد الإلكتروني
send_copy_to_email: إرسال نسخة إلى البريد الإلكتروني
sending: جارٍ الإرسال
resubmit: إعادة التقديم
@@ -6924,6 +7286,10 @@ ar:
please_reply_to_this_email_if_you_didnt_request_this: 'يرجى الرد على هذا البريد إذا لم تطلب ذلك.'
your_user_account_has_been_archived_contact_your_administrator_to_restore_access_to_your_account: تم أرشفة حسابك. يرجى التواصل مع المسؤول لاستعادة الوصول إلى حسابك.
your_email_could_not_be_reached_this_may_happen_if_there_was_a_typo_in_your_address_or_if_your_mailbox_is_not_available_please_contact_support_email_to_log_in: تعذّر الوصول إلى بريدك الإلكتروني. قد يحدث هذا في حال وجود خطأ في العنوان أو إذا كانت صندوق البريد غير متاح. يرجى التواصل مع support@docuseal.com لتسجيل الدخول.
verification_required_refresh_the_page_and_pass_2fa: مطلوب التحقق. قم بتحديث الصفحة وأكمل المصادقة الثنائية.
the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_email_html: طلب المرسل المصادقة الثنائية عبر كلمة مرور لمرة واحدة مرسلة إلى عنوان بريدك الإلكتروني <b>%{email}</b>.
please_contact_the_requester_to_specify_your_email_for_two_factor_authentication: يرجى الاتصال بالمرسل لتحديد عنوان بريدك الإلكتروني للمصادقة الثنائية.
rate_limit_exceeded: تم تجاوز الحد المسموح به
ko:
require_phone_2fa_to_open: 휴대폰 2FA를 열 때 요구함
@@ -6964,6 +7330,7 @@ ko:
completed_on_time: '%{time}에 완료됨'
document_has_been_signed_already: '문서가 이미 서명되었습니다'
form_has_been_submitted_already: 양식이 이미 제출되었습니다.
re_send_email: 이메일 다시 보내기
send_copy_to_email: 이메일로 사본 보내기
sending: 전송 중
resubmit: 다시 제출
@@ -7015,6 +7382,10 @@ ko:
please_reply_to_this_email_if_you_didnt_request_this: 요청하지 않았다면 이 이메일에 회신하세요.
your_user_account_has_been_archived_contact_your_administrator_to_restore_access_to_your_account: 사용자 계정이 보관되었습니다. 계정 접근을 복원하려면 관리자에게 문의하세요.
your_email_could_not_be_reached_this_may_happen_if_there_was_a_typo_in_your_address_or_if_your_mailbox_is_not_available_please_contact_support_email_to_log_in: 이메일에 접근할 수 없습니다. 주소에 오타가 있거나 메일박스가 사용 불가능한 경우 발생할 수 있습니다. 로그인하려면 support@docuseal.com에 문의하세요.
verification_required_refresh_the_page_and_pass_2fa: 인증이 필요합니다. 페이지를 새로 고치고 2단계 인증을 완료하세요.
the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_email_html: 발신자가 <b>%{email}</b> 이메일 주소로 전송된 일회용 비밀번호를 통해 2단계 인증을 요청했습니다.
please_contact_the_requester_to_specify_your_email_for_two_factor_authentication: 2단계 인증을 위해 이메일 주소를 지정하려면 발신자에게 문의하세요.
rate_limit_exceeded: 속도 제한 초과
ja:
require_phone_2fa_to_open: 電話による2段階認証が必要です
@@ -7055,6 +7426,7 @@ ja:
completed_on_time: '%{time} に完了'
document_has_been_signed_already: ドキュメントはすでに署名されています
form_has_been_submitted_already: フォームはすでに送信されています
re_send_email: メールを再度送信
send_copy_to_email: メールにコピーを送信
sending: 送信中
resubmit: 再送信
@@ -7106,6 +7478,10 @@ ja:
please_reply_to_this_email_if_you_didnt_request_this: このリクエストを行っていない場合は、このメールに返信してください。
your_user_account_has_been_archived_contact_your_administrator_to_restore_access_to_your_account: あなたのユーザーアカウントはアーカイブされました。アクセスを復元するには管理者に連絡してください。
your_email_could_not_be_reached_this_may_happen_if_there_was_a_typo_in_your_address_or_if_your_mailbox_is_not_available_please_contact_support_email_to_log_in: メールにアクセスできませんでした。アドレスの入力ミスやメールボックスが利用できない場合に発生することがあります。ログインするには support@docuseal.com に連絡してください。
verification_required_refresh_the_page_and_pass_2fa: 認証が必要です。ページを更新して2段階認証を完了してください。
the_sender_has_requested_a_two_factor_authentication_via_one_time_password_sent_to_your_email_html: 送信者は、<b>%{email}</b> メールアドレスに送信されたワンタイムパスワードによる2段階認証を要求しました。
please_contact_the_requester_to_specify_your_email_for_two_factor_authentication: 2段階認証用にメールアドレスを指定するために、送信者にお問い合わせください。
rate_limit_exceeded: レート制限を超えました
en-US:
<<: *en
+4 -8
View File
@@ -14,13 +14,8 @@ Rails.application.routes.draw do
get 'up' => 'rails/health#show'
get 'manifest' => 'pwa#manifest'
devise_for :users,
path: '/', only: %i[sessions passwords omniauth_callbacks],
controllers: begin
options = { sessions: 'sessions', passwords: 'passwords' }
options[:omniauth_callbacks] = 'omniauth_callbacks' if User.devise_modules.include?(:omniauthable)
options
end
devise_for :users, path: '/', only: %i[sessions passwords],
controllers: { sessions: 'sessions', passwords: 'passwords' }
devise_scope :user do
resource :invitation, only: %i[update] do
@@ -108,7 +103,7 @@ Rails.application.routes.draw do
resource :preview, only: %i[show], controller: 'templates_preview'
resource :form, only: %i[show], controller: 'templates_form_preview'
resource :code_modal, only: %i[show], controller: 'templates_code_modal'
resource :preferences, only: %i[show create], controller: 'templates_preferences'
resource :preferences, only: %i[show create destroy], controller: 'templates_preferences'
resource :share_link, only: %i[show create], controller: 'templates_share_link'
resources :recipients, only: %i[create], controller: 'templates_recipients'
resources :prefillable_fields, only: %i[create], controller: 'templates_prefillable_fields'
@@ -137,6 +132,7 @@ Rails.application.routes.draw do
end
resource :resubmit_form, controller: 'start_form', only: :update
resource :submit_form_email_2fa, only: %i[create update]
resources :start_form_email_2fa_send, only: :create
resources :submit_form, only: %i[], path: '' do
@@ -0,0 +1,7 @@
# frozen_string_literal: true
class AddAccountIdToSubmissionEvents < ActiveRecord::Migration[8.0]
def change
add_reference :submission_events, :account, null: true, foreign_key: true, index: true
end
end
@@ -0,0 +1,14 @@
# frozen_string_literal: true
class AddIsFirstToCompletedSubmitters < ActiveRecord::Migration[8.0]
def change
# rubocop:disable Rails/ThreeStateBooleanColumn
add_column :completed_submitters, :is_first, :boolean
# rubocop:enable Rails/ThreeStateBooleanColumn
add_index :completed_submitters, %i[account_id completed_at],
where: 'is_first = TRUE',
name: 'index_completed_submitters_account_id_completed_at_is_first'
add_index :completed_submitters, :submission_id, unique: true, where: 'is_first = TRUE'
end
end
@@ -0,0 +1,9 @@
# frozen_string_literal: true
class AddCompletedAtIndexToCompletedSubmitters < ActiveRecord::Migration[8.0]
def change
add_index :completed_submitters, %i[account_id completed_at],
name: 'index_completed_submitters_on_account_id_and_completed_at'
remove_index :completed_submitters, :account_id
end
end
@@ -0,0 +1,9 @@
# frozen_string_literal: true
class AddSendSmsSend2faSmsIndex < ActiveRecord::Migration[8.0]
def change
add_index :submission_events, %i[account_id created_at],
where: "event_type IN ('send_sms', 'send_2fa_sms')",
name: 'index_submissions_events_on_sms_event_types'
end
end
@@ -0,0 +1,10 @@
# frozen_string_literal: true
class AddConfirmableToUsers < ActiveRecord::Migration[8.0]
def change
add_column :users, :confirmation_token, :string
add_column :users, :confirmed_at, :datetime
add_column :users, :confirmation_sent_at, :datetime
add_column :users, :unconfirmed_email, :string
end
end
+13 -2
View File
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.0].define(version: 2025_09_22_053744) do
ActiveRecord::Schema[8.0].define(version: 2025_11_25_194305) do
# These are extensions that must be enabled in order to support this database
enable_extension "btree_gin"
enable_extension "plpgsql"
@@ -118,7 +118,10 @@ ActiveRecord::Schema[8.0].define(version: 2025_09_22_053744) do
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "verification_method"
t.index ["account_id"], name: "index_completed_submitters_on_account_id"
t.boolean "is_first"
t.index ["account_id", "completed_at"], name: "index_completed_submitters_account_id_completed_at_is_first", where: "(is_first = true)"
t.index ["account_id", "completed_at"], name: "index_completed_submitters_on_account_id_and_completed_at"
t.index ["submission_id"], name: "index_completed_submitters_on_submission_id", unique: true, where: "(is_first = true)"
t.index ["submitter_id"], name: "index_completed_submitters_on_submitter_id", unique: true
end
@@ -293,6 +296,9 @@ ActiveRecord::Schema[8.0].define(version: 2025_09_22_053744) do
t.datetime "event_timestamp", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.bigint "account_id"
t.index ["account_id", "created_at"], name: "index_submissions_events_on_sms_event_types", where: "((event_type)::text = ANY ((ARRAY['send_sms'::character varying, 'send_2fa_sms'::character varying])::text[]))"
t.index ["account_id"], name: "index_submission_events_on_account_id"
t.index ["created_at"], name: "index_submission_events_on_created_at"
t.index ["submission_id"], name: "index_submission_events_on_submission_id"
t.index ["submitter_id"], name: "index_submission_events_on_submitter_id"
@@ -445,6 +451,10 @@ ActiveRecord::Schema[8.0].define(version: 2025_09_22_053744) do
t.string "otp_secret"
t.integer "consumed_timestep"
t.boolean "otp_required_for_login", default: false, null: false
t.string "confirmation_token"
t.datetime "confirmed_at"
t.datetime "confirmation_sent_at"
t.string "unconfirmed_email"
t.index ["account_id"], name: "index_users_on_account_id"
t.index ["email"], name: "index_users_on_email", unique: true
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
@@ -506,6 +516,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_09_22_053744) do
add_foreign_key "oauth_access_grants", "users", column: "resource_owner_id"
add_foreign_key "oauth_access_tokens", "oauth_applications", column: "application_id"
add_foreign_key "oauth_access_tokens", "users", column: "resource_owner_id"
add_foreign_key "submission_events", "accounts"
add_foreign_key "submission_events", "submissions"
add_foreign_key "submission_events", "submitters"
add_foreign_key "submissions", "templates"
+37
View File
@@ -0,0 +1,37 @@
# frozen_string_literal: true
module DetectBrowserDevice
module_function
MOBILE_USER_AGENT_REGEXP = /
iPhone |
iPod |
Android.*Mobile|
Opera\ Mini |
Opera\ Mobi |
webOS |
IEMobile |
Windows\ Phone |
BlackBerry |
BB10 |
Mobile
/ix
TABLET_USER_AGENT_REGEXP = /
iPad |
Android(?!.*Mobile)|
Tablet |
Kindle |
PlayBook |
Silk
/ix
def call(user_agent)
return if user_agent.blank?
return 'mobile' if MOBILE_USER_AGENT_REGEXP.match?(user_agent)
return 'tablet' if TABLET_USER_AGENT_REGEXP.match?(user_agent)
'desktop'
end
end
+60 -21
View File
@@ -39,8 +39,25 @@ class Pdfium
FPDF_RENDER_FORCEHALFTONE = 0x400
FPDF_PRINTING = 0x800
TextNode = Struct.new(:content, :x, :y, :w, :h, keyword_init: true)
LineNode = Struct.new(:x, :y, :w, :h, :tilt, keyword_init: true)
TextNode = Struct.new(:content, :x, :y, :w, :h) do
def endx
@endx ||= x + w
end
def endy
@endy ||= y + h
end
end
LineNode = Struct.new(:x, :y, :w, :h, :tilt) do
def endy
@endy ||= y + h
end
def endx
@endx ||= x + w
end
end
# rubocop:disable Naming/ClassAndModuleCamelCase
class FPDF_LIBRARY_CONFIG < FFI::Struct
@@ -433,31 +450,47 @@ class Pdfium
return @text_nodes if char_count.zero?
char_count.times do |i|
unicode = Pdfium.FPDFText_GetUnicode(text_page, i)
left_ptr = FFI::MemoryPointer.new(:double)
right_ptr = FFI::MemoryPointer.new(:double)
bottom_ptr = FFI::MemoryPointer.new(:double)
top_ptr = FFI::MemoryPointer.new(:double)
origin_x_ptr = FFI::MemoryPointer.new(:double)
origin_y_ptr = FFI::MemoryPointer.new(:double)
char = [unicode].pack('U*')
i = 0
left_ptr = FFI::MemoryPointer.new(:double)
right_ptr = FFI::MemoryPointer.new(:double)
bottom_ptr = FFI::MemoryPointer.new(:double)
top_ptr = FFI::MemoryPointer.new(:double)
loop do
break unless i < char_count
result = Pdfium.FPDFText_GetCharBox(text_page, i, left_ptr, right_ptr, bottom_ptr, top_ptr)
box_index = i
codepoint = Pdfium.FPDFText_GetUnicode(text_page, i)
if codepoint.between?(0xD800, 0xDBFF) && (i + 1 < char_count)
codepoint2 = Pdfium.FPDFText_GetUnicode(text_page, i + 1)
if codepoint2.between?(0xDC00, 0xDFFF)
codepoint = 0x10000 + ((codepoint - 0xD800) << 10) + (codepoint2 - 0xDC00)
i += 1
end
end
char = codepoint.chr(Encoding::UTF_8)
result = Pdfium.FPDFText_GetCharBox(text_page, box_index, left_ptr, right_ptr, bottom_ptr, top_ptr)
next if result.zero?
left = left_ptr.read_double
right = right_ptr.read_double
origin_x_ptr = FFI::MemoryPointer.new(:double)
origin_y_ptr = FFI::MemoryPointer.new(:double)
Pdfium.FPDFText_GetCharOrigin(text_page, i, origin_x_ptr, origin_y_ptr)
Pdfium.FPDFText_GetCharOrigin(text_page, box_index, origin_x_ptr, origin_y_ptr)
origin_y = origin_y_ptr.read_double
origin_x = origin_x_ptr.read_double
font_size = Pdfium.FPDFText_GetFontSize(text_page, i)
font_size = Pdfium.FPDFText_GetFontSize(text_page, box_index)
font_size = 8 if font_size == 1
abs_x = left
@@ -465,15 +498,21 @@ class Pdfium
abs_width = right - left
abs_height = font_size
x = abs_x / width
x = origin_x / width
y = abs_y / height
node_width = abs_width / width
node_width = (abs_width + ((abs_x - origin_x).abs * 2)) / width
node_height = abs_height / height
@text_nodes << TextNode.new(content: char, x: x, y: y, w: node_width, h: node_height)
@text_nodes << TextNode.new(char, x, y, node_width, node_height)
ensure
i += 1
end
@text_nodes = @text_nodes.sort { |a, b| a.y == b.y ? a.x <=> b.x : a.y <=> b.y }
y_threshold = 4.0 / width
@text_nodes = @text_nodes.sort do |a, b|
(a.endy - b.endy).abs < y_threshold ? a.x <=> b.x : a.endy <=> b.endy
end
ensure
Pdfium.FPDFText_ClosePage(text_page) if text_page && !text_page.null?
end
@@ -539,10 +578,10 @@ class Pdfium
norm_w = w / width
norm_h = h / height
@line_nodes << LineNode.new(x: norm_x, y: norm_y, w: norm_w, h: norm_h, tilt: tilt)
@line_nodes << LineNode.new(norm_x, norm_y, norm_w, norm_h, tilt)
end
@line_nodes = @line_nodes.sort { |a, b| a.y == b.y ? a.x <=> b.x : a.y <=> b.y }
@line_nodes = @line_nodes.sort { |a, b| a.endy == b.endy ? a.x <=> b.x : a.endy <=> b.endy }
end
def close
+10
View File
@@ -20,4 +20,14 @@ module SubmissionEvents
**data
}.compact_blank)
end
def populate_account_id
Account.find_each do |account|
ids = account.submissions.pluck(:id)
ids.each_slice(10_000).each do |batch|
SubmissionEvent.where(submission_id: batch).update_all(account_id: account.id)
end
end
end
end
+4 -2
View File
@@ -23,6 +23,8 @@ module Submissions
RTL_REGEXP = TextUtils::RTL_REGEXP
MAX_IMAGE_HEIGHT = 100
CHECKSUM_LIMIT = 30
US_TIMEZONES = TimeUtils::US_TIMEZONES
module_function
@@ -216,7 +218,7 @@ module Submissions
composer.document.layout.formatted_text_box(
[
{ text: "#{I18n.t('original_sha256')}:\n", font: [FONT_NAME, { variant: :bold }] },
original_documents.map { |d| d.metadata['sha256'] || d.checksum }.join("\n"),
original_documents.map { |d| d.metadata['sha256'] || d.checksum }.first(CHECKSUM_LIMIT).join("\n"),
"\n",
{ text: "#{I18n.t('result_sha256')}:\n", font: [FONT_NAME, { variant: :bold }] },
document.metadata['sha256'] || document.checksum,
@@ -248,7 +250,7 @@ module Submissions
submission.submission_events.find { |e| e.submitter_id == submitter.id && e.click_email? }
verify_email_event =
submission.submission_events.find { |e| e.submitter_id == submitter.id && e.phone_verified? }
submission.submission_events.find { |e| e.submitter_id == submitter.id && e.email_verified? }
is_phone_verified =
submission.template_fields.any? do |e|
+14 -4
View File
@@ -719,20 +719,30 @@ module Submissions
begin
pdf.sign(io, write_options: { validate: false }, **sign_params)
rescue HexaPDF::MalformedPDFError, NoMethodError => e
rescue HexaPDF::Error, NoMethodError => e
Rollbar.error(e) if defined?(Rollbar)
pdf.sign(io, write_options: { validate: false, incremental: false }, **sign_params)
begin
pdf.sign(io, write_options: { validate: false, incremental: false }, **sign_params)
rescue HexaPDF::Error
pdf.validate(auto_correct: true)
pdf.sign(io, write_options: { validate: false, incremental: false }, **sign_params)
end
end
maybe_enable_ltv(io, sign_params)
else
begin
pdf.write(io, incremental: true, validate: false)
rescue HexaPDF::MalformedPDFError, NoMethodError => e
rescue HexaPDF::Error, NoMethodError => e
Rollbar.error(e) if defined?(Rollbar)
pdf.write(io, incremental: false, validate: false)
begin
pdf.write(io, incremental: false, validate: false)
rescue HexaPDF::Error
pdf.validate(auto_correct: true)
pdf.write(io, incremental: false, validate: false)
end
end
end
+3
View File
@@ -3,6 +3,7 @@
module Submissions
class TimestampHandler
HASH_ALGORITHM = 'SHA256'
TIMEOUT = 10
TimestampError = Class.new(StandardError)
@@ -32,6 +33,8 @@ module Submissions
uri = Addressable::URI.parse(tsa_url)
conn = Faraday.new(uri.origin) do |c|
c.options.read_timeout = TIMEOUT
c.options.open_timeout = TIMEOUT
c.basic_auth(uri.user, uri.password) if uri.password.present?
end
+16
View File
@@ -251,4 +251,20 @@ module Submitters
true
end
def populate_completed_is_first
Account.find_each do |account|
submissions_index = {}
CompletedSubmitter.where(account_id: account.id).order(:account_id, :completed_at).each do |cs|
submissions_index[cs.submission_id] ||= cs.submitter_id
cs.update_columns(is_first: submissions_index[cs.submission_id] == cs.submitter_id)
rescue ActiveRecord::RecordNotUnique
CompletedSubmitter.where(submission_id: cs.submission_id).update_all(is_first: false)
cs.update_columns(is_first: submissions_index[cs.submission_id] == cs.submitter_id)
end
end
end
end
+2
View File
@@ -20,6 +20,8 @@ module Submitters
def call(text, font: nil)
font = FONT_ALIASES[font] || font
text = ERB::Util.html_escape(text)
text_image = Vips::Image.text(text, font:, fontfile: FONTS[font],
width: WIDTH, height: HEIGHT, wrap: :none)
+132 -115
View File
@@ -4,7 +4,16 @@ module Templates
module DetectFields
module_function
TextFieldBox = Struct.new(:x, :y, :w, :h, keyword_init: true)
TextFieldBox = Struct.new(:x, :y, :w, :h, keyword_init: true) do
def endy
@endy ||= y + h
end
def endx
@endx ||= x + w
end
end
PageNode = Struct.new(:prev, :next, :elem, :page, :attachment_uuid, keyword_init: true)
DATE_REGEXP = /
@@ -12,8 +21,7 @@ module Templates
date
| signed\sat
| datum
)
\s*[:-]?\s*\z
)[:_\s-]*\z
/ix
NUMBER_REGEXP = /
@@ -31,8 +39,7 @@ module Templates
| menge
| anzahl
| stückzahl
)
\s*[:-]?\s*\z
)[:_\s-]*\z
/ix
SIGNATURE_REGEXP = /
@@ -45,10 +52,12 @@ module Templates
| unterschrift
| unterschreiben
| unterzeichnen
)
\s*[:-]?\s*\z
)[:_\s-]*\z
/ix
LINEBREAK = ["\n", "\r"].freeze
CHECKBOXES = ['☐', '□'].freeze
# rubocop:disable Metrics, Style
def call(io, attachment: nil, confidence: 0.3, temperature: 1, inference: Templates::ImageToFields,
nms: 0.1, split_page: false, aspect_ratio: true, padding: 20, regexp_type: true, &)
@@ -71,11 +80,13 @@ module Templates
fields = inference.call(image, confidence:, nms:, split_page:,
temperature:, aspect_ratio:, padding:)
fields = sort_fields(fields, y_threshold: 10.0 / image.height)
fields = fields.map do |f|
{
uuid: SecureRandom.uuid,
type: f.type,
required: f.type != 'checkbox',
required: f.type == 'signature',
preferences: {},
areas: [{
x: f.x,
@@ -113,6 +124,8 @@ module Templates
text_fields = extract_text_fields_from_page(page)
line_fields = extract_line_fields_from_page(page)
fields = sort_fields(fields, y_threshold: 10.0 / image.height)
fields = increase_confidence_for_overlapping_fields(fields, text_fields)
fields = increase_confidence_for_overlapping_fields(fields, line_fields)
@@ -128,7 +141,7 @@ module Templates
{
uuid: SecureRandom.uuid,
type:,
required: type != 'checkbox',
required: type == 'signature',
preferences: {},
areas: [{
x: field.x, y: field.y,
@@ -153,6 +166,12 @@ module Templates
doc.close
end
def sort_fields(fields, y_threshold: 0.01)
fields.sort do |a, b|
(a.endy - b.endy).abs < y_threshold ? a.x <=> b.x : a.endy <=> b.endy
end
end
def print_debug(head_node)
current_node = head_node
index = 0
@@ -189,121 +208,120 @@ module Templates
def build_page_nodes(page, fields, tail_node, attachment_uuid: nil)
field_nodes = []
current_text = ''.b
y_threshold = 4.0 / page.height
x_threshold = 30.0 / page.width
text_nodes = page.text_nodes
text_idx = 0
field_idx = 0
current_field = fields.shift
while text_idx < text_nodes.length || field_idx < fields.length
text_node = text_nodes[text_idx]
field = fields[field_idx]
index = 0
process_text_node = false
process_field_node = false
prev_node = nil
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
loop do
node = text_nodes[index]
if vertical_distance < y_threshold
is_underscore = text_node.content == '_'
is_left_of_field = text_node.x < field.x
break unless node
if is_underscore && is_left_of_field
text_x_end = text_node.x + text_node.w
if node.content.in?(LINEBREAK)
next_node = text_nodes[index]
distance = field.x - text_x_end
proximity_threshold = text_node.w * 3.0
if next_node && (next_node.endy - node.endy) < y_threshold
index += 1
if distance < proximity_threshold
process_field_node = true
else
process_text_node = true
end
next
end
end
elsif is_left_of_field
process_text_node = true
else
process_field_node = true
loop do
break unless current_field
if ((current_field.endy - node.endy).abs < y_threshold &&
(current_field.x <= node.x || node.content.in?(LINEBREAK))) ||
current_field.endy < node.y
if tail_node.elem.is_a?(Templates::ImageToFields::Field)
divider =
if (tail_node.elem.endy - current_field.endy).abs > y_threshold
"\n".b
elsif tail_node.elem.endx - current_field.x > x_threshold
"\t".b
else
' '.b
end
text_node = PageNode.new(prev: tail_node, elem: divider, page: page.page_index, attachment_uuid:)
tail_node.next = text_node
tail_node = text_node
elsif prev_node && (prev_node.endy - current_field.endy).abs > y_threshold
text_node = PageNode.new(prev: tail_node, elem: "\n".b, page: page.page_index, attachment_uuid:)
tail_node.next = text_node
tail_node = text_node
end
elsif text_node.y < field.y
process_text_node = true
field_node = PageNode.new(prev: tail_node, elem: current_field, page: page.page_index, attachment_uuid:)
tail_node.next = field_node
tail_node = field_node
field_nodes << tail_node
current_field = fields.shift
else
process_field_node = true
break
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
if tail_node.elem.is_a?(Templates::ImageToFields::Field)
prev_field = tail_node.elem
text_node = PageNode.new(prev: tail_node, elem: ''.b, page: page.page_index, attachment_uuid:)
tail_node.next = text_node
tail_node = text_node
if (node.endy - prev_field.endy).abs > y_threshold
tail_node.elem << "\n"
elsif (node.x - prev_field.endx) > x_threshold
tail_node.elem << "\t"
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
elsif prev_node
if (node.endy - prev_node.endy) > y_threshold && LINEBREAK.exclude?(prev_node.content)
tail_node.elem << "\n"
elsif (node.x - prev_node.endx) > x_threshold && !tail_node.elem.ends_with?("\t")
tail_node.elem << "\t"
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
if node.content != '_' || !tail_node.elem.ends_with?('___')
tail_node.elem << node.content unless CHECKBOXES.include?(node.content)
end
prev_node = node
index += 1
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
loop do
break unless current_field
field_node = PageNode.new(prev: tail_node, elem: current_field, page: page.page_index, attachment_uuid:)
tail_node.next = field_node
tail_node = field_node
field_nodes << tail_node
current_field = fields.shift
end
if tail_node.elem.is_a?(Templates::ImageToFields::Field)
text_node = PageNode.new(prev: tail_node, elem: "\n".b, page: page.page_index, attachment_uuid:)
tail_node.next = text_node
tail_node = text_node
else
tail_node.elem << "\n"
end
[field_nodes, tail_node]
@@ -399,8 +417,8 @@ module Templates
x1 = node.x
y1 = node.y
x2 = node.x + node.w
y2 = node.y + node.h
x2 = node.endx
y2 = node.endy
underscore_count = 1
@@ -417,8 +435,9 @@ module Templates
break if distance > 0.02 || height_diff > node.h * 0.5
underscore_count += 1
next_x2 = next_node.x + next_node.w
next_y2 = next_node.y + next_node.h
next_x2 = next_node.endx
next_y2 = next_node.endy
x2 = next_x2
y2 = [y2, next_y2].max
@@ -438,8 +457,8 @@ module Templates
def calculate_iou(box1, box2)
x1 = [box1.x, box2.x].max
y1 = [box1.y, box2.y].max
x2 = [box1.x + box1.w, box2.x + box2.w].min
y2 = [box1.y + box1.h, box2.y + box2.h].min
x2 = [box1.endx, box2.endx].min
y2 = [box1.endy, box2.endy].min
intersection_width = [0, x2 - x1].max
intersection_height = [0, y2 - y1].max
@@ -455,8 +474,7 @@ module Templates
end
def boxes_overlap?(box1, box2)
!(box1.x + box1.w < box2.x || box2.x + box2.w < box1.x ||
box1.y + box1.h < box2.y || box2.y + box2.h < box1.y)
!(box1.endx < box2.x || box2.endx < box1.x || box1.endy < box2.y || box2.endy < box1.y)
end
def increase_confidence_for_overlapping_fields(image_fields, text_fields, by: 1.0)
@@ -465,14 +483,13 @@ module Templates
image_fields.map do |image_field|
next if image_field.type != 'text'
field_bottom = image_field.y + image_field.h
text_fields.each do |text_field|
break if text_field.y > field_bottom
break if text_field.y > image_field.endy
next if text_field.y + text_field.h < image_field.y
next if text_field.endy < image_field.y
next unless boxes_overlap?(image_field, text_field) && calculate_iou(image_field, text_field) > 0.5
next unless boxes_overlap?(image_field, text_field)
next if calculate_iou(image_field, text_field) < 0.4
break image_field.confidence += by
end
+23 -32
View File
@@ -4,7 +4,15 @@ module Templates
module ImageToFields
module_function
Field = Struct.new(:type, :x, :y, :w, :h, :confidence, keyword_init: true)
Field = Struct.new(:type, :x, :y, :w, :h, :confidence, keyword_init: true) do
def endy
@endy ||= y + h
end
def endx
@endx ||= x + w
end
end
MODEL_PATH = Rails.root.join('tmp/model.onnx')
@@ -37,10 +45,10 @@ module Templates
transform_info[:trim_offset_x] = base_offset_x
transform_info[:trim_offset_y] = base_offset_y + r[:offset_y]
outputs = model.predict({ 'input' => input_tensor })
outputs = model.predict({ 'input' => input_tensor }, output_type: :numo)
boxes = Numo::SFloat.cast(outputs['dets'])[0, true, true]
logits = Numo::SFloat.cast(outputs['labels'])[0, true, true]
boxes = outputs['dets'][0, true, true]
logits = outputs['labels'][0, true, true]
postprocess_outputs(boxes, logits, transform_info, acc, confidence:, temperature:, resolution:)
end
@@ -50,19 +58,17 @@ module Templates
transform_info[:trim_offset_x] = base_offset_x
transform_info[:trim_offset_y] = base_offset_y
outputs = model.predict({ 'input' => input_tensor })
outputs = model.predict({ 'input' => input_tensor }, output_type: :numo)
boxes = Numo::SFloat.cast(outputs['dets'])[0, true, true]
logits = Numo::SFloat.cast(outputs['labels'])[0, true, true]
boxes = outputs['dets'][0, true, true]
logits = outputs['labels'][0, true, true]
detections = postprocess_outputs(boxes, logits, transform_info, confidence:, temperature:, resolution:)
end
detections = apply_nms(detections, nms)
fields = build_fields_from_detections(detections, image)
sort_fields(fields, y_threshold: 10.0 / image.height)
build_fields_from_detections(detections, image)
end
def build_split_image_regions(image)
@@ -77,7 +83,7 @@ module Templates
end
def build_fields_from_detections(detections, image)
Array.new(detections[:xyxy].shape[0]) do |i|
detections[:xyxy].shape[0].times.filter_map do |i|
x1 = detections[:xyxy][i, 0]
y1 = detections[:xyxy][i, 1]
x2 = detections[:xyxy][i, 2]
@@ -92,6 +98,12 @@ module Templates
x1_norm = x2 / image.width.to_f
y1_norm = y2 / image.height.to_f
x1_norm = 1 if x1_norm > 1
y1_norm = 1 if y1_norm > 1
next if x0_norm < 0 || x0_norm > 1
next if y0_norm < 0 || y0_norm > 1
type_name = ID_TO_CLASS[class_id]
Field.new(
@@ -292,27 +304,6 @@ module Templates
end
end
def sort_fields(fields, y_threshold: 0.01)
sorted_fields = fields.sort { |a, b| a.y == b.y ? a.x <=> b.x : a.y <=> b.y }
lines = []
current_line = []
sorted_fields.each do |field|
if current_line.blank? || (field.y - current_line.first.y).abs < y_threshold
current_line << field
else
lines << current_line.sort_by(&:x)
current_line = [field]
end
end
lines << current_line.sort_by(&:x) if current_line.present?
lines.flatten
end
def apply_nms(detections, threshold = 0.5)
return detections if detections[:xyxy].shape[0].zero?
+23 -3
View File
@@ -24,7 +24,7 @@ module TextUtils
text.to_s.gsub(TRANSLITERATION_REGEXP) { |e| TRANSLITERATIONS[e] }
end
def mask_value(text, unmask_size = 0)
def mask_value(text, unmask_size = 0, mask_symbol = MASK_SYMBOL)
if unmask_size.is_a?(Numeric) && !unmask_size.zero? && unmask_size.abs < text.length
if unmask_size.negative?
[
@@ -34,14 +34,34 @@ module TextUtils
elsif unmask_size.positive?
[
text.first(unmask_size),
text.last(text.length - unmask_size).gsub(MASK_REGEXP, MASK_SYMBOL)
text.last(text.length - unmask_size).gsub(MASK_REGEXP, mask_symbol)
].join
end
else
text.to_s.gsub(MASK_REGEXP, MASK_SYMBOL)
text.to_s.gsub(MASK_REGEXP, mask_symbol)
end
end
def mask_email(email, unmask_size = 2)
return email if email.exclude?('@')
local, domain = email.split('@', 2)
return email if local.blank? || domain.blank?
masked_local = mask_value(local, unmask_size, '*')
domain_parts = domain.split('.')
if domain_parts.blank? || domain_parts[0].blank?
masked_domain = mask_value(domain, 1, '*')
else
domain_parts[0] = mask_value(domain_parts[0], 1, '*')
masked_domain = domain_parts.join('.')
end
"#{masked_local}@#{masked_domain}"
end
def maybe_rtl_reverse(text)
if text.match?(RTL_REGEXP)
TwitterCldr::Shared::Bidi
+1 -1
View File
@@ -22,7 +22,7 @@ module TimeUtils
DEFAULT_DATE_FORMAT_US = 'MM/DD/YYYY'
DEFAULT_DATE_FORMAT = 'DD/MM/YYYY'
US_TIMEZONES = %w[EST CST MST PST HST AKDT].freeze
US_TIMEZONES = %w[EST EDT CST CDT MST MDT PST PDT HST HDT AKST AKDT].freeze
module_function
+1
View File
@@ -20,6 +20,7 @@
"babel-plugin-dynamic-import-node": "^2.3.3",
"babel-plugin-macros": "^3.1.0",
"canvas-confetti": "^1.6.0",
"chart.js": "^4.5.1",
"codemirror": "^6.0.2",
"compression-webpack-plugin": "10.0.0",
"css-loader": "^6.7.3",
@@ -0,0 +1,9 @@
# frozen_string_literal: true
class DeviseMailerPreview < ActionMailer::Preview
def reset_password_instructions
user = User.first
token = user.send(:set_reset_password_token)
Devise::Mailer.reset_password_instructions(user, token)
end
end
@@ -22,4 +22,8 @@ class SubmitterMailerPreview < ActionMailer::Preview
SubmitterMailer.documents_copy_email(submitter)
end
def otp_verification_email
SubmitterMailer.otp_verification_email(Submitter.last)
end
end
+72
View File
@@ -1130,4 +1130,76 @@ RSpec.describe 'Signing Form' do
end.to change(ProcessSubmitterCompletionJob.jobs, :size).by(1)
end
end
context 'when the 2FA email verification is enabled', sidekiq: :inline do
let(:template) { create(:template, account:, author:, only_field_types: %w[text]) }
let(:submission) { create(:submission, template:) }
let(:submitter) do
create(:submitter, submission:, uuid: template.submitters.first['uuid'], account:)
end
before do
template.update(preferences: { require_email_2fa: true })
create(:encrypted_config, key: EncryptedConfig::ESIGN_CERTS_KEY,
value: GenerateCertificate.call.transform_values(&:to_pem))
end
it 'completes the form if the one-time password is filled correctly' do
visit submit_form_path(slug: submitter.slug)
click_button 'Send verification code'
email = ActionMailer::Base.deliveries.last
one_time_code = email.body.encoded[%r{<b>(\d{6})</b>}, 1]
fill_in 'one_time_code', with: one_time_code
click_button 'Submit'
fill_in 'First Name', with: 'Mary'
click_button 'Complete'
expect(page).to have_content('Form has been completed!')
submitter.reload
expect(submitter.completed_at).to be_present
expect(field_value(submitter, 'First Name')).to eq 'Mary'
end
it "doesn't complete the form if the one-time code is invalid" do
visit submit_form_path(slug: submitter.slug)
click_button 'Send verification code'
fill_in 'one_time_code', with: '123456'
click_button 'Submit'
expect(page).to have_content 'Invalid code'
end
it 'completes the form after resending the one time code' do
visit submit_form_path(slug: submitter.slug)
click_button 'Send verification code'
find('#resend_label').click
email = ActionMailer::Base.deliveries.last
one_time_code = email.body.encoded[%r{<b>(\d{6})</b>}, 1]
fill_in 'one_time_code', with: one_time_code
click_button 'Submit'
fill_in 'First Name', with: 'Mary'
click_button 'Complete'
expect(page).to have_content('Form has been completed!')
submitter.reload
expect(submitter.completed_at).to be_present
expect(field_value(submitter, 'First Name')).to eq 'Mary'
end
end
end
+12
View File
@@ -1355,6 +1355,11 @@
"@jridgewell/resolve-uri" "^3.1.0"
"@jridgewell/sourcemap-codec" "^1.4.14"
"@kurkle/color@^0.3.0":
version "0.3.4"
resolved "https://registry.yarnpkg.com/@kurkle/color/-/color-0.3.4.tgz#4d4ff677e1609214fc71c580125ddddd86abcabf"
integrity sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==
"@leichtgewicht/ip-codec@^2.0.1":
version "2.0.4"
resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b"
@@ -2341,6 +2346,13 @@ chalk@^4.0, chalk@^4.0.0, chalk@^4.1.0:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
chart.js@^4.5.1:
version "4.5.1"
resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-4.5.1.tgz#19dd1a9a386a3f6397691672231cb5fc9c052c35"
integrity sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==
dependencies:
"@kurkle/color" "^0.3.0"
"chokidar@>=3.0.0 <4.0.0", chokidar@^3.5.3:
version "3.5.3"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"