diff --git a/Dockerfile b/Dockerfile
index 932d8b13..a5a8f855 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -50,7 +50,7 @@ ENV OPENSSL_CONF=/app/openssl_legacy.cnf
WORKDIR /app
-RUN echo '@edge https://dl-cdn.alpinelinux.org/alpine/edge/community' >> /etc/apk/repositories && apk add --no-cache sqlite-dev libpq-dev mariadb-dev vips-dev@edge redis libheif@edge vips-heif gcompat ttf-freefont && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf
+RUN echo '@edge https://dl-cdn.alpinelinux.org/alpine/edge/community' >> /etc/apk/repositories && apk add --no-cache sqlite-dev libpq-dev mariadb-dev vips-dev@edge redis libheif@edge vips-heif@edge gcompat ttf-freefont && mkdir /fonts && rm /usr/share/fonts/freefont/FreeSans.otf
RUN echo $'.include = /etc/ssl/openssl.cnf\n\
\n\
diff --git a/app/controllers/api/templates_controller.rb b/app/controllers/api/templates_controller.rb
index 5e8da098..25c0c537 100644
--- a/app/controllers/api/templates_controller.rb
+++ b/app/controllers/api/templates_controller.rb
@@ -100,6 +100,7 @@ module Api
permitted_params = [
:name,
:external_id,
+ :shared_link,
{
submitters: [%i[name uuid is_requester invite_by_uuid optional_invite_by_uuid linked_to_uuid email]],
fields: [[:uuid, :submitter_uuid, :name, :type,
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index a3cb0242..7500acdb 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -55,6 +55,14 @@ class ApplicationController < ActionController::Base
request.session[:impersonated_user_id] = user.uuid
end
+ def pagy_auto(collection, **keyword_args)
+ if current_ability.can?(:manage, :countless)
+ pagy_countless(collection, **keyword_args)
+ else
+ pagy(collection, **keyword_args)
+ end
+ end
+
private
def with_locale(&)
diff --git a/app/controllers/email_smtp_settings_controller.rb b/app/controllers/email_smtp_settings_controller.rb
index d41ca570..766a9545 100644
--- a/app/controllers/email_smtp_settings_controller.rb
+++ b/app/controllers/email_smtp_settings_controller.rb
@@ -9,7 +9,9 @@ class EmailSmtpSettingsController < ApplicationController
def create
if @encrypted_config.update(email_configs)
- SettingsMailer.smtp_successful_setup(@encrypted_config.value['from_email'] || current_user.email).deliver_now!
+ unless Docuseal.multitenant?
+ SettingsMailer.smtp_successful_setup(@encrypted_config.value['from_email'] || current_user.email).deliver_now!
+ end
redirect_to settings_email_index_path, notice: I18n.t('changes_have_been_saved')
else
diff --git a/app/controllers/send_submission_email_controller.rb b/app/controllers/send_submission_email_controller.rb
index 6d1f34c3..ef6ff62b 100644
--- a/app/controllers/send_submission_email_controller.rb
+++ b/app/controllers/send_submission_email_controller.rb
@@ -11,15 +11,15 @@ class SendSubmissionEmailController < ApplicationController
def create
if params[:template_slug]
- @submitter = Submitter.joins(submission: :template).find_by!(email: params[:email].to_s.downcase,
- template: { slug: params[:template_slug] })
+ @submitter = Submitter.completed.joins(submission: :template).find_by!(email: params[:email].to_s.downcase,
+ template: { slug: params[:template_slug] })
elsif params[:submission_slug]
- @submitter = Submitter.joins(:submission).find_by(email: params[:email].to_s.downcase,
- submission: { slug: params[:submission_slug] })
+ @submitter = Submitter.completed.joins(:submission).find_by(email: params[:email].to_s.downcase,
+ submission: { slug: params[:submission_slug] })
return redirect_to submissions_preview_completed_path(params[:submission_slug], status: :error) unless @submitter
else
- @submitter = Submitter.find_by!(slug: params[:submitter_slug])
+ @submitter = Submitter.completed.find_by!(slug: params[:submitter_slug])
end
RateLimit.call("send-email-#{@submitter.id}", limit: 2, ttl: 5.minutes)
diff --git a/app/controllers/start_form_controller.rb b/app/controllers/start_form_controller.rb
index c79535ec..415adab4 100644
--- a/app/controllers/start_form_controller.rb
+++ b/app/controllers/start_form_controller.rb
@@ -8,20 +8,28 @@ class StartFormController < ApplicationController
around_action :with_browser_locale, only: %i[show completed]
before_action :maybe_redirect_com, only: %i[show completed]
+ before_action :load_resubmit_submitter, only: :update
before_action :load_template
+ before_action :authorize_start!, only: :update
def show
- raise ActionController::RoutingError, I18n.t('not_found') if @template.preferences['require_phone_2fa'] == true
+ raise ActionController::RoutingError, I18n.t('not_found') if @template.preferences['require_phone_2fa']
- @submitter = @template.submissions.new(account_id: @template.account_id)
- .submitters.new(account_id: @template.account_id,
- uuid: (filter_undefined_submitters(@template).first ||
- @template.submitters.first)['uuid'])
+ if @template.shared_link?
+ @submitter = @template.submissions.new(account_id: @template.account_id)
+ .submitters.new(account_id: @template.account_id,
+ uuid: (filter_undefined_submitters(@template).first ||
+ @template.submitters.first)['uuid'])
+ else
+ Rollbar.warning("Not shared template: #{@template.id}") if defined?(Rollbar)
+
+ return render :private if current_user && current_ability.can?(:read, @template)
+
+ raise ActionController::RoutingError, I18n.t('not_found')
+ end
end
def update
- return redirect_to start_form_path(@template.slug) if @template.archived_at?
-
@submitter = find_or_initialize_submitter(@template, submitter_params)
if @submitter.completed_at?
@@ -59,6 +67,8 @@ class StartFormController < ApplicationController
end
def completed
+ return redirect_to start_form_path(@template.slug) if !@template.shared_link? || @template.archived_at?
+
@submitter = Submitter.where(submission: @template.submissions)
.where.not(completed_at: nil)
.find_by!(email: params[:email])
@@ -66,6 +76,24 @@ class StartFormController < ApplicationController
private
+ def load_resubmit_submitter
+ @resubmit_submitter =
+ if params[:resubmit].present? && !params[:resubmit].in?([true, 'true'])
+ Submitter.find_by(slug: params[:resubmit])
+ end
+ end
+
+ def authorize_start!
+ return redirect_to start_form_path(@template.slug) if @template.archived_at?
+
+ return if @resubmit_submitter
+ return if @template.shared_link? || (current_user && current_ability.can?(:read, @template))
+
+ Rollbar.warning("Not shared template: #{@template.id}") if defined?(Rollbar)
+
+ redirect_to start_form_path(@template.slug)
+ end
+
def enqueue_submission_create_webhooks(submitter)
WebhookUrls.for_account_id(submitter.account_id, 'submission.created').each do |webhook_url|
SendSubmissionCreatedWebhookRequestJob.perform_async('submission_id' => submitter.submission_id,
@@ -74,31 +102,29 @@ class StartFormController < ApplicationController
end
def find_or_initialize_submitter(template, submitter_params)
- Submitter.where(submission: template.submissions.where(expire_at: Time.current..)
- .or(template.submissions.where(expire_at: nil)).where(archived_at: nil))
- .order(id: :desc)
- .where(declined_at: nil)
- .where(external_id: nil)
- .where(ip: [nil, request.remote_ip])
- .then { |rel| params[:resubmit].present? ? rel.where(completed_at: nil) : rel }
- .find_or_initialize_by(email: submitter_params[:email], **submitter_params.compact_blank)
+ Submitter
+ .where(submission: template.submissions.where(expire_at: Time.current..)
+ .or(template.submissions.where(expire_at: nil)).where(archived_at: nil))
+ .order(id: :desc)
+ .where(declined_at: nil)
+ .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)
end
def assign_submission_attributes(submitter, template)
- resubmit_submitter =
- (Submitter.where(submission: template.submissions).find_by(slug: params[:resubmit]) if params[:resubmit].present?)
-
submitter.assign_attributes(
uuid: (filter_undefined_submitters(template).first || @template.submitters.first)['uuid'],
ip: request.remote_ip,
ua: request.user_agent,
- values: resubmit_submitter&.preferences&.fetch('default_values', nil) || {},
- preferences: resubmit_submitter&.preferences.presence || { 'send_email' => true },
- metadata: resubmit_submitter&.metadata.presence || {}
+ values: @resubmit_submitter&.preferences&.fetch('default_values', nil) || {},
+ preferences: @resubmit_submitter&.preferences.presence || { 'send_email' => true },
+ metadata: @resubmit_submitter&.metadata.presence || {}
)
if submitter.values.present?
- resubmit_submitter.attachments.each do |attachment|
+ @resubmit_submitter.attachments.each do |attachment|
submitter.attachments << attachment.dup if submitter.values.value?(attachment.uuid)
end
end
@@ -120,15 +146,21 @@ class StartFormController < ApplicationController
end
def submitter_params
+ return current_user.slice(:email) if params[:selfsign]
+ return @resubmit_submitter.slice(:name, :phone, :email) if @resubmit_submitter.present?
+
params.require(:submitter).permit(:email, :phone, :name).tap do |attrs|
attrs[:email] = Submissions.normalize_email(attrs[:email])
end
end
def load_template
- slug = params[:slug] || params[:start_form_slug]
-
- @template = Template.find_by!(slug:)
+ @template =
+ if @resubmit_submitter
+ @resubmit_submitter.template
+ else
+ Template.find_by!(slug: params[:slug] || params[:start_form_slug])
+ end
end
def multiple_submitters_error_message
diff --git a/app/controllers/submissions_archived_controller.rb b/app/controllers/submissions_archived_controller.rb
index f71638c7..3ad5d936 100644
--- a/app/controllers/submissions_archived_controller.rb
+++ b/app/controllers/submissions_archived_controller.rb
@@ -18,6 +18,6 @@ class SubmissionsArchivedController < ApplicationController
@submissions.order(id: :desc)
end
- @pagy, @submissions = pagy(@submissions.preload(submitters: :start_form_submission_events))
+ @pagy, @submissions = pagy_auto(@submissions.preload(submitters: :start_form_submission_events))
end
end
diff --git a/app/controllers/submissions_dashboard_controller.rb b/app/controllers/submissions_dashboard_controller.rb
index 3386edd8..55fb6ae0 100644
--- a/app/controllers/submissions_dashboard_controller.rb
+++ b/app/controllers/submissions_dashboard_controller.rb
@@ -19,6 +19,6 @@ class SubmissionsDashboardController < ApplicationController
@submissions.order(id: :desc)
end
- @pagy, @submissions = pagy(@submissions.preload(submitters: :start_form_submission_events))
+ @pagy, @submissions = pagy_auto(@submissions.preload(submitters: :start_form_submission_events))
end
end
diff --git a/app/controllers/template_folders_controller.rb b/app/controllers/template_folders_controller.rb
index e5cb2bbf..c22f2fed 100644
--- a/app/controllers/template_folders_controller.rb
+++ b/app/controllers/template_folders_controller.rb
@@ -9,7 +9,7 @@ class TemplateFoldersController < ApplicationController
@templates = Templates.search(@templates, params[:q])
@templates = Templates::Order.call(@templates, current_user, cookies.permanent[:dashboard_templates_order])
- @pagy, @templates = pagy(@templates, limit: 12)
+ @pagy, @templates = pagy_auto(@templates, limit: 12)
end
def edit; end
diff --git a/app/controllers/templates_archived_controller.rb b/app/controllers/templates_archived_controller.rb
index f75e83a1..f52e5af7 100644
--- a/app/controllers/templates_archived_controller.rb
+++ b/app/controllers/templates_archived_controller.rb
@@ -7,6 +7,6 @@ class TemplatesArchivedController < ApplicationController
@templates = @templates.where.not(archived_at: nil).preload(:author, :folder, :template_accesses).order(id: :desc)
@templates = Templates.search(@templates, params[:q])
- @pagy, @templates = pagy(@templates, limit: 12)
+ @pagy, @templates = pagy_auto(@templates, limit: 12)
end
end
diff --git a/app/controllers/templates_archived_submissions_controller.rb b/app/controllers/templates_archived_submissions_controller.rb
index bf023677..9c9da083 100644
--- a/app/controllers/templates_archived_submissions_controller.rb
+++ b/app/controllers/templates_archived_submissions_controller.rb
@@ -15,7 +15,7 @@ class TemplatesArchivedSubmissionsController < ApplicationController
@submissions.order(id: :desc)
end
- @pagy, @submissions = pagy(@submissions.preload(submitters: :start_form_submission_events))
+ @pagy, @submissions = pagy_auto(@submissions.preload(submitters: :start_form_submission_events))
rescue ActiveRecord::RecordNotFound
redirect_to root_path
end
diff --git a/app/controllers/templates_controller.rb b/app/controllers/templates_controller.rb
index 6eb91ed8..b3d0ccec 100644
--- a/app/controllers/templates_controller.rb
+++ b/app/controllers/templates_controller.rb
@@ -21,7 +21,7 @@ class TemplatesController < ApplicationController
submissions.order(id: :desc)
end
- @pagy, @submissions = pagy(submissions.preload(:template_accesses, submitters: :start_form_submission_events))
+ @pagy, @submissions = pagy_auto(submissions.preload(:template_accesses, submitters: :start_form_submission_events))
rescue ActiveRecord::RecordNotFound
redirect_to root_path
end
diff --git a/app/controllers/templates_dashboard_controller.rb b/app/controllers/templates_dashboard_controller.rb
index 319b0297..b4806fbc 100644
--- a/app/controllers/templates_dashboard_controller.rb
+++ b/app/controllers/templates_dashboard_controller.rb
@@ -17,7 +17,7 @@ class TemplatesDashboardController < ApplicationController
@pagy, @template_folders = pagy(
@template_folders,
- items: FOLDERS_PER_PAGE,
+ limit: FOLDERS_PER_PAGE,
page: @template_folders.count > SHOW_TEMPLATES_FOLDERS_THRESHOLD ? params[:page] : 1
)
@@ -35,7 +35,7 @@ class TemplatesDashboardController < ApplicationController
(@template_folders.size < 7 ? 9 : 6)
end
- @pagy, @templates = pagy(@templates, limit:)
+ @pagy, @templates = pagy_auto(@templates, limit:)
end
end
diff --git a/app/controllers/templates_share_link_controller.rb b/app/controllers/templates_share_link_controller.rb
new file mode 100644
index 00000000..5b84f6ca
--- /dev/null
+++ b/app/controllers/templates_share_link_controller.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+class TemplatesShareLinkController < ApplicationController
+ load_and_authorize_resource :template
+
+ def show; end
+
+ def create
+ authorize!(:update, @template)
+
+ @template.update!(template_params)
+
+ head :ok
+ end
+
+ private
+
+ def template_params
+ params.require(:template).permit(:shared_link)
+ end
+end
diff --git a/app/javascript/application.js b/app/javascript/application.js
index c2e12079..f951b8fe 100644
--- a/app/javascript/application.js
+++ b/app/javascript/application.js
@@ -24,6 +24,7 @@ import SubmitForm from './elements/submit_form'
import PromptPassword from './elements/prompt_password'
import EmailsTextarea from './elements/emails_textarea'
import ToggleOnSubmit from './elements/toggle_on_submit'
+import CheckOnClick from './elements/check_on_click'
import PasswordInput from './elements/password_input'
import SearchInput from './elements/search_input'
import ToggleAttribute from './elements/toggle_attribute'
@@ -103,6 +104,7 @@ safeRegisterElement('set-date-button', SetDateButton)
safeRegisterElement('indeterminate-checkbox', IndeterminateCheckbox)
safeRegisterElement('app-tour', AppTour)
safeRegisterElement('dashboard-dropzone', DashboardDropzone)
+safeRegisterElement('check-on-click', CheckOnClick)
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
new file mode 100644
index 00000000..8b3b9ab8
--- /dev/null
+++ b/app/javascript/elements/check_on_click.js
@@ -0,0 +1,14 @@
+export default class extends HTMLElement {
+ connectedCallback () {
+ this.addEventListener('click', () => {
+ if (!this.element.checked) {
+ this.element.checked = true
+ this.element.dispatchEvent(new Event('change', { bubbles: true }))
+ }
+ })
+ }
+
+ get element () {
+ return document.getElementById(this.dataset.elementId)
+ }
+}
diff --git a/app/javascript/elements/clipboard_copy.js b/app/javascript/elements/clipboard_copy.js
index 777727d1..4118b9e2 100644
--- a/app/javascript/elements/clipboard_copy.js
+++ b/app/javascript/elements/clipboard_copy.js
@@ -3,8 +3,6 @@ export default class extends HTMLElement {
this.clearChecked()
this.addEventListener('click', (e) => {
- e.stopPropagation()
-
const text = this.dataset.text || this.innerText.trim()
if (navigator.clipboard) {
diff --git a/app/javascript/submission_form/area.vue b/app/javascript/submission_form/area.vue
index 01819533..5aed2a7a 100644
--- a/app/javascript/submission_form/area.vue
+++ b/app/javascript/submission_form/area.vue
@@ -23,7 +23,7 @@
<%= render partial: 'templates/template', collection: @templates %>
<% templates_order_select_html = capture do %>
<% if params[:q].blank? && @pagy.pages > 1 %>
- <%= render('shared/templates_order_select', with_recently_used: @pagy.count < 10_000) %>
+ <%= render('shared/templates_order_select', with_recently_used: @pagy.count.present? && @pagy.count < 10_000) %>
<% end %>
<% end %>
<%= render 'shared/pagination', pagy: @pagy, items_name: 'templates', right_additional_html: templates_order_select_html %>
diff --git a/app/views/templates/_title.html.erb b/app/views/templates/_title.html.erb
index c33a8298..28d2e037 100644
--- a/app/views/templates/_title.html.erb
+++ b/app/views/templates/_title.html.erb
@@ -49,7 +49,12 @@
<% end %>
<% end %>
- <%= render 'shared/clipboard_copy', text: start_form_url(slug: @template.slug), id: 'share_link_clipboard', class: 'absolute md:relative bottom-0 right-0 btn btn-xs md:btn-sm whitespace-nowrap btn-neutral text-white mt-1 px-2', icon_class: 'w-4 h-4 md:w-6 md:h-6 text-white', copy_title: t('link'), copied_title: t('copied'), copy_title_md: t('link'), copied_title_md: t('copied') %>
+ <%= link_to template_share_link_path(template), class: 'absolute md:relative bottom-0 right-0 btn btn-xs md:btn-sm whitespace-nowrap btn-neutral text-white mt-1 px-2', data: { turbo_frame: :modal } do %>
+
<%= render partial: 'templates/submission', collection: @submissions, locals: { template: @template, archived: true } %>
diff --git a/app/views/templates_dashboard/index.html.erb b/app/views/templates_dashboard/index.html.erb
index bcd85b01..99f4d80e 100644
--- a/app/views/templates_dashboard/index.html.erb
+++ b/app/views/templates_dashboard/index.html.erb
@@ -12,7 +12,7 @@
<%= render 'templates/dashboard_dropzone', style: 'height: 114px' %>
<% end %>
- <% if has_archived || @pagy.count > 0 || @template_folders.present? %>
+ <% if has_archived || @pagy.count.nil? || @pagy.count > 0 || @template_folders.present? %>
<%= render 'dashboard/toggle_view', selected: 'templates' %>
@@ -45,7 +45,7 @@
<% end %>
<% templates_order_select_html = capture do %>
<% if params[:q].blank? && @pagy.pages > 1 %>
- <%= render('shared/templates_order_select', with_recently_used: @pagy.count < 10_000) %>
+ <%= render('shared/templates_order_select', with_recently_used: @pagy.count.present? && @pagy.count < 10_000) %>
<% end %>
<% end %>
<% if @template_folders.present? %>
@@ -84,9 +84,9 @@
<% if show_dropzone %>
<%= render 'templates/dropzone' %>
<% end %>
-<% if @templates.present? || params[:q].blank? %>
+<% if @templates.present? || @template_folders.present? || params[:q].blank? %>
<% if @pagy.pages > 1 %>
- <%= render 'shared/pagination', pagy: @pagy, items_name: 'templates', left_additional_html: view_archived_html, right_additional_html: templates_order_select_html %>
+ <%= render 'shared/pagination', pagy: @pagy, items_name: @templates.present? ? 'templates' : 'template_folders', left_additional_html: view_archived_html, right_additional_html: templates_order_select_html %>
<% else %>
<%= view_archived_html %>
diff --git a/app/views/templates_preferences/show.html.erb b/app/views/templates_preferences/show.html.erb
index b59fac43..47b342c7 100644
--- a/app/views/templates_preferences/show.html.erb
+++ b/app/views/templates_preferences/show.html.erb
@@ -380,8 +380,18 @@
<%= t('embedding_url') %>
-
- <%= render 'shared/clipboard_copy', icon: 'copy', text: start_form_url(slug: @template.slug), class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: t('copy'), copied_title: t('copied') %>
+ <%= form_for @template, url: template_share_link_path(@template), method: :post, html: { id: 'shared_link_form', autocomplete: 'off', class: 'w-full mt-1' }, data: { close_on_submit: false } do |f| %>
+
+
+
+ <%= render 'shared/clipboard_copy', icon: 'copy', text: start_form_url(slug: @template.slug), class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: t('copy'), copied_title: t('copied') %>
+
+
+
+ <%= t('enable_shared_link') %>
+ <%= f.check_box :shared_link, { class: 'toggle', onchange: 'this.form.requestSubmit()' }, 'true', 'false' %>
+
+ <% end %>
<%= render 'templates_code_modal/placeholder' %>
diff --git a/app/views/templates_share_link/show.html.erb b/app/views/templates_share_link/show.html.erb
new file mode 100644
index 00000000..eb2d74e0
--- /dev/null
+++ b/app/views/templates_share_link/show.html.erb
@@ -0,0 +1,16 @@
+<%= render 'shared/turbo_modal_large', title: t('share_link') do %>
+
+ <%= form_for @template, url: template_share_link_path(@template), method: :post, html: { id: 'shared_link_form', autocomplete: 'off', class: 'mt-3' }, data: { close_on_submit: false } do |f| %>
+
+ <%= t('enable_shared_link') %>
+ <%= f.check_box :shared_link, { disabled: !can?(:update, @template), class: 'toggle', onchange: 'this.form.requestSubmit()' }, 'true', 'false' %>
+
+
+
+
+ <%= render 'shared/clipboard_copy', icon: 'copy', text: start_form_url(slug: @template.slug), class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: t('copy'), copied_title: t('copied') %>
+
+
+ <% end %>
+
+<% end %>
diff --git a/config/initializers/pagy.rb b/config/initializers/pagy.rb
index dc79235a..45c364e8 100644
--- a/config/initializers/pagy.rb
+++ b/config/initializers/pagy.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+require 'pagy/extras/countless'
+
Pagy::DEFAULT[:limit] = 10
Pagy::DEFAULT.freeze
diff --git a/config/locales/i18n.yml b/config/locales/i18n.yml
index d807d374..378a6f5e 100644
--- a/config/locales/i18n.yml
+++ b/config/locales/i18n.yml
@@ -745,6 +745,10 @@ en: &en
three_months: 3 months
eu_data_residency: EU data residency
please_enter_your_email_address_associated_with_the_completed_submission: Please enter your email address associated with the completed submission.
+ esignature_disclosure: eSignature Disclosure
+ share_link: Share link
+ enable_shared_link: Enable shared link
+ share_link_is_currently_disabled: Share link is currently disabled
submission_sources:
api: API
bulk: Bulk Send
@@ -829,6 +833,22 @@ en: &en
scopes:
write: Update your data
read: Read your data
+ pagination:
+ submissions:
+ range_with_total: "%{from}-%{to} of %{count} submissions"
+ range_without_total: "%{from}-%{to} submissions"
+ templates:
+ range_with_total: "%{from}-%{to} of %{count} templates"
+ range_without_total: "%{from}-%{to} templates"
+ template_folders:
+ range_with_total: "%{from}-%{to} of %{count} folders"
+ range_without_total: "%{from}-%{to} folders"
+ users:
+ range_with_total: "%{from}-%{to} of %{count} users"
+ range_without_total: "%{from}-%{to} users"
+ items:
+ range_with_total: "%{from}-%{to} of %{count} items"
+ range_without_total: "%{from}-%{to} items"
es: &es
stripe_integration: Integración con Stripe
@@ -1557,6 +1577,10 @@ es: &es
three_months: 3 meses
eu_data_residency: Datos alojados UE
please_enter_your_email_address_associated_with_the_completed_submission: Por favor, introduce tu dirección de correo electrónico asociada con el envío completado.
+ esignature_disclosure: Uso de firma electrónica
+ share_link: Enlace para compartir
+ enable_shared_link: Habilitar enlace compartido
+ share_link_is_currently_disabled: El enlace compartido está deshabilitado actualmente
submission_sources:
api: API
bulk: Envío masivo
@@ -1641,6 +1665,22 @@ es: &es
scopes:
write: Actualizar tus datos
read: Leer tus datos
+ pagination:
+ submissions:
+ range_with_total: "%{from}-%{to} de %{count} envíos"
+ range_without_total: "%{from}-%{to} envíos"
+ templates:
+ range_with_total: "%{from}-%{to} de %{count} plantillas"
+ range_without_total: "%{from}-%{to} plantillas"
+ template_folders:
+ range_with_total: "%{from}-%{to} de %{count} carpetas"
+ range_without_total: "%{from}-%{to} carpetas"
+ users:
+ range_with_total: "%{from}-%{to} de %{count} usuarios"
+ range_without_total: "%{from}-%{to} usuarios"
+ items:
+ range_with_total: "%{from}-%{to} de %{count} elementos"
+ range_without_total: "%{from}-%{to} elementos"
it: &it
stripe_integration: Integrazione Stripe
@@ -2367,6 +2407,10 @@ it: &it
three_months: 3 mesi
eu_data_residency: "Dati nell'UE"
please_enter_your_email_address_associated_with_the_completed_submission: "Inserisci il tuo indirizzo email associato all'invio completato."
+ esignature_disclosure: Uso della firma elettronica
+ share_link: Link di condivisione
+ enable_shared_link: Abilita link condiviso
+ share_link_is_currently_disabled: Il link condiviso è attualmente disabilitato
submission_sources:
api: API
bulk: Invio massivo
@@ -2451,6 +2495,22 @@ it: &it
scopes:
write: Aggiorna i tuoi dati
read: Leggi i tuoi dati
+ pagination:
+ submissions:
+ range_with_total: "%{from}-%{to} di %{count} invii"
+ range_without_total: "%{from}-%{to} invii"
+ templates:
+ range_with_total: "%{from}-%{to} di %{count} modelli"
+ range_without_total: "%{from}-%{to} modelli"
+ template_folders:
+ range_with_total: "%{from}-%{to} di %{count} cartelle"
+ range_without_total: "%{from}-%{to} cartelle"
+ users:
+ range_with_total: "%{from}-%{to} di %{count} utenti"
+ range_without_total: "%{from}-%{to} utenti"
+ items:
+ range_with_total: "%{from}-%{to} di %{count} elementi"
+ range_without_total: "%{from}-%{to} elementi"
fr: &fr
stripe_integration: Intégration Stripe
@@ -3180,6 +3240,10 @@ fr: &fr
three_months: 3 mois
eu_data_residency: "Données dans l'UE"
please_enter_your_email_address_associated_with_the_completed_submission: "Veuillez saisir l'adresse e-mail associée à l'envoi complété."
+ esignature_disclosure: Divulgation de Signature Électronique
+ share_link: Lien de partage
+ enable_shared_link: Activer le lien de partage
+ share_link_is_currently_disabled: Le lien de partage est actuellement désactivé
submission_sources:
api: API
bulk: Envoi en masse
@@ -3264,6 +3328,22 @@ fr: &fr
scopes:
write: Mettre à jour vos données
read: Lire vos données
+ pagination:
+ submissions:
+ range_with_total: "%{from} à %{to} sur %{count} soumissions"
+ range_without_total: "%{from} à %{to} soumissions"
+ templates:
+ range_with_total: "%{from} à %{to} sur %{count} modèles"
+ range_without_total: "%{from} à %{to} modèles"
+ template_folders:
+ range_with_total: "%{from} à %{to} sur %{count} dossiers"
+ range_without_total: "%{from} à %{to} dossiers"
+ users:
+ range_with_total: "%{from} à %{to} sur %{count} utilisateurs"
+ range_without_total: "%{from} à %{to} utilisateurs"
+ items:
+ range_with_total: "%{from} à %{to} sur %{count} éléments"
+ range_without_total: "%{from} à %{to} éléments"
pt: &pt
stripe_integration: Integração com Stripe
@@ -3992,6 +4072,10 @@ pt: &pt
three_months: 3 meses
eu_data_residency: Dados na UE
please_enter_your_email_address_associated_with_the_completed_submission: Por favor, insira seu e-mail associado ao envio concluído.
+ esignature_disclosure: Uso de assinatura eletrônica
+ share_link: Link de compartilhamento
+ enable_shared_link: Ativar link compartilhado
+ share_link_is_currently_disabled: O link compartilhado está desativado no momento
submission_sources:
api: API
bulk: Envio em massa
@@ -4036,8 +4120,8 @@ pt: &pt
start: Iniciar
previous: Anterior
next: Próximo
- template_and_submissions: 'Modelos e Envios'
- template_and_submissions_description: "Você pode selecionar a visualização que melhor se adapta ao seu fluxo de trabalho. Escolha a visualização 'Modelos' para criar modelos reutilizáveis de documentos ou 'Envios' para assinar documentos individuais ou verificar o status de cada solicitação de assinatura."
+ template_and_submissions: 'Modelos e Submissões'
+ template_and_submissions_description: "Você pode selecionar a visualização que melhor se adapta ao seu fluxo de trabalho. Escolha a visualização 'Modelos' para criar modelos reutilizáveis de documentos ou 'Submissões' para assinar documentos individuais ou verificar o status de cada solicitação de assinatura."
upload_a_pdf_file: 'Enviar um arquivo PDF'
upload_a_pdf_file_description: 'Envie um documento PDF para criar um modelo de formulário de assinatura.'
select_a_signer_party: 'Selecionar parte assinante'
@@ -4077,6 +4161,22 @@ pt: &pt
scopes:
write: Atualizar seus dados
read: Ler seus dados
+ pagination:
+ submissions:
+ range_with_total: "%{from}-%{to} de %{count} submissões"
+ range_without_total: "%{from}-%{to} submissões"
+ templates:
+ range_with_total: "%{from}-%{to} de %{count} modelos"
+ range_without_total: "%{from}-%{to} modelos"
+ template_folders:
+ range_with_total: "%{from}-%{to} de %{count} pastas"
+ range_without_total: "%{from}-%{to} pastas"
+ users:
+ range_with_total: "%{from}-%{to} de %{count} usuários"
+ range_without_total: "%{from}-%{to} usuários"
+ items:
+ range_with_total: "%{from}-%{to} de %{count} itens"
+ range_without_total: "%{from}-%{to} itens"
de: &de
stripe_integration: Stripe-Integration
@@ -4805,6 +4905,10 @@ de: &de
three_months: 3 Monate
eu_data_residency: EU-Datenspeicher
please_enter_your_email_address_associated_with_the_completed_submission: Bitte gib deine E-Mail-Adresse ein, die mit der abgeschlossenen Übermittlung verknüpft ist.
+ esignature_disclosure: Nutzung der E-Signatur
+ share_link: Freigabelink
+ enable_shared_link: 'Freigabelink aktivieren'
+ share_link_is_currently_disabled: 'Freigabelink ist derzeit deaktiviert'
submission_sources:
api: API
bulk: Massenversand
@@ -4889,6 +4993,22 @@ de: &de
scopes:
write: Aktualisiere deine Daten
read: Lese deine Daten
+ pagination:
+ submissions:
+ range_with_total: "%{from}-%{to} von %{count} Einreichungen"
+ range_without_total: "%{from}-%{to} Einreichungen"
+ templates:
+ range_with_total: "%{from}-%{to} von %{count} Vorlagen"
+ range_without_total: "%{from}-%{to} Vorlagen"
+ template_folders:
+ range_with_total: "%{from}-%{to} von %{count} Ordnern"
+ range_without_total: "%{from}-%{to} Ordner"
+ users:
+ range_with_total: "%{from}-%{to} von %{count} Benutzern"
+ range_without_total: "%{from}-%{to} Benutzer"
+ items:
+ range_with_total: "%{from}-%{to} von %{count} Elementen"
+ range_without_total: "%{from}-%{to} Elemente"
pl:
require_phone_2fa_to_open: Wymagaj uwierzytelniania telefonicznego 2FA do otwarcia
@@ -4958,6 +5078,8 @@ pl:
open_source_documents_software: 'oprogramowanie do dokumentów open source'
eu_data_residency: Dane w UE
please_enter_your_email_address_associated_with_the_completed_submission: Wprowadź adres e-mail powiązany z ukończonym zgłoszeniem.
+ privacy_policy: Polityka Prywatności
+ esignature_disclosure: Użycie e-podpisu
uk:
require_phone_2fa_to_open: Вимагати двофакторну автентифікацію через телефон для відкриття
@@ -5027,6 +5149,8 @@ uk:
open_source_documents_software: 'відкрите програмне забезпечення для документів'
eu_data_residency: 'Зберігання даних в ЄС'
please_enter_your_email_address_associated_with_the_completed_submission: "Введіть адресу електронної пошти, пов'язану із завершеним поданням."
+ privacy_policy: Політика конфіденційності
+ esignature_disclosure: Використання e-підпису
cs:
require_phone_2fa_to_open: Vyžadovat otevření pomocí telefonního 2FA
@@ -5096,6 +5220,8 @@ cs:
open_source_documents_software: 'open source software pro dokumenty'
eu_data_residency: 'Uložení dat v EU'
please_enter_your_email_address_associated_with_the_completed_submission: Zadejte e-mailovou adresu spojenou s dokončeným odesláním.
+ privacy_policy: Zásady Ochrany Osobních Údajů
+ esignature_disclosure: Použití e-podpisu
he:
require_phone_2fa_to_open: דרוש אימות דו-שלבי באמצעות טלפון לפתיחה
@@ -5165,6 +5291,8 @@ he:
open_source_documents_software: 'תוכנה בקוד פתוח למסמכים'
eu_data_residency: 'נתונים באיחוד האירופי '
please_enter_your_email_address_associated_with_the_completed_submission: 'אנא הזן את כתובת הדוא"ל המשויכת למשלוח שהושלם.'
+ privacy_policy: 'מדיניות פרטיות'
+ esignature_disclosure: 'גילוי חתימה אלקטרונית'
nl:
require_phone_2fa_to_open: Vereis telefoon 2FA om te openen
@@ -5234,6 +5362,8 @@ nl:
open_source_documents_software: 'open source documenten software'
eu_data_residency: Gegevens EU
please_enter_your_email_address_associated_with_the_completed_submission: Voer het e-mailadres in dat is gekoppeld aan de voltooide indiening.
+ privacy_policy: Privacybeleid
+ esignature_disclosure: Gebruik van e-handtekening
ar:
require_phone_2fa_to_open: "تطلب فتح عبر تحقق الهاتف ذو العاملين"
@@ -5303,6 +5433,8 @@ ar:
open_source_documents_software: 'برنامج مستندات مفتوح المصدر'
eu_data_residency: 'بيانات في الاتحاد الأوروبي'
please_enter_your_email_address_associated_with_the_completed_submission: 'يرجى إدخال عنوان البريد الإلكتروني المرتبط بالإرسال المكتمل.'
+ privacy_policy: 'سياسة الخصوصية'
+ esignature_disclosure: 'إفصاح التوقيع الإلكتروني'
ko:
require_phone_2fa_to_open: 휴대폰 2FA를 열 때 요구함
@@ -5372,6 +5504,8 @@ ko:
open_source_documents_software: '오픈소스 문서 소프트웨어'
eu_data_residency: 'EU 데이터 보관'
please_enter_your_email_address_associated_with_the_completed_submission: '완료된 제출과 연결된 이메일 주소를 입력하세요.'
+ privacy_policy: 개인정보 처리방침
+ esignature_disclosure: 전자서명 공개
ja:
require_phone_2fa_to_open: 電話による2段階認証が必要です
@@ -5441,6 +5575,8 @@ ja:
open_source_documents_software: 'オープンソースのドキュメントソフトウェア'
eu_data_residency: 'EU データ居住'
please_enter_your_email_address_associated_with_the_completed_submission: '完了した提出に関連付けられたメールアドレスを入力してください。'
+ privacy_policy: プライバシーポリシー
+ esignature_disclosure: 電子署名に関する開示
en-US:
<<: *en
diff --git a/config/routes.rb b/config/routes.rb
index 331ca82d..aed50953 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -106,6 +106,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'
+ resource :share_link, only: %i[show create], controller: 'templates_share_link'
resources :recipients, only: %i[create], controller: 'templates_recipients'
resources :submissions_export, only: %i[index new]
end
@@ -131,6 +132,8 @@ Rails.application.routes.draw do
get :completed
end
+ resource :resubmit_form, controller: 'start_form', only: :update
+
resources :submit_form, only: %i[], path: '' do
get :success, on: :collection
end
diff --git a/db/migrate/20250523121121_add_shared_link_to_templates.rb b/db/migrate/20250523121121_add_shared_link_to_templates.rb
new file mode 100644
index 00000000..6fe962eb
--- /dev/null
+++ b/db/migrate/20250523121121_add_shared_link_to_templates.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+class AddSharedLinkToTemplates < ActiveRecord::Migration[8.0]
+ disable_ddl_transaction
+
+ class MigrationTemplate < ActiveRecord::Base
+ self.table_name = 'templates'
+ end
+
+ def up
+ add_column :templates, :shared_link, :boolean, if_not_exists: true
+
+ MigrationTemplate.where(shared_link: nil).in_batches.update_all(shared_link: true)
+
+ change_column_default :templates, :shared_link, from: nil, to: false
+ change_column_null :templates, :shared_link, false
+ end
+
+ def down
+ remove_column :templates, :shared_link
+ end
+end
diff --git a/db/migrate/20250530080846_add_email_events_date_index.rb b/db/migrate/20250530080846_add_email_events_date_index.rb
new file mode 100644
index 00000000..25a0d643
--- /dev/null
+++ b/db/migrate/20250530080846_add_email_events_date_index.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+class AddEmailEventsDateIndex < ActiveRecord::Migration[8.0]
+ def change
+ remove_index :email_events, :account_id
+ add_index :email_events, %i[account_id event_datetime]
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 887938d0..b024ff66 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.0].define(version: 2025_05_18_070555) do
+ActiveRecord::Schema[8.0].define(version: 2025_05_30_080846) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -174,7 +174,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_05_18_070555) do
t.text "data", null: false
t.datetime "event_datetime", null: false
t.datetime "created_at", null: false
- t.index ["account_id"], name: "index_email_events_on_account_id"
+ t.index ["account_id", "event_datetime"], name: "index_email_events_on_account_id_and_event_datetime"
t.index ["email"], name: "index_email_events_on_email"
t.index ["emailable_type", "emailable_id"], name: "index_email_events_on_emailable"
t.index ["message_id"], name: "index_email_events_on_message_id"
@@ -361,6 +361,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_05_18_070555) do
t.bigint "folder_id", null: false
t.string "external_id"
t.text "preferences", null: false
+ t.boolean "shared_link", default: false, null: false
t.index ["account_id"], name: "index_templates_on_account_id"
t.index ["author_id"], name: "index_templates_on_author_id"
t.index ["external_id"], name: "index_templates_on_external_id"
diff --git a/lib/submissions/generate_audit_trail.rb b/lib/submissions/generate_audit_trail.rb
index 32622049..cdd5945a 100644
--- a/lib/submissions/generate_audit_trail.rb
+++ b/lib/submissions/generate_audit_trail.rb
@@ -31,7 +31,9 @@ module Submissions
def call(submission)
account = submission.account
- I18n.with_locale(account.locale) do
+ last_submitter = submission.submitters.select(&:completed_at).max_by(&:completed_at)
+
+ I18n.with_locale(last_submitter.metadata.fetch('lang', account.locale)) do
document = build_audit_trail(submission)
pkcs = Accounts.load_signing_pkcs(account)
@@ -41,8 +43,6 @@ module Submissions
document.trailer.info[:Creator] = "#{Docuseal.product_name} (#{Docuseal::PRODUCT_URL})"
- last_submitter = submission.submitters.select(&:completed_at).max_by(&:completed_at)
-
if pkcs
sign_params = {
reason: sign_reason,
@@ -322,7 +322,7 @@ module Submissions
resized_image = image.resize([scale, 1].min)
io = StringIO.new(resized_image.write_to_buffer('.png'))
- width = field['type'] == 'initials' ? 100 : 200
+ width = field['type'] == 'initials' ? 50 : 200
height = resized_image.height * (width.to_f / resized_image.width)
if height > MAX_IMAGE_HEIGHT
diff --git a/lib/submissions/generate_result_attachments.rb b/lib/submissions/generate_result_attachments.rb
index e260192d..7dfdb6d9 100644
--- a/lib/submissions/generate_result_attachments.rb
+++ b/lib/submissions/generate_result_attachments.rb
@@ -152,7 +152,8 @@ module Submissions
TESTING_FOOTER
end
else
- "#{I18n.t('document_id', locale: submitter.account.locale)}: #{document_id}"
+ "#{I18n.t('document_id',
+ locale: submitter.metadata.fetch('lang', submitter.account.locale))}: #{document_id}"
end
text = HexaPDF::Layout::TextFragment.create(
@@ -182,6 +183,8 @@ module Submissions
with_headings = find_last_submitter(submitter.submission, submitter:).blank? if with_headings.nil?
+ locale = submitter.metadata.fetch('lang', account.locale)
+
submitter.submission.template_fields.each do |field|
next if field['type'] == 'heading' && !with_headings
next if field['submitter_uuid'] != submitter.uuid && field['type'] != 'heading'
@@ -258,7 +261,7 @@ module Submissions
reason_value = submitter.values[field.dig('preferences', 'reason_field_uuid')].presence
reason_string =
- I18n.with_locale(submitter.account.locale) do
+ I18n.with_locale(locale) do
"#{reason_value ? "#{I18n.t('reason')}: " : ''}#{reason_value || I18n.t('digitally_signed_by')} " \
"#{submitter.name}#{submitter.email.present? ? " <#{submitter.email}>" : ''}\n" \
"#{I18n.l(attachment.created_at.in_time_zone(submitter.account.timezone), format: :long)} " \
@@ -442,7 +445,7 @@ module Submissions
option = field['options']&.find { |o| o['uuid'] == area['option_uuid'] }
option_name = option['value'].presence
- option_name ||= "#{I18n.t('option', locale: account.locale)} #{field['options'].index(option) + 1}"
+ option_name ||= "#{I18n.t('option', locale: locale)} #{field['options'].index(option) + 1}"
value = Array.wrap(value).include?(option_name)
end
@@ -509,7 +512,7 @@ module Submissions
end
else
if field['type'] == 'date'
- value = TimeUtils.format_date_string(value, field.dig('preferences', 'format'), account.locale)
+ value = TimeUtils.format_date_string(value, field.dig('preferences', 'format'), locale)
end
value = NumberUtils.format_number(value, field.dig('preferences', 'format')) if field['type'] == 'number'
diff --git a/lib/templates/clone.rb b/lib/templates/clone.rb
index 0fe3b91b..e4b5fc60 100644
--- a/lib/templates/clone.rb
+++ b/lib/templates/clone.rb
@@ -9,6 +9,7 @@ module Templates
template = original_template.account.templates.new
template.external_id = external_id
+ template.shared_link = original_template.shared_link
template.author = author
template.name = name.presence || "#{original_template.name} (#{I18n.t('clone')})"
diff --git a/lib/templates/process_document.rb b/lib/templates/process_document.rb
index 8ee1dff5..1a5e5ae1 100644
--- a/lib/templates/process_document.rb
+++ b/lib/templates/process_document.rb
@@ -118,10 +118,10 @@ module Templates
page = page.copy(interpretation: :srgb)
- bitdepth = 2**page.stats.to_a[1..3].pluck(2).uniq.size
-
data =
if format == FORMAT
+ bitdepth = 2**page.stats.to_a[1..3].pluck(2).uniq.size
+
page.write_to_buffer(format, compression: 7, filter: 0, bitdepth:,
palette: true, Q: bitdepth == 8 ? Q : 5, dither: 0)
else
@@ -138,6 +138,8 @@ module Templates
blob
rescue Vips::Error, Pdfium::PdfiumError => e
Rollbar.warning(e) if defined?(Rollbar)
+
+ nil
ensure
doc_page&.close
end
diff --git a/lib/templates/serialize_for_api.rb b/lib/templates/serialize_for_api.rb
index f5f03ebe..b60f9023 100644
--- a/lib/templates/serialize_for_api.rb
+++ b/lib/templates/serialize_for_api.rb
@@ -6,7 +6,7 @@ module Templates
only: %w[
id archived_at fields name preferences schema
slug source submitters created_at updated_at
- author_id external_id folder_id
+ author_id external_id folder_id shared_link
],
methods: %i[application_key folder_name],
include: { author: { only: %i[id email first_name last_name] } }
diff --git a/spec/fixtures/fieldtags.docx b/spec/fixtures/fieldtags.docx
new file mode 100644
index 00000000..50655a04
Binary files /dev/null and b/spec/fixtures/fieldtags.docx differ
diff --git a/spec/requests/templates_spec.rb b/spec/requests/templates_spec.rb
index fa2a362a..7763d8cd 100644
--- a/spec/requests/templates_spec.rb
+++ b/spec/requests/templates_spec.rb
@@ -83,13 +83,15 @@ describe 'Templates API' do
end
describe 'PUT /api/templates' do
- it 'update a template' do
- template = create(:template, account:,
- author:,
- folder:,
- external_id: SecureRandom.base58(10),
- preferences: template_preferences)
+ let(:template) do
+ create(:template, account:,
+ author:,
+ folder:,
+ external_id: SecureRandom.base58(10),
+ preferences: template_preferences)
+ end
+ it 'updates a template' do
put "/api/templates/#{template.id}", headers: { 'x-auth-token': author.access_token.token }, params: {
name: 'Updated Template Name',
external_id: '123456'
@@ -106,6 +108,24 @@ describe 'Templates API' do
updated_at: template.updated_at
}.to_json))
end
+
+ it "enables the template's shared link" do
+ expect do
+ put "/api/templates/#{template.id}", headers: { 'x-auth-token': author.access_token.token }, params: {
+ shared_link: true
+ }.to_json
+ end.to change { template.reload.shared_link }.from(false).to(true)
+ end
+
+ it "disables the template's shared link" do
+ template.update(shared_link: true)
+
+ expect do
+ put "/api/templates/#{template.id}", headers: { 'x-auth-token': author.access_token.token }, params: {
+ shared_link: false
+ }.to_json
+ end.to change { template.reload.shared_link }.from(true).to(false)
+ end
end
describe 'DELETE /api/templates/:id' do
@@ -206,6 +226,7 @@ describe 'Templates API' do
name: 'sample-document'
}
],
+ shared_link: template.shared_link,
author_id: author.id,
archived_at: nil,
created_at: template.created_at,
diff --git a/spec/system/profile_settings_spec.rb b/spec/system/profile_settings_spec.rb
index 7b977d2d..3c05a664 100644
--- a/spec/system/profile_settings_spec.rb
+++ b/spec/system/profile_settings_spec.rb
@@ -33,6 +33,14 @@ RSpec.describe 'Profile Settings' do
expect(user.last_name).to eq('Beckham')
expect(user.email).to eq('david.beckham@example.com')
end
+
+ it 'does not update if email is invalid' do
+ fill_in 'Email', with: 'devid+test@example'
+
+ all(:button, 'Update')[0].click
+
+ expect(page).to have_content('Email is invalid')
+ end
end
context 'when changes password' do
diff --git a/spec/system/setup_spec.rb b/spec/system/setup_spec.rb
index 5235c9b5..da4a37da 100644
--- a/spec/system/setup_spec.rb
+++ b/spec/system/setup_spec.rb
@@ -51,6 +51,16 @@ RSpec.describe 'App Setup' do
end
context 'when invalid information' do
+ it 'does not setup the app if the email is invalid' do
+ fill_setup_form(form_data.merge(email: 'bob@example-com'))
+
+ expect do
+ click_button 'Submit'
+ end.not_to(change(User, :count))
+
+ expect(page).to have_content('Email is invalid')
+ end
+
it 'does not setup the app if the password is too short' do
fill_setup_form(form_data.merge(password: 'pass'))
diff --git a/spec/system/signing_form_spec.rb b/spec/system/signing_form_spec.rb
index 8bfa3e5e..83c1b30b 100644
--- a/spec/system/signing_form_spec.rb
+++ b/spec/system/signing_form_spec.rb
@@ -5,7 +5,9 @@ RSpec.describe 'Signing Form' do
let(:author) { create(:user, account:) }
context 'when the template form link is opened' do
- let(:template) { create(:template, account:, author:, except_field_types: %w[phone payment stamp]) }
+ let(:template) do
+ create(:template, shared_link: true, account:, author:, except_field_types: %w[phone payment stamp])
+ end
before do
visit start_form_path(slug: template.slug)
@@ -811,7 +813,9 @@ RSpec.describe 'Signing Form' do
end
context 'when the template requires multiple submitters' do
- let(:template) { create(:template, submitter_count: 2, account:, author:, only_field_types: %w[text]) }
+ let(:template) do
+ create(:template, shared_link: true, submitter_count: 2, account:, author:, only_field_types: %w[text])
+ end
context 'when default signer details are not defined' do
it 'shows an explanation error message if a logged-in user associated with the template account opens the link' do
diff --git a/spec/system/team_settings_spec.rb b/spec/system/team_settings_spec.rb
index 0286267f..01ce568f 100644
--- a/spec/system/team_settings_spec.rb
+++ b/spec/system/team_settings_spec.rb
@@ -92,6 +92,23 @@ RSpec.describe 'Team Settings' do
end
end
+ it 'does not allow to create a new user with an invalid email' do
+ click_link 'New User'
+
+ within '#modal' do
+ fill_in 'First name', with: 'Joseph'
+ fill_in 'Last name', with: 'Smith'
+ fill_in 'Email', with: 'joseph.smith@gmail'
+ fill_in 'Password', with: 'password'
+
+ expect do
+ click_button 'Submit'
+ end.not_to change(User, :count)
+
+ expect(page).to have_content('Email is invalid')
+ end
+ end
+
it 'updates a user' do
first(:link, 'Edit').click
diff --git a/spec/system/template_share_link_spec.rb b/spec/system/template_share_link_spec.rb
new file mode 100644
index 00000000..23499434
--- /dev/null
+++ b/spec/system/template_share_link_spec.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+RSpec.describe 'Template Share Link' do
+ let!(:account) { create(:account) }
+ let!(:author) { create(:user, account:) }
+ let!(:template) { create(:template, account:, author:) }
+
+ before do
+ sign_in(author)
+ end
+
+ context 'when the template is not shareable' do
+ before do
+ visit template_path(template)
+ end
+
+ it 'makes the template shareable' do
+ click_on 'Link'
+
+ expect do
+ within '#modal' do
+ check 'template_shared_link'
+ end
+ end.to change { template.reload.shared_link }.from(false).to(true)
+ end
+
+ it 'makes the template shareable on toggle' do
+ click_on 'Link'
+
+ expect do
+ within '#modal' do
+ find('#template_shared_link').click
+ end
+ end.to change { template.reload.shared_link }.from(false).to(true)
+ end
+ end
+
+ context 'when the template is already shareable' do
+ before do
+ template.update(shared_link: true)
+ visit template_path(template)
+ end
+
+ it 'makes the template unshareable' do
+ click_on 'Link'
+
+ expect do
+ within '#modal' do
+ uncheck 'template_shared_link'
+ end
+ end.to change { template.reload.shared_link }.from(true).to(false)
+ end
+ end
+end
diff --git a/spec/system/template_spec.rb b/spec/system/template_spec.rb
index a0ffe4e5..d343283e 100644
--- a/spec/system/template_spec.rb
+++ b/spec/system/template_spec.rb
@@ -16,7 +16,7 @@ RSpec.describe 'Template' do
expect(page).to have_content(template.name)
expect(page).to have_content('There are no Submissions')
expect(page).to have_content('Send an invitation to fill and complete the form')
- expect(page).to have_link('Sign it Yourself')
+ expect(page).to have_button('Sign it Yourself')
end
end