From 672008f8af103384a170d1624d06d2218ae8e693 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Mon, 6 Jul 2026 17:32:43 +0300 Subject: [PATCH 01/12] add shared templates index param --- app/controllers/api/templates_controller.rb | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/app/controllers/api/templates_controller.rb b/app/controllers/api/templates_controller.rb index 2de64605..bda6b8d2 100644 --- a/app/controllers/api/templates_controller.rb +++ b/app/controllers/api/templates_controller.rb @@ -5,6 +5,8 @@ module Api load_and_authorize_resource :template def index + @templates = Templates.shared(current_user) if params[:shared].in?(['true', true]) + templates = filter_templates(@templates, params) templates = paginate(templates.preload(:author, folder: :parent_folder)) @@ -115,7 +117,13 @@ module Api end def filter_templates(templates, params) - templates = Templates.search(current_user, templates, params[:q]) + templates = + if params[:shared].in?(['true', true]) + Templates.search_shared(current_user, templates, params[:q]) + else + Templates.search(current_user, templates, params[:q]) + end + templates = params[:archived].in?(['true', true]) ? templates.archived : templates.active templates = templates.where(external_id: params[:application_key]) if params[:application_key].present? templates = templates.where(external_id: params[:external_id]) if params[:external_id].present? From 56da326709837a46c2e5238b626a194ceab93690 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Mon, 6 Jul 2026 18:45:37 +0300 Subject: [PATCH 02/12] adjust shared query --- lib/templates.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/templates.rb b/lib/templates.rb index 26e58a3e..44ae5cae 100644 --- a/lib/templates.rb +++ b/lib/templates.rb @@ -59,6 +59,7 @@ 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 From e1d860a42c1d286829b6f190e45a4ac4eff6429f Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Tue, 7 Jul 2026 09:14:15 +0300 Subject: [PATCH 03/12] add submission created_at index --- app/models/submission.rb | 1 + .../20260707055354_add_submission_created_at_index.rb | 7 +++++++ db/schema.rb | 3 ++- 3 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20260707055354_add_submission_created_at_index.rb diff --git a/app/models/submission.rb b/app/models/submission.rb index 8a7b75b7..f6e19683 100644 --- a/app/models/submission.rb +++ b/app/models/submission.rb @@ -31,6 +31,7 @@ # index_submissions_on_account_id_and_id_pending (account_id,id) WHERE ((completed_at IS NULL) AND (archived_at IS NULL)) # index_submissions_on_account_id_and_template_id_and_id (account_id,template_id,id) WHERE (archived_at IS NULL) # index_submissions_on_account_id_and_template_id_and_id_archived (account_id,template_id,id) WHERE (archived_at IS NOT NULL) +# index_submissions_on_created_at (created_at) # index_submissions_on_created_by_user_id (created_by_user_id) # index_submissions_on_slug (slug) UNIQUE # index_submissions_on_template_id (template_id) diff --git a/db/migrate/20260707055354_add_submission_created_at_index.rb b/db/migrate/20260707055354_add_submission_created_at_index.rb new file mode 100644 index 00000000..2b68d6ec --- /dev/null +++ b/db/migrate/20260707055354_add_submission_created_at_index.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddSubmissionCreatedAtIndex < ActiveRecord::Migration[8.1] + def change + add_index :submissions, :created_at, if_not_exists: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 71023267..7e7ca23e 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.1].define(version: 2026_07_01_165617) do +ActiveRecord::Schema[8.1].define(version: 2026_07_07_055354) do # These are extensions that must be enabled in order to support this database enable_extension "btree_gin" enable_extension "pg_catalog.plpgsql" @@ -379,6 +379,7 @@ ActiveRecord::Schema[8.1].define(version: 2026_07_01_165617) do t.index ["account_id", "id"], name: "index_submissions_on_account_id_and_id_pending", where: "((completed_at IS NULL) AND (archived_at IS NULL))" t.index ["account_id", "template_id", "id"], name: "index_submissions_on_account_id_and_template_id_and_id", where: "(archived_at IS NULL)" t.index ["account_id", "template_id", "id"], name: "index_submissions_on_account_id_and_template_id_and_id_archived", where: "(archived_at IS NOT NULL)" + t.index ["created_at"], name: "index_submissions_on_created_at" t.index ["created_by_user_id"], name: "index_submissions_on_created_by_user_id" t.index ["slug"], name: "index_submissions_on_slug", unique: true t.index ["template_id"], name: "index_submissions_on_template_id" From 8a60f17d13b688fbdcb1451925693d5848192f04 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Tue, 7 Jul 2026 09:41:37 +0300 Subject: [PATCH 04/12] optimize created_at filter --- lib/submissions/filter.rb | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/lib/submissions/filter.rb b/lib/submissions/filter.rb index 7da3f534..f338a877 100644 --- a/lib/submissions/filter.rb +++ b/lib/submissions/filter.rb @@ -19,6 +19,8 @@ module Submissions created_at_to ].freeze + BIGINT_MAX = (2**63) - 1 + module_function def call(submissions, current_user, params) @@ -71,15 +73,39 @@ module Submissions end def filter_by_created_at(submissions, filters) - submissions = submissions.where(created_at: filters[:created_at_from]..) if filters[:created_at_from].present? + if filters[:created_at_from].present? + submissions = submissions.where(min_created_at_id_arel(filters[:created_at_from])) + end if filters[:created_at_to].present? - submissions = submissions.where(created_at: ..filters[:created_at_to].end_of_day) + submissions = submissions.where(max_created_at_id_arel(filters[:created_at_to].end_of_day)) end submissions end + def min_created_at_id_arel(time) + submissions = Submission.arel_table + + first_id = submissions.project(submissions[:id]) + .where(submissions[:created_at].gteq(time)) + .order(submissions[:created_at].asc, submissions[:id].asc) + .take(1) + + submissions[:id].gteq(Arel::Nodes::NamedFunction.new('COALESCE', [first_id, BIGINT_MAX])) + end + + def max_created_at_id_arel(time) + submissions = Submission.arel_table + + last_id = submissions.project(submissions[:id]) + .where(submissions[:created_at].lteq(time)) + .order(submissions[:created_at].desc, submissions[:id].desc) + .take(1) + + submissions[:id].lteq(Arel::Nodes::NamedFunction.new('COALESCE', [last_id, 0])) + end + def filter_by_folder(submissions, filters, current_user) return submissions if filters[:folder].blank? From b6fb7c6977c71d570b4b04f5f7589e68ca91b927 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Tue, 7 Jul 2026 09:50:11 +0300 Subject: [PATCH 05/12] fix find blob by checksum --- lib/submitters/normalize_values.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/submitters/normalize_values.rb b/lib/submitters/normalize_values.rb index a2c99ac5..2162e970 100644 --- a/lib/submitters/normalize_values.rb +++ b/lib/submitters/normalize_values.rb @@ -305,7 +305,13 @@ module Submitters return unless blob - return blob if blob.attachments.take&.record&.account_id == account.id + attachment = blob.attachments.take + + return if attachment.nil? || + attachment.record_type != 'Submitter' || + attachment.name != 'attachments' + + return blob if attachment.record&.account_id == account.id nil end From e88874e757ba9c75568d3b449bd9c3b934842918 Mon Sep 17 00:00:00 2001 From: Pete Matsyburka Date: Sat, 4 Jul 2026 15:47:40 +0300 Subject: [PATCH 06/12] view only party --- .../api/submission_documents_controller.rb | 2 +- app/controllers/api/submissions_controller.rb | 5 +- app/controllers/submissions_controller.rb | 21 +++ app/controllers/submit_form_controller.rb | 6 + .../submit_form_decline_controller.rb | 1 + .../submit_form_delegate_controller.rb | 1 + .../submit_form_draw_signature_controller.rb | 2 + .../submit_form_invite_controller.rb | 1 + app/controllers/submitters_controller.rb | 2 +- .../templates_preferences_controller.rb | 6 +- app/javascript/submission_form/form.vue | 9 +- app/javascript/template_builder/builder.vue | 42 ++--- app/jobs/process_submitter_completion_job.rb | 55 ++++++- .../send_submitter_invitation_email_job.rb | 7 +- app/mailers/submitter_mailer.rb | 41 +++++ app/models/account_config.rb | 8 + app/models/submitter.rb | 6 + app/views/icons/_file_check.html.erb | 6 + app/views/icons/_file_unknown.html.erb | 7 + app/views/icons/_writing_off.html.erb | 7 + app/views/submissions/_detailed_form.html.erb | 2 +- app/views/submissions/_email_form.html.erb | 2 +- .../submissions/_send_email_base.html.erb | 22 ++- app/views/submissions/new.html.erb | 7 +- app/views/submissions/show.html.erb | 24 ++- app/views/submit_form/show.html.erb | 20 +-- .../invitation_view_email.html.erb | 14 ++ app/views/submitters/edit.html.erb | 3 +- .../_recipients.html.erb | 8 +- .../_submitter_invitation_email_form.html.erb | 5 +- ...mitter_view_invitation_email_form.html.erb | 34 ++++ app/views/templates_preferences/show.html.erb | 12 ++ config/locales/i18n.yml | 154 ++++++++++++++++++ lib/submissions.rb | 27 ++- lib/submissions/assign_defined_submitters.rb | 2 + lib/submissions/create_from_submitters.rb | 30 ++++ lib/submissions/generate_audit_trail.rb | 3 + .../generate_result_attachments.rb | 5 +- lib/submitters.rb | 12 +- spec/requests/forms_spec.rb | 25 +++ spec/requests/submissions_spec.rb | 29 ++++ spec/system/signing_form_spec.rb | 23 +++ 42 files changed, 620 insertions(+), 78 deletions(-) create mode 100644 app/views/icons/_file_check.html.erb create mode 100644 app/views/icons/_file_unknown.html.erb create mode 100644 app/views/icons/_writing_off.html.erb create mode 100644 app/views/submitter_mailer/invitation_view_email.html.erb create mode 100644 app/views/templates_preferences/_submitter_view_invitation_email_form.html.erb create mode 100644 spec/requests/forms_spec.rb diff --git a/app/controllers/api/submission_documents_controller.rb b/app/controllers/api/submission_documents_controller.rb index 2f3fd78b..a26c5d86 100644 --- a/app/controllers/api/submission_documents_controller.rb +++ b/app/controllers/api/submission_documents_controller.rb @@ -30,7 +30,7 @@ module Api private def build_completed_documents(submission, merge: false) - last_submitter = submission.submitters.max_by(&:completed_at) + last_submitter = submission.submitters.select(&:completed_at?).max_by(&:completed_at) if merge if submission.merged_document_attachment.blank? diff --git a/app/controllers/api/submissions_controller.rb b/app/controllers/api/submissions_controller.rb index b620025c..ca996d32 100644 --- a/app/controllers/api/submissions_controller.rb +++ b/app/controllers/api/submissions_controller.rb @@ -80,8 +80,9 @@ module Api Submissions.send_signature_requests(submissions) submissions.each do |submission| - if submission.submitters.all?(&:completed_at?) && Submissions.maybe_update_completed_at(submission) - last_submitter = submission.submitters.max_by(&:completed_at) + if submission.submitters.all? { |s| s.viewer? || s.completed_at? } && + Submissions.maybe_update_completed_at(submission) + last_submitter = submission.submitters.reject(&:viewer?).max_by(&:completed_at) end submission.submitters.each do |submitter| diff --git a/app/controllers/submissions_controller.rb b/app/controllers/submissions_controller.rb index f4aebb36..31e568e2 100644 --- a/app/controllers/submissions_controller.rb +++ b/app/controllers/submissions_controller.rb @@ -87,6 +87,8 @@ class SubmissionsController < ApplicationController private def create_submissions(template, submissions_params, params) + normalize_message_submitter_uuids!(params) + submissions_attrs = submissions_params[:submission].to_h.values submissions_attrs, _, new_fields = @@ -111,4 +113,23 @@ class SubmissionsController < ApplicationController def submissions_params params.permit(submission: { submitters: [:uuid, :email, :phone, :name, { values: {} }] }) end + + def normalize_message_submitter_uuids!(params) + return if params[:request_email_per_submitter] == '1' + + uuids = params[:email_message_submitter_uuids] + + return if uuids.blank? + return if params[:subject].blank? && params[:body].blank? + + params[:submitter_preferences] = + Array.wrap(uuids).index_with { { 'subject' => params[:subject], 'body' => params[:body] } } + + params[:request_email_per_submitter] = '1' + + params.delete(:subject) + params.delete(:body) + + params + end end diff --git a/app/controllers/submit_form_controller.rb b/app/controllers/submit_form_controller.rb index a65c2237..1f689ece 100644 --- a/app/controllers/submit_form_controller.rb +++ b/app/controllers/submit_form_controller.rb @@ -71,6 +71,12 @@ class SubmitFormController < ApplicationController status: :unprocessable_content end + if @submitter.viewer? + Rollbar.warning("Submit viewer: #{@submitter.id}") if defined?(Rollbar) + + return render json: { error: I18n.t('form_is_view_only') }, status: :unprocessable_content + end + Submitters::SubmitValues.call(@submitter, params, request) head :ok diff --git a/app/controllers/submit_form_decline_controller.rb b/app/controllers/submit_form_decline_controller.rb index 73139f79..94054196 100644 --- a/app/controllers/submit_form_decline_controller.rb +++ b/app/controllers/submit_form_decline_controller.rb @@ -13,6 +13,7 @@ class SubmitFormDeclineController < ApplicationController @submitter.submission.archived_at? || @submitter.submission.expired? || @submitter.submission.template&.archived_at? || + @submitter.viewer? || !Submitters::AuthorizedForForm.call(@submitter, current_user, request) diff --git a/app/controllers/submit_form_delegate_controller.rb b/app/controllers/submit_form_delegate_controller.rb index 0a516509..6448a750 100644 --- a/app/controllers/submit_form_delegate_controller.rb +++ b/app/controllers/submit_form_delegate_controller.rb @@ -12,6 +12,7 @@ class SubmitFormDelegateController < ApplicationController @submitter.submission.archived_at? || @submitter.submission.expired? || @submitter.submission.template&.archived_at? || + @submitter.viewer? || !Submitters::AuthorizedForForm.call(@submitter, current_user, request) diff --git a/app/controllers/submit_form_draw_signature_controller.rb b/app/controllers/submit_form_draw_signature_controller.rb index 5ba141c1..54faa7d2 100644 --- a/app/controllers/submit_form_draw_signature_controller.rb +++ b/app/controllers/submit_form_draw_signature_controller.rb @@ -12,6 +12,8 @@ class SubmitFormDrawSignatureController < ApplicationController return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at? + return redirect_to submit_form_path(@submitter.slug) if @submitter.viewer? + if @submitter.submission.template&.archived_at? || @submitter.submission.archived_at? || !Submitters::AuthorizedForForm.call(@submitter, current_user, request) return redirect_to submit_form_path(@submitter.slug) diff --git a/app/controllers/submit_form_invite_controller.rb b/app/controllers/submit_form_invite_controller.rb index ac7f6db0..8a231e1c 100644 --- a/app/controllers/submit_form_invite_controller.rb +++ b/app/controllers/submit_form_invite_controller.rb @@ -48,6 +48,7 @@ class SubmitFormInviteController < ApplicationController !submitter.submission.archived_at? && !submitter.submission.expired? && !submitter.submission.template&.archived_at? && + !submitter.viewer? && Submitters::AuthorizedForForm.call(submitter, current_user, request) end diff --git a/app/controllers/submitters_controller.rb b/app/controllers/submitters_controller.rb index 1eae06d8..18e749f3 100644 --- a/app/controllers/submitters_controller.rb +++ b/app/controllers/submitters_controller.rb @@ -58,7 +58,7 @@ class SubmittersController < ApplicationController if params[:send_email] == '1' && submitter.email.present? is_sent_recently = Docuseal.multitenant? && EmailEvent.exists?(email: submitter.email, - tag: 'submitter_invitation', + tag: %w[submitter_invitation submitter_view_invitation], emailable: submitter, event_type: 'send', created_at: 4.hours.ago..Time.current) diff --git a/app/controllers/templates_preferences_controller.rb b/app/controllers/templates_preferences_controller.rb index 80865e94..b49202d0 100644 --- a/app/controllers/templates_preferences_controller.rb +++ b/app/controllers/templates_preferences_controller.rb @@ -5,6 +5,8 @@ class TemplatesPreferencesController < ApplicationController RESETTABLE_PREFERENCE_KEYS = { AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY => %w[request_email_subject request_email_body submitters], + AccountConfig::SUBMITTER_VIEW_INVITATION_EMAIL_KEY => %w[invitation_view_email_subject + invitation_view_email_body], AccountConfig::SUBMITTER_INVITATION_REMINDER_EMAIL_KEY => %w[invitation_reminder_email_subject invitation_reminder_email_body], AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY => %w[documents_copy_email_subject documents_copy_email_body], @@ -48,12 +50,12 @@ class TemplatesPreferencesController < ApplicationController def template_params params.require(:template).permit( preferences: %i[bcc_completed request_email_subject request_email_body + invitation_view_email_subject invitation_view_email_body invitation_reminder_email_subject invitation_reminder_email_body documents_copy_email_subject documents_copy_email_body documents_copy_email_enabled documents_copy_email_attach_audit documents_copy_email_attach_documents documents_copy_email_reply_to - completed_notification_email_attach_documents - completed_redirect_url validate_unique_submitters + completed_notification_email_attach_documents completed_redirect_url validate_unique_submitters require_all_submitters submitters_order require_phone_2fa require_email_2fa default_expire_at_duration shared_link_2fa default_expire_at request_email_enabled completed_notification_email_subject completed_notification_email_body diff --git a/app/javascript/submission_form/form.vue b/app/javascript/submission_form/form.vue index 95726537..20a08583 100644 --- a/app/javascript/submission_form/form.vue +++ b/app/javascript/submission_form/form.vue @@ -147,7 +147,7 @@ @submit.prevent="submitStep" />
!e.name && !e.required) && this.currentStepFields.length > 4 + if (this.currentField) { + return this.currentField.type === 'checkbox' && this.currentStepFields.every((e) => !e.name && !e.required) && this.currentStepFields.length > 4 + } else { + return false + } }, isButtonDisabled () { if (this.recalculateButtonDisabledKey) { diff --git a/app/javascript/template_builder/builder.vue b/app/javascript/template_builder/builder.vue index 098fcfb5..e12cc78d 100644 --- a/app/javascript/template_builder/builder.vue +++ b/app/javascript/template_builder/builder.vue @@ -3206,15 +3206,6 @@ export default { e.preventDefault() alert(this.t('please_draw_fields_to_prepare_the_document')) - } else { - const submitterWithoutFields = - this.template.submitters.find((submitter) => !this.template.fields.some((f) => f.submitter_uuid === submitter.uuid)) - - if (submitterWithoutFields) { - e.preventDefault() - - alert(this.t('please_add_fields_for_the_submitter_name_or_remove_the_submitter_name_if_not_needed').replaceAll('{submitter_name}', submitterWithoutFields.name)) - } } }, onSaveClick () { @@ -3231,32 +3222,25 @@ export default { if (!this.template.fields.length) { alert(this.t('please_draw_fields_to_prepare_the_document')) } else { - const submitterWithoutFields = - this.template.submitters.find((submitter) => !this.template.fields.some((f) => f.submitter_uuid === submitter.uuid)) - - if (submitterWithoutFields) { - alert(this.t('please_add_fields_for_the_submitter_name_or_remove_the_submitter_name_if_not_needed').replaceAll('{submitter_name}', submitterWithoutFields.name)) - } else { - this.isSaving = true + this.isSaving = true - const dynamicDocumentRefs = this.documentRefs.filter((ref) => ref.isDynamic) + const dynamicDocumentRefs = this.documentRefs.filter((ref) => ref.isDynamic) - dynamicDocumentRefs.map((ref) => ref.update()) + dynamicDocumentRefs.map((ref) => ref.update()) - this.rebuildVariablesSchema({ disable: false }) + this.rebuildVariablesSchema({ disable: false }) - const dynamicDocumentSaves = dynamicDocumentRefs.map((ref) => ref.saveBody()) + const dynamicDocumentSaves = dynamicDocumentRefs.map((ref) => ref.saveBody()) - Promise.all([this.save({ force: true }), ...dynamicDocumentSaves]).then(() => { - if (this.withRevisions) { - this.captureRevision() - } + Promise.all([this.save({ force: true }), ...dynamicDocumentSaves]).then(() => { + if (this.withRevisions) { + this.captureRevision() + } - window.Turbo.visit(`/templates/${this.template.id}`) - }).finally(() => { - this.isSaving = false - }) - } + window.Turbo.visit(`/templates/${this.template.id}`) + }).finally(() => { + this.isSaving = false + }) } }, scrollToArea (area) { diff --git a/app/jobs/process_submitter_completion_job.rb b/app/jobs/process_submitter_completion_job.rb index 932f1310..129b2363 100644 --- a/app/jobs/process_submitter_completion_job.rb +++ b/app/jobs/process_submitter_completion_job.rb @@ -13,7 +13,12 @@ class ProcessSubmitterCompletionJob if params.key?('is_last') params['is_last'] else - !submission.submitters.exists?(completed_at: nil) && + viewer_uuids = submission.template_submitters.to_a.filter_map { |s| s['uuid'] if s['is_viewer'] } + + incomplete = submission.submitters.where(completed_at: nil) + incomplete = incomplete.where.not(uuid: viewer_uuids) if viewer_uuids.present? + + !incomplete.exists? && submitter.completed_at == submission.submitters.maximum(:completed_at) end @@ -33,7 +38,9 @@ class ProcessSubmitterCompletionJob if !submission.completed_at && submission.submitters_order_preserved? && params['send_invitation_email'] != false && Submission.exists?(id: submission.id, completed_at: nil) - enqueue_next_submitter_request_notification(submitter) + next_submitters = enqueue_next_submitter_request_notification(submitter) + + enqueue_next_submitter_viewer_notification(submission, next_submitters) unless is_last end enqueue_completed_webhooks(submitter, is_last:) @@ -145,7 +152,7 @@ class ProcessSubmitterCompletionJob return if configs.value['enabled'] == false to = submitter.submission.submitters.reject { |e| e.preferences['send_email'] == false } - .sort_by(&:completed_at).select(&:email?).map(&:friendly_name) + .sort_by { |e| e.completed_at || Time.current }.select(&:email?).map(&:friendly_name) return if to.blank? @@ -165,9 +172,9 @@ class ProcessSubmitterCompletionJob bcc.to_s.scan(User::EMAIL_REGEXP) end - def enqueue_next_submitter_request_notification(submitter) + def enqueue_next_submitter_request_notification(submitter) # rubocop:disable Metrics/PerceivedComplexity submission = submitter.submission - submitters_index = submission.submitters.index_by(&:uuid) + submitters_index = submission.submitters.reject(&:viewer?).index_by(&:uuid) next_submitter_items = if submission.template_submitters.any? { |s| s['order'] } @@ -196,5 +203,43 @@ class ProcessSubmitterCompletionJob next_submitters = submitters_index.values_at(*Array.wrap(next_submitter_items).pluck('uuid')).compact Submitters.send_signature_requests(next_submitters) + + next_submitters + end + + def enqueue_next_submitter_viewer_notification(submission, next_submitters) + viewers = submission.submitters.select(&:viewer?) + + return [] if viewers.blank? + + next_submitter_uuids = next_submitters.to_set(&:uuid) + viewers_index = viewers.index_by(&:uuid) + + next_viewers = + if submission.template_submitters.any? { |s| s['order'] } + next_orders = submission.template_submitters + .select { |s| next_submitter_uuids.include?(s['uuid']) } + .pluck('order') + + submission.template_submitters.filter_map do |s| + viewers_index[s['uuid']] if next_orders.include?(s['order']) + end + else + preceding_submitter_uuid = nil + + submission.template_submitters.filter_map do |template_submitter| + viewer = viewers_index[template_submitter['uuid']] + + if viewer + viewer if next_submitter_uuids.include?(preceding_submitter_uuid) + else + preceding_submitter_uuid = template_submitter['uuid'] + + nil + end + end + end + + Submitters.send_signature_requests(next_viewers) end end diff --git a/app/jobs/send_submitter_invitation_email_job.rb b/app/jobs/send_submitter_invitation_email_job.rb index d236010b..4f4686f4 100644 --- a/app/jobs/send_submitter_invitation_email_job.rb +++ b/app/jobs/send_submitter_invitation_email_job.rb @@ -19,7 +19,12 @@ class SendSubmitterInvitationEmailJob return end - mail = SubmitterMailer.invitation_email(submitter) + mail = + if submitter.viewer? + SubmitterMailer.invitation_view_email(submitter) + else + SubmitterMailer.invitation_email(submitter) + end Submitters::ValidateSending.call(submitter, mail) diff --git a/app/mailers/submitter_mailer.rb b/app/mailers/submitter_mailer.rb index ebb8f224..9713d1ac 100644 --- a/app/mailers/submitter_mailer.rb +++ b/app/mailers/submitter_mailer.rb @@ -45,6 +45,45 @@ class SubmitterMailer < ApplicationMailer end end + def invitation_view_email(submitter) + @current_account = submitter.submission.account + @submitter = submitter + + if submitter.preferences['email_message_uuid'] + @email_message = submitter.account.email_messages.find_by(uuid: submitter.preferences['email_message_uuid']) + end + + template_submitters_index = @email_message.blank? ? build_submitter_preferences_index(@submitter) : {} + + @body = @email_message&.normalized_body.presence || + @submitter.template&.preferences&.dig('invitation_view_email_body').presence || + template_submitters_index.dig(@submitter.uuid, 'request_email_body').presence + + @subject = @email_message&.subject.presence || + @submitter.template&.preferences&.dig('invitation_view_email_subject').presence || + template_submitters_index.dig(@submitter.uuid, 'request_email_subject').presence + + @email_config = AccountConfigs.find_for_account(@current_account, AccountConfig::SUBMITTER_VIEW_INVITATION_EMAIL_KEY) + @body ||= fetch_config_email_body(@email_config, @submitter) + + assign_message_metadata('submitter_view_invitation', @submitter) + + reply_to = build_submitter_reply_to(@submitter, email_config: @email_config) + + maybe_set_custom_domain(@submitter) + + I18n.with_locale(@current_account.locale) do + subject = build_invite_subject(@subject, @email_config, submitter) + + mail( + to: @submitter.friendly_name, + from: from_address_for_submitter(submitter), + subject:, + reply_to: + ) + end + end + def completed_email(submitter, user, to: nil) @current_account = submitter.submission.account @submitter = submitter @@ -216,6 +255,8 @@ class SubmitterMailer < ApplicationMailer def build_invite_subject(subject, email_config, submitter) if email_config || subject ReplaceEmailVariables.call(subject || email_config.value['subject'], submitter:) + elsif submitter.viewer? + I18n.t(:you_are_invited_to_view_a_document) elsif submitter.with_signature_fields? I18n.t(:you_are_invited_to_sign_a_document) else diff --git a/app/models/account_config.rb b/app/models/account_config.rb index 2db37d70..857f0081 100644 --- a/app/models/account_config.rb +++ b/app/models/account_config.rb @@ -22,6 +22,7 @@ # class AccountConfig < ApplicationRecord SUBMITTER_INVITATION_EMAIL_KEY = 'submitter_invitation_email' + SUBMITTER_VIEW_INVITATION_EMAIL_KEY = 'submitter_view_invitation_email' SUBMITTER_INVITATION_REMINDER_EMAIL_KEY = 'submitter_invitation_reminder_email' SUBMITTER_COMPLETED_EMAIL_KEY = 'submitter_completed_email' SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY = 'submitter_documents_copy_email' @@ -63,6 +64,7 @@ class AccountConfig < ApplicationRecord EMAIL_VARIABLES = { SUBMITTER_INVITATION_EMAIL_KEY => %w[template.name submitter.link account.name].freeze, + SUBMITTER_VIEW_INVITATION_EMAIL_KEY => %w[template.name submitter.link account.name].freeze, SUBMITTER_COMPLETED_EMAIL_KEY => %w[template.name submission.submitters submission.link].freeze, SUBMITTER_INVITATION_REMINDER_EMAIL_KEY => %w[template.name submitter.link account.name].freeze, SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY => %w[template.name documents.link account.name].freeze @@ -75,6 +77,12 @@ class AccountConfig < ApplicationRecord 'body' => I18n.t(:submitter_invitation_email_sign_body) } }, + SUBMITTER_VIEW_INVITATION_EMAIL_KEY => lambda { + { + 'subject' => I18n.t(:you_are_invited_to_view_a_document), + 'body' => I18n.t(:submitter_invitation_email_view_body) + } + }, SUBMITTER_INVITATION_REMINDER_EMAIL_KEY => lambda { { 'subject' => I18n.t(:you_are_invited_to_sign_a_document), diff --git a/app/models/submitter.rb b/app/models/submitter.rb index 2c515ae2..3d21e2d4 100644 --- a/app/models/submitter.rb +++ b/app/models/submitter.rb @@ -116,6 +116,12 @@ class Submitter < ApplicationRecord end end + def viewer? + return false if submission.template_submitters.blank? + + submission.template_submitters.any? { |s| s['uuid'] == uuid && s['is_viewer'] } + end + private def anonymize_email_events diff --git a/app/views/icons/_file_check.html.erb b/app/views/icons/_file_check.html.erb new file mode 100644 index 00000000..fe2c9db4 --- /dev/null +++ b/app/views/icons/_file_check.html.erb @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/views/icons/_file_unknown.html.erb b/app/views/icons/_file_unknown.html.erb new file mode 100644 index 00000000..eab4952e --- /dev/null +++ b/app/views/icons/_file_unknown.html.erb @@ -0,0 +1,7 @@ + + + + + + + diff --git a/app/views/icons/_writing_off.html.erb b/app/views/icons/_writing_off.html.erb new file mode 100644 index 00000000..d6ba9c06 --- /dev/null +++ b/app/views/icons/_writing_off.html.erb @@ -0,0 +1,7 @@ + + + + + + + diff --git a/app/views/submissions/_detailed_form.html.erb b/app/views/submissions/_detailed_form.html.erb index aa0ce8ee..86e2f29d 100644 --- a/app/views/submissions/_detailed_form.html.erb +++ b/app/views/submissions/_detailed_form.html.erb @@ -92,7 +92,7 @@ <%= local_assigns[:variables_form] %>
<%= render('submitters_order', f:, template:) if can_send_emails %> - <%= render 'send_email', f:, template:, can_send_emails: %> + <%= render 'send_email', f:, template:, can_send_emails:, viewer_submitter_uuids: local_assigns[:viewer_submitter_uuids] %> <% if has_phone_field %> <%= render 'send_sms', f: %> <% end %> diff --git a/app/views/submissions/_email_form.html.erb b/app/views/submissions/_email_form.html.erb index e5f32a6c..e552cf9d 100644 --- a/app/views/submissions/_email_form.html.erb +++ b/app/views/submissions/_email_form.html.erb @@ -70,7 +70,7 @@ <%= local_assigns[:variables_form] %>
<%= render('submitters_order', f:, template:) if can_send_emails %> - <%= render 'send_email', f:, template:, can_send_emails: %> + <%= render 'send_email', f:, template:, can_send_emails:, viewer_submitter_uuids: local_assigns[:viewer_submitter_uuids] %> <%= render 'extra_fields', f: %>
diff --git a/app/views/submissions/_send_email_base.html.erb b/app/views/submissions/_send_email_base.html.erb index 6b035cf7..92ab06ab 100644 --- a/app/views/submissions/_send_email_base.html.erb +++ b/app/views/submissions/_send_email_base.html.erb @@ -38,9 +38,20 @@ <% end %>
<% config = AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY) %> +<% view_config = AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_VIEW_INVITATION_EMAIL_KEY) %> +<% view_template_subject = template&.preferences&.dig('invitation_view_email_subject').presence %> +<% view_template_body = template&.preferences&.dig('invitation_view_email_body').presence %> +<% default_subject = template&.preferences&.dig('request_email_subject').presence || config.value['subject'] %> +<% default_body = template&.preferences&.dig('request_email_body').presence || config.value['body'] %> +<% is_edit_viewer = local_assigns[:submitter] && local_assigns[:viewer_submitter_uuids].include?(local_assigns[:submitter].uuid) %>
- <%= render 'submissions/send_email', f:, template: @submitter.template, submitter: @submitter, resend_email: @submitter.sent_at?, submitter_email_message: @submitter_email_message, disable_save_as_default_template_option: true, message_per_submitter: false, can_send_emails: Accounts.can_send_emails?(current_account) %> + <% viewer_submitter_uuids = Set.new((@submitter.submission.template_submitters || @submitter.template.submitters).pluck('uuid')) - Set.new((@submitter.submission.template_fields || @submitter.template.fields).to_a.pluck('submitter_uuid')) %> + <%= render 'submissions/send_email', f:, template: @submitter.template, submitter: @submitter, resend_email: @submitter.sent_at?, submitter_email_message: @submitter_email_message, disable_save_as_default_template_option: true, message_per_submitter: false, can_send_emails: Accounts.can_send_emails?(current_account), viewer_submitter_uuids: %> <%= render 'submissions/send_sms', f:, resend_sms: @submitter.sent_at? %>
diff --git a/app/views/templates_preferences/_recipients.html.erb b/app/views/templates_preferences/_recipients.html.erb index 4e5eb61b..272f2cab 100644 --- a/app/views/templates_preferences/_recipients.html.erb +++ b/app/views/templates_preferences/_recipients.html.erb @@ -1,5 +1,6 @@ <% close_on_submit = local_assigns.fetch(:close_on_submit, true) %> <% is_order_set = template.submitters.any? { |s| s['order'] } %> +<% field_submitter_uuids = Set.new(template.fields.pluck('submitter_uuid')) %> <%= 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 %> @@ -10,6 +11,7 @@ <%= f.fields_for :submitters, item = Struct.new(:name, :uuid, :is_requester, :email, :invite_by_uuid, :invite_via_field_uuid, :optional_invite_by_uuid, :linked_to_uuid, :order, :option).new(*submitter.values_at('name', 'uuid', 'is_requester', 'email', 'invite_by_uuid', 'invite_via_field_uuid', 'optional_invite_by_uuid', 'linked_to_uuid', 'order')), 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}" : (item.invite_via_field_uuid.present? ? 'invite_via_field' : ''))))) %> <%= ff.hidden_field :uuid %> + <% is_viewer_row = field_submitter_uuids.exclude?(submitter['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 %> @@ -21,6 +23,8 @@ <%= t('edit_order') %> + <% elsif is_viewer_row %> + <%= t('view_only') %> <% end %>
<% end %> @@ -141,11 +145,13 @@
<% if template.submitters.size > 2 && !is_order_set %>