Compare commits

..

15 Commits

Author SHA1 Message Date
Alex Turchyn 5fe75c84ff
Merge from docusealco/wip
2 weeks ago
Alex Turchyn 4549a04fc5 disable turbo on sign out form
2 weeks ago
Pete Matsyburka 0ea6b912a3 file download variables
3 weeks ago
Pete Matsyburka 9b86fa3d40 add date formats config
3 weeks ago
Pete Matsyburka 374f66c59f send view only
3 weeks ago
Pete Matsyburka da43d881f1 edit message per 10 parties
3 weeks ago
Pete Matsyburka 9cbfa3a0ba fix condition on complete
3 weeks ago
Pete Matsyburka 77a925f33b fix invite form return to fields
3 weeks ago
Pete Matsyburka 3f7d7beedd fix sharing toggle
3 weeks ago
Pete Matsyburka 40d91df732 Revert "adjust shared query"
3 weeks ago
Pete Matsyburka c8e783a210 multi select context menu readonly and required
3 weeks ago
Pete Matsyburka 61fc24d35d add with signature id completed config
3 weeks ago
Pete Matsyburka 73734b0de7 add variables_schema response
3 weeks ago
Pete Matsyburka 09bd831d54 adjust submission.name variable
3 weeks ago
Pete Matsyburka 2c9f1b999e update icon
3 weeks ago

@ -164,6 +164,7 @@ safeRegisterElement('template-builder', class extends HTMLElement {
this.app = createApp(TemplateBuilder, {
template,
customFields: reactive(JSON.parse(this.dataset.customFields || '[]')),
dateFormats: JSON.parse(this.dataset.dateFormats || '[]'),
dynamicDocuments: reactive(JSON.parse(this.dataset.dynamicDocuments || '[]')),
backgroundColor: '#faf7f5',
locale: this.dataset.locale,

@ -1575,6 +1575,7 @@ export default {
}
},
goToStep (stepIndex, scrollToArea = false, clickUpload = false) {
this.isInvite = false
this.currentStep = stepIndex
this.showFillAllRequiredFields = false
@ -1599,6 +1600,10 @@ export default {
})
},
saveStep (formData) {
if (!formData && !this.$refs.form) {
return
}
const currentFieldUuids = this.currentStepFields.map((f) => f.uuid)
const currentFieldType = this.currentField.type

@ -319,7 +319,7 @@ export default {
default: false
}
},
emits: ['start-resize', 'stop-resize', 'start-drag', 'stop-drag', 'remove', 'scroll-to', 'add-custom-field', 'click-title'],
emits: ['start-resize', 'stop-resize', 'start-drag', 'stop-drag', 'remove', 'scroll-to', 'add-custom-field', 'click-title', 'multi-select'],
data () {
return {
isContenteditable: false,
@ -798,6 +798,8 @@ export default {
this.selectedAreasRef.value.splice(this.selectedAreasRef.value.indexOf(this.area), 1)
}
this.$emit('multi-select', e)
return
}

@ -57,6 +57,7 @@
@scroll-to="$emit('scroll-to', $event)"
@add-custom-field="$emit('add-custom-field', $event)"
@contextmenu="openAreaContextMenu($event, item.area, item.field)"
@multi-select="openMultiSelectContextMenu"
@click-title="closeContextMenu"
/>
<FieldArea
@ -408,6 +409,12 @@ export default {
return
}
if (this.selectedAreasRef.value.length >= 2) {
this.openSelectionContextMenu(event)
return
}
event.preventDefault()
event.stopPropagation()
@ -443,6 +450,13 @@ export default {
areas: this.selectedAreasRef.value
}
},
openMultiSelectContextMenu (event) {
if (this.selectedAreasRef.value.length >= 2) {
this.openSelectionContextMenu(event)
} else {
this.closeContextMenu()
}
},
handleSelectionCopy () {
this.$emit('copy-selected-areas')

@ -8,6 +8,62 @@
:style="menuStyle"
@mousedown.stop
@pointerdown.stop
>
<label
v-if="requiredFields.length"
class="field-settings-required w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm cursor-pointer"
@click.stop
>
<input
:checked="isAllRequired"
:indeterminate="isMixedRequired"
type="checkbox"
class="toggle toggle-xs"
:disabled="!editable"
@change="handleToggleRequired($event.target.checked)"
@click.stop
>
<span>{{ t('required') }}</span>
</label>
<label
v-if="readOnlyFields.length"
class="field-settings-read-only w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm cursor-pointer"
@click.stop
>
<input
:checked="isAllReadOnly"
:indeterminate="isMixedReadOnly"
type="checkbox"
class="toggle toggle-xs"
:disabled="!editable"
@change="handleToggleReadOnly($event.target.checked)"
@click.stop
>
<span>{{ t('read_only') }}</span>
</label>
<hr
v-if="requiredFields.length || readOnlyFields.length"
class="my-1 border-neutral-200"
>
<button
v-if="showFont"
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
@click.stop="openFontModal"
>
<IconTypography class="w-4 h-4" />
<span>{{ t('font') }}</span>
</button>
<button
v-if="showCondition"
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
@click.stop="openConditionModal"
>
<IconRouteAltLeft class="w-4 h-4" />
<span>{{ t('condition') }}</span>
</button>
<hr
v-if="showFont || showCondition"
class="my-1 border-neutral-200"
>
<ContextSubmenu
:icon="IconLayoutAlignMiddle"
@ -61,26 +117,6 @@
<span>{{ t('height') }}</span>
</button>
</ContextSubmenu>
<hr
v-if="showFont || showCondition"
class="my-1 border-neutral-200"
>
<button
v-if="showFont"
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
@click.stop="openFontModal"
>
<IconTypography class="w-4 h-4" />
<span>{{ t('font') }}</span>
</button>
<button
v-if="showCondition"
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center space-x-2 text-sm"
@click.stop="openConditionModal"
>
<IconRouteAltLeft class="w-4 h-4" />
<span>{{ t('condition') }}</span>
</button>
<hr class="my-1 border-neutral-200">
<button
class="w-full px-2 py-1 rounded-md hover:bg-neutral-100 flex items-center justify-between text-sm"
@ -192,6 +228,24 @@ export default {
return this.template.fields.find((f) => f.areas?.includes(area))
}).filter(Boolean)
},
requiredFields () {
return this.selectedFields.filter((f) => !['phone', 'stamp', 'verification', 'strikethrough', 'heading'].includes(f.type))
},
readOnlyFields () {
return this.selectedFields.filter((f) => ['text', 'number', 'radio', 'multiple', 'select'].includes(f.type))
},
isAllRequired () {
return this.requiredFields.every((f) => f.required)
},
isMixedRequired () {
return !this.isAllRequired && this.requiredFields.some((f) => f.required)
},
isAllReadOnly () {
return this.readOnlyFields.every((f) => f.readonly)
},
isMixedReadOnly () {
return !this.isAllReadOnly && this.readOnlyFields.some((f) => f.readonly)
},
isMac () {
return (navigator.userAgentData?.platform || navigator.platform)?.toLowerCase()?.includes('mac')
},
@ -234,6 +288,16 @@ export default {
}
}
},
handleToggleRequired (value) {
this.requiredFields.forEach((field) => { field.required = value })
this.save()
},
handleToggleReadOnly (value) {
this.readOnlyFields.forEach((field) => { field.readonly = value })
this.save()
},
onKeyDown (event) {
if (event.key === 'Escape') {
event.preventDefault()

@ -48,6 +48,7 @@ class AccountConfig < ApplicationRecord
WITH_SIGNATURE_ID = 'with_signature_id'
WITH_FILE_LINKS_KEY = 'with_file_links'
WITH_SIGNATURE_ID_REASON_KEY = 'with_signature_id_reason'
WITH_SIGNATURE_ID_COMPLETED_AT_KEY = 'with_signature_id_completed_at'
RECIPIENT_FORM_FIELDS_KEY = 'recipient_form_fields'
WITH_AUDIT_VALUES_KEY = 'with_audit_values'
WITH_AUDIT_SENDER_KEY = 'with_audit_sender'
@ -59,6 +60,7 @@ class AccountConfig < ApplicationRecord
COMBINE_PDF_RESULT_KEY = 'combine_pdf_result_key'
DOCUMENT_FILENAME_FORMAT_KEY = 'document_filename_format'
TEMPLATE_CUSTOM_FIELDS_KEY = 'template_custom_fields'
TEMPLATE_DATE_FORMATS_KEY = 'template_date_formats'
POLICY_LINKS_KEY = 'policy_links'
ENABLE_MCP_KEY = 'enable_mcp'

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" class="<%= local_assigns[:class] %>" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M8 9h8" />
<path d="M8 13h6" />
<path d="M18 4a3 3 0 0 1 3 3v8a3 3 0 0 1 -3 3h-5l-5 3v-3h-2a3 3 0 0 1 -3 -3v-8a3 3 0 0 1 3 -3h12" />
</svg>

After

Width:  |  Height:  |  Size: 431 B

@ -75,7 +75,7 @@
<%= svg_icon('logout', class: 'w-5 h-5 flex-shrink-0 stroke-2 inline') %>
<span class="whitespace-nowrap"><%= t('sign_out') %></span>
</button>
<%= button_to '', destroy_user_session_path, method: :delete, data: { turbo: false }, form: { id: 'destroy_user_session_form' }, form_class: 'hidden' %>
<%= button_to '', destroy_user_session_path, method: :delete, form: { id: 'destroy_user_session_form', data: { turbo: false } }, form_class: 'hidden' %>
</li>
</ul>
</div>

@ -132,7 +132,7 @@
<%= capture do %>
<div class="tooltip" data-tip="<%= t('ai_assistant') %>">
<a href="<%= Docuseal::CHATGPT_URL %>" target="_blank" class="btn btn-circle btn-primary btn-md">
<%= svg_icon('brand_openai', class: 'w-8 h-8') %>
<%= svg_icon('message', class: 'w-8 h-8') %>
</a>
</div>
<% end %>

@ -47,7 +47,7 @@
<% is_edit_viewer = local_assigns[:submitter] && local_assigns[:viewer_submitter_uuids].include?(local_assigns[:submitter].uuid) %>
<div id="<%= message_field_id %>" class="card card-compact bg-base-300/40 hidden">
<div class="card-body">
<%= tag.input id: toggle_uuid = SecureRandom.uuid, value: '1', name: 'request_email_per_submitter', class: 'peer', type: 'checkbox', hidden: true, checked: local_assigns[:message_per_submitter] != false && template_submitters.size < 5 && template&.preferences&.dig('submitters').to_a.size > 1 %>
<%= tag.input id: toggle_uuid = SecureRandom.uuid, value: '1', name: 'request_email_per_submitter', class: 'peer', type: 'checkbox', hidden: true, checked: local_assigns[:message_per_submitter] != false && template_submitters.size < 11 && template&.preferences&.dig('submitters').to_a.size > 1 %>
<% if local_assigns[:viewer_submitter_uuids].present? && local_assigns[:submitter].blank? %>
<% (template_submitters.pluck('uuid') - local_assigns[:viewer_submitter_uuids].to_a).each do |signer_uuid| %>
<%= hidden_field_tag 'email_message_submitter_uuids[]', signer_uuid %>
@ -57,7 +57,7 @@
<div class="form-control">
<div class="flex justify-between">
<%= f.label :subject, t('subject'), class: 'label' %>
<% if template_submitters.size > 1 && template_submitters.size < 5 && local_assigns[:message_per_submitter] != false %>
<% if template_submitters.size > 1 && template_submitters.size < 11 && local_assigns[:message_per_submitter] != false %>
<label for="<%= toggle_uuid %>" class="label underline">
<%= t('edit_per_party') %>
</label>
@ -78,16 +78,18 @@
</div>
<%= render 'submissions/message_fields' %>
</div>
<% if template_submitters.size > 1 && template_submitters.size < 5 && local_assigns[:message_per_submitter] != false %>
<% if template_submitters.size > 1 && template_submitters.size < 11 && local_assigns[:message_per_submitter] != false %>
<div class="hidden peer-checked:block form-control space-y-2">
<% uuid = SecureRandom.uuid %>
<% options = template_submitters.map { |e| [e['name'], "request_email_#{uuid}_#{e['uuid']}"] } %>
<toggle-visible data-element-ids="<%= options.map(&:last).to_json %>" class="flex relative px-1">
<ul class="tabs w-full flex flex-nowrap">
<ul class="tabs w-full min-w-0 flex flex-nowrap">
<% options.each_with_index do |(label, val), index| %>
<div class="w-full">
<div class="w-full min-w-0 has-[:checked]:min-w-fit">
<%= f.radio_button :selected, val, checked: index.zero?, id: "#{val}_radio", data: { action: 'click:toggle-visible#trigger' }, class: 'hidden peer' %>
<%= f.label :selected, label, value: val, for: "#{val}_radio", class: 'tab w-full tab-lifted peer-checked:tab-active !bg-transparent' %>
<%= f.label :selected, value: val, for: "#{val}_radio", class: 'tab w-full tab-lifted peer-checked:tab-active !bg-transparent !px-2' do %>
<span class="truncate" title="<%= label %>"><%= label %></span>
<% end %>
</div>
<% end %>
</ul>

@ -29,7 +29,8 @@
<div>
<% timezone = local_assigns[:with_submitter_timezone] ? (submitter.timezone || local_assigns[:timezone]) : local_assigns[:timezone] %>
<% time_format = local_assigns[:with_timestamp_seconds] ? :detailed : :long %>
<%= l(attachment.created_at.in_time_zone(timezone), format: time_format, locale: local_assigns[:locale]) %> <%= TimeUtils.timezone_abbr(timezone, attachment.created_at) %>
<% signature_timestamp = (local_assigns[:with_signature_id_completed_at] ? submitter.completed_at : nil) || attachment.created_at %>
<%= l(signature_timestamp.in_time_zone(timezone), format: time_format, locale: local_assigns[:locale]) %> <%= TimeUtils.timezone_abbr(timezone, signature_timestamp) %>
</div>
</div>
<% end %>

@ -2,12 +2,13 @@
<%= render 'submissions/preview_tags' %>
<% end %>
<% font_scale = 1040.0 / PdfUtils::US_LETTER_W %>
<% configs = AccountConfig.where(account_id: @submission.account_id, key: [AccountConfig::COMBINE_PDF_RESULT_KEY, AccountConfig::WITH_SIGNATURE_ID, AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY, AccountConfig::WITH_SIGNATURE_ID_REASON_KEY, AccountConfig::WITH_TIMESTAMP_SECONDS_KEY]) %>
<% configs = AccountConfig.where(account_id: @submission.account_id, key: [AccountConfig::COMBINE_PDF_RESULT_KEY, AccountConfig::WITH_SIGNATURE_ID, AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY, AccountConfig::WITH_SIGNATURE_ID_REASON_KEY, AccountConfig::WITH_TIMESTAMP_SECONDS_KEY, AccountConfig::WITH_SIGNATURE_ID_COMPLETED_AT_KEY]) %>
<% with_signature_id = configs.find { |e| e.key == AccountConfig::WITH_SIGNATURE_ID }&.value == true %>
<% is_combined_enabled = configs.find { |e| e.key == AccountConfig::COMBINE_PDF_RESULT_KEY }&.value == true && !@submission.template_fields&.any? { |f| f['type'] == 'verification' } %>
<% with_submitter_timezone = configs.find { |e| e.key == AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY }&.value == true %>
<% with_timestamp_seconds = configs.find { |e| e.key == AccountConfig::WITH_TIMESTAMP_SECONDS_KEY }&.value == true %>
<% with_signature_id_reason = configs.find { |e| e.key == AccountConfig::WITH_SIGNATURE_ID_REASON_KEY }&.value != false %>
<% with_signature_id_completed_at = configs.find { |e| e.key == AccountConfig::WITH_SIGNATURE_ID_COMPLETED_AT_KEY }&.value == true %>
<main style="max-width: 1600px" class="mx-auto pl-4">
<div class="flex justify-between py-1.5 items-center pr-4 sticky top-0 md:relative z-10 bg-base-100">
<a href="<%= signed_in? && @submission.account_id == current_account&.id && @submission.template ? template_path(@submission.template) : '/' %>" class="flex items-center space-x-3 py-1">
@ -144,7 +145,7 @@
</span>
</span>
<% else %>
<%= render 'submissions/value', page_width: width, page_height: height, font_scale:, area:, field:, attachments_index:, value: mask.present? ? Array.wrap(value).map { |e| TextUtils.mask_value(e, mask) }.join(', ') : value, locale: @submission.account.locale, timezone: @submission.account.timezone, submitter:, with_signature_id:, with_submitter_timezone:, with_timestamp_seconds:, with_signature_id_reason: %>
<%= render 'submissions/value', page_width: width, page_height: height, font_scale:, area:, field:, attachments_index:, value: mask.present? ? Array.wrap(value).map { |e| TextUtils.mask_value(e, mask) }.join(', ') : value, locale: @submission.account.locale, timezone: @submission.account.timezone, submitter:, with_signature_id:, with_submitter_timezone:, with_timestamp_seconds:, with_signature_id_reason:, with_signature_id_completed_at: %>
<% end %>
<% elsif field['readonly'] != true && submitter && !submitter.completed_at? %>
<% submitters_order_index ||= (@submission.template_submitters || @submission.template.submitters).each_with_index.to_h { |s, i| [s['uuid'], i] } %>

@ -152,7 +152,7 @@
<% next if field['conditions'].present? && values[field['uuid']].blank? && field['submitter_uuid'] != @submitter.uuid %>
<% next if field['conditions'].present? && field['submitter_uuid'] == @submitter.uuid %>
<% next if field.dig('preferences', 'formula').present? && field['submitter_uuid'] == @submitter.uuid %>
<%= render 'submissions/value', page_width: width, page_height: height, font_scale:, area:, field:, attachments_index: @attachments_index, value: field.dig('preferences', 'mask').present? ? TextUtils.mask_value(value, field.dig('preferences', 'mask')) : value, locale: @submitter.account.locale, timezone: @submitter.account.timezone, submitter: submitters_index[field['submitter_uuid']], with_signature_id: @form_configs[:with_signature_id], with_submitter_timezone: @form_configs[:with_submitter_timezone], with_timestamp_seconds: @form_configs[:with_timestamp_seconds], with_signature_id_reason: @form_configs[:with_signature_id_reason] %>
<%= render 'submissions/value', page_width: width, page_height: height, font_scale:, area:, field:, attachments_index: @attachments_index, value: field.dig('preferences', 'mask').present? ? TextUtils.mask_value(value, field.dig('preferences', 'mask')) : value, locale: @submitter.account.locale, timezone: @submitter.account.timezone, submitter: submitters_index[field['submitter_uuid']], with_signature_id: @form_configs[:with_signature_id], with_submitter_timezone: @form_configs[:with_submitter_timezone], with_timestamp_seconds: @form_configs[:with_timestamp_seconds], with_signature_id_reason: @form_configs[:with_signature_id_reason], with_signature_id_completed_at: @form_configs[:with_signature_id_completed_at] %>
<% end %>
</div>
</page-container>

@ -6,4 +6,7 @@
<%= button_to nil, user_configs_path, method: :post, params: { user_config: { key: UserConfig::SHOW_APP_TOUR, value: true } }, class: 'hidden', id: 'start_tour_button' %>
<% end %>
<% end %>
<template-builder class="grid" data-template="<%= @template_data.to_json %>" data-custom-fields="<%= (current_account.account_configs.find_or_initialize_by(key: AccountConfig::TEMPLATE_CUSTOM_FIELDS_KEY).value || []).to_json %>" data-with-sign-yourself-button="<%= !@template.archived_at? %>" data-with-fields-detection="true" data-with-send-button="<%= !@template.archived_at? && can?(:create, @template.submissions.new(account: current_account)) %>" data-with-revisions-menu="<%= @template.template_versions.exists? %>" data-locale="<%= I18n.locale %>" data-show-tour-start-form="<%= @show_tour_start_form %>"></template-builder>
<% account_configs = current_account.account_configs.where(key: [AccountConfig::TEMPLATE_CUSTOM_FIELDS_KEY, AccountConfig::TEMPLATE_DATE_FORMATS_KEY]).to_a %>
<% custom_fields = account_configs.find { |e| e.key == AccountConfig::TEMPLATE_CUSTOM_FIELDS_KEY }&.value || [] %>
<% date_formats = account_configs.find { |e| e.key == AccountConfig::TEMPLATE_DATE_FORMATS_KEY }&.value || [] %>
<template-builder class="grid" data-template="<%= @template_data.to_json %>" data-custom-fields="<%= custom_fields.to_json %>" data-date-formats="<%= date_formats.to_json %>" data-with-sign-yourself-button="<%= !@template.archived_at? %>" data-with-fields-detection="true" data-with-send-button="<%= !@template.archived_at? && can?(:create, @template.submissions.new(account: current_account)) %>" data-with-revisions-menu="<%= @template.template_versions.exists? %>" data-locale="<%= I18n.locale %>" data-show-tour-start-form="<%= @show_tour_start_form %>"></template-builder>

@ -26,7 +26,7 @@
<%= t('share_template_with_test_mode') %>
</span>
<submit-form data-on="change">
<%= f.check_box :value, class: 'toggle', checked: @template.template_sharings.exists?(account_id: current_account.testing_accounts) %>
<%= f.check_box :value, class: 'toggle', checked: @template.template_sharings.exists?(account_id: true_user.account.testing_accounts) %>
</submit-form>
</div>
<% end %>

@ -5,7 +5,7 @@
<% viewer_template_email_preferences_values = @template.preferences.values_at('invitation_view_email_subject', 'invitation_view_email_body').compact_blank.presence %>
<% field_submitter_uuids = Set.new(@template.fields.pluck('submitter_uuid')) %>
<% is_custom_template_email = template_email_preferences_values.present? %>
<% multiple_submitters = @template.submitters.size > 1 && @template.submitters.size < 5 %>
<% multiple_submitters = @template.submitters.size > 1 && @template.submitters.size < 11 %>
<% if is_custom_template_email || @template.preferences['submitters'].to_a.any? %>
<%= button_to nil, template_preferences_path(@template), id: 'submitter_invitation_email_reset_link', method: :delete, class: 'hidden', params: { config_key: AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY }, data: { turbo_confirm: t('are_you_sure_') }, form: { data: { close_on_submit: false } } %>
<% end %>
@ -39,11 +39,13 @@
<div class="hidden peer-checked:block">
<% options = @template.submitters.map { |e| [e['name'], "request_email_#{e['uuid']}"] } %>
<toggle-visible data-element-ids="<%= options.map(&:last).to_json %>" class="flex relative px-1">
<ul class="tabs w-full flex flex-nowrap mb-2">
<ul class="tabs w-full min-w-0 flex flex-nowrap mb-2">
<% options.each_with_index do |(label, val), index| %>
<div class="w-full">
<div class="w-full min-w-0 has-[:checked]:min-w-fit">
<%= f.radio_button :selected, val, checked: index.zero?, id: "#{val}_radio", data: { action: 'click:toggle-visible#trigger' }, class: 'hidden peer' %>
<%= f.label :selected, label, value: val, for: "#{val}_radio", class: 'tab w-full tab-lifted peer-checked:tab-active' %>
<%= f.label :selected, value: val, for: "#{val}_radio", class: 'tab w-full tab-lifted peer-checked:tab-active !px-2' do %>
<span class="truncate" title="<%= label %>"><%= label %></span>
<% end %>
</div>
<% end %>
</ul>

@ -105,7 +105,7 @@
<div class="collapse-title text-xl font-medium">
<%= t('signature_request_email') %>
</div>
<div class="collapse-content">
<div class="collapse-content min-w-0">
<%= render 'templates_preferences/submitter_invitation_email_form' %>
</div>
</div>
@ -190,7 +190,7 @@
<%= t('share_template_with_test_mode') %>
</span>
<submit-form data-on="change" class="flex">
<%= f.check_box :value, class: 'toggle', checked: @template.template_sharings.exists?(account_id: current_account.testing_accounts) %>
<%= f.check_box :value, class: 'toggle', checked: @template.template_sharings.exists?(account_id: true_user.account.testing_accounts) %>
</submit-form>
</div>
<% end %>

@ -35,7 +35,9 @@ module ReplaceEmailVariables
text = replace(text, TEMPLATE_NAME, html_escape:) do
(submitter.template || submitter.submission.template || submitter.submission).name
end
text = replace(text, SUBMISSION_NAME, html_escape:) { submitter.submission.name }
text = replace(text, SUBMISSION_NAME, html_escape:) do
submitter.submission.name.presence || submitter.submission.template&.name
end
text = replace(text, TEMPLATE_ID, html_escape:) { submitter.template.id }
text = replace(text, SUBMITTER_ID, html_escape:) { submitter.id }
text = replace(text, SUBMITTER_SLUG, html_escape:) { submitter.slug }

@ -157,24 +157,21 @@ module Submissions
submissions.each_with_index do |submission, index|
delay_seconds = (delay + index).seconds if delay
template_submitters = submission.template_submitters
submitters_index = submission.submitters.reject(&:completed_at?).index_by(&:uuid)
if template_submitters.any? { |s| s['order'] }
min_order = template_submitters.map.with_index { |s, i| s['order'] || i }.min
first_submitters = template_submitters.filter_map do |s|
submitters_index[s['uuid']] if s['order'] == min_order
end
if submission.template_submitters.any? { |s| s['order'] }
first_submitters = find_first_order_submitters(submission, submitters_index)
Submitters.send_signature_requests(first_submitters, delay_seconds:)
elsif submission.submitters_order_preserved?
first_submitter = template_submitters.filter_map { |s| submitters_index[s['uuid']] }.find { |s| !s.viewer? }
first_submitter = find_first_submitter(submission, submitters_index)
if first_submitter
first_viewers = find_first_viewers(first_submitter, submitters_index)
Submitters.send_signature_requests([first_submitter, *first_viewers], delay_seconds:)
elsif submission.submitters.all?(&:viewer?)
Submitters.send_signature_requests(submitters_index.values, delay_seconds:)
end
else
Submitters.send_signature_requests(submitters_index.values, delay_seconds:)
@ -182,6 +179,18 @@ module Submissions
end
end
def find_first_order_submitters(submission, submitters_index)
template_submitters = submission.template_submitters
min_order = template_submitters.map.with_index { |s, i| s['order'] || i }.min
template_submitters.filter_map { |s| submitters_index[s['uuid']] if s['order'] == min_order }
end
def find_first_submitter(submission, submitters_index)
submission.template_submitters.filter_map { |s| submitters_index[s['uuid']] }.find { |s| !s.viewer? }
end
def find_first_viewers(first_submitter, submitters_index)
first_submitter.submission.template_submitters.each_with_object([]) do |s, viewers|
submitter = submitters_index[s['uuid']]

@ -18,7 +18,8 @@ module Submissions
AccountConfig::WITH_TIMESTAMP_SECONDS_KEY,
AccountConfig::ROTATE_INCREMENTAL_PDF_KEY,
AccountConfig::WITH_FILE_LINKS_KEY,
AccountConfig::WITH_SIGNATURE_ID_REASON_KEY])
AccountConfig::WITH_SIGNATURE_ID_REASON_KEY,
AccountConfig::WITH_SIGNATURE_ID_COMPLETED_AT_KEY])
with_signature_id = configs.find { |c| c.key == AccountConfig::WITH_SIGNATURE_ID }&.value == true
with_file_links = configs.find { |c| c.key == AccountConfig::WITH_FILE_LINKS_KEY }&.value == true
@ -28,6 +29,8 @@ module Submissions
with_timestamp_seconds = configs.find { |c| c.key == AccountConfig::WITH_TIMESTAMP_SECONDS_KEY }&.value == true
with_signature_id_reason =
configs.find { |c| c.key == AccountConfig::WITH_SIGNATURE_ID_REASON_KEY }&.value != false
with_signature_id_completed_at =
configs.find { |c| c.key == AccountConfig::WITH_SIGNATURE_ID_COMPLETED_AT_KEY }&.value == true
file_links_expire_at = Accounts.link_expires_at(submission.account) if with_file_links
@ -45,7 +48,7 @@ module Submissions
with_signature_id:, is_flatten:, with_headings: index.zero?,
with_submitter_timezone:, with_file_links:,
file_links_expire_at:, with_signature_id_reason:,
with_timestamp_seconds:)
with_timestamp_seconds:, with_signature_id_completed_at:)
end
template = submission.template

@ -140,7 +140,8 @@ module Submissions
AccountConfig::WITH_TIMESTAMP_SECONDS_KEY,
AccountConfig::ROTATE_INCREMENTAL_PDF_KEY,
AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY,
AccountConfig::WITH_SIGNATURE_ID_REASON_KEY])
AccountConfig::WITH_SIGNATURE_ID_REASON_KEY,
AccountConfig::WITH_SIGNATURE_ID_COMPLETED_AT_KEY])
with_signature_id = configs.find { |c| c.key == AccountConfig::WITH_SIGNATURE_ID }&.value == true
is_flatten = configs.find { |c| c.key == AccountConfig::FLATTEN_RESULT_PDF_KEY }&.value != false
@ -150,6 +151,8 @@ module Submissions
with_file_links = configs.find { |c| c.key == AccountConfig::WITH_FILE_LINKS_KEY }&.value == true
with_signature_id_reason =
configs.find { |c| c.key == AccountConfig::WITH_SIGNATURE_ID_REASON_KEY }&.value != false
with_signature_id_completed_at =
configs.find { |c| c.key == AccountConfig::WITH_SIGNATURE_ID_COMPLETED_AT_KEY }&.value == true
file_links_expire_at = Accounts.link_expires_at(submitter.account) if with_file_links
@ -201,12 +204,14 @@ module Submissions
with_file_links:,
with_timestamp_seconds:,
with_signature_id_reason:,
with_signature_id_completed_at:,
file_links_expire_at:)
end
def fill_submitter_fields(submitter, account, pdfs_index, with_signature_id:, is_flatten:, with_headings: nil,
with_submitter_timezone: false, with_signature_id_reason: true,
with_timestamp_seconds: false, with_file_links: nil,
with_timestamp_seconds: false, with_signature_id_completed_at: false,
with_file_links: nil,
file_links_expire_at: Accounts.link_expires_at(account))
cell_layouters = Hash.new do |hash, valign|
hash[valign] = HexaPDF::Layout::TextLayouter.new(text_valign: valign.to_sym, text_align: :center)
@ -337,14 +342,17 @@ module Submissions
time_format = with_timestamp_seconds ? :detailed : :long
signature_timestamp =
(with_signature_id_completed_at ? submitter.completed_at : nil) || attachment.created_at
if with_signature_id_reason || field.dig('preferences', 'reasons').present?
"#{"#{I18n.t('reason')}: " if reason_value}#{reason_value || I18n.t('digitally_signed_by')} " \
"#{submitter.name}#{" <#{submitter.email}>" if submitter.email.present?}\n" \
"#{I18n.l(attachment.created_at.in_time_zone(timezone), format: time_format)} " \
"#{TimeUtils.timezone_abbr(timezone, attachment.created_at)}"
"#{I18n.l(signature_timestamp.in_time_zone(timezone), format: time_format)} " \
"#{TimeUtils.timezone_abbr(timezone, signature_timestamp)}"
else
"#{I18n.l(attachment.created_at.in_time_zone(timezone), format: time_format)} " \
"#{TimeUtils.timezone_abbr(timezone, attachment.created_at)}"
"#{I18n.l(signature_timestamp.in_time_zone(timezone), format: time_format)} " \
"#{TimeUtils.timezone_abbr(timezone, signature_timestamp)}"
end
end

@ -213,9 +213,9 @@ module Submitters
def build_document_filename(submitter, blob, filename_format)
return blob.filename.to_s if filename_format.blank?
filename = ReplaceEmailVariables.call(filename_format, submitter:)
filename = filename_format.gsub('{document.name}', blob.filename.base)
filename = ReplaceEmailVariables.call(filename, submitter:)
filename = filename.gsub('{document.name}', blob.filename.base)
filename = filename.gsub(' - {submission.status}') do
if submitter.submission.completed_at?
status =

@ -18,6 +18,7 @@ module Submitters
AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY,
AccountConfig::WITH_TIMESTAMP_SECONDS_KEY,
AccountConfig::WITH_SIGNATURE_ID_REASON_KEY,
AccountConfig::WITH_SIGNATURE_ID_COMPLETED_AT_KEY,
*(Docuseal.multitenant? ? [] : [AccountConfig::POLICY_LINKS_KEY])].freeze
module_function
@ -40,6 +41,8 @@ module Submitters
with_submitter_timezone = find_safe_value(configs, AccountConfig::WITH_SUBMITTER_TIMEZONE_KEY) == true
with_timestamp_seconds = find_safe_value(configs, AccountConfig::WITH_TIMESTAMP_SECONDS_KEY) == true
with_signature_id_reason = find_safe_value(configs, AccountConfig::WITH_SIGNATURE_ID_REASON_KEY) != false
with_signature_id_completed_at =
find_safe_value(configs, AccountConfig::WITH_SIGNATURE_ID_COMPLETED_AT_KEY) == true
with_field_labels = find_safe_value(configs, AccountConfig::WITH_FIELD_LABELS_KEY) != false
policy_links = find_safe_value(configs, AccountConfig::POLICY_LINKS_KEY)
@ -47,7 +50,8 @@ module Submitters
reuse_signature:, with_decline:, with_delegate:, with_partial_download:,
policy_links:, enforce_signing_order:, completed_message:,
require_signing_reason:, prefill_signature:, with_submitter_timezone:,
with_signature_id_reason:, with_signature_id:, with_field_labels:, with_timestamp_seconds: }
with_signature_id_reason:, with_signature_id:, with_field_labels:, with_timestamp_seconds:,
with_signature_id_completed_at: }
keys.each do |key|
attrs[key.to_sym] = configs.find { |e| e.key == key.to_s }&.value

@ -286,7 +286,10 @@ module Submitters
attachments_index =
if has_document_conditions
Submissions.filtered_conditions_schema(submission).index_by { |i| i['attachment_uuid'] }
submitters_values = merge_submitters_values(submitter)
Submissions.filtered_conditions_schema(submission, values: submitters_values)
.index_by { |i| i['attachment_uuid'] }
end
submission.template_fields.each do |field|
@ -295,8 +298,7 @@ module Submitters
required_field_uuids_acc.add(field['uuid']) if required_field_uuids_acc && required_editable_field?(field)
if has_document_conditions && !check_field_areas_attachments(field, attachments_index)
submitter.values.delete(field['uuid'])
required_field_uuids_acc&.delete(field['uuid'])
delete_field_value!(field, submitter, submitters_values, required_field_uuids_acc)
end
if has_other_submitters && !submitters_values &&
@ -305,14 +307,19 @@ module Submitters
end
unless check_field_conditions(submitters_values || submitter.values, field, submission.fields_uuid_index)
submitter.values.delete(field['uuid'])
required_field_uuids_acc&.delete(field['uuid'])
delete_field_value!(field, submitter, submitters_values, required_field_uuids_acc)
end
end
submitter.values
end
def delete_field_value!(field, submitter, submitters_values = nil, required_field_uuids_acc = nil)
submitter.values.delete(field['uuid'])
submitters_values&.delete(field['uuid'])
required_field_uuids_acc&.delete(field['uuid'])
end
def submission_has_document_conditions?(submission)
(submission.template_schema || submission.template.schema).any? { |e| e['conditions'].present? }
end
@ -331,6 +338,7 @@ module Submitters
def merge_submitters_values(submitter)
submitter.submission.submitters
.reject { |sub| sub.uuid == submitter.uuid }
.reduce({}) { |acc, sub| acc.merge(sub.values) }
.merge(submitter.values)
end

@ -59,7 +59,6 @@ module Templates
.select(1).arel.exists
Template.where(id: TemplateSharing.where(account_id: shared_account_ids).select(:template_id))
.where(account_id: account.linked_account_account.account_id)
.where.not(exists_access)
end

@ -4,9 +4,9 @@ module Templates
module SerializeForApi
SERIALIZE_PARAMS = {
only: %w[
id archived_at fields name preferences schema
slug source submitters created_at updated_at
author_id external_id folder_id shared_link
id name slug schema submitters fields variables_schema
preferences source created_at updated_at archived_at author_id
external_id folder_id shared_link
],
methods: %i[application_key folder_name],
include: { author: { only: %i[id email first_name last_name] } }
@ -22,7 +22,16 @@ module Templates
preview_image_attachments ||= preload_preview_image_attachments(schema_documents, dynamic_documents)
json['documents'] = template.schema.filter_map do |item|
json['variables_schema'] ||= {}
json['documents'] = build_documents_array(template, schema_documents, dynamic_documents,
preview_image_attachments, expires_at)
json
end
def build_documents_array(template, schema_documents, dynamic_documents, preview_image_attachments, expires_at)
template.schema.filter_map do |item|
if item['dynamic']
dynamic_document = dynamic_documents.find { |e| e.uuid == item['attachment_uuid'] }
@ -44,8 +53,6 @@ module Templates
'filename' => attachment.filename
}
end
json
end
def preload_dynamic_documents(template)

@ -230,6 +230,7 @@ describe 'Templates API' do
name: 'sample-document'
}
],
variables_schema: template.variables_schema || {},
shared_link: template.shared_link,
author_id: author.id,
archived_at: nil,

Loading…
Cancel
Save