diff --git a/.rubocop.yml b/.rubocop.yml index a679c86a..e3315548 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -66,7 +66,7 @@ RSpec/ExampleLength: Max: 50 RSpec/MultipleMemoizedHelpers: - Max: 9 + Max: 15 Metrics/BlockNesting: Max: 5 diff --git a/app/controllers/api/attachments_controller.rb b/app/controllers/api/attachments_controller.rb index eacabbd6..37e827b5 100644 --- a/app/controllers/api/attachments_controller.rb +++ b/app/controllers/api/attachments_controller.rb @@ -10,6 +10,12 @@ module Api def create 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) if params[:remember_signature] == 'true' && submitter.email.present? diff --git a/app/controllers/api/submissions_controller.rb b/app/controllers/api/submissions_controller.rb index af784ba2..ef735dfd 100644 --- a/app/controllers/api/submissions_controller.rb +++ b/app/controllers/api/submissions_controller.rb @@ -187,11 +187,12 @@ module Api def submissions_params permitted_attrs = [ :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], submitters: [[:send_email, :send_sms, :completed_redirect_url, :uuid, :name, :email, :role, :completed, :phone, :application_key, :external_id, :reply_to, :go_to_last, + :require_phone_2fa, { metadata: {}, values: {}, roles: [], readonly_fields: [], message: %i[subject body], fields: [:name, :uuid, :default_value, :value, :title, :description, :readonly, :required, :validation_pattern, :invalid_message, diff --git a/app/controllers/api/submitters_controller.rb b/app/controllers/api/submitters_controller.rb index 3b9a4680..70fb107f 100644 --- a/app/controllers/api/submitters_controller.rb +++ b/app/controllers/api/submitters_controller.rb @@ -82,7 +82,7 @@ module Api submitter_params.permit( :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], fields: [[:name, :uuid, :default_value, :value, :required, :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['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') submitter.preferences['go_to_last'] = submitter_preferences['go_to_last'] end diff --git a/app/controllers/start_form_controller.rb b/app/controllers/start_form_controller.rb index fb2a9786..5fd663b0 100644 --- a/app/controllers/start_form_controller.rb +++ b/app/controllers/start_form_controller.rb @@ -33,12 +33,12 @@ class StartFormController < ApplicationController @submitter = find_or_initialize_submitter(@template, submitter_params) 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 if filter_undefined_submitters(@template).size > 1 && @submitter.new_record? @error_message = multiple_submitters_error_message - return render :show + return render :show, status: :unprocessable_entity end 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) end - if @submitter.save + if @submitter.errors.blank? && @submitter.save if is_new_record enqueue_submission_create_webhooks(@submitter) @@ -63,7 +63,7 @@ class StartFormController < ApplicationController redirect_to submit_form_path(@submitter.slug) else - render :show + render :show, status: :unprocessable_entity end end end @@ -71,9 +71,20 @@ class StartFormController < ApplicationController def completed 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) .where.not(completed_at: nil) - .find_by!(email: params[:email]) + .find_by!(required_params) end private @@ -104,7 +115,16 @@ class StartFormController < ApplicationController end def find_or_initialize_submitter(template, submitter_params) - Submitter + 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 .where(submission: template.submissions.where(expire_at: Time.current..) .or(template.submissions.where(expire_at: nil)).where(archived_at: nil)) .order(id: :desc) @@ -112,7 +132,17 @@ class StartFormController < ApplicationController .where(external_id: nil) .where(ip: [nil, request.remote_ip]) .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 def assign_submission_attributes(submitter, template) @@ -125,6 +155,8 @@ class StartFormController < ApplicationController metadata: @resubmit_submitter&.metadata.presence || {} ) + submitter.assign_attributes(@resubmit_submitter.slice(:name, :email, :phone)) if @resubmit_submitter + if submitter.values.present? @resubmit_submitter.attachments.each do |attachment| submitter.attachments << attachment.dup if submitter.values.value?(attachment.uuid) diff --git a/app/controllers/submit_form_draw_signature_controller.rb b/app/controllers/submit_form_draw_signature_controller.rb index f8352ade..773eb9e7 100644 --- a/app/controllers/submit_form_draw_signature_controller.rb +++ b/app/controllers/submit_form_draw_signature_controller.rb @@ -12,7 +12,7 @@ class SubmitFormDrawSignatureController < ApplicationController 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) end diff --git a/app/controllers/submit_form_invite_controller.rb b/app/controllers/submit_form_invite_controller.rb index 1d42779c..0f400ad9 100644 --- a/app/controllers/submit_form_invite_controller.rb +++ b/app/controllers/submit_form_invite_controller.rb @@ -45,7 +45,7 @@ class SubmitFormInviteController < ApplicationController !submitter.completed_at? && !submitter.submission.archived_at? && !submitter.submission.expired? && - !submitter.submission.template.archived_at? + !submitter.submission.template&.archived_at? end def filter_invite_submitters(submitter, key = 'invite_by_uuid') diff --git a/app/controllers/submit_form_values_controller.rb b/app/controllers/submit_form_values_controller.rb index 2c4a2ad3..e1a6b9ab 100644 --- a/app/controllers/submit_form_values_controller.rb +++ b/app/controllers/submit_form_values_controller.rb @@ -8,7 +8,7 @@ class SubmitFormValuesController < ApplicationController submitter = Submitter.find_by!(slug: params[:submit_form_slug]) 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.expired? diff --git a/app/controllers/templates_dashboard_controller.rb b/app/controllers/templates_dashboard_controller.rb index 51922c48..69843d15 100644 --- a/app/controllers/templates_dashboard_controller.rb +++ b/app/controllers/templates_dashboard_controller.rb @@ -55,10 +55,8 @@ class TemplatesDashboardController < ApplicationController rel = Template.where( Template.arel_table[:id].in( - Arel::Nodes::Union.new( - rel.where(folder_id: current_account.default_template_folder.id).select(:id).arel, - shared_template_ids.arel - ) + rel.where(folder_id: current_account.default_template_folder.id).select(:id).arel + .union(shared_template_ids.arel) ) ) else diff --git a/app/controllers/templates_preferences_controller.rb b/app/controllers/templates_preferences_controller.rb index e2ec9ee3..eae2fa9f 100644 --- a/app/controllers/templates_preferences_controller.rb +++ b/app/controllers/templates_preferences_controller.rb @@ -31,7 +31,7 @@ class TemplatesPreferencesController < ApplicationController completed_notification_email_subject completed_notification_email_body completed_notification_email_enabled completed_notification_email_attach_audit] + [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| attrs[:preferences].delete(:submitters) if params[:request_email_per_submitter] != '1' diff --git a/app/controllers/templates_share_link_controller.rb b/app/controllers/templates_share_link_controller.rb index 5b84f6ca..5dfed111 100644 --- a/app/controllers/templates_share_link_controller.rb +++ b/app/controllers/templates_share_link_controller.rb @@ -10,7 +10,11 @@ class TemplatesShareLinkController < ApplicationController @template.update!(template_params) - head :ok + if params[:redir].present? + redirect_to params[:redir] + else + head :ok + end end private diff --git a/app/javascript/application.js b/app/javascript/application.js index f951b8fe..6ff4b401 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -35,6 +35,7 @@ import SetDateButton from './elements/set_date_button' import IndeterminateCheckbox from './elements/indeterminate_checkbox' import AppTour from './elements/app_tour' import DashboardDropzone from './elements/dashboard_dropzone' +import RequiredCheckboxGroup from './elements/required_checkbox_group' import * as TurboInstantClick from './lib/turbo_instant_click' @@ -105,6 +106,7 @@ safeRegisterElement('indeterminate-checkbox', IndeterminateCheckbox) safeRegisterElement('app-tour', AppTour) safeRegisterElement('dashboard-dropzone', DashboardDropzone) safeRegisterElement('check-on-click', CheckOnClick) +safeRegisterElement('required-checkbox-group', RequiredCheckboxGroup) safeRegisterElement('template-builder', class extends HTMLElement { connectedCallback () { diff --git a/app/javascript/elements/check_on_click.js b/app/javascript/elements/check_on_click.js index 8b3b9ab8..e940dff3 100644 --- a/app/javascript/elements/check_on_click.js +++ b/app/javascript/elements/check_on_click.js @@ -1,7 +1,7 @@ export default class extends HTMLElement { connectedCallback () { this.addEventListener('click', () => { - if (!this.element.checked) { + if (this.element && !this.element.disabled && !this.element.checked) { this.element.checked = true this.element.dispatchEvent(new Event('change', { bubbles: true })) } diff --git a/app/javascript/elements/required_checkbox_group.js b/app/javascript/elements/required_checkbox_group.js new file mode 100644 index 00000000..e4dc13d7 --- /dev/null +++ b/app/javascript/elements/required_checkbox_group.js @@ -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 + } +} diff --git a/app/javascript/submission_form/initials_step.vue b/app/javascript/submission_form/initials_step.vue index 531b9784..376fb7e9 100644 --- a/app/javascript/submission_form/initials_step.vue +++ b/app/javascript/submission_form/initials_step.vue @@ -203,7 +203,7 @@ export default { emits: ['attached', 'update:model-value', 'start', 'minimize', 'focus'], data () { return { - isInitialsStarted: !!this.previousValue, + isInitialsStarted: false, isUsePreviousValue: true, isDrawInitials: false, uploadImageInputKey: Math.random().toString() @@ -218,6 +218,9 @@ export default { } } }, + created () { + this.isInitialsStarted = !!this.computedPreviousValue + }, async mounted () { this.$nextTick(() => { if (this.$refs.canvas) { @@ -362,11 +365,20 @@ export default { formData.append('file', file) formData.append('submitter_slug', this.submitterSlug) formData.append('name', 'attachments') + formData.append('type', 'initials') return fetch(this.baseUrl + '/api/attachments', { method: 'POST', 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('update:model-value', attachment.uuid) diff --git a/app/javascript/submission_form/signature_step.vue b/app/javascript/submission_form/signature_step.vue index 2011b77a..d1625726 100644 --- a/app/javascript/submission_form/signature_step.vue +++ b/app/javascript/submission_form/signature_step.vue @@ -201,7 +201,7 @@ 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('update:model-value', attachment.uuid) diff --git a/app/javascript/template_builder/import_list.vue b/app/javascript/template_builder/import_list.vue index eeec5185..d5f00711 100644 --- a/app/javascript/template_builder/import_list.vue +++ b/app/javascript/template_builder/import_list.vue @@ -406,7 +406,7 @@ export default { 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 }) } }) diff --git a/app/models/submission.rb b/app/models/submission.rb index 002933e5..4e61c691 100644 --- a/app/models/submission.rb +++ b/app/models/submission.rb @@ -40,7 +40,7 @@ class Submission < ApplicationRecord belongs_to :account 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 :submission_events, dependent: :destroy diff --git a/app/models/submitter.rb b/app/models/submitter.rb index b684dfbe..205a3b0c 100644 --- a/app/models/submitter.rb +++ b/app/models/submitter.rb @@ -43,7 +43,7 @@ class Submitter < ApplicationRecord belongs_to :submission belongs_to :account 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 :preferences, :string, default: -> { {} } diff --git a/app/models/template.rb b/app/models/template.rb index 41b635a0..59f2c36d 100644 --- a/app/models/template.rb +++ b/app/models/template.rb @@ -44,7 +44,7 @@ class Template < ApplicationRecord belongs_to :account 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 diff --git a/app/views/accounts/show.html.erb b/app/views/accounts/show.html.erb index 7f371b9d..ac63ff87 100644 --- a/app/views/accounts/show.html.erb +++ b/app/views/accounts/show.html.erb @@ -14,7 +14,10 @@
<%= ff.label :timezone, t('time_zone'), class: 'label' %> <%= 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 %>
diff --git a/app/views/start_form/completed.html.erb b/app/views/start_form/completed.html.erb index 003005d8..c21b3d45 100644 --- a/app/views/start_form/completed.html.erb +++ b/app/views/start_form/completed.html.erb @@ -28,7 +28,7 @@ <% 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? %> - <%= 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' %> <% end %>
diff --git a/app/views/start_form/private.html.erb b/app/views/start_form/private.html.erb index 4e16eba4..6b8104d6 100644 --- a/app/views/start_form/private.html.erb +++ b/app/views/start_form/private.html.erb @@ -1,28 +1,31 @@ <% content_for(:html_title, "#{@template.name} | DocuSeal") %> -<% content_for(:html_description, t('share_link_is_currently_disabled')) %> -
-
-
-
- <%= render 'banner' %> -

- <%= t('share_link_is_currently_disabled') %> -

+<% I18n.with_locale(@template.account.locale) do %> + <% content_for(:html_description, t('share_link_is_currently_disabled')) %> +<% end %> +
+
+ <%= render 'banner' %> +

+ <%= t('share_link_is_currently_disabled') %> +

+
+
+
+
+ <%= svg_icon('writing_sign', class: 'w-10 h-10') %>
-
-
-
- <%= svg_icon('writing_sign', class: 'w-10 h-10') %> -
-
-

<%= @template.name %>

- <% if @template.archived_at? %> -

<%= t('form_has_been_deleted_by_html', name: @template.account.name) %>

- <% end %> -
-
+
+

<%= @template.name %>

+ <% if @template.archived_at? %> +

<%= t('form_has_been_deleted_by_html', name: @template.account.name) %>

+ <% end %>
+ <% if can?(:update, @template) %> + + <%= 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' %> + + <% end %>
<%= render 'shared/attribution', link_path: '/start', account: @template.account %> diff --git a/app/views/start_form/show.html.erb b/app/views/start_form/show.html.erb index 7eb0b96f..75c42e28 100644 --- a/app/views/start_form/show.html.erb +++ b/app/views/start_form/show.html.erb @@ -1,5 +1,7 @@ <% 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 %>
@@ -27,13 +29,32 @@
<% 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| %> -
- <%= 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') %> - <% if @error_message %> - <%= @error_message %> - <% end %> -
+ <% if @error_message %> +
+ <%= svg_icon('info_circle', class: 'stroke-current shrink-0 h-6 w-6 mt-1') %> +
<%= @error_message %>
+
+ <% end %> + <% link_form_fields = @template.preferences.fetch('link_form_fields', ['email']) %> + <% multiple_fields = link_form_fields.size > 1 %> + <% if link_form_fields.include?('name') %> +
+ <%= 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') %> +
+ <% end %> + <% if link_form_fields.include?('email') %> +
+ <%= 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(multiple_fields ? 'provide_your_email' : 'provide_your_email_to_start') %> +
+ <% end %> + <% if link_form_fields.include?('phone') %> +
+ <%= 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') %> +
+ <% end %> <%= f.button button_title(title: t('start'), disabled_with: t('starting')), class: 'base-button' %> diff --git a/app/views/submissions/_value.html.erb b/app/views/submissions/_value.html.erb index 546ce26f..38ae36ea 100644 --- a/app/views/submissions/_value.html.erb +++ b/app/views/submissions/_value.html.erb @@ -28,9 +28,9 @@
<% end %>
- <% elsif field['type'].in?(['image', 'initials', 'stamp']) %> + <% elsif field['type'].in?(['image', 'initials', 'stamp']) && attachments_index[value].image? %> - <% elsif field['type'].in?(['file', 'payment']) %> + <% elsif field['type'].in?(['file', 'payment', 'image']) %>
<% Array.wrap(value).each do |val| %> diff --git a/app/views/submissions/show.html.erb b/app/views/submissions/show.html.erb index ded86629..edadd1a7 100644 --- a/app/views/submissions/show.html.erb +++ b/app/views/submissions/show.html.erb @@ -74,12 +74,14 @@ <% schema = Submissions.filtered_conditions_schema(@submission, values:) %> <% schema.each do |item| %> <% document = @submission.schema_documents.find { |a| item['attachment_uuid'] == a.uuid } %> - " onclick="[event.preventDefault(), window[event.target.closest('a').href.split('#')[1]].scrollIntoView({ behavior: 'smooth', block: 'start' })]" class="block cursor-pointer"> - -
- <%= item['name'].presence || document.filename.base %> -
-
+ <% if document.preview_images.first %> + " onclick="[event.preventDefault(), window[event.target.closest('a').href.split('#')[1]].scrollIntoView({ behavior: 'smooth', block: 'start' })]" class="block cursor-pointer"> + +
+ <%= item['name'].presence || document.filename.base %> +
+
+ <% end %> <% end %>
@@ -92,7 +94,7 @@ <% document = @submission.schema_documents.find { |e| e.uuid == item['attachment_uuid'] } %> <% 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 } : {} %> - <% 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| %> <% page = preview_images_index[index] || page_blob_struct.new(metadata: lazyload_metadata, url: preview_document_page_path(document.signed_uuid, "#{index}.jpg")) %>
" class="relative"> @@ -242,9 +244,9 @@
- <% elsif field['type'].in?(['image', 'stamp']) %> + <% elsif field['type'].in?(['image', 'stamp']) && attachments_index[value].image? %> - <% elsif field['type'] == 'file' || field['type'] == 'payment' %> + <% elsif field['type'].in?(['file', 'payment', 'image']) %>
<% Array.wrap(value).each do |val| %> diff --git a/app/views/submit_form/show.html.erb b/app/views/submit_form/show.html.erb index bdd5351f..5eb06d9a 100644 --- a/app/views/submit_form/show.html.erb +++ b/app/views/submit_form/show.html.erb @@ -1,5 +1,7 @@ <% 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) %> <% values = @submitter.submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) } %> <% submitters_index = @submitter.submission.submitters.index_by(&:uuid) %> diff --git a/app/views/templates_preferences/_recipients.html.erb b/app/views/templates_preferences/_recipients.html.erb new file mode 100644 index 00000000..0ca918ab --- /dev/null +++ b/app/views/templates_preferences/_recipients.html.erb @@ -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 %> + + <% end %> +
+ <% template.submitters.each_with_index do |submitter, index| %> +
+ <%= 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 %> +
+ <%= 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 %> + + <%= 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' %> + + <%= 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 %> +
+ <% if template.submitters.size == 2 %> + + <% if local_assigns[:with_submission_requester] != false %> + + <% end %> + <% if index == 1 %> + + <% end %> + + <% end %> + <% end %> +
+ <% end %> +
+<% 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| %> +
+ + <%= t('enforce_recipients_order') %> + + <%= 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 %> +
+ <% 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| %> +
+ + <%= t('ensure_unique_recipients') %> + + <%= 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 %> +
+ <% end %> + <% end %> +<% end %> +
+ <%= button_tag button_title(title: t('save'), disabled_with: t('updating')), class: 'base-button', form: :submitters_form %> + <% unless close_on_submit %> +
+ +
+ <% end %> +
diff --git a/app/views/templates_preferences/show.html.erb b/app/views/templates_preferences/show.html.erb index 165def27..5d7cb397 100644 --- a/app/views/templates_preferences/show.html.erb +++ b/app/views/templates_preferences/show.html.erb @@ -287,91 +287,8 @@ <%= render 'templates_code_modal/preferences', class: 'pt-2' %>
<% if show_recipients %> -