mirror of
https://github.com/docusealco/docuseal.git
synced 2026-08-07 07:14:43 +00:00
Merge from docusealco/wip
This commit is contained in:
@@ -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')
|
||||
|
||||
|
||||
@@ -9,6 +9,62 @@
|
||||
@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"
|
||||
:label="t('align')"
|
||||
@@ -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 }
|
||||
|
||||
+17
-8
@@ -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
|
||||
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user