diff --git a/app/controllers/api/templates_controller.rb b/app/controllers/api/templates_controller.rb
index 7341b916..d24c58cc 100644
--- a/app/controllers/api/templates_controller.rb
+++ b/app/controllers/api/templates_controller.rb
@@ -97,7 +97,7 @@ module Api
:name,
:external_id,
{
- submitters: [%i[name uuid]],
+ submitters: [%i[name uuid is_requester linked_to_uuid email]],
fields: [[:uuid, :submitter_uuid, :name, :type,
:required, :readonly, :default_value,
:title, :description,
diff --git a/app/controllers/templates_controller.rb b/app/controllers/templates_controller.rb
index f973831a..4118c48d 100644
--- a/app/controllers/templates_controller.rb
+++ b/app/controllers/templates_controller.rb
@@ -103,7 +103,7 @@ class TemplatesController < ApplicationController
params.require(:template).permit(
:name,
{ schema: [%i[attachment_uuid name]],
- submitters: [%i[name uuid]],
+ submitters: [%i[name uuid is_requester linked_to_uuid email]],
fields: [[:uuid, :submitter_uuid, :name, :type,
:required, :readonly, :default_value,
:title, :description,
diff --git a/app/controllers/templates_recipients_controller.rb b/app/controllers/templates_recipients_controller.rb
new file mode 100644
index 00000000..b07491be
--- /dev/null
+++ b/app/controllers/templates_recipients_controller.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+class TemplatesRecipientsController < ApplicationController
+ load_and_authorize_resource :template
+
+ def create
+ authorize!(:update, @template)
+
+ @template.submitters =
+ submitters_params.map { |s| s.reject { |_, v| v.is_a?(String) && v.blank? } }
+
+ @template.save!
+
+ render json: { submitters: @template.submitters }
+ end
+
+ private
+
+ def submitters_params
+ params.require(:template).permit(
+ submitters: [%i[name uuid is_requester linked_to_uuid email option]]
+ ).fetch(:submitters, {}).values.filter_map do |s|
+ next if s[:uuid].blank?
+
+ if s[:is_requester] == '1'
+ s[:is_requester] = true
+ else
+ s.delete(:is_requester)
+ end
+
+ option = s.delete(:option)
+
+ if option.present?
+ case option
+ when 'is_requester'
+ s[:is_requester] = true
+ when 'not_set'
+ s.delete(:is_requester)
+ s.delete(:email)
+ s.delete(:linked_to_uuid)
+ when /\Alinked_to_(.*)\z/
+ s[:linked_to_uuid] = ::Regexp.last_match(-1)
+ end
+ end
+
+ s
+ end
+ end
+end
diff --git a/app/javascript/application.js b/app/javascript/application.js
index 098ea339..090fef07 100644
--- a/app/javascript/application.js
+++ b/app/javascript/application.js
@@ -26,6 +26,8 @@ import EmailsTextarea from './elements/emails_textarea'
import ToggleOnSubmit from './elements/toggle_on_submit'
import PasswordInput from './elements/password_input'
import SearchInput from './elements/search_input'
+import ToggleAttribute from './elements/toggle_attribute'
+import LinkedInput from './elements/linked_input'
import * as TurboInstantClick from './lib/turbo_instant_click'
@@ -89,9 +91,13 @@ safeRegisterElement('toggle-cookies', ToggleCookies)
safeRegisterElement('toggle-on-submit', ToggleOnSubmit)
safeRegisterElement('password-input', PasswordInput)
safeRegisterElement('search-input', SearchInput)
+safeRegisterElement('toggle-attribute', ToggleAttribute)
+safeRegisterElement('linked-input', LinkedInput)
safeRegisterElement('template-builder', class extends HTMLElement {
connectedCallback () {
+ document.addEventListener('turbo:submit-end', this.onSubmit)
+
this.appElem = document.createElement('div')
this.appElem.classList.add('md:h-screen')
@@ -114,12 +120,22 @@ safeRegisterElement('template-builder', class extends HTMLElement {
acceptFileTypes: this.dataset.acceptFileTypes
})
- this.app.mount(this.appElem)
+ this.component = this.app.mount(this.appElem)
this.appendChild(this.appElem)
}
+ onSubmit = (e) => {
+ if (e.detail.success && e.detail?.formSubmission?.formElement?.id === 'submitters_form') {
+ e.detail.fetchResponse.response.json().then((data) => {
+ this.component.template.submitters = data.submitters
+ })
+ }
+ }
+
disconnectedCallback () {
+ document.removeEventListener('turbo:submit-end', this.onSubmit)
+
this.app?.unmount()
this.appElem?.remove()
}
diff --git a/app/javascript/elements/linked_input.js b/app/javascript/elements/linked_input.js
new file mode 100644
index 00000000..0c0cfe7f
--- /dev/null
+++ b/app/javascript/elements/linked_input.js
@@ -0,0 +1,31 @@
+export default class extends HTMLElement {
+ connectedCallback () {
+ if (this.target) {
+ this.input.value = this.target.value
+
+ this.target.addEventListener('input', (e) => {
+ this.input.value = e.target.value
+ })
+
+ this.target.addEventListener('linked-input.update', (e) => {
+ this.input.value = e.target.value
+ })
+ }
+ }
+
+ get input () {
+ return this.querySelector('input')
+ }
+
+ get target () {
+ if (this.dataset.targetId) {
+ const listItem = this.closest('[data-targets="dynamic-list.items"]')
+
+ if (listItem) {
+ return listItem.querySelector(`#${this.dataset.targetId}`)
+ } else {
+ return document.getElementById(this.dataset.targetId)
+ }
+ }
+ }
+}
diff --git a/app/javascript/elements/submitter_autocomplete.js b/app/javascript/elements/submitter_autocomplete.js
index 08db3808..5fc539bf 100644
--- a/app/javascript/elements/submitter_autocomplete.js
+++ b/app/javascript/elements/submitter_autocomplete.js
@@ -24,6 +24,7 @@ export default class extends HTMLElement {
if (input && item[field]) {
input.value = item[field]
+ input.dispatchEvent(new CustomEvent('linked-input.update', { bubbles: true }))
}
if (textarea && item[field]) {
diff --git a/app/javascript/elements/toggle_attribute.js b/app/javascript/elements/toggle_attribute.js
new file mode 100644
index 00000000..e9ee3075
--- /dev/null
+++ b/app/javascript/elements/toggle_attribute.js
@@ -0,0 +1,28 @@
+export default class extends HTMLElement {
+ connectedCallback () {
+ this.input.addEventListener('change', (event) => {
+ if (this.dataset.attribute) {
+ this.target[this.dataset.attribute] = event.target.checked
+ }
+
+ if (this.dataset.className) {
+ this.target.classList.toggle(this.dataset.className, event.target.value !== this.dataset.value)
+ if (this.dataset.className === 'hidden' && this.target.tagName === 'INPUT') {
+ this.target.disabled = event.target.value !== this.dataset.value
+ }
+ }
+
+ if (this.dataset.attribute === 'disabled') {
+ this.target.value = ''
+ }
+ })
+ }
+
+ get input () {
+ return this.querySelector('input[type="checkbox"]') || this.querySelector('select')
+ }
+
+ get target () {
+ return document.getElementById(this.dataset.targetId)
+ }
+}
diff --git a/app/javascript/elements/turbo_modal.js b/app/javascript/elements/turbo_modal.js
index 5009f60b..69c20549 100644
--- a/app/javascript/elements/turbo_modal.js
+++ b/app/javascript/elements/turbo_modal.js
@@ -22,7 +22,7 @@ export default actionable(class extends HTMLElement {
}
onSubmit = (e) => {
- if (e.detail.success) {
+ if (e.detail.success && e.detail?.formSubmission?.formElement?.dataset?.closeOnSubmit !== 'false') {
this.close()
}
}
diff --git a/app/views/submissions/_detailed_form.html.erb b/app/views/submissions/_detailed_form.html.erb
index f237becd..597dfb70 100644
--- a/app/views/submissions/_detailed_form.html.erb
+++ b/app/views/submissions/_detailed_form.html.erb
@@ -18,14 +18,20 @@
<% end %>
- <%= tag.input type: 'text', name: 'submission[1][submitters][][name]', autocomplete: 'off', class: 'input input-sm input-bordered w-full', placeholder: 'Name', required: index.zero?, value: params[:selfsign] && index.zero? ? current_user.full_name : '', dir: 'auto' %>
+ ">
+ <%= tag.input type: 'text', name: 'submission[1][submitters][][name]', autocomplete: 'off', class: 'input input-sm input-bordered w-full', placeholder: 'Name', required: index.zero?, value: (params[:selfsign] && index.zero?) || item['is_requester'] ? current_user.full_name : '', dir: 'auto', id: "detailed_name_#{item['uuid']}" %>
+
-
+ ">
+
+
-
+ ">
+
+
diff --git a/app/views/submissions/_email_form.html.erb b/app/views/submissions/_email_form.html.erb
index cb34187f..193e21dd 100644
--- a/app/views/submissions/_email_form.html.erb
+++ b/app/views/submissions/_email_form.html.erb
@@ -27,7 +27,9 @@
- <%= tag.input type: 'email', multiple: true, name: 'submission[1][submitters][][email]', autocomplete: 'off', class: 'input input-sm input-bordered w-full', placeholder: 'Email', required: index.zero?, value: params[:selfsign] && index.zero? ? current_user.email : '' %>
+ ">
+ <%= tag.input type: 'email', multiple: true, name: 'submission[1][submitters][][email]', autocomplete: 'off', class: 'input input-sm input-bordered w-full', placeholder: 'Email', required: index.zero?, value: item['email'].presence || ((params[:selfsign] && index.zero?) || item['is_requester'] ? current_user.email : ''), id: "email_#{item['uuid']}" %>
+
<% end %>
diff --git a/app/views/submissions/_phone_form.html.erb b/app/views/submissions/_phone_form.html.erb
index 1445cf47..25a2d11c 100644
--- a/app/views/submissions/_phone_form.html.erb
+++ b/app/views/submissions/_phone_form.html.erb
@@ -19,18 +19,24 @@
<% end %>
- <%= tag.input type: 'tel', pattern: '^\+[0-9\s\-]+$', oninvalid: "this.value ? this.setCustomValidity('Use internatioanl format: +1xxx...') : ''", oninput: "this.setCustomValidity('')", name: 'submission[1][submitters][][phone]', autocomplete: 'off', class: 'input input-sm input-bordered w-full', placeholder: 'Phone', required: index.zero? %>
+ ">
+ <%= tag.input type: 'tel', pattern: '^\+[0-9\s\-]+$', oninvalid: "this.value ? this.setCustomValidity('Use internatioanl format: +1xxx...') : ''", oninput: "this.setCustomValidity('')", name: 'submission[1][submitters][][phone]', autocomplete: 'off', class: 'input input-sm input-bordered w-full', placeholder: 'Phone', required: index.zero?, id: "phone_phone_#{item['uuid']}" %>
+
<% if template.submitters.size > 1 %>
-
+ ">
+
+
<% end %>
<% if template.submitters.size == 1 %>
-
+ ">
+
+
<% end %>
diff --git a/app/views/templates_preferences/show.html.erb b/app/views/templates_preferences/show.html.erb
index 2b897653..e3704ee3 100644
--- a/app/views/templates_preferences/show.html.erb
+++ b/app/views/templates_preferences/show.html.erb
@@ -1,6 +1,8 @@
-<%= render 'shared/turbo_modal_large', title: 'Preferences', close_after_submit: false do %>
+<%= render 'shared/turbo_modal_large', title: 'Preferences' do %>
<% show_api = Docuseal.multitenant? || current_account.testing? || !current_account.linked_account_account %>
+ <% show_recipients = @template.submitters.to_a.length > 1 %>
<% options = [%w[General general]] %>
+ <% options << %w[Recipients recipients] if show_recipients %>
<% options << ['API and Embedding', 'api'] if show_api %>
<% if options.size > 1 %>
@@ -8,7 +10,7 @@
<% options.each_with_index do |(label, value), index| %>
<%= radio_button_tag 'option', value, value == 'general', class: 'peer hidden', data: { action: 'change:toggle-visible#trigger' } %>
-
@@ -17,7 +19,7 @@
<% end %>
- <%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-2' } do |f| %>
+ <%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-2' }, data: { close_on_submit: false } do |f| %>
<%= f.fields_for :preferences, Struct.new(:bcc_completed).new(@template.preferences['bcc_completed']) do |ff| %>
@@ -46,7 +48,7 @@
Signature request email
- <%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-1' } do |f| %>
+ <%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-1' }, data: { close_on_submit: false } do |f| %>
<%= f.fields_for :preferences, Struct.new(:request_email_subject, :request_email_body).new(*(@template.preferences.values_at('request_email_subject', 'request_email_body').compact_blank.presence || AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_INVITATION_EMAIL_KEY).value.values_at('subject', 'body'))) do |ff| %>
@@ -80,7 +82,7 @@
Documents copy email
- <%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-1' } do |f| %>
+ <%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-1' }, data: { close_on_submit: false } do |f| %>
<% configs = AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_DOCUMENTS_COPY_EMAIL_KEY).value %>
<%= f.fields_for :preferences, Struct.new(:documents_copy_email_subject, :documents_copy_email_body, :documents_copy_email_enabled, :documents_copy_email_attach_audit).new(@template.preferences['documents_copy_email_subject'].presence || configs['subject'], @template.preferences['documents_copy_email_body'].presence || configs['body'], @template.preferences['documents_copy_email_enabled'], configs['attach_audit_log'] != false && @template.preferences['documents_copy_email_attach_audit'] != false) do |ff| %>
@@ -127,7 +129,7 @@
Completed notification email
- <%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-1' } do |f| %>
+ <%= form_for @template, url: template_preferences_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-1' }, data: { close_on_submit: false } do |f| %>
<% configs = AccountConfigs.find_or_initialize_for_key(current_account, AccountConfig::SUBMITTER_COMPLETED_EMAIL_KEY).value %>
<%= f.fields_for :preferences, Struct.new(:completed_notification_email_subject, :completed_notification_email_body, :completed_notification_email_enabled, :completed_notification_email_attach_audit, :completed_notification_email_attach_documents).new(@template.preferences['completed_notification_email_subject'].presence || configs['subject'], @template.preferences['completed_notification_email_body'].presence || configs['body'], @template.preferences['completed_notification_email_enabled'], configs['attach_audit_log'] != false && @template.preferences['completed_notification_email_attach_audit'] != false, configs['attach_documents'] != false && @template.preferences['completed_notification_email_attach_documents'] != false) do |ff| %>
@@ -176,6 +178,48 @@
+ <% if show_recipients %>
+
+ <%= form_for @template, url: template_recipients_path(@template), method: :post, html: { autocomplete: 'off', class: 'mt-1', id: :submitters_form } do |f| %>
+
+ <% @template.submitters.each_with_index do |submitter, index| %>
+
+ <%= f.fields_for :submitters, item = Struct.new(:name, :uuid, :is_requester, :email, :linked_to_uuid, :option).new(*submitter.values_at('name', 'uuid', 'is_requester', 'email', '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}" : '')) %>
+ <%= 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 %>
+ <%= ff.email_field :email, class: 'base-input', autocomplete: 'off', placeholder: 'Default Email', disabled: ff.object.is_requester, id: field_uuid = SecureRandom.uuid %>
+ <% else %>
+
+ <%= ff.select :option, [['Not specified', 'not_set'], ['Submission requester', 'is_requester'], ['Specified email', 'email'], *(@template.submitters - [submitter]).map { |e| ["Same as #{e['name']}", "linked_to_#{e['uuid']}"] }], {}, class: 'base-select mb-3' %>
+
+ <%= ff.email_field :email, class: "base-input #{'hidden' if item.option != 'email'}", autocomplete: 'off', placeholder: 'Default Email', id: email_field_uuid %>
+ <% end %>
+
+ <% if @template.submitters.size == 2 %>
+
+
+
+ <%= ff.check_box :is_requester, class: 'base-checkbox' %>
+
+
+ Submission requester
+
+
+
+ <% end %>
+ <% end %>
+
+ <% end %>
+
+
+ <%= f.button button_title(title: 'Save', disabled_with: 'Updating'), class: 'base-button' %>
+
+ <% end %>
+
+ <% end %>
<% if show_api %>
diff --git a/config/routes.rb b/config/routes.rb
index d24756af..f5a0a1a5 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -97,6 +97,7 @@ Rails.application.routes.draw do
resource :form, only: %i[show], controller: 'templates_form_preview'
resource :code_modal, only: %i[show], controller: 'templates_code_modal'
resource :preferences, only: %i[show create], controller: 'templates_preferences'
+ resources :recipients, only: %i[create], controller: 'templates_recipients'
resources :submissions_export, only: %i[index new]
end
resources :preview_document_page, only: %i[show], path: '/preview/:signed_uuid'