Merge from docusealco/wip

pull/502/head 2.0.5
Alex Turchyn 4 months ago committed by GitHub
commit 4f94858901
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -66,7 +66,7 @@ RSpec/ExampleLength:
Max: 50 Max: 50
RSpec/MultipleMemoizedHelpers: RSpec/MultipleMemoizedHelpers:
Max: 9 Max: 15
Metrics/BlockNesting: Metrics/BlockNesting:
Max: 5 Max: 5

@ -10,6 +10,12 @@ module Api
def create def create
submitter = Submitter.find_by!(slug: params[:submitter_slug]) submitter = Submitter.find_by!(slug: params[:submitter_slug])
if params[:type].in?(%w[initials signature]) && ImageUtils.blank?(Vips::Image.new_from_file(params[:file].path))
Rollbar.error("Empty signature: #{submitter.id}") if defined?(Rollbar)
return render json: { error: "#{params[:type]} is empty" }, status: :unprocessable_entity
end
attachment = Submitters.create_attachment!(submitter, params) attachment = Submitters.create_attachment!(submitter, params)
if params[:remember_signature] == 'true' && submitter.email.present? if params[:remember_signature] == 'true' && submitter.email.present?

@ -187,11 +187,12 @@ module Api
def submissions_params def submissions_params
permitted_attrs = [ permitted_attrs = [
:send_email, :send_sms, :bcc_completed, :completed_redirect_url, :reply_to, :go_to_last, :send_email, :send_sms, :bcc_completed, :completed_redirect_url, :reply_to, :go_to_last,
:expire_at, :name, :require_phone_2fa, :expire_at, :name,
{ {
message: %i[subject body], message: %i[subject body],
submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role, submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role,
:completed, :phone, :application_key, :external_id, :reply_to, :go_to_last, :completed, :phone, :application_key, :external_id, :reply_to, :go_to_last,
:require_phone_2fa,
{ metadata: {}, values: {}, roles: [], readonly_fields: [], message: %i[subject body], { metadata: {}, values: {}, roles: [], readonly_fields: [], message: %i[subject body],
fields: [:name, :uuid, :default_value, :value, :title, :description, fields: [:name, :uuid, :default_value, :value, :title, :description,
:readonly, :required, :validation_pattern, :invalid_message, :readonly, :required, :validation_pattern, :invalid_message,

@ -82,7 +82,7 @@ module Api
submitter_params.permit( submitter_params.permit(
:send_email, :send_sms, :reply_to, :completed_redirect_url, :uuid, :name, :email, :role, :send_email, :send_sms, :reply_to, :completed_redirect_url, :uuid, :name, :email, :role,
:completed, :phone, :application_key, :external_id, :go_to_last, :completed, :phone, :application_key, :external_id, :go_to_last, :require_phone_2fa,
{ metadata: {}, values: {}, readonly_fields: [], message: %i[subject body], { metadata: {}, values: {}, readonly_fields: [], message: %i[subject body],
fields: [[:name, :uuid, :default_value, :value, :required, fields: [[:name, :uuid, :default_value, :value, :required,
:readonly, :validation_pattern, :invalid_message, :readonly, :validation_pattern, :invalid_message,
@ -193,6 +193,10 @@ module Api
submitter.preferences['send_sms'] = submitter_preferences['send_sms'] if submitter_preferences.key?('send_sms') submitter.preferences['send_sms'] = submitter_preferences['send_sms'] if submitter_preferences.key?('send_sms')
submitter.preferences['reply_to'] = submitter_preferences['reply_to'] if submitter_preferences.key?('reply_to') submitter.preferences['reply_to'] = submitter_preferences['reply_to'] if submitter_preferences.key?('reply_to')
if submitter_preferences.key?('require_phone_2fa')
submitter.preferences['require_phone_2fa'] = submitter_preferences['require_phone_2fa']
end
if submitter_preferences.key?('go_to_last') if submitter_preferences.key?('go_to_last')
submitter.preferences['go_to_last'] = submitter_preferences['go_to_last'] submitter.preferences['go_to_last'] = submitter_preferences['go_to_last']
end end

@ -33,12 +33,12 @@ class StartFormController < ApplicationController
@submitter = find_or_initialize_submitter(@template, submitter_params) @submitter = find_or_initialize_submitter(@template, submitter_params)
if @submitter.completed_at? if @submitter.completed_at?
redirect_to start_form_completed_path(@template.slug, email: submitter_params[:email]) redirect_to start_form_completed_path(@template.slug, submitter_params.compact_blank)
else else
if filter_undefined_submitters(@template).size > 1 && @submitter.new_record? if filter_undefined_submitters(@template).size > 1 && @submitter.new_record?
@error_message = multiple_submitters_error_message @error_message = multiple_submitters_error_message
return render :show return render :show, status: :unprocessable_entity
end end
if (is_new_record = @submitter.new_record?) if (is_new_record = @submitter.new_record?)
@ -49,7 +49,7 @@ class StartFormController < ApplicationController
@submitter.assign_attributes(ip: request.remote_ip, ua: request.user_agent) @submitter.assign_attributes(ip: request.remote_ip, ua: request.user_agent)
end end
if @submitter.save if @submitter.errors.blank? && @submitter.save
if is_new_record if is_new_record
enqueue_submission_create_webhooks(@submitter) enqueue_submission_create_webhooks(@submitter)
@ -63,7 +63,7 @@ class StartFormController < ApplicationController
redirect_to submit_form_path(@submitter.slug) redirect_to submit_form_path(@submitter.slug)
else else
render :show render :show, status: :unprocessable_entity
end end
end end
end end
@ -71,9 +71,20 @@ class StartFormController < ApplicationController
def completed def completed
return redirect_to start_form_path(@template.slug) if !@template.shared_link? || @template.archived_at? return redirect_to start_form_path(@template.slug) if !@template.shared_link? || @template.archived_at?
submitter_params = params.permit(:name, :email, :phone).tap do |attrs|
attrs[:email] = Submissions.normalize_email(attrs[:email])
end
required_fields = @template.preferences.fetch('link_form_fields', ['email'])
required_params = required_fields.index_with { |key| submitter_params[key] }
raise ActionController::RoutingError, I18n.t('not_found') if required_params.any? { |_, v| v.blank? } ||
required_params.except('name').compact_blank.blank?
@submitter = Submitter.where(submission: @template.submissions) @submitter = Submitter.where(submission: @template.submissions)
.where.not(completed_at: nil) .where.not(completed_at: nil)
.find_by!(email: params[:email]) .find_by!(required_params)
end end
private private
@ -104,6 +115,15 @@ class StartFormController < ApplicationController
end end
def find_or_initialize_submitter(template, submitter_params) def find_or_initialize_submitter(template, submitter_params)
required_fields = template.preferences.fetch('link_form_fields', ['email'])
required_params = required_fields.index_with { |key| submitter_params[key] }
find_params = required_params.except('name')
submitter = Submitter.new if find_params.compact_blank.blank?
submitter ||=
Submitter Submitter
.where(submission: template.submissions.where(expire_at: Time.current..) .where(submission: template.submissions.where(expire_at: Time.current..)
.or(template.submissions.where(expire_at: nil)).where(archived_at: nil)) .or(template.submissions.where(expire_at: nil)).where(archived_at: nil))
@ -112,7 +132,17 @@ class StartFormController < ApplicationController
.where(external_id: nil) .where(external_id: nil)
.where(ip: [nil, request.remote_ip]) .where(ip: [nil, request.remote_ip])
.then { |rel| params[:resubmit].present? || params[:selfsign].present? ? rel.where(completed_at: nil) : rel } .then { |rel| params[:resubmit].present? || params[:selfsign].present? ? rel.where(completed_at: nil) : rel }
.find_or_initialize_by(email: submitter_params[:email], **submitter_params.compact_blank) .find_or_initialize_by(find_params)
submitter.name = required_params['name'] if submitter.new_record?
unless @resubmit_submitter
required_params.each do |key, value|
submitter.errors.add(key.to_sym, :blank) if value.blank?
end
end
submitter
end end
def assign_submission_attributes(submitter, template) def assign_submission_attributes(submitter, template)
@ -125,6 +155,8 @@ class StartFormController < ApplicationController
metadata: @resubmit_submitter&.metadata.presence || {} metadata: @resubmit_submitter&.metadata.presence || {}
) )
submitter.assign_attributes(@resubmit_submitter.slice(:name, :email, :phone)) if @resubmit_submitter
if submitter.values.present? if submitter.values.present?
@resubmit_submitter.attachments.each do |attachment| @resubmit_submitter.attachments.each do |attachment|
submitter.attachments << attachment.dup if submitter.values.value?(attachment.uuid) submitter.attachments << attachment.dup if submitter.values.value?(attachment.uuid)

@ -12,7 +12,7 @@ class SubmitFormDrawSignatureController < ApplicationController
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at? return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?
if @submitter.submission.template.archived_at? || @submitter.submission.archived_at? if @submitter.submission.template&.archived_at? || @submitter.submission.archived_at?
return redirect_to submit_form_path(@submitter.slug) return redirect_to submit_form_path(@submitter.slug)
end end

@ -45,7 +45,7 @@ class SubmitFormInviteController < ApplicationController
!submitter.completed_at? && !submitter.completed_at? &&
!submitter.submission.archived_at? && !submitter.submission.archived_at? &&
!submitter.submission.expired? && !submitter.submission.expired? &&
!submitter.submission.template.archived_at? !submitter.submission.template&.archived_at?
end end
def filter_invite_submitters(submitter, key = 'invite_by_uuid') def filter_invite_submitters(submitter, key = 'invite_by_uuid')

@ -8,7 +8,7 @@ class SubmitFormValuesController < ApplicationController
submitter = Submitter.find_by!(slug: params[:submit_form_slug]) submitter = Submitter.find_by!(slug: params[:submit_form_slug])
return render json: {} if submitter.completed_at? || submitter.declined_at? return render json: {} if submitter.completed_at? || submitter.declined_at?
return render json: {} if submitter.submission.template.archived_at? || return render json: {} if submitter.submission.template&.archived_at? ||
submitter.submission.archived_at? || submitter.submission.archived_at? ||
submitter.submission.expired? submitter.submission.expired?

@ -55,10 +55,8 @@ class TemplatesDashboardController < ApplicationController
rel = Template.where( rel = Template.where(
Template.arel_table[:id].in( Template.arel_table[:id].in(
Arel::Nodes::Union.new( rel.where(folder_id: current_account.default_template_folder.id).select(:id).arel
rel.where(folder_id: current_account.default_template_folder.id).select(:id).arel, .union(shared_template_ids.arel)
shared_template_ids.arel
)
) )
) )
else else

@ -31,7 +31,7 @@ class TemplatesPreferencesController < ApplicationController
completed_notification_email_subject completed_notification_email_body completed_notification_email_subject completed_notification_email_body
completed_notification_email_enabled completed_notification_email_attach_audit] + completed_notification_email_enabled completed_notification_email_attach_audit] +
[completed_message: %i[title body], [completed_message: %i[title body],
submitters: [%i[uuid request_email_subject request_email_body]]] submitters: [%i[uuid request_email_subject request_email_body]], link_form_fields: []]
).tap do |attrs| ).tap do |attrs|
attrs[:preferences].delete(:submitters) if params[:request_email_per_submitter] != '1' attrs[:preferences].delete(:submitters) if params[:request_email_per_submitter] != '1'

@ -10,8 +10,12 @@ class TemplatesShareLinkController < ApplicationController
@template.update!(template_params) @template.update!(template_params)
if params[:redir].present?
redirect_to params[:redir]
else
head :ok head :ok
end end
end
private private

@ -35,6 +35,7 @@ import SetDateButton from './elements/set_date_button'
import IndeterminateCheckbox from './elements/indeterminate_checkbox' import IndeterminateCheckbox from './elements/indeterminate_checkbox'
import AppTour from './elements/app_tour' import AppTour from './elements/app_tour'
import DashboardDropzone from './elements/dashboard_dropzone' import DashboardDropzone from './elements/dashboard_dropzone'
import RequiredCheckboxGroup from './elements/required_checkbox_group'
import * as TurboInstantClick from './lib/turbo_instant_click' import * as TurboInstantClick from './lib/turbo_instant_click'
@ -105,6 +106,7 @@ safeRegisterElement('indeterminate-checkbox', IndeterminateCheckbox)
safeRegisterElement('app-tour', AppTour) safeRegisterElement('app-tour', AppTour)
safeRegisterElement('dashboard-dropzone', DashboardDropzone) safeRegisterElement('dashboard-dropzone', DashboardDropzone)
safeRegisterElement('check-on-click', CheckOnClick) safeRegisterElement('check-on-click', CheckOnClick)
safeRegisterElement('required-checkbox-group', RequiredCheckboxGroup)
safeRegisterElement('template-builder', class extends HTMLElement { safeRegisterElement('template-builder', class extends HTMLElement {
connectedCallback () { connectedCallback () {

@ -1,7 +1,7 @@
export default class extends HTMLElement { export default class extends HTMLElement {
connectedCallback () { connectedCallback () {
this.addEventListener('click', () => { this.addEventListener('click', () => {
if (!this.element.checked) { if (this.element && !this.element.disabled && !this.element.checked) {
this.element.checked = true this.element.checked = true
this.element.dispatchEvent(new Event('change', { bubbles: true })) this.element.dispatchEvent(new Event('change', { bubbles: true }))
} }

@ -0,0 +1,17 @@
export default class extends HTMLElement {
connectedCallback () {
this.querySelectorAll('input[type="checkbox"]').forEach(checkbox => {
checkbox.addEventListener('change', this.handleChange)
})
}
handleChange = () => {
if (this.checkedCount !== 0) {
this.closest('form')?.requestSubmit()
}
}
get checkedCount () {
return this.querySelectorAll('input[type="checkbox"]:checked').length
}
}

@ -203,7 +203,7 @@ export default {
emits: ['attached', 'update:model-value', 'start', 'minimize', 'focus'], emits: ['attached', 'update:model-value', 'start', 'minimize', 'focus'],
data () { data () {
return { return {
isInitialsStarted: !!this.previousValue, isInitialsStarted: false,
isUsePreviousValue: true, isUsePreviousValue: true,
isDrawInitials: false, isDrawInitials: false,
uploadImageInputKey: Math.random().toString() uploadImageInputKey: Math.random().toString()
@ -218,6 +218,9 @@ export default {
} }
} }
}, },
created () {
this.isInitialsStarted = !!this.computedPreviousValue
},
async mounted () { async mounted () {
this.$nextTick(() => { this.$nextTick(() => {
if (this.$refs.canvas) { if (this.$refs.canvas) {
@ -362,11 +365,20 @@ export default {
formData.append('file', file) formData.append('file', file)
formData.append('submitter_slug', this.submitterSlug) formData.append('submitter_slug', this.submitterSlug)
formData.append('name', 'attachments') formData.append('name', 'attachments')
formData.append('type', 'initials')
return fetch(this.baseUrl + '/api/attachments', { return fetch(this.baseUrl + '/api/attachments', {
method: 'POST', method: 'POST',
body: formData body: formData
}).then((resp) => resp.json()).then((attachment) => { }).then(async (resp) => {
if (resp.status === 422 || resp.status === 500) {
const data = await resp.json()
return Promise.reject(new Error(data.error))
}
const attachment = await resp.json()
this.$emit('attached', attachment) this.$emit('attached', attachment)
this.$emit('update:model-value', attachment.uuid) this.$emit('update:model-value', attachment.uuid)

@ -201,7 +201,7 @@
</div> </div>
</div> </div>
<input <input
v-if="isTextSignature" v-if="isTextSignature && !modelValue && !computedPreviousValue"
id="signature_text_input" id="signature_text_input"
ref="textInput" ref="textInput"
class="base-input !text-2xl w-full mt-6" class="base-input !text-2xl w-full mt-6"
@ -391,7 +391,7 @@ export default {
emits: ['attached', 'update:model-value', 'start', 'minimize', 'update:reason'], emits: ['attached', 'update:model-value', 'start', 'minimize', 'update:reason'],
data () { data () {
return { return {
isSignatureStarted: !!this.previousValue, isSignatureStarted: false,
isShowQr: false, isShowQr: false,
isOtherReason: false, isOtherReason: false,
isUsePreviousValue: true, isUsePreviousValue: true,
@ -422,6 +422,8 @@ export default {
} }
}, },
created () { created () {
this.isSignatureStarted = !!this.computedPreviousValue
if (this.requireSigningReason) { if (this.requireSigningReason) {
this.field.preferences ||= {} this.field.preferences ||= {}
this.field.preferences.reason_field_uuid ||= v4() this.field.preferences.reason_field_uuid ||= v4()
@ -716,11 +718,20 @@ export default {
formData.append('submitter_slug', this.submitterSlug) formData.append('submitter_slug', this.submitterSlug)
formData.append('name', 'attachments') formData.append('name', 'attachments')
formData.append('remember_signature', this.rememberSignature) formData.append('remember_signature', this.rememberSignature)
formData.append('type', 'signature')
return fetch(this.baseUrl + '/api/attachments', { return fetch(this.baseUrl + '/api/attachments', {
method: 'POST', method: 'POST',
body: formData body: formData
}).then((resp) => resp.json()).then((attachment) => { }).then(async (resp) => {
if (resp.status === 422 || resp.status === 500) {
const data = await resp.json()
return Promise.reject(new Error(data.error))
}
const attachment = await resp.json()
this.$emit('attached', attachment) this.$emit('attached', attachment)
this.$emit('update:model-value', attachment.uuid) this.$emit('update:model-value', attachment.uuid)

@ -406,7 +406,7 @@ export default {
this.mappings.every((m) => m.column_index !== index) this.mappings.every((m) => m.column_index !== index)
}) })
if (columnIndex !== -1) { if (columnIndex !== -1 && this.rows.some((row) => row[columnIndex])) {
this.mappings.push({ uuid: v4(), field_name: field.name, column_index: columnIndex, submitter_uuid: submitter.uuid }) this.mappings.push({ uuid: v4(), field_name: field.name, column_index: columnIndex, submitter_uuid: submitter.uuid })
} }
}) })

@ -40,7 +40,7 @@ class Submission < ApplicationRecord
belongs_to :account belongs_to :account
belongs_to :created_by_user, class_name: 'User', optional: true belongs_to :created_by_user, class_name: 'User', optional: true
has_one :search_entry, as: :record, inverse_of: :record, dependent: :destroy has_one :search_entry, as: :record, inverse_of: :record, dependent: :destroy if SearchEntry.table_exists?
has_many :submitters, dependent: :destroy has_many :submitters, dependent: :destroy
has_many :submission_events, dependent: :destroy has_many :submission_events, dependent: :destroy

@ -43,7 +43,7 @@ class Submitter < ApplicationRecord
belongs_to :submission belongs_to :submission
belongs_to :account belongs_to :account
has_one :template, through: :submission has_one :template, through: :submission
has_one :search_entry, as: :record, inverse_of: :record, dependent: :destroy has_one :search_entry, as: :record, inverse_of: :record, dependent: :destroy if SearchEntry.table_exists?
attribute :values, :string, default: -> { {} } attribute :values, :string, default: -> { {} }
attribute :preferences, :string, default: -> { {} } attribute :preferences, :string, default: -> { {} }

@ -44,7 +44,7 @@ class Template < ApplicationRecord
belongs_to :account belongs_to :account
belongs_to :folder, class_name: 'TemplateFolder' belongs_to :folder, class_name: 'TemplateFolder'
has_one :search_entry, as: :record, inverse_of: :record, dependent: :destroy has_one :search_entry, as: :record, inverse_of: :record, dependent: :destroy if SearchEntry.table_exists?
before_validation :maybe_set_default_folder, on: :create before_validation :maybe_set_default_folder, on: :create

@ -14,7 +14,10 @@
<div class="form-control"> <div class="form-control">
<%= ff.label :timezone, t('time_zone'), class: 'label' %> <%= ff.label :timezone, t('time_zone'), class: 'label' %>
<%= ff.select :timezone, nil, {}, class: 'base-select' do %> <%= ff.select :timezone, nil, {}, class: 'base-select' do %>
<%= time_zone_options_for_select(current_account.timezone) %> <% tzinfo = TZInfo::Timezone.get(ActiveSupport::TimeZone::MAPPING[current_account.timezone] || current_account.timezone) %>
<% items = ActiveSupport::TimeZone.all.map { |z| [z.to_s, z.name] } %>
<% items.unshift([tzinfo.to_s, current_account.timezone]) unless ActiveSupport::TimeZone.all.find { |e| e.tzinfo == tzinfo } %>
<%= options_for_select(items, current_account.timezone) %>
<% end %> <% end %>
</div> </div>
<div class="form-control"> <div class="form-control">

@ -28,7 +28,7 @@
<% end %> <% end %>
<% if Templates.filter_undefined_submitters(@template.submitters).size == 1 && %w[api embed].exclude?(@submitter.submission.source) && @submitter.account.account_configs.find_or_initialize_by(key: AccountConfig::ALLOW_TO_RESUBMIT).value != false && @template.shared_link? %> <% if Templates.filter_undefined_submitters(@template.submitters).size == 1 && %w[api embed].exclude?(@submitter.submission.source) && @submitter.account.account_configs.find_or_initialize_by(key: AccountConfig::ALLOW_TO_RESUBMIT).value != false && @template.shared_link? %>
<toggle-submit class="block"> <toggle-submit class="block">
<%= button_to button_title(title: t('resubmit'), disabled_with: t('resubmit'), icon: svg_icon('reload', class: 'w-6 h-6')), start_form_path(@template.slug), params: { submitter: { email: params[:email] }, resubmit: true }, method: :put, class: 'white-button w-full' %> <%= button_to button_title(title: t('resubmit'), disabled_with: t('resubmit'), icon: svg_icon('reload', class: 'w-6 h-6')), start_form_path(@template.slug), params: { submitter: params.permit(:name, :email, :phone).compact_blank, resubmit: true }, method: :put, class: 'white-button w-full' %>
</toggle-submit> </toggle-submit>
<% end %> <% end %>
</div> </div>

@ -1,8 +1,8 @@
<% content_for(:html_title, "#{@template.name} | DocuSeal") %> <% content_for(:html_title, "#{@template.name} | DocuSeal") %>
<% content_for(:html_description, t('share_link_is_currently_disabled')) %> <% I18n.with_locale(@template.account.locale) do %>
<div class="max-w-md mx-auto px-2 mt-12 mb-4"> <% content_for(:html_description, t('share_link_is_currently_disabled')) %>
<div class="space-y-6 mx-auto"> <% end %>
<div class="space-y-6"> <div class="max-w-md space-y-6 mx-auto px-2 mt-12 mb-4">
<div class="text-center w-full space-y-6"> <div class="text-center w-full space-y-6">
<%= render 'banner' %> <%= render 'banner' %>
<p class="text-xl font-semibold text-center"> <p class="text-xl font-semibold text-center">
@ -22,7 +22,10 @@
</div> </div>
</div> </div>
</div> </div>
</div> <% if can?(:update, @template) %>
</div> <toggle-submit class="block">
<%= button_to button_title(title: t('enable_shared_link'), icon: svg_icon('lock_open', class: 'w-6 h-6')), template_share_link_path(@template), params: { template: { shared_link: true }, redir: start_form_path(slug: @template.slug) }, method: :post, class: 'white-button w-full' %>
</toggle-submit>
<% end %>
</div> </div>
<%= render 'shared/attribution', link_path: '/start', account: @template.account %> <%= render 'shared/attribution', link_path: '/start', account: @template.account %>

@ -1,5 +1,7 @@
<% content_for(:html_title, "#{@template.name} | DocuSeal") %> <% content_for(:html_title, "#{@template.name} | DocuSeal") %>
<% content_for(:html_description, "#{@template.account.name} has invited you to fill and sign documents online effortlessly with a secure, fast, and user-friendly digital document signing solution.") %> <% I18n.with_locale(@template.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: @template.account.name)) %>
<% end %>
<div class="max-w-md mx-auto px-2 mt-12 mb-4"> <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 mx-auto">
<div class="space-y-6"> <div class="space-y-6">
@ -27,13 +29,32 @@
</div> </div>
<% if !@template.archived_at? && !@template.account.archived_at? %> <% if !@template.archived_at? && !@template.account.archived_at? %>
<%= form_for @submitter, url: start_form_path(@template.slug), data: { turbo_frame: :_top }, method: :put, html: { class: 'space-y-4' } do |f| %> <%= form_for @submitter, url: start_form_path(@template.slug), data: { turbo_frame: :_top }, method: :put, html: { class: 'space-y-4' } do |f| %>
<% if @error_message %>
<div class="alert items-start bg-base-100 border border-red-400 text-red-400">
<%= svg_icon('info_circle', class: 'stroke-current shrink-0 h-6 w-6 mt-1') %>
<div class="text-sm"><%= @error_message %></div>
</div>
<% end %>
<% link_form_fields = @template.preferences.fetch('link_form_fields', ['email']) %>
<% multiple_fields = link_form_fields.size > 1 %>
<% if link_form_fields.include?('name') %>
<div dir="auto" class="form-control !mt-0">
<%= f.label :name, t('name'), class: 'label' %>
<%= f.text_field :name, value: current_user&.full_name || params[:name] || @submitter.name, required: true, class: 'base-input', placeholder: t(multiple_fields ? 'provide_your_name' : 'provide_your_name_to_start') %>
</div>
<% end %>
<% if link_form_fields.include?('email') %>
<div dir="auto" class="form-control !mt-0"> <div dir="auto" class="form-control !mt-0">
<%= f.label :email, t('email'), class: 'label' %> <%= f.label :email, t('email'), class: 'label' %>
<%= f.email_field :email, value: current_user&.email || params[:email] || @submitter.email, required: true, class: 'base-input', placeholder: t('provide_your_email_to_start') %> <%= f.email_field :email, value: current_user&.email || params[:email] || @submitter.email, required: true, class: 'base-input', placeholder: t(multiple_fields ? 'provide_your_email' : 'provide_your_email_to_start') %>
<% if @error_message %> </div>
<span class="label-text-alt text-red-400 mt-1"><%= @error_message %></span>
<% end %> <% end %>
<% if link_form_fields.include?('phone') %>
<div dir="auto" class="form-control !mt-0">
<%= f.label :phone, t('phone'), class: 'label' %>
<%= f.telephone_field :phone, value: params[:phone] || @submitter.phone, pattern: '^\+[0-9\s\-]+$', oninvalid: "this.value ? this.setCustomValidity('#{t('use_international_format_1xxx_')}') : ''", oninput: "this.setCustomValidity('')", required: true, class: 'base-input', placeholder: t(multiple_fields ? 'provide_your_phone_in_international_format' : 'provide_your_phone_in_international_format_to_start') %>
</div> </div>
<% end %>
<toggle-submit dir="auto" class="form-control"> <toggle-submit dir="auto" class="form-control">
<%= f.button button_title(title: t('start'), disabled_with: t('starting')), class: 'base-button' %> <%= f.button button_title(title: t('start'), disabled_with: t('starting')), class: 'base-button' %>
</toggle-submit> </toggle-submit>

@ -28,9 +28,9 @@
</div> </div>
<% end %> <% end %>
</div> </div>
<% elsif field['type'].in?(['image', 'initials', 'stamp']) %> <% elsif field['type'].in?(['image', 'initials', 'stamp']) && attachments_index[value].image? %>
<img class="object-contain mx-auto" src="<%= attachments_index[value].url %>" loading="lazy"> <img class="object-contain mx-auto" src="<%= attachments_index[value].url %>" loading="lazy">
<% elsif field['type'].in?(['file', 'payment']) %> <% elsif field['type'].in?(['file', 'payment', 'image']) %>
<autosize-field></autosize-field> <autosize-field></autosize-field>
<div class="px-0.5 flex flex-col justify-center"> <div class="px-0.5 flex flex-col justify-center">
<% Array.wrap(value).each do |val| %> <% Array.wrap(value).each do |val| %>

@ -74,6 +74,7 @@
<% schema = Submissions.filtered_conditions_schema(@submission, values:) %> <% schema = Submissions.filtered_conditions_schema(@submission, values:) %>
<% schema.each do |item| %> <% schema.each do |item| %>
<% document = @submission.schema_documents.find { |a| item['attachment_uuid'] == a.uuid } %> <% document = @submission.schema_documents.find { |a| item['attachment_uuid'] == a.uuid } %>
<% if document.preview_images.first %>
<a href="#<%= "page-#{document.uuid}-0" %>" onclick="[event.preventDefault(), window[event.target.closest('a').href.split('#')[1]].scrollIntoView({ behavior: 'smooth', block: 'start' })]" class="block cursor-pointer"> <a href="#<%= "page-#{document.uuid}-0" %>" onclick="[event.preventDefault(), window[event.target.closest('a').href.split('#')[1]].scrollIntoView({ behavior: 'smooth', block: 'start' })]" class="block cursor-pointer">
<img src="<%= Docuseal::URL_CACHE.fetch([document.id, document.uuid, 0].join(':'), expires_in: 10.minutes) { document.preview_images.first.url } %>" width="<%= document.preview_images.first.metadata['width'] %>" height="<%= document.preview_images.first.metadata['height'] %>" class="rounded border" loading="lazy"> <img src="<%= Docuseal::URL_CACHE.fetch([document.id, document.uuid, 0].join(':'), expires_in: 10.minutes) { document.preview_images.first.url } %>" width="<%= document.preview_images.first.metadata['width'] %>" height="<%= document.preview_images.first.metadata['height'] %>" class="rounded border" loading="lazy">
<div class="pb-2 pt-1.5 text-center" dir="auto"> <div class="pb-2 pt-1.5 text-center" dir="auto">
@ -81,6 +82,7 @@
</div> </div>
</a> </a>
<% end %> <% end %>
<% end %>
</div> </div>
<div id="document_view" class="w-full overflow-y-auto overflow-x-hidden mt-0.5 pt-0.5"> <div id="document_view" class="w-full overflow-y-auto overflow-x-hidden mt-0.5 pt-0.5">
<div class="pr-3.5 pl-0.5"> <div class="pr-3.5 pl-0.5">
@ -92,7 +94,7 @@
<% document = @submission.schema_documents.find { |e| e.uuid == item['attachment_uuid'] } %> <% document = @submission.schema_documents.find { |e| e.uuid == item['attachment_uuid'] } %>
<% document_annots_index = document.metadata.dig('pdf', 'annotations')&.group_by { |e| e['page'] } || {} %> <% document_annots_index = document.metadata.dig('pdf', 'annotations')&.group_by { |e| e['page'] } || {} %>
<% preview_images_index = document.preview_images.loaded? ? document.preview_images.index_by { |e| e.filename.base.to_i } : {} %> <% preview_images_index = document.preview_images.loaded? ? document.preview_images.index_by { |e| e.filename.base.to_i } : {} %>
<% lazyload_metadata = document.preview_images.first.metadata %> <% lazyload_metadata = document.preview_images.first&.metadata || {} %>
<% (document.metadata.dig('pdf', 'number_of_pages') || (document.preview_images.loaded? ? preview_images_index.size : document.preview_images.size)).times do |index| %> <% (document.metadata.dig('pdf', 'number_of_pages') || (document.preview_images.loaded? ? preview_images_index.size : document.preview_images.size)).times do |index| %>
<% page = preview_images_index[index] || page_blob_struct.new(metadata: lazyload_metadata, url: preview_document_page_path(document.signed_uuid, "#{index}.jpg")) %> <% page = preview_images_index[index] || page_blob_struct.new(metadata: lazyload_metadata, url: preview_document_page_path(document.signed_uuid, "#{index}.jpg")) %>
<div id="<%= "page-#{document.uuid}-#{index}" %>" class="relative"> <div id="<%= "page-#{document.uuid}-#{index}" %>" class="relative">
@ -242,9 +244,9 @@
<div class="w-full bg-base-300 py-1"> <div class="w-full bg-base-300 py-1">
<img class="object-contain mx-auto" style="max-height: <%= field['type'] == 'signature' ? 100 : 50 %>px" height="<%= attachments_index[value].metadata['height'] %>" width="<%= attachments_index[value].metadata['width'] %>" src="<%= attachments_index[value].url %>" loading="lazy"> <img class="object-contain mx-auto" style="max-height: <%= field['type'] == 'signature' ? 100 : 50 %>px" height="<%= attachments_index[value].metadata['height'] %>" width="<%= attachments_index[value].metadata['width'] %>" src="<%= attachments_index[value].url %>" loading="lazy">
</div> </div>
<% elsif field['type'].in?(['image', 'stamp']) %> <% elsif field['type'].in?(['image', 'stamp']) && attachments_index[value].image? %>
<img class="object-contain mx-auto max-h-28" style="max-height: 200px" height="<%= attachments_index[value].metadata['height'] %>" width="<%= attachments_index[value].metadata['width'] %>" src="<%= attachments_index[value].url %>" loading="lazy"> <img class="object-contain mx-auto max-h-28" style="max-height: 200px" height="<%= attachments_index[value].metadata['height'] %>" width="<%= attachments_index[value].metadata['width'] %>" src="<%= attachments_index[value].url %>" loading="lazy">
<% elsif field['type'] == 'file' || field['type'] == 'payment' %> <% elsif field['type'].in?(['file', 'payment', 'image']) %>
<div class="flex flex-col justify-center"> <div class="flex flex-col justify-center">
<% Array.wrap(value).each do |val| %> <% Array.wrap(value).each do |val| %>
<a target="_blank" class="flex items-center space-x-1" href="<%= attachments_index[val].url %>"> <a target="_blank" class="flex items-center space-x-1" href="<%= attachments_index[val].url %>">

@ -1,5 +1,7 @@
<% content_for(:html_title, "#{@submitter.submission.name || @submitter.submission.template.name} | DocuSeal") %> <% content_for(:html_title, "#{@submitter.submission.name || @submitter.submission.template.name} | DocuSeal") %>
<% content_for(:html_description, "#{@submitter.account.name} has invited you to fill and sign documents online effortlessly with a secure, fast, and user-friendly digital document signing solution.") %> <% 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 %>
<% fields_index = Templates.build_field_areas_index(@submitter.submission.template_fields || @submitter.submission.template.fields) %> <% fields_index = Templates.build_field_areas_index(@submitter.submission.template_fields || @submitter.submission.template.fields) %>
<% values = @submitter.submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) } %> <% values = @submitter.submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) } %>
<% submitters_index = @submitter.submission.submitters.index_by(&:uuid) %> <% submitters_index = @submitter.submission.submitters.index_by(&:uuid) %>

@ -0,0 +1,87 @@
<% close_on_submit = local_assigns.fetch(:close_on_submit, true) %>
<%= form_for template, url: template_recipients_path(template), method: :post, html: { autocomplete: 'off', class: 'mt-1', id: :submitters_form }, data: { close_on_submit: } do |f| %>
<% unless close_on_submit %>
<toggle-on-submit data-element-id="form_saved_alert"></toggle-on-submit>
<% end %>
<div class="space-y-3 divide-y">
<% template.submitters.each_with_index do |submitter, index| %>
<div class="<%= 'pt-3' if index.positive? %>">
<%= f.fields_for :submitters, item = Struct.new(:name, :uuid, :is_requester, :email, :invite_by_uuid, :optional_invite_by_uuid, :linked_to_uuid, :option).new(*submitter.values_at('name', 'uuid', 'is_requester', 'email', 'invite_by_uuid', 'optional_invite_by_uuid', 'linked_to_uuid')), index: do |ff| %>
<% item.option = item.is_requester.present? ? 'is_requester' : (item.email.present? ? 'email' : (item.linked_to_uuid.present? ? "linked_to_#{item.linked_to_uuid}" : (item.invite_by_uuid.present? ? "invite_by_#{item.invite_by_uuid}" : (item.optional_invite_by_uuid.present? ? "optional_invite_by_#{item.optional_invite_by_uuid}" : '')))) %>
<%= ff.hidden_field :uuid %>
<div class="form-control">
<%= ff.text_field :name, class: 'w-full outline-none border-transparent focus:border-transparent focus:ring-0 bg-base-100 px-1 peer mb-2', autocomplete: 'off', placeholder: "#{index + 1}#{(index + 1).ordinal} Party", required: true %>
<% if template.submitters.size == 2 %>
<%= tag.input name: ff.field_name(:email), value: ff.object.email, type: :email, class: 'base-input', multiple: true, autocomplete: 'off', placeholder: t('default_email'), disabled: ff.object.is_requester || ff.object.invite_by_uuid.present? || ff.object.optional_invite_by_uuid.present?, id: field_uuid = SecureRandom.uuid %>
<% else %>
<toggle-attribute data-target-id="<%= email_field_uuid = SecureRandom.uuid %>" data-class-name="hidden" data-value="email">
<%= ff.select :option, [[t('not_specified'), 'not_set'], (local_assigns[:with_submission_requester] != false ? [t('submission_requester'), 'is_requester'] : nil), [t('specified_email'), 'email'], *(template.submitters - [submitter]).flat_map { |e| [[t('invite_by_name', name: e['name']), "invite_by_#{e['uuid']}"], [t('invite_by_name', name: e['name']) + " (#{t(:optional).capitalize})", "optional_invite_by_#{e['uuid']}"]] }, *(template.submitters - [submitter]).map { |e| [t('same_as_name', name: e['name']), "linked_to_#{e['uuid']}"] }].compact, {}, class: 'base-select mb-3' %>
</toggle-attribute>
<%= tag.input name: ff.field_name(:email), type: :email, value: ff.object.email, multiple: true, class: "base-input #{'hidden' if item.option != 'email'}", autocomplete: 'off', placeholder: t('default_email'), id: email_field_uuid %>
<% end %>
</div>
<% if template.submitters.size == 2 %>
<checkbox-group class="mt-3 flex items-center space-x-4">
<% if local_assigns[:with_submission_requester] != false %>
<label class="flex items-center space-x-2 cursor-pointer">
<toggle-attribute data-target-id="<%= field_uuid %>" class="flex" data-attribute="disabled">
<%= ff.check_box :is_requester, class: 'base-checkbox' %>
</toggle-attribute>
<span class="select-none">
<%= t('submission_requester') %>
</span>
</label>
<% end %>
<% if index == 1 %>
<label class="flex items-center space-x-2 cursor-pointer">
<toggle-attribute data-target-id="<%= field_uuid %>" class="flex" data-attribute="disabled">
<indeterminate-checkbox data-indeterminate="<%= ff.object.optional_invite_by_uuid.present? %>" data-show-indeterminate-id="invite_optional" data-name="<%= ff.field_name(:invite_by_uuid) %>" data-indeterminate-name="<%= ff.field_name(:optional_invite_by_uuid) %>" class="flex">
<%= ff.check_box ff.object.optional_invite_by_uuid.present? ? :optional_invite_by_uuid : :invite_by_uuid, { class: 'base-checkbox' }, template.submitters.first['uuid'], '' %>
</indeterminate-checkbox>
</toggle-attribute>
<span class="select-none">
<%= t('invite_by_name', name: template.submitters.first['name']) %> <span id="invite_optional" class="<%= 'hidden' if ff.object.optional_invite_by_uuid.blank? %>">(<%= t(:optional).capitalize %>)</span>
</span>
</label>
<% end %>
</checkbox-group>
<% end %>
<% end %>
</div>
<% end %>
</div>
<% end %>
<% if local_assigns[:with_toggles] != false %>
<% unless current_account.account_configs.exists?(key: AccountConfig::ENFORCE_SIGNING_ORDER_KEY, value: true) %>
<%= form_for template, url: template_preferences_path(template), method: :post, html: { autocomplete: 'off', class: 'mt-2' }, data: { close_on_submit: false } do |f| %>
<div class="flex items-center pt-4 mt-4 justify-between border-t w-full">
<span>
<%= t('enforce_recipients_order') %>
</span>
<%= f.fields_for :preferences, Struct.new(:submitters_order).new(template.preferences['submitters_order']) do |ff| %>
<%= ff.check_box :submitters_order, { class: 'toggle', onchange: 'this.form.requestSubmit()' }, 'preserved', '' %>
<% end %>
</div>
<% end %>
<% end %>
<% if can?(:manage, :personalization_advanced) %>
<%= form_for template, url: template_preferences_path(template), method: :post, html: { autocomplete: 'off', class: 'mt-2' }, data: { close_on_submit: false } do |f| %>
<div class="flex items-center mt-4 justify-between w-full">
<span>
<%= t('ensure_unique_recipients') %>
</span>
<%= f.fields_for :preferences, Struct.new(:validate_unique_submitters).new(template.preferences['validate_unique_submitters']) do |ff| %>
<%= ff.check_box :validate_unique_submitters, { class: 'toggle', onchange: 'this.form.requestSubmit()' }, 'true', '' %>
<% end %>
</div>
<% end %>
<% end %>
<% end %>
<div class="form-control mt-5">
<%= button_tag button_title(title: t('save'), disabled_with: t('updating')), class: 'base-button', form: :submitters_form %>
<% unless close_on_submit %>
<div class="flex justify-center">
<span id="form_saved_alert" class="text-sm invisible font-normal mt-0.5"><%= t('changes_have_been_saved') %></span>
</div>
<% end %>
</div>

@ -287,91 +287,8 @@
<%= render 'templates_code_modal/preferences', class: 'pt-2' %> <%= render 'templates_code_modal/preferences', class: 'pt-2' %>
</div> </div>
<% if show_recipients %> <% if show_recipients %>
<div id="recipients" class="hidden mt-2 mb-4 px-5"> <div id="recipients" class="hidden mt-2 mb-4 px-5 pt-1 pb-2">
<%= form_for @template, url: template_recipients_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-1', id: :submitters_form } do |f| %> <%= render 'recipients', template: @template %>
<div class="space-y-3 divide-y">
<% @template.submitters.each_with_index do |submitter, index| %>
<div class="pt-3">
<%= f.fields_for :submitters, item = Struct.new(:name, :uuid, :is_requester, :email, :invite_by_uuid, :optional_invite_by_uuid, :linked_to_uuid, :option).new(*submitter.values_at('name', 'uuid', 'is_requester', 'email', 'invite_by_uuid', 'optional_invite_by_uuid', 'linked_to_uuid')), index: do |ff| %>
<% item.option = item.is_requester.present? ? 'is_requester' : (item.email.present? ? 'email' : (item.linked_to_uuid.present? ? "linked_to_#{item.linked_to_uuid}" : (item.invite_by_uuid.present? ? "invite_by_#{item.invite_by_uuid}" : (item.optional_invite_by_uuid.present? ? "optional_invite_by_#{item.optional_invite_by_uuid}" : '')))) %>
<%= ff.hidden_field :uuid %>
<div class="form-control">
<%= ff.text_field :name, class: 'w-full outline-none border-transparent focus:border-transparent focus:ring-0 bg-base-100 px-1 peer mb-2', autocomplete: 'off', placeholder: "#{index + 1}#{(index + 1).ordinal} Party", required: true %>
<% if @template.submitters.size == 2 %>
<%= tag.input name: ff.field_name(:email), value: ff.object.email, type: :email, class: 'base-input', multiple: true, autocomplete: 'off', placeholder: t('default_email'), disabled: ff.object.is_requester || ff.object.invite_by_uuid.present? || ff.object.optional_invite_by_uuid.present?, id: field_uuid = SecureRandom.uuid %>
<% else %>
<toggle-attribute data-target-id="<%= email_field_uuid = SecureRandom.uuid %>" data-class-name="hidden" data-value="email">
<%= ff.select :option, [[t('not_specified'), 'not_set'], [t('submission_requester'), 'is_requester'], [t('specified_email'), 'email'], *(@template.submitters - [submitter]).flat_map { |e| [[t('invite_by_name', name: e['name']), "invite_by_#{e['uuid']}"], [t('invite_by_name', name: e['name']) + " (#{t(:optional).capitalize})", "optional_invite_by_#{e['uuid']}"]] }, *(@template.submitters - [submitter]).map { |e| [t('same_as_name', name: e['name']), "linked_to_#{e['uuid']}"] }], {}, class: 'base-select mb-3' %>
</toggle-attribute>
<%= tag.input name: ff.field_name(:email), type: :email, value: ff.object.email, multiple: true, class: "base-input #{'hidden' if item.option != 'email'}", autocomplete: 'off', placeholder: t('default_email'), id: email_field_uuid %>
<% end %>
</div>
<% if @template.submitters.size == 2 %>
<checkbox-group class="mt-3 flex items-center space-x-4">
<label class="flex items-center space-x-2 cursor-pointer">
<toggle-attribute data-target-id="<%= field_uuid %>" class="flex" data-attribute="disabled">
<%= ff.check_box :is_requester, class: 'base-checkbox' %>
</toggle-attribute>
<span class="select-none">
<%= t('submission_requester') %>
</span>
</label>
<% if index == 1 %>
<label class="flex items-center space-x-2 cursor-pointer">
<toggle-attribute data-target-id="<%= field_uuid %>" class="flex" data-attribute="disabled">
<indeterminate-checkbox data-indeterminate="<%= ff.object.optional_invite_by_uuid.present? %>" data-show-indeterminate-id="invite_optional" data-name="<%= ff.field_name(:invite_by_uuid) %>" data-indeterminate-name="<%= ff.field_name(:optional_invite_by_uuid) %>" class="flex">
<%= ff.check_box ff.object.optional_invite_by_uuid.present? ? :optional_invite_by_uuid : :invite_by_uuid, { class: 'base-checkbox' }, @template.submitters.first['uuid'], '' %>
</indeterminate-checkbox>
</toggle-attribute>
<span class="select-none">
<%= t('invite_by_name', name: @template.submitters.first['name']) %> <span id="invite_optional" class="<%= 'hidden' if ff.object.optional_invite_by_uuid.blank? %>">(<%= t(:optional).capitalize %>)</span>
</span>
</label>
<% end %>
</checkbox-group>
<% end %>
<% end %>
</div>
<% end %>
</div>
<% end %>
<% unless current_account.account_configs.exists?(key: AccountConfig::ENFORCE_SIGNING_ORDER_KEY, value: true) %>
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-2' }, data: { close_on_submit: false } do |f| %>
<div class="flex items-center pt-4 mt-4 justify-between border-t w-full">
<span>
<%= t('enforce_recipients_order') %>
</span>
<%= f.fields_for :preferences, Struct.new(:submitters_order).new(@template.preferences['submitters_order']) do |ff| %>
<%= ff.check_box :submitters_order, { class: 'toggle', onchange: 'this.form.requestSubmit()' }, 'preserved', '' %>
<% end %>
</div>
<% end %>
<% end %>
<% if can?(:manage, :personalization_advanced) %>
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-2' }, data: { close_on_submit: false } do |f| %>
<div class="flex items-center mt-4 justify-between w-full">
<span>
<%= t('ensure_unique_recipients') %>
</span>
<%= f.fields_for :preferences, Struct.new(:validate_unique_submitters).new(@template.preferences['validate_unique_submitters']) do |ff| %>
<%= ff.check_box :validate_unique_submitters, { class: 'toggle', onchange: 'this.form.requestSubmit()' }, 'true', '' %>
<% end %>
</div>
<% end %>
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-2' }, data: { close_on_submit: false } do |f| %>
<div class="flex items-center mt-4 justify-between w-full">
<span>
<%= t('require_all_recipients') %>
</span>
<%= f.fields_for :preferences, Struct.new(:require_all_submitters).new(@template.preferences['require_all_submitters']) do |ff| %>
<%= ff.check_box :require_all_submitters, { class: 'toggle', onchange: 'this.form.requestSubmit()' }, 'true', '' %>
<% end %>
</div>
<% end %>
<% end %>
<div class="form-control mt-5 pb-2">
<%= button_tag button_title(title: t('save'), disabled_with: t('updating')), class: 'base-button', form: :submitters_form %>
</div>
</div> </div>
<% end %> <% end %>
<% if show_api %> <% if show_api %>

@ -1,10 +1,10 @@
<%= render 'shared/turbo_modal_large', title: t('share_link') do %> <%= render 'shared/turbo_modal_large', title: t('share_link') do %>
<div class="mt-2 mb-4 px-5"> <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| %> <%= 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| %>
<div class="flex items-center justify-between gap-1 px-1"> <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> <span><%= t('enable_shared_link') %></span>
<%= f.check_box :shared_link, { disabled: !can?(:update, @template), class: 'toggle', onchange: 'this.form.requestSubmit()' }, 'true', 'false' %> <%= f.check_box :shared_link, { disabled: !can?(:update, @template), class: 'toggle', onchange: 'this.form.requestSubmit()' }, 'true', 'false' %>
</div> </label>
<div class="flex gap-2 mt-3"> <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> <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"> <check-on-click data-element-id="template_shared_link">
@ -12,5 +12,34 @@
</check-on-click> </check-on-click>
</div> </div>
<% end %> <% end %>
<%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-4 mb-5' }, data: { close_on_submit: false } do |f| %>
<% link_form_fields = @template.preferences.fetch('link_form_fields', ['email']) %>
<label class="pl-1"><%= t('link_form_fields') %></label>
<required-checkbox-group class="flex flex-col md:flex-row items-center gap-2 w-full mt-2">
<% %w[name email phone].each do |field| %>
<%= label_tag "link_form_fields_#{field}", t(field), class: 'relative flex w-full md:w-1/3 items-center h-12 border-base-300 py-3.5 border rounded-xl' do %>
<%= check_box_tag 'template[preferences][link_form_fields][]', field, link_form_fields.include?(field), class: 'absolute !animate-none checkbox left-3', id: "link_form_fields_#{field}" %>
<span class="font-medium w-full text-center"><%= t(field) %></span>
<% end %>
<% end %>
</required-checkbox-group>
<% end %>
<% if Templates.filter_undefined_submitters(@template.submitters).size > 1 %>
<div class="alert items-start bg-base-100 border-base-300 mt-4">
<%= svg_icon('info_circle', class: 'stroke-current shrink-0 h-6 w-6 mt-1') %>
<div><%= t('this_template_has_multiple_parties_which_prevents_the_use_of_a_sharing_link') %></div>
</div>
<% end %>
<% if @template.submitters.to_a.length > 1 %>
<div class="collapse collapse-arrow join-item border border-base-300 mt-4 mb-5">
<input type="checkbox" name="accordion">
<div class="collapse-title text-xl font-medium">
<%= 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 %>
</div> </div>
<% end %> <% end %>

@ -22,6 +22,7 @@ en: &en
hi_there: Hi there hi_there: Hi there
thanks: Thanks thanks: Thanks
private: Private private: Private
default_parties: Default parties
authenticate_embedded_form_preview_with_token: Authenticate embedded form preview with token authenticate_embedded_form_preview_with_token: Authenticate embedded form preview with token
stripe_integration: Stripe Integration stripe_integration: Stripe Integration
require_all_recipients: Require all recipients require_all_recipients: Require all recipients
@ -115,6 +116,11 @@ en: &en
role: Role role: Role
reason: Reason reason: Reason
provide_your_email_to_start: Provide your email to start provide_your_email_to_start: Provide your email to start
provide_your_email: Provide your email
provide_your_name_to_start: Provide your name to start
provide_your_name: Provide your name
provide_your_phone_in_international_format_to_start: Provide your phone in international format to start
provide_your_phone_in_international_format: Provide your phone in international format
start: Start start: Start
enforce_recipients_order: Enforce recipients order enforce_recipients_order: Enforce recipients order
starting: Starting starting: Starting
@ -752,6 +758,11 @@ en: &en
enable_shared_link: Enable shared link enable_shared_link: Enable shared link
share_link_is_currently_disabled: Share link is currently disabled share_link_is_currently_disabled: Share link is currently disabled
select_data_residency: Select data residency select_data_residency: Select data residency
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} has invited you to fill and sign documents online effortlessly with a secure, fast, and user-friendly digital document signing solution.'
review_or_download_completed_documents_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution: Review or download completed documents. Fill and sign documents online effortlessly with a secure, fast, and user-friendly digital document signing solution.
link_form_fields: Link form fields
at_least_one_field_must_be_displayed_in_the_form: At least one field must be displayed in the form.
this_template_has_multiple_parties_which_prevents_the_use_of_a_sharing_link: This template has multiple parties, which prevents the use of a shared link as it's unclear which party is responsible for specific fields. To resolve this, define the default party details.
submission_sources: submission_sources:
api: API api: API
bulk: Bulk Send bulk: Bulk Send
@ -854,6 +865,7 @@ en: &en
range_without_total: "%{from}-%{to} items" range_without_total: "%{from}-%{to} items"
es: &es es: &es
default_parties: Partes predeterminadas
authenticate_embedded_form_preview_with_token: Autenticar vista previa del formulario incrustado con token authenticate_embedded_form_preview_with_token: Autenticar vista previa del formulario incrustado con token
require_all_recipients: Requerir a todos los destinatarios require_all_recipients: Requerir a todos los destinatarios
stripe_integration: Integración con Stripe stripe_integration: Integración con Stripe
@ -951,6 +963,11 @@ es: &es
role: Rol role: Rol
reason: Razón reason: Razón
provide_your_email_to_start: Proporciona tu correo electrónico para comenzar provide_your_email_to_start: Proporciona tu correo electrónico para comenzar
provide_your_email: Proporciona tu correo electrónico
provide_your_name_to_start: Proporciona tu nombre para comenzar
provide_your_name: Proporciona tu nombre
provide_your_phone_in_international_format_to_start: Proporciona tu teléfono en formato internacional para comenzar
provide_your_phone_in_international_format: Proporciona tu teléfono en formato internacional
start: Comenzar start: Comenzar
starting: Comenzando starting: Comenzando
form_has_been_deleted_by_html: 'El formulario ha sido eliminado por <span class="font-semibold">%{name}</span>.' form_has_been_deleted_by_html: 'El formulario ha sido eliminado por <span class="font-semibold">%{name}</span>.'
@ -1587,6 +1604,11 @@ es: &es
enable_shared_link: Habilitar enlace compartido enable_shared_link: Habilitar enlace compartido
share_link_is_currently_disabled: El enlace compartido está deshabilitado actualmente share_link_is_currently_disabled: El enlace compartido está deshabilitado actualmente
select_data_residency: Seleccionar ubicación de datos select_data_residency: Seleccionar ubicación de datos
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} te ha invitado a completar y firmar documentos en línea fácilmente con una solución de firma digital segura, rápida y fácil de usar.'
review_or_download_completed_documents_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution: Revisa o descarga los documentos completados. Completa y firma documentos en línea fácilmente con una solución de firma digital segura, rápida y fácil de usar.
link_form_fields: Vincular campos del formulario
at_least_one_field_must_be_displayed_in_the_form: Al menos un campo debe mostrarse en el formulario.
this_template_has_multiple_parties_which_prevents_the_use_of_a_sharing_link: Esta plantilla tiene varias partes, lo que impide el uso de un enlace compartido porque no está claro qué parte es responsable de campos específicos. Para resolverlo, define los detalles predeterminados de la parte.
submission_sources: submission_sources:
api: API api: API
bulk: Envío masivo bulk: Envío masivo
@ -1689,6 +1711,7 @@ es: &es
range_without_total: "%{from}-%{to} elementos" range_without_total: "%{from}-%{to} elementos"
it: &it it: &it
default_parties: Parti predefiniti
authenticate_embedded_form_preview_with_token: "Autentica l'anteprima del modulo incorporato con il token" authenticate_embedded_form_preview_with_token: "Autentica l'anteprima del modulo incorporato con il token"
require_all_recipients: Richiedi tutti i destinatari require_all_recipients: Richiedi tutti i destinatari
stripe_integration: Integrazione Stripe stripe_integration: Integrazione Stripe
@ -1785,6 +1808,11 @@ it: &it
role: Ruolo role: Ruolo
reason: Ragione reason: Ragione
provide_your_email_to_start: Fornisci la tua email per iniziare provide_your_email_to_start: Fornisci la tua email per iniziare
provide_your_email: Fornisci la tua email
provide_your_name_to_start: Fornisci il tuo nome per iniziare
provide_your_name: Fornisci il tuo nome
provide_your_phone_in_international_format_to_start: Fornisci il tuo telefono in formato internazionale per iniziare
provide_your_phone_in_international_format: Fornisci il tuo telefono in formato internazionale
start: Inizia start: Inizia
starting: Iniziando starting: Iniziando
form_has_been_deleted_by_html: 'Il modulo è stato eliminato da <span class="font-semibold">%{name}</span>.' form_has_been_deleted_by_html: 'Il modulo è stato eliminato da <span class="font-semibold">%{name}</span>.'
@ -2420,6 +2448,11 @@ it: &it
enable_shared_link: Abilita link condiviso enable_shared_link: Abilita link condiviso
share_link_is_currently_disabled: Il link condiviso è attualmente disabilitato share_link_is_currently_disabled: Il link condiviso è attualmente disabilitato
select_data_residency: Seleziona la residenza dei dati select_data_residency: Seleziona la residenza dei dati
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} ti ha invitato a compilare e firmare documenti online con facilità utilizzando una soluzione di firma digitale sicura, veloce e facile da usare.'
review_or_download_completed_documents_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution: Rivedi o scarica i documenti completati. Compila e firma documenti online facilmente con una soluzione di firma digitale sicura, veloce e facile da usare.
link_form_fields: Collega i campi del modulo
at_least_one_field_must_be_displayed_in_the_form: Almeno un campo deve essere visualizzato nel modulo.
this_template_has_multiple_parties_which_prevents_the_use_of_a_sharing_link: Questo modello ha più parti, il che impedisce luso di un link di condivisione perché non è chiaro quale parte sia responsabile di campi specifici. Per risolvere, definisci i dettagli predefiniti della parte.
submission_sources: submission_sources:
api: API api: API
bulk: Invio massivo bulk: Invio massivo
@ -2522,6 +2555,7 @@ it: &it
range_without_total: "%{from}-%{to} elementi" range_without_total: "%{from}-%{to} elementi"
fr: &fr fr: &fr
default_parties: Parties par défaut
authenticate_embedded_form_preview_with_token: Authentifier laperçu du formulaire intégré avec un jeton authenticate_embedded_form_preview_with_token: Authentifier laperçu du formulaire intégré avec un jeton
require_all_recipients: Exiger tous les destinataires require_all_recipients: Exiger tous les destinataires
stripe_integration: Intégration Stripe stripe_integration: Intégration Stripe
@ -2618,7 +2652,12 @@ fr: &fr
digitally_signed_by: Signé numériquement par digitally_signed_by: Signé numériquement par
role: Rôle role: Rôle
reason: Raison reason: Raison
provide_your_email_to_start: Entrez votre adresse email pour commencer provide_your_email_to_start: Fournissez votre e-mail pour commencer
provide_your_email: Fournissez votre e-mail
provide_your_name_to_start: Fournissez votre nom pour commencer
provide_your_name: Fournissez votre nom
provide_your_phone_in_international_format_to_start: Fournissez votre téléphone au format international pour commencer
provide_your_phone_in_international_format: Fournissez votre téléphone au format international
start: Démarrer start: Démarrer
starting: Démarrage starting: Démarrage
form_has_been_deleted_by_html: 'Le formulaire a été supprimé par <span class="font-semibold">%{name}</span>.' form_has_been_deleted_by_html: 'Le formulaire a été supprimé par <span class="font-semibold">%{name}</span>.'
@ -3256,6 +3295,11 @@ fr: &fr
enable_shared_link: Activer le lien de partage enable_shared_link: Activer le lien de partage
share_link_is_currently_disabled: Le lien de partage est actuellement désactivé share_link_is_currently_disabled: Le lien de partage est actuellement désactivé
select_data_residency: Sélectionner la résidence des données select_data_residency: Sélectionner la résidence des données
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} vous a invité à remplir et signer des documents en ligne facilement avec une solution de signature numérique sécurisée, rapide et conviviale.'
review_or_download_completed_documents_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution: Consultez ou téléchargez les documents complétés. Remplissez et signez des documents en ligne facilement avec une solution de signature numérique sécurisée, rapide et conviviale.
link_form_fields: Lier les champs du formulaire
at_least_one_field_must_be_displayed_in_the_form: Au moins un champ doit être affiché dans le formulaire.
this_template_has_multiple_parties_which_prevents_the_use_of_a_sharing_link: Ce modèle contient plusieurs parties, ce qui empêche lutilisation dun lien de partage car il nest pas clair quelle partie est responsable de certains champs. Pour résoudre cela, définissez les détails de la partie par défaut.
submission_sources: submission_sources:
api: API api: API
bulk: Envoi en masse bulk: Envoi en masse
@ -3358,6 +3402,7 @@ fr: &fr
range_without_total: "%{from} à %{to} éléments" range_without_total: "%{from} à %{to} éléments"
pt: &pt pt: &pt
default_parties: Partes padrão
authenticate_embedded_form_preview_with_token: Autenticar visualização incorporada do formulário com token authenticate_embedded_form_preview_with_token: Autenticar visualização incorporada do formulário com token
require_all_recipients: Exigir todos os destinatários require_all_recipients: Exigir todos os destinatários
stripe_integration: Integração com Stripe stripe_integration: Integração com Stripe
@ -3454,7 +3499,12 @@ pt: &pt
digitally_signed_by: Assinado digitalmente por digitally_signed_by: Assinado digitalmente por
role: Função role: Função
reason: Motivo reason: Motivo
provide_your_email_to_start: Forneça o seu email para começar provide_your_email_to_start: Informe seu e-mail para começar
provide_your_email: Informe seu e-mail
provide_your_name_to_start: Informe seu nome para começar
provide_your_name: Informe seu nome
provide_your_phone_in_international_format_to_start: Informe seu telefone no formato internacional para começar
provide_your_phone_in_international_format: Informe seu telefone no formato internacional
start: Iniciar start: Iniciar
starting: Iniciando starting: Iniciando
form_has_been_deleted_by_html: 'O formulário foi eliminado por <span class="font-semibold">%{name}</span>.' form_has_been_deleted_by_html: 'O formulário foi eliminado por <span class="font-semibold">%{name}</span>.'
@ -4091,6 +4141,11 @@ pt: &pt
enable_shared_link: Ativar link compartilhado enable_shared_link: Ativar link compartilhado
share_link_is_currently_disabled: O link compartilhado está desativado no momento share_link_is_currently_disabled: O link compartilhado está desativado no momento
select_data_residency: Selecionar local dos dados select_data_residency: Selecionar local dos dados
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} convidou você para preencher e assinar documentos online com facilidade usando uma solução de assinatura digital segura, rápida e fácil de usar.'
review_or_download_completed_documents_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution: Reveja ou baixe os documentos concluídos. Preencha e assine documentos online com facilidade usando uma solução de assinatura digital segura, rápida e fácil de usar.
link_form_fields: Vincular campos do formulário
at_least_one_field_must_be_displayed_in_the_form: Pelo menos um campo deve ser exibido no formulário.
this_template_has_multiple_parties_which_prevents_the_use_of_a_sharing_link: Este modelo tem várias partes, o que impede o uso de um link de compartilhamento, pois não está claro qual parte é responsável por campos específicos. Para resolver isso, defina os detalhes padrão da parte.
submission_sources: submission_sources:
api: API api: API
bulk: Envio em massa bulk: Envio em massa
@ -4194,6 +4249,7 @@ pt: &pt
range_without_total: "%{from}-%{to} itens" range_without_total: "%{from}-%{to} itens"
de: &de de: &de
default_parties: Standardparteien
authenticate_embedded_form_preview_with_token: Authentifizieren Sie die eingebettete Formularvorschau mit Token authenticate_embedded_form_preview_with_token: Authentifizieren Sie die eingebettete Formularvorschau mit Token
require_all_recipients: Alle Empfänger erforderlich require_all_recipients: Alle Empfänger erforderlich
stripe_integration: Stripe-Integration stripe_integration: Stripe-Integration
@ -4290,7 +4346,12 @@ de: &de
digitally_signed_by: Digital signiert von digitally_signed_by: Digital signiert von
role: Rolle role: Rolle
reason: Grund reason: Grund
provide_your_email_to_start: Gib deine E-Mail-Adresse ein, um zu starten provide_your_email_to_start: Geben Sie Ihre E-Mail-Adresse ein, um zu starten
provide_your_email: Geben Sie Ihre E-Mail-Adresse ein
provide_your_name_to_start: Geben Sie Ihren Namen ein, um zu starten
provide_your_name: Geben Sie Ihren Namen ein
provide_your_phone_in_international_format_to_start: Geben Sie Ihre Telefonnummer im internationalen Format ein, um zu starten
provide_your_phone_in_international_format: Geben Sie Ihre Telefonnummer im internationalen Format ein
start: Starten start: Starten
starting: Starten starting: Starten
form_has_been_deleted_by_html: 'Das Formular wurde von <span class="font-semibold">%{name}</span> gelöscht.' form_has_been_deleted_by_html: 'Das Formular wurde von <span class="font-semibold">%{name}</span> gelöscht.'
@ -4927,6 +4988,11 @@ de: &de
enable_shared_link: 'Freigabelink aktivieren' enable_shared_link: 'Freigabelink aktivieren'
share_link_is_currently_disabled: 'Freigabelink ist derzeit deaktiviert' share_link_is_currently_disabled: 'Freigabelink ist derzeit deaktiviert'
select_data_residency: Datenstandort auswählen select_data_residency: Datenstandort auswählen
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} hat Sie eingeladen, Dokumente mühelos online mit einer sicheren, schnellen und benutzerfreundlichen digitalen Signaturlösung auszufüllen und zu unterschreiben.'
review_or_download_completed_documents_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution: Überprüfen oder laden Sie abgeschlossene Dokumente herunter. Füllen und unterschreiben Sie Dokumente mühelos online mit einer sicheren, schnellen und benutzerfreundlichen digitalen Signaturlösung.
link_form_fields: Formularfelder verknüpfen
at_least_one_field_must_be_displayed_in_the_form: Mindestens ein Feld muss im Formular angezeigt werden.
this_template_has_multiple_parties_which_prevents_the_use_of_a_sharing_link: Diese Vorlage enthält mehrere Parteien, was die Verwendung eines Freigabelinks verhindert, da unklar ist, welche Partei für bestimmte Felder verantwortlich ist. Um dies zu beheben, definieren Sie die Standardparteidetails.
submission_sources: submission_sources:
api: API api: API
bulk: Massenversand bulk: Massenversand
@ -5051,7 +5117,13 @@ pl:
digitally_signed_by: Podpis cyfrowy przez digitally_signed_by: Podpis cyfrowy przez
form_expired_at_html: 'Formularz wygasł o <span class="font-semibold">%{time}</span>' form_expired_at_html: 'Formularz wygasł o <span class="font-semibold">%{time}</span>'
role: Rola role: Rola
provide_your_email_to_start: Podaj swój adres email, aby rozpocząć provide_your_email_to_start: Podaj swój e-mail, aby rozpocząć
provide_your_email: Podaj swój e-mail
provide_your_name_to_start: Podaj swoje imię, aby rozpocząć
provide_your_name: Podaj swoje imię
provide_your_phone_in_international_format_to_start: Podaj numer telefonu w formacie międzynarodowym, aby rozpocząć
provide_your_phone_in_international_format: Podaj numer telefonu w formacie międzynarodowym
use_international_format_1xxx_: 'Użyj formatu międzynarodowego: +1xxx...'
start: Rozpocznij start: Rozpocznij
reason: Powód reason: Powód
starting: Rozpoczynanie starting: Rozpoczynanie
@ -5124,7 +5196,13 @@ uk:
digitally_signed_by: Цифровий підпис від digitally_signed_by: Цифровий підпис від
verification_code_code: 'Код підтвердження: %{code}' verification_code_code: 'Код підтвердження: %{code}'
role: Роль role: Роль
provide_your_email_to_start: Введіть свій email, щоб почати provide_your_email_to_start: Введіть свій емейл, щоб почати
provide_your_email: Введіть свій емейл
provide_your_name_to_start: Введіть своє ім’я, щоб почати
provide_your_name: Введіть своє ім’я
provide_your_phone_in_international_format_to_start: Введіть номер телефону у міжнародному форматі, щоб почати
provide_your_phone_in_international_format: Введіть номер телефону у міжнародному форматі
use_international_format_1xxx_: 'Використовуйте міжнародний формат: +1xxx...'
form_expired_at_html: 'Строк подачі завершився о <span class="font-semibold">%{time}</span>' form_expired_at_html: 'Строк подачі завершився о <span class="font-semibold">%{time}</span>'
start: Почати start: Почати
reason: Причина reason: Причина
@ -5198,7 +5276,13 @@ cs:
digitally_signed_by: Digitálně podepsáno uživatelem digitally_signed_by: Digitálně podepsáno uživatelem
verification_code_code: 'Ověřovací kód: %{code}' verification_code_code: 'Ověřovací kód: %{code}'
role: Role role: Role
provide_your_email_to_start: Zadejte svůj email pro zahájení provide_your_email_to_start: Zadejte svůj e-mail pro zahájení
provide_your_email: Zadejte svůj e-mail
provide_your_name_to_start: Zadejte své jméno pro zahájení
provide_your_name: Zadejte své jméno
provide_your_phone_in_international_format_to_start: Zadejte své telefonní číslo v mezinárodním formátu pro zahájení
provide_your_phone_in_international_format: Zadejte své telefonní číslo v mezinárodním formátu
use_international_format_1xxx_: 'Použijte mezinárodní formát: +1xxx...'
reason: Důvod reason: Důvod
form_expired_at_html: 'Formulář vypršel <span class="font-semibold">%{time}</span>' form_expired_at_html: 'Formulář vypršel <span class="font-semibold">%{time}</span>'
start: Zahájit start: Zahájit
@ -5273,7 +5357,13 @@ he:
role: תפקיד role: תפקיד
reason: סיבה reason: סיבה
verification_code_code: 'קוד אימות: %{code}' verification_code_code: 'קוד אימות: %{code}'
provide_your_email_to_start: ספק את כתובת הדוא"ל שלך כדי להתחיל provide_your_email_to_start: הזן את האימייל שלך כדי להתחיל
provide_your_email: הזן את האימייל שלך
provide_your_name_to_start: הזן את שמך כדי להתחיל
provide_your_name: הזן את שמך
provide_your_phone_in_international_format_to_start: הזן את מספר הטלפון שלך בפורמט בינלאומי כדי להתחיל
provide_your_phone_in_international_format: הזן את מספר הטלפון שלך בפורמט בינלאומי
use_international_format_1xxx_: 'השתמש בפורמט בינלאומי: +1xxx...'
start: התחל start: התחל
starting: מתחיל starting: מתחיל
form_expired_at_html: 'הטופס פג תוקף ב- <span class="font-semibold">%{time}</span>' form_expired_at_html: 'הטופס פג תוקף ב- <span class="font-semibold">%{time}</span>'
@ -5346,7 +5436,13 @@ nl:
digitally_signed_by: Digitaal ondertekend door digitally_signed_by: Digitaal ondertekend door
role: Rol role: Rol
verification_code_code: 'Verificatiecode: %{code}' verification_code_code: 'Verificatiecode: %{code}'
provide_your_email_to_start: Geef uw e-mailadres om te beginnen provide_your_email_to_start: Voer je e-mailadres in om te beginnen
provide_your_email: Voer je e-mailadres in
provide_your_name_to_start: Voer je naam in om te beginnen
provide_your_name: Voer je naam in
provide_your_phone_in_international_format_to_start: Voer je telefoonnummer in internationaal formaat in om te beginnen
provide_your_phone_in_international_format: Voer je telefoonnummer in internationaal formaat in
use_international_format_1xxx_: 'Gebruik het internationale formaat: +1xxx...'
start: Start start: Start
reason: Reden reason: Reden
form_expired_at_html: 'Formulier is verlopen op <span class="font-semibold">%{time}</span>' form_expired_at_html: 'Formulier is verlopen op <span class="font-semibold">%{time}</span>'
@ -5419,7 +5515,13 @@ ar:
email: البريد الإلكتروني email: البريد الإلكتروني
digitally_signed_by: تم التوقيع الرقمي بواسطة digitally_signed_by: تم التوقيع الرقمي بواسطة
role: الدور role: الدور
provide_your_email_to_start: قدم بريدك الإلكتروني للبدء provide_your_email_to_start: أدخل بريدك الإلكتروني للبدء
provide_your_email: أدخل بريدك الإلكتروني
provide_your_name_to_start: أدخل اسمك للبدء
provide_your_name: أدخل اسمك
provide_your_phone_in_international_format_to_start: أدخل رقم هاتفك بصيغة دولية للبدء
provide_your_phone_in_international_format: أدخل رقم هاتفك بصيغة دولية
use_international_format_1xxx_: 'استخدم التنسيق الدولي: +1xxx...'
start: بدء start: بدء
starting: بداية starting: بداية
verification_code_code: 'رمز التحقق: %{code}' verification_code_code: 'رمز التحقق: %{code}'
@ -5493,7 +5595,13 @@ ko:
email: 이메일 email: 이메일
digitally_signed_by: 디지털로 서명됨 digitally_signed_by: 디지털로 서명됨
role: 역할 role: 역할
provide_your_email_to_start: 시작하려면 이메일을 제공하세요 provide_your_email_to_start: 시작하려면 이메일을 입력하세요
provide_your_email: 이메일을 입력하세요
provide_your_name_to_start: 시작하려면 이름을 입력하세요
provide_your_name: 이름을 입력하세요
provide_your_phone_in_international_format_to_start: 시작하려면 국제 형식의 전화번호를 입력하세요
provide_your_phone_in_international_format: 국제 형식의 전화번호를 입력하세요
use_international_format_1xxx_: '국제 형식을 사용하세요: +1xxx...'
start: 시작 start: 시작
reason: 이유 reason: 이유
starting: 시작 중 starting: 시작 중
@ -5568,6 +5676,12 @@ ja:
digitally_signed_by: 電子署名者 digitally_signed_by: 電子署名者
role: 役割 role: 役割
provide_your_email_to_start: 開始するにはメールアドレスを入力してください provide_your_email_to_start: 開始するにはメールアドレスを入力してください
provide_your_email: メールアドレスを入力してください
provide_your_name_to_start: 開始するには名前を入力してください
provide_your_name: 名前を入力してください
provide_your_phone_in_international_format_to_start: 開始するには国際形式の電話番号を入力してください
provide_your_phone_in_international_format: 国際形式の電話番号を入力してください
use_international_format_1xxx_: '国際形式を使用してください:+1xxx...'
start: 開始 start: 開始
reason: 理由 reason: 理由
starting: 開始中 starting: 開始中

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -132,7 +132,27 @@ const token = jwt.sign({
"fieldTypes": { "fieldTypes": {
"type": "array", "type": "array",
"required": false, "required": false,
"description": "Field type names to be used in the form builder. All field types are used by default." "description": "Field type names to be used in the form builder. All field types are used by default.",
"enum": [
"heading",
"text",
"signature",
"initials",
"date",
"datenow",
"number",
"image",
"checkbox",
"multiple",
"file",
"radio",
"select",
"cells",
"stamp",
"payment",
"phone",
"verification"
]
}, },
"drawFieldType": { "drawFieldType": {
"type": "string", "type": "string",

@ -317,7 +317,27 @@
"type": "string", "type": "string",
"required": false, "required": false,
"description": "Comma separated field type names to be used in the form builder. All field types are used by default.", "description": "Comma separated field type names to be used in the form builder. All field types are used by default.",
"example": "text,date" "example": "text,date",
"enum": [
"heading",
"text",
"signature",
"initials",
"date",
"datenow",
"number",
"image",
"checkbox",
"multiple",
"file",
"radio",
"select",
"cells",
"stamp",
"payment",
"phone",
"verification"
]
}, },
"data-draw-field-type": { "data-draw-field-type": {
"type": "string", "type": "string",

@ -123,7 +123,27 @@ const token = jwt.sign({
"fieldTypes": { "fieldTypes": {
"type": "array", "type": "array",
"required": false, "required": false,
"description": "Field type names to be used in the form builder. All field types are used by default." "description": "Field type names to be used in the form builder. All field types are used by default.",
"enum": [
"heading",
"text",
"signature",
"initials",
"date",
"datenow",
"number",
"image",
"checkbox",
"multiple",
"file",
"radio",
"select",
"cells",
"stamp",
"payment",
"phone",
"verification"
]
}, },
"drawFieldType": { "drawFieldType": {
"type": "string", "type": "string",

@ -144,7 +144,27 @@ const token = jwt.sign({
"field-types": { "field-types": {
"type": "array", "type": "array",
"required": false, "required": false,
"description": "Field type names to be used in the form builder. All field types are used by default." "description": "Field type names to be used in the form builder. All field types are used by default.",
"enum": [
"heading",
"text",
"signature",
"initials",
"date",
"datenow",
"number",
"image",
"checkbox",
"multiple",
"file",
"radio",
"select",
"cells",
"stamp",
"payment",
"phone",
"verification"
]
}, },
"draw-field-type": { "draw-field-type": {
"type": "string", "type": "string",

@ -48,6 +48,41 @@ export class AppComponent {}
"description": "The role name or title of the signer.", "description": "The role name or title of the signer.",
"example": "First Party" "example": "First Party"
}, },
"token": {
"type": "string",
"doc_type": "object",
"description": "JSON Web Token (JWT HS256) with a payload signed using the API key. <b>JWT can be generated only on the backend.</b>.",
"required": false,
"properties": {
"slug": {
"type": "string",
"required": true,
"description": "Template or Submitter slug. When Submitter slug is used no need to pass additional email param."
},
"email": {
"type": "string",
"required": false,
"description": "Email address of the signer. Additional email form step will be displayed if the email attribute is not specified with Template slug."
},
"external_id": {
"type": "string",
"required": false,
"description": "Your application-specific unique string key to identify signer within your app."
},
"preview": {
"type": "boolean",
"required": false,
"default": false,
"description": "Show form in preview mode without ability to submit it."
}
}
},
"preview": {
"type": "boolean",
"required": false,
"default": false,
"description": "Show form in preview mode without ability to submit it. Completed documents embedded in preview mode require `data-token` authentication."
},
"expand": { "expand": {
"type": "boolean", "type": "boolean",
"required": false, "required": false,
@ -66,11 +101,6 @@ export class AppComponent {}
"default": false, "default": false,
"description": "Order form fields based on their position on the pages." "description": "Order form fields based on their position on the pages."
}, },
"externalId": {
"type": "string",
"required": false,
"description": "Your application-specific unique string key to identify signer within your app."
},
"logo": { "logo": {
"type": "string", "type": "string",
"required": false, "required": false,
@ -87,12 +117,6 @@ export class AppComponent {}
"default": "{}", "default": "{}",
"description": "Object that contains i18n keys to replace the default UI text with custom values. See <a href=\"https://github.com/docusealco/docuseal/blob/master/app/javascript/submission_form/i18n.js\" class=\"link\" target=\"_blank\" rel=\"nofollow\">submission_form/i18n.js</a> for available i18n keys." "description": "Object that contains i18n keys to replace the default UI text with custom values. See <a href=\"https://github.com/docusealco/docuseal/blob/master/app/javascript/submission_form/i18n.js\" class=\"link\" target=\"_blank\" rel=\"nofollow\">submission_form/i18n.js</a> for available i18n keys."
}, },
"preview": {
"type": "boolean",
"required": false,
"default": false,
"description": "Show form in preview mode without ability to submit it."
},
"goToLast": { "goToLast": {
"type": "boolean", "type": "boolean",
"required": false, "required": false,
@ -242,6 +266,11 @@ export class AppComponent {}
"description": "Pre-assigned values for form fields.", "description": "Pre-assigned values for form fields.",
"example": "{ 'First Name': 'Jon', 'Last Name': 'Doe' }" "example": "{ 'First Name': 'Jon', 'Last Name': 'Doe' }"
}, },
"externalId": {
"type": "string",
"required": false,
"description": "Your application-specific unique string key to identify signer within your app."
},
"metadata": { "metadata": {
"type": "object", "type": "object",
"required": false, "required": false,

@ -42,6 +42,41 @@
"description": "The role name or title of the signer.", "description": "The role name or title of the signer.",
"example": "First Party" "example": "First Party"
}, },
"data-token": {
"type": "string",
"doc_type": "object",
"description": "JSON Web Token (JWT HS256) with a payload signed using the API key. <b>JWT can be generated only on the backend.</b>.",
"required": false,
"properties": {
"slug": {
"type": "string",
"required": true,
"description": "Template or Submitter slug. When Submitter slug is used no need to pass additional email param."
},
"email": {
"type": "string",
"required": false,
"description": "Email address of the signer. Additional email form step will be displayed if the email attribute is not specified with Template slug."
},
"external_id": {
"type": "string",
"required": false,
"description": "Your application-specific unique string key to identify signer within your app."
},
"preview": {
"type": "boolean",
"required": false,
"default": false,
"description": "Show form in preview mode without ability to submit it."
}
}
},
"data-preview": {
"type": "boolean",
"required": false,
"default": false,
"description": "Show form in preview mode without ability to submit it. Completed documents embedded in preview mode require `data-token` authentication."
},
"data-expand": { "data-expand": {
"type": "boolean", "type": "boolean",
"required": false, "required": false,
@ -60,12 +95,6 @@
"default": false, "default": false,
"description": "Order form fields based on their position on the pages." "description": "Order form fields based on their position on the pages."
}, },
"data-preview": {
"type": "boolean",
"required": false,
"default": false,
"description": "Show form in preview mode without ability to submit it."
},
"data-logo": { "data-logo": {
"type": "string", "type": "string",
"required": false, "required": false,

@ -45,6 +45,41 @@ export function App() {
"description": "The role name or title of the signer.", "description": "The role name or title of the signer.",
"example": "First Party" "example": "First Party"
}, },
"token": {
"type": "string",
"doc_type": "object",
"description": "JSON Web Token (JWT HS256) with a payload signed using the API key. <b>JWT can be generated only on the backend.</b>.",
"required": false,
"properties": {
"slug": {
"type": "string",
"required": true,
"description": "Template or Submitter slug. When Submitter slug is used no need to pass additional email param."
},
"email": {
"type": "string",
"required": false,
"description": "Email address of the signer. Additional email form step will be displayed if the email attribute is not specified with Template slug."
},
"external_id": {
"type": "string",
"required": false,
"description": "Your application-specific unique string key to identify signer within your app."
},
"preview": {
"type": "boolean",
"required": false,
"default": false,
"description": "Show form in preview mode without ability to submit it."
}
}
},
"preview": {
"type": "boolean",
"required": false,
"default": false,
"description": "Show form in preview mode without ability to submit it. Completed documents embedded in preview mode require `token` authentication."
},
"expand": { "expand": {
"type": "boolean", "type": "boolean",
"required": false, "required": false,
@ -63,11 +98,6 @@ export function App() {
"default": false, "default": false,
"description": "Order form fields based on their position on the pages." "description": "Order form fields based on their position on the pages."
}, },
"externalId": {
"type": "string",
"required": false,
"description": "Your application-specific unique string key to identify signer within your app."
},
"logo": { "logo": {
"type": "string", "type": "string",
"required": false, "required": false,
@ -84,12 +114,6 @@ export function App() {
"default": "{}", "default": "{}",
"description": "Object that contains i18n keys to replace the default UI text with custom values. See <a href=\"https://github.com/docusealco/docuseal/blob/master/app/javascript/submission_form/i18n.js\" class=\"link\" target=\"_blank\" rel=\"nofollow\">submission_form/i18n.js</a> for available i18n keys." "description": "Object that contains i18n keys to replace the default UI text with custom values. See <a href=\"https://github.com/docusealco/docuseal/blob/master/app/javascript/submission_form/i18n.js\" class=\"link\" target=\"_blank\" rel=\"nofollow\">submission_form/i18n.js</a> for available i18n keys."
}, },
"preview": {
"type": "boolean",
"required": false,
"default": false,
"description": "Show form in preview mode without ability to submit it."
},
"goToLast": { "goToLast": {
"type": "boolean", "type": "boolean",
"required": false, "required": false,
@ -239,6 +263,11 @@ export function App() {
"description": "Pre-assigned values for form fields.", "description": "Pre-assigned values for form fields.",
"example": "{ 'First Name': 'Jon', 'Last Name': 'Doe' }" "example": "{ 'First Name': 'Jon', 'Last Name': 'Doe' }"
}, },
"externalId": {
"type": "string",
"required": false,
"description": "Your application-specific unique string key to identify signer within your app."
},
"metadata": { "metadata": {
"type": "object", "type": "object",
"required": false, "required": false,

@ -54,11 +54,41 @@ export default {
"description": "The role name or title of the signer.", "description": "The role name or title of the signer.",
"example": "First Party" "example": "First Party"
}, },
"external-id": { "token": {
"type": "string",
"doc_type": "object",
"description": "JSON Web Token (JWT HS256) with a payload signed using the API key. <b>JWT can be generated only on the backend.</b>.",
"required": false,
"properties": {
"slug": {
"type": "string",
"required": true,
"description": "Template or Submitter slug. When Submitter slug is used no need to pass additional email param."
},
"email": {
"type": "string",
"required": false,
"description": "Email address of the signer. Additional email form step will be displayed if the email attribute is not specified with Template slug."
},
"external_id": {
"type": "string", "type": "string",
"required": false, "required": false,
"description": "Your application-specific unique string key to identify signer within your app." "description": "Your application-specific unique string key to identify signer within your app."
}, },
"preview": {
"type": "boolean",
"required": false,
"default": false,
"description": "Show form in preview mode without ability to submit it."
}
}
},
"preview": {
"type": "boolean",
"required": false,
"default": false,
"description": "Show form in preview mode without ability to submit it. Completed documents embedded in preview mode require `data-token` authentication."
},
"expand": { "expand": {
"type": "boolean", "type": "boolean",
"required": false, "required": false,
@ -93,12 +123,6 @@ export default {
"default": "{}", "default": "{}",
"description": "Object that contains i18n keys to replace the default UI text with custom values. See <a href=\"https://github.com/docusealco/docuseal/blob/master/app/javascript/submission_form/i18n.js\" class=\"link\" target=\"_blank\" rel=\"nofollow\">submission_form/i18n.js</a> for available i18n keys." "description": "Object that contains i18n keys to replace the default UI text with custom values. See <a href=\"https://github.com/docusealco/docuseal/blob/master/app/javascript/submission_form/i18n.js\" class=\"link\" target=\"_blank\" rel=\"nofollow\">submission_form/i18n.js</a> for available i18n keys."
}, },
"preview": {
"type": "boolean",
"required": false,
"default": false,
"description": "Show form in preview mode without ability to submit it."
},
"go-to-last": { "go-to-last": {
"type": "boolean", "type": "boolean",
"required": false, "required": false,
@ -249,6 +273,11 @@ export default {
"description": "Pre-assigned values for form fields.", "description": "Pre-assigned values for form fields.",
"example": "{ 'First Name': 'Jon', 'Last Name': 'Doe' }" "example": "{ 'First Name': 'Jon', 'Last Name': 'Doe' }"
}, },
"external-id": {
"type": "string",
"required": false,
"description": "Your application-specific unique string key to identify signer within your app."
},
"metadata": { "metadata": {
"type": "object", "type": "object",
"required": false, "required": false,

File diff suppressed because it is too large Load Diff

@ -13,7 +13,7 @@ module Abilities
TemplateSharing.where({ ability:, account_id: [user.account_id, TemplateSharing::ALL_ID] }.compact) TemplateSharing.where({ ability:, account_id: [user.account_id, TemplateSharing::ALL_ID] }.compact)
.select(:template_id) .select(:template_id)
Template.where(Template.arel_table[:id].in(Arel::Nodes::Union.new(templates.select(:id).arel, shared_ids.arel))) Template.where(Template.arel_table[:id].in(templates.select(:id).arel.union(shared_ids.arel)))
end end
def entity(template, user:, ability: nil) def entity(template, user:, ability: nil)

@ -0,0 +1,15 @@
# frozen_string_literal: true
module ImageUtils
module_function
def blank?(image)
min = (0...image.bands).map { |i| image.stats.getpoint(0, i)[0] }
max = (0...image.bands).map { |i| image.stats.getpoint(1, i)[0] }
return true if min.all?(255) && max.all?(255)
return true if min.all?(0) && max.all?(0)
false
end
end

@ -24,6 +24,7 @@ class Pdfium
typedef :pointer, :FPDF_PAGE typedef :pointer, :FPDF_PAGE
typedef :pointer, :FPDF_BITMAP typedef :pointer, :FPDF_BITMAP
typedef :pointer, :FPDF_FORMHANDLE typedef :pointer, :FPDF_FORMHANDLE
typedef :pointer, :FPDF_TEXTPAGE
MAX_SIZE = 32_767 MAX_SIZE = 32_767
@ -72,6 +73,11 @@ class Pdfium
attach_function :FPDF_RenderPageBitmap, %i[FPDF_BITMAP FPDF_PAGE int int int int int int], :void attach_function :FPDF_RenderPageBitmap, %i[FPDF_BITMAP FPDF_PAGE int int int int int int], :void
attach_function :FPDFText_LoadPage, [:FPDF_PAGE], :FPDF_TEXTPAGE
attach_function :FPDFText_ClosePage, [:FPDF_TEXTPAGE], :void
attach_function :FPDFText_CountChars, [:FPDF_TEXTPAGE], :int
attach_function :FPDFText_GetText, %i[FPDF_TEXTPAGE int int pointer], :int
typedef :int, :FPDF_BOOL typedef :int, :FPDF_BOOL
typedef :pointer, :IPDF_JSPLATFORM typedef :pointer, :IPDF_JSPLATFORM
@ -346,6 +352,40 @@ class Pdfium
Pdfium.FPDFBitmap_Destroy(bitmap_ptr) if bitmap_ptr && !bitmap_ptr.null? Pdfium.FPDFBitmap_Destroy(bitmap_ptr) if bitmap_ptr && !bitmap_ptr.null?
end end
def text
return @text if @text
ensure_not_closed!
text_page = Pdfium.FPDFText_LoadPage(page_ptr)
if text_page.null?
Pdfium.check_last_error("Failed to load text page #{page_index}")
raise PdfiumError, "Failed to load text page #{page_index}, pointer is NULL."
end
char_count = Pdfium.FPDFText_CountChars(text_page)
return @text = '' if char_count.zero?
buffer_char_capacity = char_count + 1
buffer = FFI::MemoryPointer.new(:uint16, buffer_char_capacity)
chars_written = Pdfium.FPDFText_GetText(text_page, 0, buffer_char_capacity, buffer)
if chars_written <= 0
Pdfium.check_last_error("Failed to extract text from page #{page_index}")
return @text = ''
end
@text = buffer.read_bytes((chars_written * 2) - 2).force_encoding('UTF-16LE').encode('UTF-8')
ensure
Pdfium.FPDFText_ClosePage(text_page) if text_page && !text_page.null?
end
def close def close
return if closed? return if closed?

@ -56,7 +56,7 @@ module SearchEntries
end end
[sql, number, number.length > 1 ? number.delete_prefix('0') : number, keyword] [sql, number, number.length > 1 ? number.delete_prefix('0') : number, keyword]
elsif keyword.match?(/[^\p{L}\d&@._\-]/) || keyword.match?(/\A['"].*['"]\z/) elsif keyword.match?(/[^\p{L}\d&@.\-]/) || keyword.match?(/\A['"].*['"]\z/)
['tsvector @@ plainto_tsquery(?)', TextUtils.transliterate(keyword.downcase)] ['tsvector @@ plainto_tsquery(?)', TextUtils.transliterate(keyword.downcase)]
else else
keyword = TextUtils.transliterate(keyword.downcase).squish keyword = TextUtils.transliterate(keyword.downcase).squish
@ -180,15 +180,21 @@ module SearchEntries
end end
def index_template(template) def index_template(template)
sql = SearchEntry.sanitize_sql_array( text = TextUtils.transliterate(template.name.to_s.downcase.squish).delete("\0")
["SELECT to_tsvector(:text), to_tsvector('simple', :text)",
{ text: TextUtils.transliterate(template.name.to_s.downcase).delete("\0") }] sql = SearchEntry.sanitize_sql_array(["SELECT to_tsvector(:text), to_tsvector('simple', :text)", { text: }])
)
entry = template.search_entry || template.build_search_entry entry = template.search_entry || template.build_search_entry
entry.account_id = template.account_id entry.account_id = template.account_id
entry.tsvector, ngram = SearchEntry.connection.select_rows(sql).first entry.tsvector, ngram = SearchEntry.connection.select_rows(sql).first
hyphens = text.scan(/\b[^\s]*?\d-[^\s]+?\b/) + text.scan(/\b[^\s]+-\d[^\s]*?\b/)
hyphens.uniq.each_with_index do |item, index|
entry.tsvector += " '#{item.delete("'")}':#{index + 1}" unless entry.tsvector.include?(item)
end
entry.ngram = build_ngram(ngram) entry.ngram = build_ngram(ngram)
return if entry.tsvector.blank? return if entry.tsvector.blank?
@ -205,15 +211,21 @@ module SearchEntries
def index_submission(submission) def index_submission(submission)
return if submission.name.blank? return if submission.name.blank?
sql = SearchEntry.sanitize_sql_array( text = TextUtils.transliterate(submission.name.to_s.downcase.squish).delete("\0")
["SELECT to_tsvector(:text), to_tsvector('simple', :text)",
{ text: TextUtils.transliterate(submission.name.to_s.downcase).delete("\0") }] sql = SearchEntry.sanitize_sql_array(["SELECT to_tsvector(:text), to_tsvector('simple', :text)", { text: }])
)
entry = submission.search_entry || submission.build_search_entry entry = submission.search_entry || submission.build_search_entry
entry.account_id = submission.account_id entry.account_id = submission.account_id
entry.tsvector, ngram = SearchEntry.connection.select_rows(sql).first entry.tsvector, ngram = SearchEntry.connection.select_rows(sql).first
hyphens = text.scan(/\b[^\s]*?\d-[^\s]+?\b/) + text.scan(/\b[^\s]+-\d[^\s]*?\b/)
hyphens.uniq.each_with_index do |item, index|
entry.tsvector += " '#{item.delete("'")}':#{index + 1}" unless entry.tsvector.include?(item)
end
entry.ngram = build_ngram(ngram) entry.ngram = build_ngram(ngram)
return if entry.tsvector.blank? return if entry.tsvector.blank?

@ -50,7 +50,8 @@ module Submissions
arel, arel,
Submission.where( Submission.where(
template_id: SearchEntry.where(record_type: 'Template') template_id: SearchEntry.where(record_type: 'Template')
.where(account_id: current_user.account_id) .where(account_id: [current_user.account_id,
current_user.account.linked_account_account&.account_id].compact)
.where(*SearchEntries.build_tsquery(keyword)) .where(*SearchEntries.build_tsquery(keyword))
.select(:record_id) .select(:record_id)
).select(:id).arel ).select(:id).arel

@ -312,8 +312,9 @@ module Submissions
text_align: field_name.to_s.match?(RTL_REGEXP) ? :right : :left, text_align: field_name.to_s.match?(RTL_REGEXP) ? :right : :left,
line_spacing: 1.3, padding: [0, 0, 2, 0] line_spacing: 1.3, padding: [0, 0, 2, 0]
), ),
if field['type'].in?(%w[image signature initials stamp]) if field['type'].in?(%w[image signature initials stamp]) &&
attachment = submitter.attachments.find { |a| a.uuid == value } (attachment = submitter.attachments.find { |a| a.uuid == value }) &&
attachment.image?
image = image =
begin begin
@ -340,7 +341,7 @@ module Submissions
composer.image(io, width:, height:, margin: [5, 0, 10, 0]) composer.image(io, width:, height:, margin: [5, 0, 10, 0])
composer.formatted_text_box([{ text: '' }]) composer.formatted_text_box([{ text: '' }])
elsif field['type'].in?(%w[file payment]) elsif field['type'].in?(%w[file payment image])
if field['type'] == 'payment' if field['type'] == 'payment'
unit = CURRENCY_SYMBOLS[field['preferences']['currency']] || field['preferences']['currency'] unit = CURRENCY_SYMBOLS[field['preferences']['currency']] || field['preferences']['currency']

@ -223,7 +223,7 @@ module Submissions
font_name = FONT_NAME unless font_name.in?(DEFAULT_FONTS) font_name = FONT_NAME unless font_name.in?(DEFAULT_FONTS)
if font_variant != :none && font_name == FONT_NAME if font_variant != :none && font_name == FONT_NAME
font_name = FONT_VARIANS[font_variant] font_name = FONT_VARIANS[font_variant] if FONT_VARIANS[font_variant]
font_variant = nil unless font_name.in?(DEFAULT_FONTS) font_variant = nil unless font_name.in?(DEFAULT_FONTS)
end end
@ -252,7 +252,11 @@ module Submissions
canvas = page.canvas(type: :overlay) canvas = page.canvas(type: :overlay)
canvas.font(FONT_NAME, size: font_size) canvas.font(FONT_NAME, size: font_size)
case field['type'] field_type = field['type']
field_type = 'file' if field_type == 'image' &&
!submitter.attachments.find { |a| a.uuid == value }.image?
case field_type
when ->(type) { type == 'signature' && (with_signature_id || field.dig('preferences', 'reason_field_uuid')) } when ->(type) { type == 'signature' && (with_signature_id || field.dig('preferences', 'reason_field_uuid')) }
attachment = submitter.attachments.find { |a| a.uuid == value } attachment = submitter.attachments.find { |a| a.uuid == value }

@ -53,7 +53,7 @@ module Submitters
end end
[sql, number, weight, number.length > 1 ? number.delete_prefix('0') : number, weight] [sql, number, weight, number.length > 1 ? number.delete_prefix('0') : number, weight]
elsif keyword.match?(/[^\p{L}\d&@._\-]/) elsif keyword.match?(/[^\p{L}\d&@.\-]/)
terms = TextUtils.transliterate(keyword.downcase).split(/\b/).map(&:squish).compact_blank.uniq terms = TextUtils.transliterate(keyword.downcase).split(/\b/).map(&:squish).compact_blank.uniq
if terms.size > 1 if terms.size > 1
@ -135,6 +135,7 @@ module Submitters
preferences['email_message_uuid'] = email_message.uuid if email_message preferences['email_message_uuid'] = email_message.uuid if email_message
preferences['send_email'] = params['send_email'].in?(TRUE_VALUES) if params.key?('send_email') preferences['send_email'] = params['send_email'].in?(TRUE_VALUES) if params.key?('send_email')
preferences['send_sms'] = params['send_sms'].in?(TRUE_VALUES) if params.key?('send_sms') preferences['send_sms'] = params['send_sms'].in?(TRUE_VALUES) if params.key?('send_sms')
preferences['require_phone_2fa'] = params['require_phone_2fa'].in?(TRUE_VALUES) if params.key?('require_phone_2fa')
preferences['bcc_completed'] = params['bcc_completed'] if params.key?('bcc_completed') preferences['bcc_completed'] = params['bcc_completed'] if params.key?('bcc_completed')
preferences['reply_to'] = params['reply_to'] if params.key?('reply_to') preferences['reply_to'] = params['reply_to'] if params.key?('reply_to')
preferences['go_to_last'] = params['go_to_last'] if params.key?('go_to_last') preferences['go_to_last'] = params['go_to_last'] if params.key?('go_to_last')

@ -7,6 +7,8 @@ module Submitters
module_function module_function
def call(submitter, params, cookies = nil, attachments = []) def call(submitter, params, cookies = nil, attachments = [])
attachments = attachments.select { |e| e.record_id == submitter.id && e.record_type == 'Submitter' }
if (value = params[:signature_src].presence || params[:signature].presence) if (value = params[:signature_src].presence || params[:signature].presence)
find_or_create_signature_from_value(submitter, value, attachments) find_or_create_signature_from_value(submitter, value, attachments)
elsif params[:signed_signature_uuids].present? elsif params[:signed_signature_uuids].present?

@ -219,8 +219,7 @@ module Submitters
submitters_values = nil submitters_values = nil
has_other_submitters = submission.template_submitters.size > 1 has_other_submitters = submission.template_submitters.size > 1
has_document_conditions = has_document_conditions = submission_has_document_conditions?(submission)
(submission.template_schema || submission.template.schema).any? { |e| e['conditions'].present? }
attachments_index = attachments_index =
if has_document_conditions if has_document_conditions
@ -234,7 +233,7 @@ module Submitters
if has_document_conditions && !check_field_areas_attachments(field, attachments_index) if has_document_conditions && !check_field_areas_attachments(field, attachments_index)
submitter.values.delete(field['uuid']) submitter.values.delete(field['uuid'])
required_field_uuids_acc.delete(field['uuid']) required_field_uuids_acc&.delete(field['uuid'])
end end
if has_other_submitters && !submitters_values && if has_other_submitters && !submitters_values &&
@ -244,13 +243,17 @@ module Submitters
unless check_field_conditions(submitters_values || submitter.values, field, submission.fields_uuid_index) unless check_field_conditions(submitters_values || submitter.values, field, submission.fields_uuid_index)
submitter.values.delete(field['uuid']) submitter.values.delete(field['uuid'])
required_field_uuids_acc.delete(field['uuid']) required_field_uuids_acc&.delete(field['uuid'])
end end
end end
submitter.values submitter.values
end end
def submission_has_document_conditions?(submission)
(submission.template_schema || submission.template.schema).any? { |e| e['conditions'].present? }
end
def required_editable_field?(field) def required_editable_field?(field)
return false if NONEDITABLE_FIELD_TYPES.include?(field['type']) return false if NONEDITABLE_FIELD_TYPES.include?(field['type'])

@ -56,7 +56,8 @@ module Templates
templates.where( templates.where(
id: SearchEntry.where(record_type: 'Template') id: SearchEntry.where(record_type: 'Template')
.where(account_id: current_user.account_id) .where(account_id: [current_user.account_id,
current_user.account.linked_account_account&.account_id].compact)
.where(*SearchEntries.build_tsquery(keyword)) .where(*SearchEntries.build_tsquery(keyword))
.select(:record_id) .select(:record_id)
) )

@ -69,7 +69,7 @@ module Templates
bitdepth = 2**image.stats.to_a[1..3].pluck(2).uniq.size bitdepth = 2**image.stats.to_a[1..3].pluck(2).uniq.size
io = StringIO.new(image.write_to_buffer(FORMAT, compression: 7, filter: 0, bitdepth:, io = StringIO.new(image.write_to_buffer(FORMAT, compression: 7, filter: 0, bitdepth:,
palette: true, Q: bitdepth == 8 ? Q : 5, dither: 0)) palette: true, Q: Q, dither: 0))
ActiveStorage::Attachment.create!( ActiveStorage::Attachment.create!(
blob: ActiveStorage::Blob.create_and_upload!( blob: ActiveStorage::Blob.create_and_upload!(
@ -141,7 +141,7 @@ module Templates
bitdepth = 2**page.stats.to_a[1..3].pluck(2).uniq.size bitdepth = 2**page.stats.to_a[1..3].pluck(2).uniq.size
page.write_to_buffer(format, compression: 7, filter: 0, bitdepth:, page.write_to_buffer(format, compression: 7, filter: 0, bitdepth:,
palette: true, Q: bitdepth == 8 ? Q : 5, dither: 0) palette: true, Q: Q, dither: 0)
else else
page.write_to_buffer(format, interlace: true, Q: JPEG_Q) page.write_to_buffer(format, interlace: true, Q: JPEG_Q)
end end

@ -6,7 +6,7 @@
"@babel/plugin-transform-runtime": "7.21.4", "@babel/plugin-transform-runtime": "7.21.4",
"@babel/preset-env": "7.21.5", "@babel/preset-env": "7.21.5",
"@babel/runtime": "7.21.5", "@babel/runtime": "7.21.5",
"@eid-easy/eideasy-widget": "^2.132.1", "@eid-easy/eideasy-widget": "^2.159.0",
"@github/catalyst": "^2.0.0-beta", "@github/catalyst": "^2.0.0-beta",
"@hotwired/turbo": "https://github.com/docusealco/turbo#main", "@hotwired/turbo": "https://github.com/docusealco/turbo#main",
"@hotwired/turbo-rails": "^7.3.0", "@hotwired/turbo-rails": "^7.3.0",

@ -9,18 +9,78 @@ RSpec.describe 'Signing Form' do
create(:template, shared_link: true, account:, author:, except_field_types: %w[phone payment stamp]) create(:template, shared_link: true, account:, author:, except_field_types: %w[phone payment stamp])
end end
before do it 'displays only the email step when only email is required' do
visit start_form_path(slug: template.slug)
expect(page).to have_content('You have been invited to submit a form')
expect(page).to have_content("Invited by #{account.name}")
expect(page).to have_field('Email', type: 'email', placeholder: 'Provide your email to start')
expect(page).not_to have_field('Phone', type: 'tel')
expect(page).not_to have_field('Name', type: 'text')
expect(page).to have_button('Start')
end
it 'displays name, email, and phone fields together when all are required' do
template.update(preferences: { link_form_fields: %w[email name phone] })
visit start_form_path(slug: template.slug)
expect(page).to have_content('You have been invited to submit a form')
expect(page).to have_content("Invited by #{account.name}")
expect(page).to have_field('Email', type: 'email', placeholder: 'Provide your email')
expect(page).to have_field('Name', type: 'text', placeholder: 'Provide your name')
expect(page).to have_field('Phone', type: 'tel', placeholder: 'Provide your phone in international format')
expect(page).to have_button('Start')
end
it 'displays only the name step when only name is required' do
template.update(preferences: { link_form_fields: %w[name] })
visit start_form_path(slug: template.slug) visit start_form_path(slug: template.slug)
expect(page).to have_content('You have been invited to submit a form')
expect(page).to have_content("Invited by #{account.name}")
expect(page).to have_field('Name', type: 'text', placeholder: 'Provide your name to start')
expect(page).not_to have_field('Phone', type: 'tel')
expect(page).not_to have_field('Email', type: 'email')
expect(page).to have_button('Start')
end end
it 'shows the email step' do it 'displays only the phone step when only phone is required' do
template.update(preferences: { link_form_fields: %w[phone] })
visit start_form_path(slug: template.slug)
expect(page).to have_content('You have been invited to submit a form') expect(page).to have_content('You have been invited to submit a form')
expect(page).to have_content("Invited by #{account.name}") expect(page).to have_content("Invited by #{account.name}")
expect(page).to have_field('Email', type: 'email') expect(page).to have_field('Phone', type: 'tel',
placeholder: 'Provide your phone in international format to start')
expect(page).not_to have_field('Name', type: 'text')
expect(page).not_to have_field('Email', type: 'email')
expect(page).to have_button('Start') expect(page).to have_button('Start')
end end
it 'prevents starting the form if phone is not in international format' do
template.update(preferences: { link_form_fields: %w[phone] })
visit start_form_path(slug: template.slug)
fill_in 'Phone', with: '12345'
expect { click_button 'Start' }.not_to(change { current_path })
end
it 'prevents starting the form if email is invali' do
visit start_form_path(slug: template.slug)
fill_in 'Email', with: 'invalid-email'
expect { click_button 'Start' }.not_to(change { current_path })
end
it 'completes the form' do it 'completes the form' do
visit start_form_path(slug: template.slug)
# Submit's email step # Submit's email step
fill_in 'Email', with: 'john.dou@example.com' fill_in 'Email', with: 'john.dou@example.com'
click_button 'Start' click_button 'Start'
@ -100,6 +160,97 @@ RSpec.describe 'Signing Form' do
expect(field_value(submitter, 'Attachment')).to be_present expect(field_value(submitter, 'Attachment')).to be_present
expect(field_value(submitter, 'Cell code')).to eq '123' expect(field_value(submitter, 'Cell code')).to eq '123'
end end
# rubocop:disable RSpec/ExampleLength
it 'completes the form when name, email, and phone are required' do
template.update(preferences: { link_form_fields: %w[email name phone] })
visit start_form_path(slug: template.slug)
# Submit's name, email, and phone step
fill_in 'Email', with: 'john.dou@example.com'
fill_in 'Name', with: 'John Doe'
fill_in 'Phone', with: '+17732298825'
click_button 'Start'
# Text step
fill_in 'First Name', with: 'John'
click_button 'next'
# Date step
fill_in 'Birthday', with: I18n.l(20.years.ago, format: '%Y-%m-%d')
click_button 'next'
# Checkbox step
check 'Do you agree?'
click_button 'next'
# Radio step
choose 'Boy'
click_button 'next'
# Signature step
draw_canvas
click_button 'next'
# Number step
fill_in 'House number', with: '123'
click_button 'next'
# Multiple choice step
%w[Red Blue].each { |color| check color }
click_button 'next'
# Select step
select 'Male', from: 'Gender'
click_button 'next'
# Initials step
draw_canvas
click_button 'next'
# Image step
find('#dropzone').click
find('input[type="file"]', visible: false).attach_file(Rails.root.join('spec/fixtures/sample-image.png'))
click_button 'next'
# File step
find('#dropzone').click
find('input[type="file"]', visible: false).attach_file(Rails.root.join('spec/fixtures/sample-document.pdf'))
click_button 'next'
# Cell step
fill_in 'Cell code', with: '123'
click_on 'Complete'
expect(page).to have_button('Download')
expect(page).to have_content('Document has been signed!')
submitter = template.submissions.last.submitters.last
expect(submitter.email).to eq('john.dou@example.com')
expect(submitter.name).to eq('John Doe')
expect(submitter.phone).to eq('+17732298825')
expect(submitter.ip).to eq('127.0.0.1')
expect(submitter.ua).to be_present
expect(submitter.opened_at).to be_present
expect(submitter.completed_at).to be_present
expect(submitter.declined_at).to be_nil
expect(field_value(submitter, 'First Name')).to eq 'John'
expect(field_value(submitter, 'Birthday')).to eq 20.years.ago.strftime('%Y-%m-%d')
expect(field_value(submitter, 'Do you agree?')).to be_truthy
expect(field_value(submitter, 'First child')).to eq 'Boy'
expect(field_value(submitter, 'Signature')).to be_present
expect(field_value(submitter, 'House number')).to eq 123
expect(field_value(submitter, 'Colors')).to contain_exactly('Red', 'Blue')
expect(field_value(submitter, 'Gender')).to eq 'Male'
expect(field_value(submitter, 'Initials')).to be_present
expect(field_value(submitter, 'Avatar')).to be_present
expect(field_value(submitter, 'Attachment')).to be_present
expect(field_value(submitter, 'Cell code')).to eq '123'
end
# rubocop:enable RSpec/ExampleLength
end end
context 'when the submitter form link is opened' do context 'when the submitter form link is opened' do
@ -848,6 +999,33 @@ RSpec.describe 'Signing Form' do
end end
end end
context 'when the template shared link is disabled' do
let(:template) do
create(:template, shared_link: false, account:, author:, only_field_types: %w[text])
end
context 'when user is logged in' do
before do
login_as author
visit start_form_path(slug: template.slug)
end
it 'shows a warning that the shared link is disabled and provides an option to enable it' do
expect(page).to have_content('Share link is currently disabled')
expect(page).to have_content(template.name)
expect(page).to have_button('Enable shared link')
end
it 'enables the shared link' do
expect do
click_button 'Enable shared link'
end.to change { template.reload.shared_link }.from(false).to(true)
expect(page).to have_content('You have been invited to submit a form')
end
end
end
it 'sends completed email' do it 'sends completed email' do
template = create(:template, account:, author:, only_field_types: %w[text signature]) template = create(:template, account:, author:, only_field_types: %w[text signature])
submission = create(:submission, template:) submission = create(:submission, template:)

@ -1035,22 +1035,22 @@
resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70"
integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==
"@eid-easy/eideasy-browser-client@2.104.1": "@eid-easy/eideasy-browser-client@2.124.0":
version "2.104.1" version "2.124.0"
resolved "https://registry.yarnpkg.com/@eid-easy/eideasy-browser-client/-/eideasy-browser-client-2.104.1.tgz#e492e0fc8ef1ff470c66522d530d5636264f30b3" resolved "https://registry.yarnpkg.com/@eid-easy/eideasy-browser-client/-/eideasy-browser-client-2.124.0.tgz#940ccb9d4d853f0bd32e49cc455f2c7b13f3bbd1"
integrity sha512-d4fgqF5U+pWuPpfgbb/+BxI2CYThkbviAfl9NbLoKMOMO4bh7HZFxLdW5uU2O8yIeldGFunJWxEbMTLv/2PlTA== integrity sha512-hZuUeg0CcyvgJSseRBQixRStAIr58bulmNcikcASBo6z8wv+/R8nAnUoE7qdNW1l4ZyYxLmVOwDs6+W+FHy6hQ==
dependencies: dependencies:
axios "1.7.8" axios "1.8.2"
jsencrypt "3.2.1" jsencrypt "3.2.1"
lodash "^4.17.21" lodash "^4.17.21"
serialize-error "^9.1.1" serialize-error "^9.1.1"
"@eid-easy/eideasy-widget@^2.132.1": "@eid-easy/eideasy-widget@^2.159.0":
version "2.132.1" version "2.159.0"
resolved "https://registry.yarnpkg.com/@eid-easy/eideasy-widget/-/eideasy-widget-2.132.1.tgz#634c077c55d7e582846dbeae47f6f37796b76bd6" resolved "https://registry.yarnpkg.com/@eid-easy/eideasy-widget/-/eideasy-widget-2.159.0.tgz#f2291dad292bd5f7941496f0db0aa0a180226b6b"
integrity sha512-bZNdKsxja4iIUm6B4x6pOyiBdoMNNUB3615oJgNUWallrfeiIbBQxLwG+m5nmivmHMgdaIQylmtfrhc6e7kgYA== integrity sha512-527uCNrN5MVY/PaOUoZ3J2XZ0C+xt6057sJ4xSO0/FPYj2cxOn+qyCJODQjOhjBL7GcnpShWPpYAfv1OIStYwQ==
dependencies: dependencies:
"@eid-easy/eideasy-browser-client" "2.104.1" "@eid-easy/eideasy-browser-client" "2.124.0"
core-js "^3.8.3" core-js "^3.8.3"
i18n-iso-countries "^6.7.0" i18n-iso-countries "^6.7.0"
lodash.defaultsdeep "^4.6.1" lodash.defaultsdeep "^4.6.1"
@ -1914,10 +1914,10 @@ available-typed-arrays@^1.0.5:
resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7"
integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==
axios@1.7.8: axios@1.8.2:
version "1.7.8" version "1.8.2"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.8.tgz#1997b1496b394c21953e68c14aaa51b7b5de3d6e" resolved "https://registry.yarnpkg.com/axios/-/axios-1.8.2.tgz#fabe06e241dfe83071d4edfbcaa7b1c3a40f7979"
integrity sha512-Uu0wb7KNqK2t5K+YQyVCLM76prD5sRFjKHbJYCP1J7JFGEQ6nN7HWn9+04LAeiJ3ji54lgS/gZCH1oxyrf1SPw== integrity sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==
dependencies: dependencies:
follow-redirects "^1.15.6" follow-redirects "^1.15.6"
form-data "^4.0.0" form-data "^4.0.0"

Loading…
Cancel
Save