diff --git a/.eslintrc b/.eslintrc
index e7045bb5..dd91e7c3 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -8,7 +8,8 @@
},
"rules": {
"vue/no-deprecated-html-element-is": 0,
- "vue/no-mutating-props": 0
+ "vue/no-mutating-props": 0,
+ "vue/one-component-per-file": 0
},
"parserOptions": {
"ecmaVersion": 2022,
diff --git a/.rubocop.yml b/.rubocop.yml
index 2a10dcf8..d538abe7 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -44,7 +44,7 @@ Layout/LineLength:
AllowedPatterns: ['\A\s*#']
Metrics/AbcSize:
- Max: 40
+ Max: 45
Metrics/ModuleLength:
Max: 500
diff --git a/README.md b/README.md
index b5a8b726..e3514f86 100644
--- a/README.md
+++ b/README.md
@@ -52,6 +52,7 @@ DocuSeal is an open source platform that provides secure and efficient digital d
- Automated reminders
- Invitation and identify verification via SMS
- Conditional fields and formulas
+- Bulk send with CSV, XLSX spreadsheet import
- SSO / SAML
- Template creation with HTML API ([Guide](https://www.docuseal.co/guides/create-pdf-document-fillable-form-with-html-api))
- Template creation with PDF or DOCX and field tags API ([Guide](https://www.docuseal.co/guides/use-embedded-text-field-tags-in-the-pdf-to-create-a-fillable-form))
diff --git a/app/javascript/application.js b/app/javascript/application.js
index be54143b..fc2e95ce 100644
--- a/app/javascript/application.js
+++ b/app/javascript/application.js
@@ -3,6 +3,7 @@ import { encodeMethodIntoRequestBody } from '@hotwired/turbo-rails/app/javascrip
import { createApp, reactive } from 'vue'
import TemplateBuilder from './template_builder/builder'
+import ImportList from './template_builder/import_list'
import ToggleVisible from './elements/toggle_visible'
import DisableHidden from './elements/disable_hidden'
@@ -111,3 +112,23 @@ window.customElements.define('template-builder', class extends HTMLElement {
this.appElem?.remove()
}
})
+
+window.customElements.define('import-list', class extends HTMLElement {
+ connectedCallback () {
+ this.appElem = document.createElement('div')
+
+ this.app = createApp(ImportList, {
+ template: JSON.parse(this.dataset.template),
+ authenticityToken: document.querySelector('meta[name="csrf-token"]')?.content
+ })
+
+ this.app.mount(this.appElem)
+
+ this.appendChild(this.appElem)
+ }
+
+ disconnectedCallback () {
+ this.app?.unmount()
+ this.appElem?.remove()
+ }
+})
diff --git a/app/javascript/application.scss b/app/javascript/application.scss
index f0d6429a..26ad688a 100644
--- a/app/javascript/application.scss
+++ b/app/javascript/application.scss
@@ -83,6 +83,11 @@ button[disabled] .enabled {
bottom: auto;
}
+.tooltip-pre:before {
+ white-space: pre;
+ text-align: left;
+}
+
.tooltip-bottom-end:after {
transform: translateX(-25%);
border-color: transparent transparent var(--tooltip-color) transparent;
diff --git a/app/javascript/template_builder/import_list.vue b/app/javascript/template_builder/import_list.vue
new file mode 100644
index 00000000..39284929
--- /dev/null
+++ b/app/javascript/template_builder/import_list.vue
@@ -0,0 +1,383 @@
+
+
+
+
+
+
+
+
+ {{ submitter.name }}
+
+
+
+ Recipient field
+
+
+ Spreadsheet column
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Total entries: {{ submissionsData.length }}
+
+ / 1000
+
+
+
+
+
+
+
+
+
+
diff --git a/app/views/submissions/_bulk_send_placeholder.html.erb b/app/views/submissions/_bulk_send_placeholder.html.erb
new file mode 100644
index 00000000..acbafe3d
--- /dev/null
+++ b/app/views/submissions/_bulk_send_placeholder.html.erb
@@ -0,0 +1,11 @@
+
+ <%= svg_icon('info_circle', class: 'w-6 h-6') %>
+
+
diff --git a/app/views/submissions/_list_form.html.erb b/app/views/submissions/_list_form.html.erb
new file mode 100644
index 00000000..30358ba4
--- /dev/null
+++ b/app/views/submissions/_list_form.html.erb
@@ -0,0 +1 @@
+<%= render 'submissions/bulk_send_placeholder' %>
diff --git a/app/views/submissions/new.html.erb b/app/views/submissions/new.html.erb
index 9dccd553..fd08441c 100644
--- a/app/views/submissions/new.html.erb
+++ b/app/views/submissions/new.html.erb
@@ -1,5 +1,5 @@
<%= render 'shared/turbo_modal', title: params[:selfsign] ? 'Add Recipients' : 'Add New Recipients' do %>
- <% options = [['via Email', 'email'], ['via Phone', 'phone'], %w[Detailed detailed], (Docuseal.multitenant? && params[:with_link] && @template.submitters.to_a.size < 2 ? ['via Link', 'link'] : nil)].compact %>
+ <% options = [['via Email', 'email'], ['via Phone', 'phone'], %w[Detailed detailed], ['Upload List', 'list']].compact %>
<% options.each_with_index do |(label, value), index| %>
@@ -22,11 +22,9 @@
<%= render 'detailed_form', template: @template %>
- <% if Docuseal.multitenant? && params[:with_link] && @template.submitters.to_a.size < 2 %>
-
- <%= render 'link_form', template: @template %>
-
- <% end %>
+
+ <%= render 'list_form', template: @template %>
+
<%= content_for(:modal_extra) %>
<% end %>
diff --git a/db/migrate/20231112224432_update_field_options.rb b/db/migrate/20231112224432_update_field_options.rb
index b72db381..ea488e75 100644
--- a/db/migrate/20231112224432_update_field_options.rb
+++ b/db/migrate/20231112224432_update_field_options.rb
@@ -9,7 +9,6 @@ class UpdateFieldOptions < ActiveRecord::Migration[7.0]
self.table_name = 'submissions'
end
- # rubocop:disable Metrics
def up
MigrationTemplate.find_each do |template|
next if template.fields.blank?
@@ -43,7 +42,6 @@ class UpdateFieldOptions < ActiveRecord::Migration[7.0]
submission.update_columns(template_fields: new_fields.to_json) if template_fields != new_fields
end
end
- # rubocop:enable Metrics
def down
nil
diff --git a/lib/submissions/create_from_submitters.rb b/lib/submissions/create_from_submitters.rb
index 191d9779..d4f4fd92 100644
--- a/lib/submissions/create_from_submitters.rb
+++ b/lib/submissions/create_from_submitters.rb
@@ -7,7 +7,7 @@ module Submissions
def call(template:, user:, submissions_attrs:, source:, submitters_order:, mark_as_sent: false, params: {})
preferences = Submitters.normalize_preferences(user.account, user, params)
- Array.wrap(submissions_attrs).map do |attrs|
+ Array.wrap(submissions_attrs).filter_map do |attrs|
submission_preferences = Submitters.normalize_preferences(user.account, user, attrs)
submission_preferences = preferences.merge(submission_preferences)
@@ -36,6 +36,8 @@ module Submissions
preferences: preferences.merge(submission_preferences))
end
+ next if submission.submitters.blank?
+
submission.tap(&:save!)
end
end
diff --git a/lib/submissions/generate_result_attachments.rb b/lib/submissions/generate_result_attachments.rb
index aa936814..e142b6bf 100644
--- a/lib/submissions/generate_result_attachments.rb
+++ b/lib/submissions/generate_result_attachments.rb
@@ -43,7 +43,7 @@ module Submissions
template = submitter.submission.template
- account = submitter.submission.template.account
+ account = submitter.account
pkcs = Accounts.load_signing_pkcs(account)
tsa_url = Accounts.load_timeserver_url(account)
attachments_data_cache = {}